Computer Use helper: answer machine/consent and re-check consent at execution
Order: lands first, with manaflow-ai/cmux-cua#82, once its focused fleet suites, a compile and an independent review pass at its head. The acpmux bridge half (#27216) is in feat-cmux-next. #27404 is stacked on it.
Leo's ask this serves: "just have this sort of thing for, I guess, general onboarding of adding remote machines and whatnot and whatever, which includes, I guess, screen sharing potentially."
What
On a machine joined to a cmux team, the person at it grants screen and control locally. acpmux's Computer Use bridge asks the helper v2 for that state before each tools/call. This PR is the helper's half of that contract, plus a second check where the tool actually runs.
The machine/consent method.
- The bridge sends
{id, method: "machine/consent"}on its admitted helper connection. - The helper replies
{id, ok: true, result: {consentRequired, consent: {screen, control}}}, with each grant"none"or"granted".
Where the answer comes from.
- The helper has no daemon connection of its own, so it asks the cmux app over its private control pipe:
{"type":"machine_consent","id":N}. - The app asks its own acpmux daemon (
_acpmux/machine_consent_get), using the daemon socket the app resolved for itself. - The question carries no parameters. Nothing in the helper's environment, endpoint.json or the bridge's request can redirect it, so an alternate
ACPMUX_HOMEchanges nothing. - Nothing is cached.
- The helper reads the answer strictly. Any of the following means "joined, nothing granted":
- no host;
- no reply within 1.5 s (the app's own daemon deadline is 1.2 s, both below the bridge's 2 s);
- a daemon error;
- a malformed result.
The execution boundary. CuaDriverRuntime.invoke checks consent again after the target guard. That check is the last await before the driver runs, so a revoke that lands after the bridge's preflight still refuses the call.
- A refused call executes nothing.
- It returns
{ok: false, code: "consent_required", surface}. - The refusal goes to the app, which records it with the daemon. That uses
_acpmux/machine_consent_refused {surface}, best effort; a daemon without that method ignores it. - The connection stays open and keeps answering through refuse, grant and revoke.
Gating.
tools/listis never gated.- A tool the driver's own inventory marks
readOnlyHint: trueneedsscreen. - Every other tool, including any the inventory doesn't list, needs
screenandcontrol.
Size
About 1,820 changed lines against feat-cmux-next (+1,818/-46): about 900 in the helper and app sources and about 915 in tests, 210 of them the bridge integration test. Since the first review (89c053e), the re-review delta is +1,124/-95, the host secret commit included.
It isn't split because the parts make one consent state machine. The helper's gate, its host consent socket with the per-connection code and secret check, the app's host link that answers it, and removing the stdin answers and stdin call only make the helper safe together. Any one of them merged alone leaves either a helper that trusts its launcher or one that refuses every call. The tests check that same machine end to end against the bridge's wire.
No capture: nothing visible changes. This is the helper's consent protocol and the app's daemon query; no view, string or layout is touched.
Follow-up
A real signed-host test on a fleet Mac (a signed NIGHTLY accepted; a signed helper, an exec chain and an ad hoc build refused) is tracked in manaflow-ai/cmuxterm-hq#5110. The tests here mock the signature step, except for one that shows the real check refuses an unsigned process.
Risk
The helper v2 refuses every tool call until the app's acpmux daemon answers machine_consent_get with consentRequired. That field is added in #27216, so this lands with or after it. The legacy driver (computerUse.driver = "legacy") is not touched here.
Tests
CmuxCuaHelperCoreTests/MachineConsentTests:
- the daemon's answer passes through exactly, and every malformed variant is "no answer";
- observing needs screen, acting needs both, unjoined needs nothing;
- the inventory decides what only observes;
- no host, or a host that never answers, means unavailable;
- the control pipe carries the host's answer, and
ok:falseor an error result fails closed withACPMUX_HOMEpointed elsewhere; - on one admitted socket connection: deny, then grant, then revoke, with alternate-home fields in every request and
ACPMUX_HOMEset to another directory. Each answer follows the host only, refusals keep the connection open, and nothing is cached; - the preflight passes, consent is revoked, then the call:
consent_required, nothing executed, the refusal recorded.
CmuxNextAppTests/ComputerUseHelperConsentTests:
- the app relays the daemon's answer and fails closed on no answer;
- every question goes to the daemon, and fields in the question change nothing.
š¤ Generated with Claude Code
Agent session: agent session
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Adds machine-join consent checking to the Computer Use helper v2: it answers the bridge's machine/consent question and re-checks consent as the last step before every tool call executes, so a revoke that lands after the bridge's preflight still refuses the call.
Consent always comes fresh from the cmux app over a dedicated host consent socket beside the helper socket. Each connection's peer is verified by its audit token as a team-signed cmux main app (7WLXT3NR37; helper ids are excluded) and must then present this launch's host secret as its first line, so another team-signed cmux install of this user cannot take over. The secret travels only in configure on the helper's stdin and never touches disk. Without a host_socket and a 16+ byte host_secret, the helper starts no host server and refuses with host_unverified. The app asks its own acpmux daemon only over a connection verified as this user's acpmux executable, so a same-user process that took over either socket never answers. The helper's stdin carries no consent and runs no tool; its call path is removed. Nothing is cached, a reply counts only on the connection that was asked, and any failure (no host, timeout, daemon error, malformed result) means "joined, nothing granted."
Behavior
- Observing tools (
readOnlyHint: true) needscreen; every other tool needsscreenandcontrol.tools/listis never gated. - A refused call executes nothing and returns
{ok: false, code: "consent_required", surface}; the connection stays open. The app records refusals best-effort via_acpmux/machine_consent_refused. - The socket admission reply advertises the
machine-consent-gate-v1capability. - Applies only to the v2 driver; the legacy driver is unchanged.
- Lands together with the acpmux bridge half (#27216), which adds
machine_consent_getto the daemon.