cmux NEXT: the pane may answer a session offer (offer_respond, scoped)

Merged
TE
22d ago
+162-7

Summary

The agent pane page sends _acpmux/offer_respond since #20337: Dismiss on an offer chip sends {sessionId, offerId, outcome: "dismissed"}. Start sends {sessionId, offerId, outcome: "accepted", childSessionId} right after the page's own session/new for the child, then a session/prompt to the child. The host allowlist did not have the method, so the host refused both, and cmux-agent-pane-policy/tests/typescript.rs::the_allowlist_is_what_the_page_sends was red on base.

This PR lets the method through, scoped to the pane's own sessions, in Swift (AcpmuxPaneMethods, +Params, AgentPaneTransport+Check) and in its Rust mirror (policy.json, data.rs, params.rs):

  • It is in requests and sessionScoped / session_scoped. sessionId, the chat that holds the offer, must be in the pane's scope.
  • New per-method optional scoped-params map optionalScopedSessionParams / optional_scoped_session_params, with "_acpmux/offer_respond": ["childSessionId"]. An absent param passes. A present one must be a string the pane holds; null or a non-string is refused. This is needed because Dismiss names no child, and the existing scopedSessionParams requires its params to be present.
  • Params allowlist: sessionId, offerId, outcome, childSessionId. No _meta, because no gesture ticket is involved.
  • No gesture rule. Dismiss declines. Start only links the page's own new chat, and the work it begins is its session/prompt, which already needs a gesture.

Security

  • Local command/content execution: none. The frame only asks acpmux to record the answer to an offer and, on accept, to link a child chat the pane already started. It carries no command, path, prompt text or environment, and unknown params are refused. The agent turn that follows is a separate session/prompt, which keeps its gesture rule.
  • Access to unowned objects: both session ids are pane-scoped. sessionId goes through sessionScoped. childSessionId, when present, must also be a session the pane started or shows, so the page cannot link a session it does not hold. offerId is checked by the daemon against that chat (#20305). Agents cannot call the method: the daemon accepts it only from the person (#20305's person gate).
  • The optional-param rule, and why _acpmux/side stays strict: the optional map exists only because a dismiss has no child. A named child is held to the same rule as sessionId, and an explicit null is refused, not read as absent. _acpmux/side always names its chat, so it stays in the strict scopedSessionParams map. A side frame without chat is still refused by the host, and this PR does not loosen it.
  • Local-state exposure: the reply is the daemon's ack and nothing else. No replyShapes entry is added: only _acpmux/status has one, and no write method of this kind does.

Testing

Shared parity cases (tests/cases/sessions.json, tests/cases/params.json) were committed first, before the fix:

  • accepted, with the chat and the child in scope: passes
  • child out of scope: refused
  • chat out of scope: refused
  • dismissed with no child: passes
  • null child: refused
  • unknown param: refused

Hosted focused verification (scripts/verify-cmux-tui-hosted.sh):

Depends on #20305 for the daemon handler; merge after it.

Changelog

none

🤖 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

Lets the agent pane's host allowlist accept _acpmux/offer_respond, which the page sends when a session offer chip is dismissed or accepted. The host previously refused the message because it was not allowlisted, so both Dismiss and Start failed.

Scoping rules

  • The method is session-scoped on sessionId; the chat holding the offer must be the pane's.
  • A new optional scoped-params map covers childSessionId (Start only): an absent param passes, a present one must be a pane-held string, and null is refused.
  • _acpmux/side stays strict; its chat param is always required.
  • No gesture rule: Dismiss declines, and Start only links the page's own new chat, whose session/prompt already requires a gesture.