Remote node host (system.run)

Use a node host when your Gateway runs on one machine and you want commands to execute on another. The model still talks to the gateway; the gateway forwards exec calls to the node host when host=node is selected.

Role Responsibility
Gateway host Receives messages, runs the model, routes tool calls.
Node host Executes system.run/system.which on the node machine.
Approvals Enforced on the node host via ~/.apus/state/apus.sqlite#exec_approvals_config.

Approval note:

Gateway deployments that cannot host nodes

A Gateway can remain healthy for browser users while node hosting is unavailable. Run apus doctor on the Gateway before onboarding nodes, and check these preconditions:

For a Cloudflare Access-fronted Gateway:

  1. In Cloudflare Zero Trust, create an Access service token. Copy its Client ID and Client Secret when Cloudflare displays them.

  2. Add a Service Auth policy that accepts the token on the Access application protecting the Gateway. If /j/* and /__apus__/worker are separate Access applications, add the same policy to both.

  3. On the node, provide the conventional environment fallback and connect:

    export CF_ACCESS_CLIENT_ID="<client-id>"
    export CF_ACCESS_CLIENT_SECRET="<client-secret>"
    apus connect https://gateway.example/j/<code> --service
    

The canonical node connection keys are gateway.cloudflareAccess.clientId and gateway.cloudflareAccess.clientSecret; both accept SecretInput values. The environment fallback above persists those keys as env SecretRefs, not copied plaintext. For installed nodes, Apus stores the environment values in the managed service environment file rather than inline in launchd, systemd, or Task Scheduler definitions. Resolved values are bound to the configured Gateway origin and are not followed across redirects. Apus rejects the pair before resolution on plaintext http:// or ws:// routes; credential-free loopback and private-network plaintext behavior is unchanged.

Start a node host (foreground)

On the node machine:

apus node run --host <gateway-host> --port 18789 --display-name "Build Node"

For one-paste setup, create a Node host setup link from the Control UI Devices page, then run its copyable command on the node machine:

apus node run --pair "oc-pair://<setup-code>"

The link is single-use and expires after 10 minutes. It supplies the endpoint, bootstrap token, TLS mode, and certificate pin when available. Explicit gateway flags override the corresponding --pair values. Administrator-minted bootstrap enrollment approves the device and its first declared command surface, including system.run and system.which when declared. Later command, capability, or permission expansion still creates an approval request. Gateway command policy and the node host's exec approvals still apply. Local exec approvals default to full with ask: "off"; configure them before using the link if that access is too broad. See Node pairing.

node run also accepts --pair, --context-path (Gateway WS context path), --tls, --tls-fingerprint <sha256>, and --node-id (override the legacy client instance ID; this does not reset pairing). On macOS, pass --share-installed-apps to advertise device.apps; sharing is off by default. Use --no-share-installed-apps to disable a previously saved opt-in.

Pass --session-host to enable worker hosting for this foreground process without changing the saved preference. Automatic restarts preserve this choice.

Remote gateway via SSH tunnel (loopback bind)

If the Gateway binds to loopback (gateway.bind=loopback, default in local mode), remote node hosts cannot connect directly. Create an SSH tunnel and point the node host at the local end of the tunnel.

Example (node host -> gateway host):

# Terminal A (keep running): forward local 18790 -> gateway 127.0.0.1:18789
ssh -N -L 18790:127.0.0.1:18789 user@gateway-host

# Terminal B: export the gateway token and connect through the tunnel
export APUS_GATEWAY_TOKEN="<gateway-token>"
apus node run --host 127.0.0.1 --port 18790 --display-name "Build Node"

Notes:

Restrict the node command surface

Pass --commands <ids> to apus node run, apus node install, or apus connect to advertise only an explicit comma-separated list of exact command IDs. For example, a Session Share node can publish sessions without exposing execution or other machine capabilities:

apus connect <join-url> --service \
  --commands apus.sessions.list.v1,apus.sessions.read.v1

The flag is repeatable. The allowlist is saved in the node's durable machine state, including for installed services; omitting it on a later start keeps the saved list. The node advertises only commands that are both available and allowlisted, with only their required capabilities. Startup fails if no requested command is available. The Gateway pairing approval shows exactly the declared commands; Gateway command policy still applies to invocation.

An explicit allowlist also disables computer use, skill scanning and publication, plugin-tool publication, MCP servers, and worker hosting. An allowlist does not enable a disabled plugin or make an unavailable command available.

Restore the full default surface with apus node run --all-commands in the foreground or apus node install --force --all-commands for an installed service. When enrolling with apus connect, add --all-commands and optionally --service. This durably removes the saved allowlist and replaces the service's --commands arguments. Do not combine --all-commands with --commands.

Start a node host (service)

apus node install --host <gateway-host> --port 18789 --display-name "Build Node"
apus node start
apus node restart

node install also accepts --context-path, --tls, --tls-fingerprint, --node-id (legacy client instance ID only), --share-installed-apps / --no-share-installed-apps, --runtime <node|bun> (default: node), and --force to reinstall. Bun requires version 1.4+ with WAL-reset-safe node:sqlite and is an explicit opt-in; Node remains recommended. node status, node stop, and node uninstall are also available.

Node shutdown waits for plugin availability watchers and active computer executions to finish cleanup, and reports failures from those cleanup operations. If a command reports Node disconnect cleanup failed, reconnect the node to retry disconnect cleanup before sending another command.

Session-host workspace permissions

A node started with --session-host checks workspace staging before advertising session capacity. On Unix, the state directory (APUS_STATE_DIR, or ~/.apus) and its canonical ancestors must satisfy the filesystem owner's checks. Group- or world-writable ancestors without sticky protection prevent session hosting, even when the node-host directory itself is private.

The node log and environments.list identify the offending directory and the remediation: run chmod go-w '<reported-path>' on the node, then restart the node host. Review shared-directory permissions before changing them; alternatively, move node state under a private directory. Apus does not change ancestor permissions or bypass staging checks. A trusted sticky system temporary directory remains supported.

A session-host failure does not disconnect the node or disable its desktop and other available commands. environments.list keeps a connected node available, reports sessionHost: false and the hosting diagnostic, and refuses session placement until the hosting problem is repaired.

Disabled-host reasons are published only when the Gateway advertises node-worker-host-diagnostics-v1. Older Gateways still receive disabled hosting; the node log retains the reason. Workspace transfers also report the same remediation if permissions change after startup.

Automatic node updates

Packaged headless nodes check for updates hourly by default, in both foreground and service mode. They prepare a separate runtime, wait until all node work is idle, then restart and reconnect with the same identity, pairing, and launch options. A node update does not replace the global CLI package or a co-located Gateway. Automatic activations are at least 12 hours apart, and busy work can defer an update indefinitely.

Set nodeHost.autoUpdate.enabled: false on the node to opt out. The shared update.checkOnStart: false and APUS_NO_AUTO_UPDATE=1 opt-outs also apply. Source checkouts, native app nodes, private workers, dev, and extended-stable installs do not auto-apply. Releases requiring database migrations defer to the normal update workflow. See Headless node updates for the idle-work rules and configuration.

Pair + name

On the Gateway host, approve the device request:

apus devices list
apus devices approve <deviceRequestId>

If the node retries with changed auth details, re-run apus devices list and approve the current requestId.

The node keeps reconnecting while device approval is pending, with exponential backoff capped at 30 seconds. After approval, its next reconnect creates a separate command-surface request. On the Gateway:

apus nodes pending
apus nodes approve <nodeRequestId>
apus nodes describe --node <id|name|ip>

If an older client already reports that reconnect is paused, restart the installed node with apus node restart, or stop and rerun its foreground apus node run command once.

The device and node request IDs are distinct. An initial unapproved surface has no effective commands. SSH-verified and bootstrap enrollment can approve the first surface automatically; trusted-network device approval alone does not. Later expansions need approval, while previously approved commands that remain declared and allowed can still run. Gateway command policy and node-local exec approvals remain separate gates.

Naming options:

Headless identity state

The headless node keeps three separate state records in shared SQLite:

For a signed node, the Gateway uses the cryptographic device ID for pairing and node routing. The client instance ID is only connection metadata. Changing --node-id or migrating a retired node.json therefore does not reset pairing. See Identity and pairing state for the supported revoke-and-re-pair flow and upgrade notes.

Retired identity/device.json and identity/device-auth.json files are Doctor-owned migration inputs. Stop the node host and run apus doctor --fix; Doctor imports and verifies their rows in SQLite before removing the old files.

The headless node and macOS app worker check this state before preparing capabilities. Pending device auth, exec approvals, or a missing canonical identity with retired identity data requires Doctor; startup preserves the inputs and does not create replacement keys or import execution policy. A valid canonical identity keeps precedence over stale identity/device.json data.

System commands (node host / mac node)

The macOS node and headless node host both expose system.run.prepare, system.run, system.which, and system.execApprovals.get/set; the macOS node also exposes system.notify.

Examples:

apus nodes notify --node <idOrNameOrIp> --title "Ping" --body "Gateway ready"
apus nodes invoke --node <idOrNameOrIp> --command system.which --params '{"bins":["git"]}'

Notes:

Headless node host (cross-platform)

Apus can run a headless node host (no UI) that connects to the Gateway WebSocket and exposes system.run / system.which. This is useful on Linux/Windows or for running a minimal node alongside a server.

Start it:

apus node run --host <gateway-host> --port 18789

Notes:

Mac node mode