agent-pane-policy: the gesture pick is the Swift host's: hand-off prompt ids, answer digests, Double numbers

LA
4d ago
+639-23

A gesture ticket is bound to a pick: the method and the params it was taken for. The Swift agent pane host builds that pick three ways that the crate does not, so a host that runs the crate would never redeem these tickets:

  • Hand-off prompts: a session/prompt picks its own id without a hand-off turn's handoff-<turn>- label (AgentPaneHandoffPrompt.ownID, 07efcedc1bb). The crate used the raw id, so the ticket of a Cloud hand-off prompt never redeemed.
  • Question answers: a permission answer's answers and annotations are picked by the sha256 of their canonical JSON (AcpmuxSecretKey.canonicalDigest, 8c3e8297749). The crate picked nothing for a frame with answers, so the ticket of a question answer never redeemed.
  • Numbers: a number param is held as a Double, so 1 and 1.0 give the same pick.

What changes in the crate:

  • src/canonical.rs ports canonicalJSON/canonicalDigest. These are the same bytes as acpmux's approvers::canonical_json: keys in UTF-8 byte order, no whitespace, only ", \ and control characters escaped, and an integral float below 1e15 written as an integer. A value with no canonical form has no digest, and its frame picks nothing.
  • GesturePick::new (src/check.rs) follows Swift AgentPaneGesturePick: own_prompt_id, the digests, a null answers is no key, and as_double.
  • The test is tests/gesture_pick.rs, with 24 cases in tests/cases/picks.json. The expected values come from a line-for-line Python copy of the Swift code. That copy asserts Swift's own canonical JSON vectors (AgentPaneRelayedAllowTests) and acpmux's fixed digest (the_wire_payload_is_fixed, 44ba30ad…), so the digests match Swift byte for byte.
  • One tests/cases/check.json case now expects the answers digest, as Swift gives it, not "no pick". I renamed it to say so.
  • sha2 is the workspace's 0.10.9. cmux-tasks-core already brings it into the app FFI lock, so no new package goes into the app. cmux-tui/Cargo.lock gains one dependency line.

This is the last of the Swift-only drifts that would break features if the app called the crate (rm-pane-policy, layer-ownership V-23). The app does not call the crate yet, and cmux-app-ffi does not link it, so nothing changes in the app and this PR is no FFI input.

Security

A ticket binds a question answer to exactly those answers and annotations, by digest, as in the running Swift host. Before, Rust picked nothing for such a frame, so the ticket could not redeem it at all. No method or param is newly allowed, and the default stays deny.

Changelog

none

Observed / Not run

Observed (fleet, Linux, scripts/ci/cmux-tui-rust-check.sh):

Known differences, all fail closed (a ticket does not redeem; none reachable through check_frame today, because the answers rule refuses numbers in answers): -0 in answers digests as -0 here and 0 in Swift; acpmux's canonical_json writes 2.0 where Swift and this crate write 2; a hand-off id handoff-12- followed by a combining mark loses its label here and not in Swift (Character vs scalar split).

Not run: no Swift compile (no Swift change).