acpmux, agent pane: a question answered in the pane counts on another machine

Closed
TE
11d ago
+569-34

Answering an agent's question in the Mac app's chat pane failed for a chat on an ssh peer (Leo hit it live on huge-blue): the peer's acpmux refused it with "approve this on the Mac app: allowing a permission is accepted only from the cmux app".

Cause

A chat on another machine gets its answers through a connection that is never the person, so the peer counts an allow only with the person's decision proof (cx-0q7o). That proof never covered question answers: person_decided and respond_permission refused any proof when answers was present. Every AskUserQuestion answered in the pane for a peer chat was refused. The rule dates from the design (9c4543dd), not a recent change.

Fix

  • acpmux: a new proof acpmux-person-decision-answers-v1 covers the answers too: the allow's five parts plus the sha256 of the canonical answers JSON, the same digest a phone's signed answer uses (approvers::answers_digest). An answer counts only under that proof, so a relay still cannot swap the answers; a plain allow keeps the v1 proof. Nonces are still spent once, under the prompt's lock.
  • Mac pane: signRelayedAllow signs a frame's answers with the bound key (AcpmuxSecretKey.decisionProof(..., answers:), canonical JSON byte-for-byte as acpmux's).
  • A shared vector (the same key, inputs and expected proof in a Rust and a Swift test) keeps the two sides' bytes equal.
  • Pane page: an answer now takes the person's gesture ticket for its session, prompt and option, as an option pick does (direct.ts), and the Swift pick no longer skips frames with answers (AgentPaneGestureIntent.swift). Without the ticket the host never signed the answer, so the first fix alone did not reach the peer (the review caught this).

The ticket covers the click on the option, not the answers themselves: the page fills the answers in after the person's click, which is the same trust the local daemon already gives the page. The proof then binds those exact answers, so nothing past the host can change them. answeredBy is not covered by the proof, as with plain allows.

The peer must run an acpmux with this change; an older peer keeps refusing the same way.

Tests

  • cargo test -p acpmux --test person_allow a_relayed_answer_counts_with_the_persons_proof_over_exactly_its_answers and the_answers_decision_proof_matches_the_apps_vector: red on f07876323bba (the test commit; the answer is refused, the vector does not compile), green on the head.
  • Web: cd webviews && bun test src/agent-session/acpmux/questionAnswerTicket.test.ts: red at bf80b3286553 (fleet step ff550a72710e1b8e2ae50508: "a question's answer carries the person's ticket for its session, prompt and option" fails), green at 8028cf33f9ac (0 failed).
  • Swift: AgentPaneRelayedAllowTests anAnswerToAnotherMachinesQuestionCarriesTheProofOverItsAnswers and theAnswersProofMatchesAcpmuxsVector.

Observed / Not run

  • Observed: fleet cmux-ci compile-check --app passed at 8028cf33f9ac9d673e2b2cb93bbd6e29f1f93a44 (job b352dc53c1eaf8fe20455a0d). Independent review: SHIP at 8028cf33f9ac9d673e2b2cb93bbd6e29f1f93a44. scripts/cmux-next/prepush-gate.py passes; rustfmt --check is clean. The Rust vector was computed independently with Python (HMAC-SHA256 over the canonical JSON).
  • Not run on this Mac: the cargo and Swift tests above (no local builds here); they run on CI for this head. Not dogfooded against huge-blue.
  • Reds at this head that are not this PR's: cmux-next crash-class rules (CEFFileDragRelay.swift:162 needs a selector probe), cmux-next web bundles and react-apps-check (inspector/ContextSection.css is missing from build-agent-pane-web.sh), and cmux-next CI guard group (workflow-guard-tests on workflow files). None of those files are in this diff.

No capture: the change is in the proof and the gesture plumbing, and the question card looks the same.

Changelog

Fixed: Answering an agent's question in a chat on another machine no longer fails with "accepted only from the cmux app".

risk:high: this changes what the person's decision proof covers (auth).

Agent session: cosmic-gray

šŸ¤– Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes answering an agent's question in the Mac app's chat pane for a chat on another machine, which the peer's acpmux always refused with "accepted only from the cmux app".

A relayed allow's decision proof never covered question answers. The pane now signs an answer with acpmux-person-decision-answers-v1 — the allow's five parts plus the sha256 of the canonical answers JSON — and acpmux counts an answer only under that proof, so a relay cannot swap the answers. An answer also takes the person's gesture ticket for its session, prompt and option, as an option pick does.

The answer's answers now reach that machine's acpmux: RemoteAcpmuxWire and agent-session-permission previously dropped or refused them, so the peer checked the answers proof against a plain allow and refused the answer. The daemon passes a JSON object of at most 64 KiB to acpmux unchanged.

The peer must run an acpmux with this change; an older peer still refuses the answer.