Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ jobs:
outputs:
backend: ${{ steps.filter.outputs.backend }}
cli: ${{ steps.filter.outputs.cli }}
machine: ${{ steps.filter.outputs.machine }}
rust: ${{ steps.filter.outputs.rust }}
frontend: ${{ steps.filter.outputs.frontend }}
mobile: ${{ steps.filter.outputs.mobile }}
Expand All @@ -86,8 +87,22 @@ jobs:
# YAML anchors aren't used here -- aliased sequences expand to
# nested lists in js-yaml, which paths-filter does not flatten.
filters: |
machine:
- 'frontend/scripts/machine-seccomp.ts'
- 'machine/**'
- 'cli/src/node/machine/**'
- 'cli/resources/machine-browser/**'
- 'cli/resources/machine-container/**'
- 'cli/container/**'
- 'cli/Dockerfile.machine'
- 'cli/tests/machine*'
- 'cli/tests/Dockerfile.machine'
- 'cli/Cargo.toml'
- 'Cargo.toml'
- 'Cargo.lock'
backend:
- 'backend/**'
- 'machine/**'
- 'integrations/oracle/cdp-worker/worker.mjs'
- 'cloud-auth/**'
- 'service-adapters/**'
Expand All @@ -103,6 +118,7 @@ jobs:
- '.config/nextest.toml'
cli:
- 'cli/**'
- 'machine/**'
- 'skills/nyxid/scripts/**'
- 'cloud-auth/**'
- 'service-adapters/**'
Expand All @@ -121,6 +137,7 @@ jobs:
- 'backend/**'
- 'integrations/oracle/cdp-worker/worker.mjs'
- 'cli/**'
- 'machine/**'
- 'cloud-auth/**'
- 'service-adapters/**'
- 'nyxid-crypto/**'
Expand Down Expand Up @@ -368,6 +385,36 @@ jobs:
# ---------------------------------------------------------------------------
# Rust: CLI / node-agent build + test (no DB required)
# ---------------------------------------------------------------------------
machine-container:
name: Machine Container E2E
needs: changes
permissions:
contents: read
if: inputs.force-all || needs.changes.outputs.ci == 'true' || needs.changes.outputs.machine == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v4
- name: Build machine image
uses: docker/build-push-action@v7
with:
context: .
file: cli/Dockerfile.machine
load: true
tags: nyxid-node-machine:ci
cache-from: type=gha,scope=machine-pr
cache-to: type=gha,mode=max,scope=machine-pr
- name: Verify sandbox, isolation, saved logins, takeover and live desktop
run: |
docker build --build-arg MACHINE_IMAGE=nyxid-node-machine:ci -f cli/tests/Dockerfile.machine -t nyxid-machine-e2e:ci .
docker run --rm --shm-size=256m --security-opt seccomp=cli/resources/machine-container/seccomp.json nyxid-machine-e2e:ci
- name: Remove test images and build cache
if: always()
run: |
docker image rm -f nyxid-machine-e2e:ci nyxid-node-machine:ci || true
docker builder prune -f

cli-test:
name: CLI Test
needs: changes
Expand All @@ -389,8 +436,14 @@ jobs:
- name: Build CLI
run: cargo build -p nyxid-cli

- uses: actions/setup-node@v6
with:
node-version-file: .node-version
- name: Test saved-login extension and signed package freshness
run: node --test cli/tests/machine_filler.test.mjs

- name: Run CLI tests
run: cargo nextest run -p nyxid-cli --profile ci
run: cargo nextest run -p nyxid-cli -p nyxid-machine --profile ci

- name: Publish test summary
if: always()
Expand Down Expand Up @@ -1037,6 +1090,7 @@ jobs:
- backend-billing-smoke
- backend-image-inputs
- cli-test
- machine-container
- rust-features
- frontend
- mobile
Expand All @@ -1063,6 +1117,7 @@ jobs:
backend-billing-smoke=${{ needs.backend-billing-smoke.result }}
backend-image-inputs=${{ needs.backend-image-inputs.result }}
cli-test=${{ needs.cli-test.result }}
machine-container=${{ needs.machine-container.result }}
rust-features=${{ needs.rust-features.result }}
frontend=${{ needs.frontend.result }}
mobile=${{ needs.mobile.result }}
Expand Down
21 changes: 17 additions & 4 deletions .github/workflows/publish-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: Publish Images
# - every push to `main` -> tags: main, main-<sha>, edge
# - every pushed tag matching v*.*.* -> tags: <version>, <major>.<minor>, <major>, latest
#
# Components: backend, frontend, node-agent.
# Components: backend, frontend, node-agent, nyxid-node-machine.
# mcp-proxy is intentionally excluded (source not yet in repo).
#
# Build strategy: native runner matrix (no QEMU). The `build` job fans out
Expand Down Expand Up @@ -58,7 +58,7 @@ jobs:
strategy:
fail-fast: false
matrix:
component: [backend, frontend, node-agent]
component: [backend, frontend, node-agent, nyxid-node-machine]
platform: [linux/amd64, linux/arm64]
include:
- platform: linux/amd64
Expand All @@ -79,6 +79,9 @@ jobs:
- component: node-agent
context: .
file: cli/Dockerfile.node
- component: nyxid-node-machine
context: .
file: cli/Dockerfile.machine
steps:
- uses: actions/checkout@v6

