Skip to content

acpmux: a peer counts an enrolled Mac's signed group Allow once (peer-route approvals 3/5) - #26198

Merged
teamleaderleo merged 2 commits into
feat-cmux-nextfrom
lane/hb-peer-approvals-groups
Oct 27, 2026
Merged

teamleaderleo merged 2 commits into
feat-cmux-nextfrom
lane/hb-peer-approvals-groups

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Peer-route approvals, part 3 of 5: a peer counts an enrolled Mac's signed Allow once on a group of tool prompts. Parts 1 (#26131) and 2 (#26133) have landed; this PR is on feat-cmux-next.

The card answers tool prompts (execute, edit, read, …) in batches through _acpmux/permission_group_respond, not through permission_respond. Part 1 only covered the single-prompt answer. A peer refused every group allow that did not come from its own person. This Mac's daemon also marked every forwarded group read allowRefused, so a peer-route chat's card showed only Deny ("Only the cmux app can approve this"). A live capture on a fleet-built app confirmed it.

  • The peer (B), gate (hub/person.rs):
    • A non-person allow_once group answer passes only with _meta.acpmux.approverDecisions. That must hold, for every pending member of the group at the request's revision, {decisionNonce, approverDecision}: the member's own part-1 signature over its single allow_once option, with no answers.
    • A missing member is refused, and so is an entry for anything else, a wrong nonce, a stale revision, or any signature that does not count.
    • Deny passes as before.
  • Only an enrolled Mac's signature counts for a group member, never a phone approver's (PersonGate::macs_prove/macs_spend). A Mac's message binds the member's tool-call id, and its may_allow holds every member to a plain tool ask.
  • The peer, answer (hub/permission_groups.rs respond_permission_group): under the session's permission lock and one hold of the Mac set's lock (MacApprovers::all_verify_then), it verifies every member's proof and spends every nonce together (PersonGate::spend_decisions, all or none) before any member is answered. Anything else gets permission.person_required, nothing is answered and no nonce is spent.
  • The peer, read: a non-person _acpmux/permission_groups read says macAllowOnce: true beside allowRefused.
  • Protocol: AnswerMeta declares approverDecision and approverDecisions (ApproverDecision, MemberApproverDecision), and PermissionGroupList declares macAllowOnce. The IR, spec and generated web types are regenerated.
  • This Mac's daemon (A), forward (server/peer_forward.rs):
    • A forwarded group read drops allowRefused and allow_chat only when all three hold: the reader is A's own person, A holds this Mac's approver key (thisMac, set by the app), and the peer says macAllowOnce. The card then draws Allow once and Deny, never Allow for this chat.
    • Every other reader still gets allowRefused.
    • A refused group answer names the peer (data.peer, part 2), so the app can say where to enroll.

