Skip to content
Merged
95 changes: 95 additions & 0 deletions .github/workflows/nix-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Nix Build & Integration Tests

on:
pull_request:
paths:
- 'nix/**'
- 'flake.nix'
- 'flake.lock'
- 'libs/cua-driver/rust/**'
- '.github/workflows/nix-build.yml'
push:
branches: [main]
paths:
- 'nix/**'
- 'flake.nix'
- 'flake.lock'
- 'libs/cua-driver/rust/**'
- '.github/workflows/nix-build.yml'
workflow_dispatch:

permissions:
id-token: write
contents: read

env:
AWS_REGION: us-west-2
NIX_CACHE_BUCKET: trycua-nix-cache
NIX_CACHE_SECRET: nix-cache/trycua-nix-cache/signing-key

jobs:
build-and-test:
name: Build cua-driver & run integration tests
runs-on: ubuntu-latest
timeout-minutes: 60

steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Configure AWS Credentials via OIDC
uses: aws-actions/configure-aws-credentials@ff717079ee2060e4bcee96c4779b553acc87447c # v4
with:
role-to-assume: arn:aws:iam::296062593712:role/github-actions-nix-cache
aws-region: ${{ env.AWS_REGION }}

- name: Get Nix signing key
id: nix-key
run: |
SECRET=$(aws secretsmanager get-secret-value \
--secret-id "${{ env.NIX_CACHE_SECRET }}" \
--query 'SecretString' --output text)

SECRET_KEY=$(echo "$SECRET" | jq -r '.secret_key')
echo "::add-mask::$SECRET_KEY"
echo "$SECRET_KEY" > "${{ runner.temp }}/signing-key.sec"
chmod 600 "${{ runner.temp }}/signing-key.sec"

PUBLIC_KEY=$(echo "$SECRET" | jq -r '.public_key')
echo "public_key=$PUBLIC_KEY" >> "$GITHUB_OUTPUT"

- name: Setup AWS credentials file for Nix
run: |
mkdir -p ~/.aws
printf '[default]\naws_access_key_id = %s\naws_secret_access_key = %s\naws_session_token = %s\nregion = %s\n' \
"$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" "$AWS_SESSION_TOKEN" "$AWS_REGION" > ~/.aws/credentials
chmod 600 ~/.aws/credentials

sudo mkdir -p /root/.aws
sudo bash -c "printf '[default]\naws_access_key_id = %s\naws_secret_access_key = %s\naws_session_token = %s\nregion = %s\n' \
'$AWS_ACCESS_KEY_ID' '$AWS_SECRET_ACCESS_KEY' '$AWS_SESSION_TOKEN' '$AWS_REGION' > /root/.aws/credentials"
sudo chmod 600 /root/.aws/credentials

- name: Install Nix
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
with:
extra_nix_config: |
experimental-features = nix-command flakes
substituters = s3://${{ env.NIX_CACHE_BUCKET }}?region=${{ env.AWS_REGION }} https://cache.nixos.org
trusted-substituters = s3://${{ env.NIX_CACHE_BUCKET }}?region=${{ env.AWS_REGION }}
trusted-public-keys = ${{ steps.nix-key.outputs.public_key }} cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=

- name: Run NixOS integration test
timeout-minutes: 8
run: nix build .#checks.x86_64-linux.cua-driver-integration --print-build-logs --show-trace

- name: Sign and upload to Nix cache
if: always()
run: |
echo "Signing and uploading build artifacts to Nix cache..."
nix store sign --key-file "${{ runner.temp }}/signing-key.sec" --all
nix copy --to "s3://${{ env.NIX_CACHE_BUCKET }}?region=${{ env.AWS_REGION }}&want-mass-query=true" --all -L

- name: Cleanup signing key
if: always()
run: rm -f "${{ runner.temp }}/signing-key.sec"
61 changes: 61 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
description = "CUA - Computer Use Agent";

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};