Expand Down Expand Up @@ -132,6 +135,16 @@ jobs:
cache-to: type=gha,mode=max,scope=${{ matrix.component }}-${{ matrix.platform_pair }}
provenance: false

- name: Verify machine browser and desktop
if: matrix.component == 'nyxid-node-machine'
timeout-minutes: 10
env:
MACHINE_IMAGE: ${{ steps.repo.outputs.image }}@${{ steps.build.outputs.digest }}
run: |
docker build --build-arg MACHINE_IMAGE="$MACHINE_IMAGE" \
-f cli/tests/Dockerfile.machine -t nyxid-machine-e2e:ci .
docker run --rm --shm-size=256m --security-opt seccomp=cli/resources/machine-container/seccomp.json nyxid-machine-e2e:ci

- name: Export digest
run: |
mkdir -p /tmp/digests
Expand All @@ -157,7 +170,7 @@ jobs:
strategy:
fail-fast: false
matrix:
component: [backend, frontend, node-agent]
component: [backend, frontend, node-agent, nyxid-node-machine]
steps:
- name: Compute lowercase image repo
id: repo
Expand Down Expand Up @@ -227,7 +240,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
owner="${GITHUB_REPOSITORY_OWNER,,}"
for component in backend frontend node-agent; do
for component in backend frontend node-agent nyxid-node-machine; do
package="nyxid/${component}"
encoded=$(printf '%s' "$package" | jq -sRr @uri)
echo "Ensuring ghcr.io/${owner}/${package} is public..."
Expand Down
17 changes: 17 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Strict separation: `handlers/` -> `services/` -> `models/`
- 12100 `AssistantTurnActive` (HTTP 409, `turn_active`): a persisted NyxAgent conversation already has an active turn.
- 12200 `AdminUsageQueryTimeout` (HTTP 503): bounded admin usage aggregation timed out; retry with a narrower window or filters.
- 12300 `WorkspaceDestinationsNotActivated` (HTTP 503): incomplete automatic Drive/Workspace editor reconciliation; excluded from proxy-fault telemetry
- 12400-12413 machine nodes: 12400 `MachineCapabilityDisabled`, 12401 `MachineNotAllowed`, 12402 `MachinePathOutsideRoots`, 12403 `MachineJobNotFound`, 12404 `MachineConfirmationPending`, 12405 `MachineConfirmationDeclined`, 12406 `MachineComputerUnavailable`, 12407 `MachineLimitExceeded`, 12408 `MachineOwnerInControl`, 12409 `MachineNotIsolated`, 12410 `MachineLoginNotFound`, 12411 `MachineLoginOriginMismatch`, 12412 `MachineLoginWrongField`, 12413 `MachineBrowserUnavailable`.

### 4. Frontend Patterns

Expand Down Expand Up @@ -105,6 +106,11 @@ Add new entries here when introducing additional vendored URN types.
- Admin node endpoints (`handlers/admin_nodes.rs`) require admin role and have no ownership check
- `nyxid node daemon` manages background service lifecycle (`cli/src/node/daemon.rs`): launchd LaunchAgent on macOS / systemd user unit on Linux. All node commands support `--profile` for multi-instance: service labels `dev.nyxid.node.{profile}` (macOS) / `nyxid-node-{profile}.service` (Linux), config at `~/.nyxid-node/profiles/{name}/`.