The five conditions

  1. Bound, short, single use. Only a Mac's signature counts. Each member's signature binds the peer's instance, the Mac id, the session, that member's permission id and tool-call id, its allow_once option, its decision nonce and an expiry of at most 300 s (part 1's message). Each entry is matched to its member by permission id and nonce, so one member's proof cannot answer another member, another group or another revision. Every nonce is spent before the answer, and a replay after it is refused.
  2. One-time only, no mode change. Only allow_once counts. allow_chat from a non-person is still refused, and A never draws it for a peer-route chat. Every member needs a single allow_once option, and part 1's may_allow refuses plans, mode switches, folder trust and questions. Groups only ever hold plain tool asks (eligible).
  3. Unsigned refused, Deny counts. An unsigned or partly signed group allow is refused with person_required, and Deny from any client passes as before.
  4. Unenrolling stops Allows at once. All members' proofs are checked, and all nonces spent, under one hold of the Mac set's lock. A host approvals rm/unenroll reload takes that lock, so it lands before the whole answer (nothing spent, nothing answered, every member still answerable) or after it, never between two members. The integration test removes the Mac and shows the next allow refused.
  5. No new signing path. The daemon signs nothing; only the app holds the key (parts 4 and 5). A's relaxation applies only to A's own person's read.

Tests (they fail before the fix and pass after)

They ran remotely through cmux-ci test.

Before (the gate arm, the forward condition and the proof match neutralised):

test server::peer_forward::tests::a_peers_group_read_draws_allow_once_only_for_a_mac_that_signs ... FAILED
test hub::permission_groups::approver_proofs_tests::every_pending_member_needs_its_own_entry_and_nothing_else ... FAILED
test a_peer_counts_an_enrolled_macs_signed_group_allow_for_every_pending_member ... FAILED

Before the review fix (a phone's proof accepted per member; members verified and spent one at a time):

test hub::mac_approvers::tests::a_groups_allow_takes_only_macs_and_spends_every_nonce_or_none ... FAILED
test hub::mac_approvers::tests::a_reload_during_a_groups_commit_waits_for_the_whole_answer ... FAILED

After (head of this PR):

test hub::mac_approvers::tests::a_groups_allow_takes_only_macs_and_spends_every_nonce_or_none ... ok
test hub::mac_approvers::tests::a_reload_during_a_groups_commit_waits_for_the_whole_answer ... ok
lib (mac_approvers permission_groups person peer_forward protocol host_approvals): 54 passed; 0 failed
--test peer_integration: 50 passed (a_peer_counts_an_enrolled_macs_signed_group_allow_for_every_pending_member ... ok)
--test person_allow: 25 passed; --test hub_integration permission: 31 passed
protocol_ir_is_current ok; webviews acpmux-protocol:check current; cargo fmt --check and clippy -D warnings clean

The integration test uses a 3-member group on two hubs. Each of these is refused with permission.person_required and data.peer, and the group stays pending at its revision: unsigned, a missing member, a wrong nonce, a stale revision, an expired signature, the other option, a Mac that is not enrolled, an extra entry, allow_chat, and a Mac unenrolled by a reload. A fully signed allow resolves all 3 members, and a replay is refused.

The webviews methods.ts drift (_acpmux/file_put_status / file_put_data missing) is on feat-cmux-next already; this PR regenerates only types.ts and changes no method.

No capture: daemon only. Part 5 carries the card's before and after captures from a fleet-built app of the whole stack.

Overlap (files only, not the same fix): #22030, #24797, #26127, #26093 and #25697 touch requests.rs/peer_forward.rs.

Agent session: agent session

🤖 Generated with Claude Code

…-route approvals 3/5)

The card answers tool asks as a group (_acpmux/permission_group_respond),
which a peer refused for every caller that is not its person, and the Mac's
daemon marked every forwarded group read allowRefused, so a --host chat drew
no Allow at all.

- B, the gate (hub/person.rs): allow_once from a non-person counts when
  _meta.acpmux.approverDecisions holds, for EVERY pending member of the group
  at the request's revision, {decisionNonce, approverDecision}: the member's
  single-prompt Mac signature over its one-time allow option. A missing
  member, an entry for anything else, a wrong nonce, a stale revision or one
  signature that does not count refuses it all; allow_chat stays the
  person's.
- B, the answer (respond_permission_group, new `person` parameter): under the
  prompt state's lock, before any member is answered, every signature is
  verified, then every nonce spent (approver_spends; same lock order as a
  single answer).
- B, the read: a non-person reader gets macAllowOnce: true beside
  allowRefused (protocol IR, spec and web types).
- A, the forward: the person here whose app gave this Mac's key reads a peer
  group that says macAllowOnce with no allowRefused and no allow_chat; a
  refused group answer names the peer (data.peer).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review d449805: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr26198 -f ref=lane/hb-peer-approvals-groups -f platform=macos -f backend_mode=local

Unclear: web / react-apps-check failed: no known signature; failed step: Check webview test shards.

CI failed on d44980564b (run 40518031710 attempt 1): 4 code, 1 unknown.

Job Verdict Why
web / react-apps-base-check (4) code a web test failed
web / react-apps-base-check (3) code a web test failed
web / react-apps-base-check (2) code a web test failed
web / react-apps-base-check (1) code a web test failed
web / react-apps-check unknown no known signature; failed step: Check webview test shards

Failures:

  • seen on other PRs agent-pane-mermaid.test.tsx a source both renderers refuse shows the source and why: failed (job)
  • seen on other PRs agent-pane-peer-chat.test.ts a chat on an SSH machine > counts its running turn from the prompt and offers no machine switch mid-...: failed (job)
  • seen on other PRs agent-pane-queue-row-click.test.ts queued row with a chip's menu open (chromium) > one press on more closes the model menu and does wha...: failed (job)
  • seen on other PRs agent-pane-queue-row-click.test.ts queued row with a chip's menu open (webkit) > one press on more closes the model menu and does what ...: failed (job)
  • seen on other PRs agent-pane-step-rows.test.ts every step is one 26px row on one rhythm, across transcript rows: failed (job)
  • seen on other PRs agent-pane-turn-tokens-muted.test.tsx the live line at the bottom (Thinking.tsx): the count is its own span, which the quieter rule keys o...: failed (job)
  • seen on other PRs agent-pane-turn-tokens-muted.test.tsx the line over the turn (WorkingFor.tsx): the count is its own span, which the quieter rule keys on: failed (job)
  • seen on other PRs changelog-pictures.test.tsx a change's picture opens enlarged on a click, and Close closes it: failed (job)
  • seen on other PRs agent-pane-background-tasks.test.ts the summary card lists the chat's background tasks: running ones first, the finished folded: failed (job)
  • seen on other PRs agent-pane-hairline-focus.test.ts composer at 1x: focus is the resting hairline a step brighter, no ring: failed (job)
  • ... 22 more in the job logs
Matched log lines
web / react-apps-base-check (4): (fail) a source both renderers refuse shows the source and why [5010.38ms]
web / react-apps-base-check (3): (fail) the summary card lists the chat's background tasks: running ones first, the finished folded [959.30ms]
web / react-apps-base-check (2): (fail) only page.css imports the page's sheets [23.07ms]
web / react-apps-base-check (1): (fail) no orange or system accent and no opaque glass on 2303 gallery stages [529734.95ms]

Not re-run automatically: web / react-apps-base-check (4), web / react-apps-base-check (3), web / react-apps-base-check (2), web / react-apps-base-check (1), web / react-apps-check are not machine failures.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

red: cmux-next checks (god files, concurrency, crash safety, l10n); no finished base run on 2457dc6 to compare; do not merge until green; see lane-rules
red: web / Web status; NEW: green on the base 2457dc6; do not merge until green; see lane-rules
red: web / react-apps-base-check (1); no finished base run on 2457dc6 to compare; do not merge until green; see lane-rules
red: web / react-apps-base-check (2); no finished base run on 2457dc6 to compare; do not merge until green; see lane-rules
red: web / react-apps-base-check (3); no finished base run on 2457dc6 to compare; do not merge until green; see lane-rules
red: web / react-apps-base-check (4); no finished base run on 2457dc6 to compare; do not merge until green; see lane-rules
red: web / react-apps-check; no finished base run on 2457dc6 to compare; do not merge until green; see lane-rules

Gallery before/after for d449805: 16 states rendered, 6 images differ, 2063 more touched states not rendered. Images are in the run's artifact, not on this public PR.

gh run download 40518031234 -R manaflow-ai/cmux -n gallery-advisory-26198-d44980564b9be1912d06fb5d2459fdd43a16921a (then read gallery-review.json)

States
State Theme Status Diff Picked by
agent-pane.chat-inspector/subagents-done Apple System Colors Light unchanged 0% imports types.ts (2 hops)
agent-pane.chat-inspector/subagents-done Apple System Colors unchanged 0% imports types.ts (2 hops)
agent-pane.chat-inspector/full Apple System Colors changed 0.0422% imports types.ts (2 hops)
agent-pane.chat-inspector/full Apple System Colors Light unchanged 0% imports types.ts (2 hops)
agent-pane.chat-inspector/workflow-running Apple System Colors Light changed 0.0049% imports types.ts (2 hops)
agent-pane.chat-inspector/workflow-running Apple System Colors changed 0.0041% imports types.ts (2 hops)
agent-pane.chat-inspector/workflow-finished Apple System Colors Light unchanged 0% imports types.ts (2 hops)
agent-pane.chat-inspector/workflow-finished Apple System Colors unchanged 0% imports types.ts (2 hops)
agent-pane.chat-inspector/layout-glance Apple System Colors changed 0.0423% imports types.ts (2 hops)
agent-pane.chat-inspector/layout-glance Apple System Colors Light unchanged 0% imports types.ts (2 hops)
agent-pane.chat-inspector/layout-glance-open Apple System Colors Light unchanged 0% imports types.ts (2 hops)
agent-pane.chat-inspector/layout-glance-open Apple System Colors unchanged 0% imports types.ts (2 hops)
agent-pane.chat-inspector/layout-rules Apple System Colors Light changed 0.0593% imports types.ts (2 hops)
agent-pane.chat-inspector/layout-rules Apple System Colors unchanged 0% imports types.ts (2 hops)
agent-pane.chat-inspector/layout-groups Apple System Colors changed 0.0445% imports types.ts (2 hops)
agent-pane.chat-inspector/layout-groups Apple System Colors Light unchanged 0% imports types.ts (2 hops)
agent-pane.chat-width/standard Apple System Colors Light unchanged 0% imports types.ts (2 hops)
agent-pane.chat-width/standard Apple System Colors unchanged 0% imports types.ts (2 hops)
agent-pane.chat-width/wide Apple System Colors Light unchanged 0% imports types.ts (2 hops)
agent-pane.chat-width/wide Apple System Colors unchanged 0% imports types.ts (2 hops)
agent-pane.chat-width/wide-blocks Apple System Colors Light unchanged 0% imports types.ts (2 hops)
agent-pane.chat-width/wide-blocks Apple System Colors unchanged 0% imports types.ts (2 hops)
agent-pane.composer-floats/scrolled-to-end Apple System Colors Light unchanged 0% imports types.ts (2 hops)
agent-pane.composer-floats/scrolled-to-end Apple System Colors unchanged 0% imports types.ts (2 hops)
agent-pane.composer-floats/composer-grown Apple System Colors Light unchanged 0% imports types.ts (2 hops)
agent-pane.composer-floats/composer-grown Apple System Colors unchanged 0% imports types.ts (2 hops)
agent-pane.docked-chat/new-chat Apple System Colors Light unchanged 0% imports types.ts (2 hops)
agent-pane.docked-chat/new-chat Apple System Colors unchanged 0% imports types.ts (2 hops)
agent-pane.docked-chat/after-turn Apple System Colors Light unchanged 0% imports types.ts (2 hops)
agent-pane.docked-chat/after-turn Apple System Colors unchanged 0% imports types.ts (2 hops)
agent-pane.docked-chat/working Apple System Colors Light unchanged 0% imports types.ts (2 hops)
agent-pane.docked-chat/working Apple System Colors unchanged 0% imports types.ts (2 hops)

Pick other states with a Gallery: entry.id, entry.id/variant line in the PR body.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-iu3059 created for this PR by the cmux coordinator.

@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 27, 2026
@blacksmith-sh

blacksmith-sh Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Found 184 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmp5f9_gadz/summary-missing.json (live):/summary-missing.json (live): View Logs
/tmp/tmp9fed49pj/summary-fail.json (live):/summary-fail.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs
nightly.yml must derive the embedded key and sign every appcast through the helper/
nightly.yml must derive the embedded key and sign every appcast through the helper
View Logs
src/pages/settings/layout.test.ts/layout.test View Logs
src/pages/settings/layout.test.ts/layout.test View Logs
test/accent-glass-invariants.test.ts/accent-glass-invariants.test View Logs

...and 174 more test failures. View all on Blacksmith: run 40518031710, run 40518031828

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

… every nonce at once

Review of #26198: the group path used the generic approver check, which also
took a phone's signature (no tool call in its message, no may_allow), and it
spent members' nonces one at a time, so a revocation between two could burn
earlier nonces with nothing answered.

- PersonGate::macs_prove / macs_spend: only an enrolled Mac's signature
  counts for a group member; every member verified and every nonce spent
  together under one hold of the Mac set's lock (MacApprovers::
  all_verify_then, PersonGate::spend_decisions), so a reload lands before
  or after the whole answer and a refusal spends nothing.
- protocol: AnswerMeta declares approverDecision and approverDecisions
  (ApproverDecision, MemberApproverDecision); spec and web types
  regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
@github-actions

Copy link
Copy Markdown
Contributor

Shipped in:

This branch was previously deployed

3 inactive deployments
artifacts — d4498056 Deployed Oct 27, 2026 by teamleaderleo via publish the Windows daemon to R2 #65148
sentry-dsyms — d4498056 Deployed Oct 27, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #65148
ci-routing-agent — d4498056 Deployed Oct 27, 2026 by teamleaderleo via wait for same-key tree publisher #65148
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant