* fix(nodes): report camera positions the hardware actually reached
`camera.ptz.control` returned a position it never verified, and
`camera.snap`/`camera.clip` could capture from a camera the caller did
not ask for. Both told the agent an action succeeded when it had not.
PTZ read its post-write status from the same UVC connection that issued
the write. Gimbal cameras echo a pending setpoint back on the writing
connection, so the check confirmed its own write. Those cameras also
service camera-terminal controls only while a video stream is active, and
no capture session was held, so writes could be discarded entirely while
reads returned phantom values.
Hold a frame-discarding capture session across every PTZ operation, close
the writing controller, and verify through a fresh connection against each
axis's advertised resolution. An axis that misses now reports through the
existing CAMERA_PTZ_PARTIAL outcome with observed versus requested values
and what to check next.
Apple camera selection accepted an explicit deviceId and silently fell
back to the default camera when nothing matched. Linux already rejected
this, and CameraPTZService already rejected it in the same app. Centralize
exact selection in OpenClawKit so macOS and iOS both fail with a
device-not-found error; the facing/default fallback stays only for
requests that supply no deviceId.
camera.ptz.status now activates the camera and its privacy indicator for
the duration of the read. That is the cost of returning real positions.
* fix(nodes): tell callers how to recover from an unknown camera ID
Device IDs change when cameras are reconnected, so a bare
device-not-found error dead-ends the caller. Both Apple errors and the
docs now point at camera.list for current IDs.
Addresses the ClawSweeper P2 finding on #128595.
* feat(nodes): opt-in container isolation for node-hosted worker sessions
nodeHost.workerRuns.isolation=container runs each worker session inside a
Docker-compatible container (docker/OrbStack/podman): stdio launch transport,
exactly two bind mounts (bundle read-only, workspace read-write), allowlisted
env, and the container itself as the durable launch identity — created stopped
and journaled before the descriptor is delivered, killed/removed on cancel,
fence, shutdown, and recovery, with an owned-orphan sweep at startup. Missing
or changed engines disable hosting with an explicit diagnostic instead of
falling back to bare processes. Additive nullable worker_container_json column
on node_worker_launches (schema version unchanged).
* fix(test): base-shape fixture skips additive columns of stripped tables
node_worker_launches is excluded from claw-scoped state schemas, so the
read-only compat fixture cannot drop its additive column there; drop only
columns whose owning table exists.
* fix(test): guard split destructuring in base-shape fixture
* test(state): record worker_container_json in the canonical additive-column list
* fix(nodes): harden container isolation per review findings
- Revalidate the engine daemon target immediately before container creation;
a replaced daemon receives zero create/start requests (regression proves it).
- A pending launch keeps its worker slot until the journaled container is
killed/removed; cancellation cannot free capacity over a live container.
- Windows node hosts fail closed at startup for isolation=container with an
actionable diagnostic (native paths cannot be container mount targets).
- Container identity moves from an additive column to the same-version
companion table node_worker_launch_containers (bare STRICT, lazily ensured
on first container write, pruned with the launch journal): the launch table
is contract-optional and shipped readers reject additive columns there. An
exact v9 predecessor now provably opens and uses the journal after the
candidate populated container rows.
* feat(nodes): automatic device placement for sessions.dispatch
sessions.dispatch gains autoDevice: true — the gateway selects the eligible
session-host node with the most available worker slots (deterministic
tie-break), retries up to three candidates when a node churns at the
pre-provisioning eligibility fence, and reports the chosen device in the
placement runner projection. Control UI offers Any available node with
actionable disabled reasons. No-eligible-host failures state why.
* fix(ui): break draft-place-state/draft-session-placement import cycle
resolveDraftSessionPlacement only needs four scalar fields; a structural
param type replaces the Pick<DraftPlaceState,...> import that created the
madge cycle.
* fix(ui): keep the devices section hidden when no devices are paired
The Any available node row lives inside the Your devices section; rendering
it with zero paired devices resurrected the section on gateway-only setups.
Gate it on device presence — Connect a machine remains the discoverability
path — and cover both the empty and non-hostable cases.
* fix(gateway): project dispatch runner state through the canonical reader
The dispatch reply no longer synthesizes an available device runner; it uses
the fenced workerPlacementRunnerAvailabilityReader (and disk-space reader)
exactly like session reads, so a node lost after durable provisioning
projects offline consistently. Documents placement.runner.deviceId in the
protocol reference.
The exec docs claimed a per-call `host=node` request is always allowed
from `tools.exec.host=auto`, gating only `host=gateway` on whether a
sandbox runtime is active. The runtime treats both identically:
`isRequestedExecTargetAllowed` rejects `node` and `gateway` alike from
`auto` while a sandbox is available, and allows both when it is not.
Align the three pages that carried the asymmetric claim with the
test-locked runtime contract, and point readers at the explicit
`tools.exec.host=node` path the rejection error already recommends.
Refs #61009
Node session hosting was capped at a hardcoded 2 concurrent worker sessions.
The default is now one worker slot per available CPU core (clamped to 1..1024),
and nodeHost.workerRuns.capacity overrides it explicitly. The capacity option
was already plumbed through the supervisor; this wires it from config and
deletes the hardcoded constant. Docs and config baselines regenerated.
* fix(node-host): recover MCP catalogs and sessions
Share placement-neutral MCP lifecycle and result projection while keeping Gateway session ownership and node process ownership separate. Refresh node catalogs live, recover closed or expired transports without replay, and preserve MCP application errors across node.invoke.\n\nFixes #125044
* test: register node MCP CI inventory
* fix(node-host): stop MCP recovery after abort
* fix(gateway): stop a plugin dangerous flag from revoking desktop computer.act
`resolveNodeCommandAllowlistInternal` subtracted every plugin-declared
dangerous command from the composed allowlist, including core's own
`PLATFORM_DEFAULTS` entries and including the pairing allowlist, where core's
dangerous defaults are exempted. `computer.act` is both a desktop platform
default (grant = node-local enablement + pairing approval) and a command that
`registerComputerUseProvider` marks dangerous. Since `cua-computer` became
enabled-by-default on darwin, every macOS-hosted Gateway stripped `computer.act`
from both allowlists, so `normalizeDeclaredNodeCommands` dropped it from the
node's declaration with no pairing upgrade, no prompt, and no record. The
`computer` capability survived because caps were never allowlist-filtered, and
the `computerUse` descriptor was then dropped for the missing command.
Scope the plugin-dangerous subtraction to commands outside the platform-default
base: the flag still keeps a plugin's own surface behind an explicit allow and
still forces a registered invoke policy, but it no longer revokes a command core
declares itself.
Also make cap-without-command unrepresentable. `retainFulfilledNodeCapabilities`
drops a capability when policy withheld commands from its family and admitted
none, and the reconciler records the withheld commands so a refused declaration
is never silent.
* test(gateway): type the computer-use fixture against its contract
* fix(computer-use): unblock the macOS live-rig proof flow
The rig ran its operator CLI and its proof runner from one state dir, so both
shared one device identity. A paired operator device is pinned to the scopes of
its first connect, and `nodes list` connects first for `node.pair.list`
(operator.pairing); the proof runner then needs operator.write, which is a scope
upgrade the gateway never approves silently and which no rig client can approve
for itself. The proof runner is a GATEWAY_CLIENT/BACKEND client, so on a
loopback auth-none gateway it is admitted unpaired with the scopes it asks for:
giving the CLI its own `cli-state` identity is enough, and `agent-state` now
never accumulates a pairing row.
`nodes list` also read `node.list` through the plain CLI client while
`nodes status`/`describe` used the diagnostics ladder. On any gateway where the
CLI must pair, the unfiltered list silently dropped connected/commands/
computerUse and `--connected` failed outright, so the documented rig gate could
not confirm the node. Both call sites now use `callNodeDiagnosticsGatewayCli`.
Docs drop the `devices approve <requestId>` instruction, which was circular:
that invocation is its own new device identity.
* test(cli): share the runtime-log formatter across nodes CLI e2e files
The extracted diagnostics-auth file stringified captured log arguments directly, which the type-aware core lint stripe rejects (no-base-to-string). Move the existing formatter into the shared node test helpers instead of duplicating it.
* feat(workers): run device sessions from Gateway bundles
Install the current Gateway bundle before a device environment becomes ready, verify it at attach and tunnel boundaries, launch only from the immutable namespaced bundle directory, and retire stale environments for idempotent reprovisioning. Remove the local execution mode and preserve the node-local build claim only as temporary inventory metadata for the final projection/cleanup slice.
* docs(runners): record Gateway bundle cutover
* test(ci): repair runner validation fixtures
# Conflicts:
# src/scripts/test-projects.test.ts
* fix(workers): surface outdated node recovery
Keep legacy runner inventory diagnostic-only while exposing the update-and-reconnect action through node, environment, provider, placement, and Control UI surfaces.
* fix(workers): reject legacy inventory with recovery
* fix(workers): bundle worker deploy closure
* test(workers): close bundle cutover gates
* fix(workers): compose browser runtime at build
* fix(workers): satisfy bundle cutover gates
* fix(workers): route temp runtime through infra
* docs(workers): align bundle host guidance
* fix(ui): fence outdated session destinations
* fix(cua-computer): prove Linux X11 live vertical
* test(computer-use): authenticate isolated Linux rig
* fix(gateway): refresh computer use after node approval
* refactor(cua-computer): resolve the plugin manifest by static import
* fix(gateway): break plugin runtime import cycle
* fix(computer-use): bind live rig to committed helpers
* feat(cua-computer): add browser action family
* refactor(cua-computer): split browser action files
* refactor(cua-computer): move the shared act-params type to the leaf module
* fix(cua-computer): carry the contract params import with the moved type
* feat(gateway): provision paired node worker installs
* docs: record node local-install PR chain
* chore(protocol): regenerate node worker manifest models
* fix(gateway): resolve node worker build through the device runtime
Repairs a rebase artifact: startup referenced a stale deviceNodeRegistry
binding; the device runtime now owns the connected-node lookup via
resolveWorkerBuild.
Let eligible embedded host runs inspect root-approved unsupported documents after final sandbox, filesystem, provider, owner, and tool-policy gates. Generic ACP, sandboxed, URL-only, and restricted-tool paths retain the plain marker.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Inbound image, audio, and video attachments could vanish with no recorded outcome and no model-visible explanation: attachments dropped by first-only selection, disabled or unconfigured capabilities, stage failures, URL-only images, and ACP-undelivered images all left the model unaware the media existed. Fixes#122044; completes the outcome custody started for documents in #122025.
Media capabilities now record one closed disposition per pre-truncation candidate at the decision site that owns the reason, with a memoized lazy native-vision probe (fires only when a marker could render; failure yields unknown and never alters outcomes; explicit image models never pay a catalog lookup). One late reader renders bounded, actionable markers from recorded facts only: native-vision and ACP-delivered images suppress per verified index, recorded failures always render, and document plus media markers share one five-marker budget with a reason-neutral overflow summary. The shipped SDK decision type stays additive (optional dispositions map).
Proof: 906 + 103 focused tests including nine adversarial-review regressions, ClawSweeper local review clean (round 9, zero findings, security cleared), live Telegram image drive showing ref preservation with no false marker.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Inbound document attachments could vanish silently (DOCX and other Office files, read failures, disabled URL sources). File intake now classifies every attachment into a closed outcome union rendered through one exhaustiveness-checked path: unsupported formats, policy-rejected types, unreadable files, and disabled URL sources all produce bounded model-visible markers (max five per message plus one overflow summary), with strict MIME-token and URL-basename sanitization so attachment metadata cannot inject prompt text or leak signed-URL credentials. Media-stage outcome custody is tracked separately in #122044.
Proof: focused media suites (1855 tests), pre-fix regression, exact-head ci-gate green, ClawSweeper local review clean, and a live Telegram E2E showing the DOCX marker in the model payload.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Use a manifest-first inventory with independent coverage for manifest-only bundled capabilities.
Retire the undocumented thread-ownership plugin while Doctor removes stale references.
Document Talk voice and persist only provider-scoped voice selection.
Closes#121353