- Machine nodes add locally-authoritative `shell`/`files`/`computer` capabilities (all off by default), mandatory signed requests, bounded jobs/files, cua MCP stdio, job-bound service gateway and human-only live desktops. Only owner-turn assistant chat keys can use them; guests never. Specialists store `machine_node_ids` and `saved_login_ids` beside `grants`. Owner settings (`machine_confirm`, single-user saved-login opt-in) are human-only. Browser policies/extension/native host belong to the supervisor; separated children run as `browser` or `agent`. Saved logins are encrypted, write-only, exact-origin fills, with no secret-bearing Debug, logs, audit or tool results. Setup/control watches queue their event transactionally. No extra machine DB reads on unrelated proxy/MCP/turn paths. See `docs/MACHINE_NODES.md` for the binding contract and `docs/NYXID_NODE.md` for setup and warnings.
- Machine exec `services` is an explicit per-job least-privilege declaration (default none), bound as ID+slug on MachineJob, shown on cards/audit and rechecked at gateway execution. Server catalog `inference.wire_protocol` and `git_http` metadata generate the signed SDK/git environment; no node slug mappings. Reuse the shared service visibility resolver and middleware API-key identity constructor. Preserve Content-Encoding with Content-Length through both streaming hops. Non-isolated shell warnings must explicitly mention access to node tokens/signing secrets/stored credentials; recommend the container or `--separate-users`, never refuse solely for owner-machine risk. Container Chromium uses user-namespace/seccomp sandboxing via the shipped profile; every Linux agent/file child sets NoNewPrivs and denies namespace syscalls through a per-process filter; browser/cua retain sandbox namespace access. Human live view uses X11/XTest or ScreenCaptureKit/separate human cua input, at 30 Hz with JPEG dirty rectangles and zero idle payload; agent actions/observations stay on cua. Controller revisions cancel in-flight agent work without locks across I/O. Run extension freshness/unit tests and machine container e2e in PR CI. Performance measurements and repeatable commands: `docs/MACHINE_NODES.md#validation-and-measurements`.
- Machine automation turns retain live machine/login grants and owner-control fences. Webhook confirmation is additive to `machine_confirm`; one exact, one-use owner card satisfies both. Exec, file writes/saves, job cancellation and mutating computer input are destructive; checked login fills and owner-control requests are changing only. Apply the gate in the machine adapter after normalization so direct and universal tool calls agree. Machine gateway streamed uploads bind exact services and never retry another node or pool member; declared pools support buffered SDK/JSON requests with normal failover and live member ACLs. Catalog discovery projects one ID-bounded batch. All human machine routes reuse `login_client_context::require_first_party_human`; desktop upgrades also retain the `/assistant/nyxagent/*` OAuth-client rejection layer.


### 7. OpenClaw Integration

OpenClaw is a self-hosted AI gateway integrated at three levels (details: `docs/OPENCLAW_INTEGRATION.md`):
Expand Down Expand Up @@ -543,6 +549,9 @@ nyxid node start | agent-status | credentials list
nyxid node openclaw connect --url http://localhost:18789 # --credential-env for non-interactive
nyxid node openclaw status | disconnect
nyxid node daemon install|start|stop|restart|status|logs --follow|uninstall # launchd/systemd; supports --profile
nyxid node setup --machine [--computer] [--profile NAME] # pairing or page-issued --token; Linux isolation: sudo + --separate-users
nyxid node machine enable|disable|status # independent shell/files/computer; local authority
nyxid node docker start --machine # desktop image, persistent identity/workspace
nyxid node docker build|start|stop|status|logs [--profile <name>] # Docker alternative to native daemon

# Oracle relay
Expand Down Expand Up @@ -616,3 +625,11 @@ In QA mode, flag any code that doesn't match DESIGN.md.
- The `aurinko` channel adapter has independent encrypted account-token/signing-secret storage, signed raw-byte POST validation, bound subscriptions, and bounded inline producer retries. Never return 422 to Aurinko.
- ADR-013 still applies: persist only email subscription bindings, batch digest/cursor, stable UUID-v4 receipts, and send-attempt barriers. Receipts/sends have no TTL while their bot exists; owner/bot deletion must fence in-flight effects before cleanup. Do not consume a retryable Aurinko reply token before preflight or resend an uncertain POST. Legacy adapter behavior remains unchanged.
- See `docs/AURINKO_INTEGRATION.md` for lifecycle, callback routing, filtering, reply authority, scopes, and validation limits. Aurinko is included in the catalog overlay drift map.

Assistant workspace navigation places Automations (`/assistant/automations`) and
Machines (`/assistant/machines`, Saved logins at `?tab=logins`) beside Plugins and
Approvals for both engines. Setup/pairing stay in `AssistantShell`; the desktop is
standalone under `/assistant/machines/{id}/desktop`. Studio Nodes shows only a
read-only machine summary linking to assistant settings; Developer → Triggers
retains secrets/replay. `/automations` redirects with `setup` and `agent` intact.
Server-generated browser URLs use `services::assistant_links::AssistantPage`.
Loading
Loading