outputs =
{
self,
nixpkgs,
flake-utils,
...
}:
flake-utils.lib.eachSystem
[
"x86_64-linux"
"aarch64-linux"
]
(
system:
let
pkgs = import nixpkgs { inherit system; };

rustSrc = ./libs/cua-driver/rust;

cuaDriverPackage = import ./nix/cua-driver/package.nix {
inherit pkgs;
src = rustSrc;
};
in
{
packages = {
cua-driver = cuaDriverPackage;
default = cuaDriverPackage;
};

checks =
{
cua-driver-build = cuaDriverPackage;
}
// pkgs.lib.optionalAttrs (system == "x86_64-linux") {
# NixOS VM integration test (x86_64-linux only)
cua-driver-integration = import ./nix/cua-driver/tests/integration.nix {
inherit pkgs;
inherit (pkgs) lib;
cuaDriverModule = {
imports = [ ./nix/cua-driver/module.nix ];
services.cua-driver.package = cuaDriverPackage;
};
};
};
}
)
// {
# NixOS module — consumers must set services.cua-driver.package
# (or use the per-system package from self.packages)
nixosModules.cua-driver = ./nix/cua-driver/module.nix;
};
}
47 changes: 47 additions & 0 deletions nix/cua-driver/module.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# CUA Driver NixOS Module
#
# Installs the cua-driver binary and its runtime dependencies for
# Linux computer-use automation (X11, AT-SPI accessibility, ImageMagick).
#
# Usage in a NixOS configuration:
# imports = [ ./nix/cua-driver/module.nix ];
# services.cua-driver = {
# enable = true;
# package = cuaDriverPackage;
# };
#
{
config,
lib,
pkgs,
...
}:

let
cfg = config.services.cua-driver;
in
{
options.services.cua-driver = {
enable = lib.mkEnableOption "CUA Driver MCP server for computer-use automation";

package = lib.mkOption {
type = lib.types.package;
description = "The cua-driver package to use.";
};
};

config = lib.mkIf cfg.enable {
environment.systemPackages = [
cfg.package
pkgs.imagemagick # `import` command used by capture.rs for window screenshots
pkgs.at-spi2-core # AT-SPI bus daemon for accessibility tree queries
];

# D-Bus is required for AT-SPI communication
services.dbus.enable = true;

environment.variables = {
CUA_DRIVER_BIN = "${cfg.package}/bin/cua-driver";
};
};
}
53 changes: 53 additions & 0 deletions nix/cua-driver/package.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Builds cua-driver (Rust MCP server binary) for Linux.
#
# The cua-driver binary speaks MCP JSON-RPC 2.0 over stdio and provides
# 40+ tools for screen capture, mouse/keyboard input, window management,
# and accessibility-based element interaction.
#
# Usage:
# cuaDriver = import ./package.nix { inherit pkgs; src = ./../../libs/cua-driver/rust; };
#
{
pkgs,
src,
...
}:

pkgs.rustPlatform.buildRustPackage {
pname = "cua-driver";
version = "0.3.2";

inherit src;

# Use cargoHash (fetchCargoVendor) rather than cargoLock.lockFile because
# the workspace Cargo.lock includes macOS-only crates (apple-metal, apple-cf)
# that may be unreachable from crates.io. fetchCargoVendor handles this
# gracefully via `cargo vendor`.
cargoHash = "sha256-nkVI+O2P/QSTx15dCraNNGDAsEw3m7c/u/V6BXXSTww=";

# Build only the main binary crate. The workspace also contains
# platform-macos, platform-windows, cua-driver-uia, and focus-monitor-win
# which are gated behind cfg(target_os) and won't compile on Linux.
# Using -p cua-driver ensures Cargo only resolves Linux dependencies.
cargoBuildFlags = [ "-p" "cua-driver" ];
cargoTestFlags = [ "-p" "cua-driver" ];

# The entire Linux dependency chain is pure Rust:
# x11rb -> RustConnection (no libxcb C binding)
# ureq -> rustls (no openssl)
# tiny-skia -> pure Rust 2D graphics
# ring -> compiles own C/asm via stdenv's cc
nativeBuildInputs = [ ];
buildInputs = [ ];

# Skip tests that require a running X11 display or AT-SPI bus
doCheck = false;

meta = with pkgs.lib; {
description = "Cross-platform MCP server for computer-use automation";
homepage = "https://github.com/trycua/cua";
license = licenses.mit;
mainProgram = "cua-driver";
platforms = platforms.linux;
};
}
Loading
Loading