Skip to content

cmux-next: a peer-route chat's card says "Enroll this Mac on HOST" and enrolls it (peer-route approvals 5/5) - #26202

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

teamleaderleo merged 6 commits into
feat-cmux-nextfrom
lane/hb-peer-approvals-hint

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Peer-route approvals, part 5 of 5: a peer-route chat's card says "Enroll this Mac on HOST to allow from here", and its button enrolls this Mac. This replaces "Remote approvals are off", which gave no way forward. Parts 1 to 4 (#26131, #26133, #26198, #26201) have landed; this PR is on feat-cmux-next.

Leo (2026-10-27): "Why wouldn't you just split it up? ... get the proper reasonable features in. Period. You know? Good, reasonable UI, good reasonable features."

  • A peer refuses an Allow because it has not enrolled this Mac (permission.person_required with data.peer, parts 2 and 3). The pane then shows "Enroll this Mac on HOST to allow from here", a line that says how it works, and an Enroll this Mac button. HOST is the local daemon's name for that peer.
  • The button runs the app's own cmux-tui acp host approvals enroll HOST --json (part 2) against the daemon this pane talks to (its ACPMUX_HOME/ACPMUX_SOCKET, recorded when the app registered its key there), over the person's ssh environment, the same one Connect to Machine uses.
    • On ok or fixed the notice goes away, and the next Allow goes through.
    • Otherwise the notice shows "Could not enroll this Mac on HOST: …" with the command's own detail and fix (for example acpmux host update HOST for a host too old).
  • An always-allow, Allow for this chat, or an answer with notes on a peer-route prompt shows "Only a one-time Allow works from this Mac", not the generic off notice, because the peer would not count anything else (condition 2).
  • Only a refusal of an Allow that went out signed offers Enroll. Each permission answer records, by its page request id, whether it carried this Mac's signature, right before it goes (after the request id is admitted); its reply is read against that and retires it, error or result. An Allow that went out unsigned (no key, a prompt that changed while signing, a pick without its ticket) is refused without a hint, and replies that come back out of order name the right peer.
  • The Enroll button runs against the daemon the pane talks to when it is clicked; a reconnect while it runs does not move it, and a pane with no daemon does nothing.
  • A peer that counts an answer takes the notice away; a late reply of the connection before a reconnect says nothing. A new reason replaces the notice, and closing the pane removes it.
  • docs/remote-agents-and-cloud.md replaces the "Peer-route approvals are in progress" bullet with what works now, how to enroll and unenroll, and the DEV-build caveat.
  • New strings: 5 keys, en and ja translated, the other locales marked needs_review. They are most of this PR's line count (Localizable.xcstrings, +655); the code and tests are about 500 lines.

The five conditions

This PR changes only what the card says and how the Mac enrolls. What it signs and what the peer accepts are unchanged from parts 1 to 4.

  1. Bound, short, single use: unchanged (part 4 signs, parts 1 and 3 verify).
  2. One-time only, no mode change: the app still signs only allow_once, and now says so on the card.
  3. An unsigned Allow is refused, and a Deny counts: unchanged. The hint shows only after the peer's refusal.
  4. Unenrolling stops Allows at once: the docs name host approvals unenroll and rm. After an unenroll the next Allow is refused, and the hint comes back.
  5. The key stays where no other agent can sign: Enroll sends only the public key, and the private key stays in the Secure Enclave.

Tests (they fail before the fix and pass after)

Swift suites ran on the build fleet (scripts/ci/package-test-lane.sh).

  • AgentPaneRelayedAllowTests.aPeersRefusalOffersToEnrollThisMacThere checks that a peer's refusal gives .notEnrolled(peer: "far"), not the generic off notice.
  • anAllowForAPeersPromptCarriesThisMacsSignedOneTimeAllow checks that an always-allow gives .onceOnly, and aGroupAllowForAPeersPromptsSignsEachMember that Allow for this chat does too.
  • anUnsignedAllowsRefusalOffersNoEnroll, eachRefusalIsReadAgainstItsOwnRequest and anAnswerRefusedBeforeItGoesLeavesTheSignedOnesHint check that only a signed Allow's refusal offers Enroll: not an unsigned one's, not after an unsigned answer failed some other way, not out of order, and not when a duplicate page id is refused before it goes.
  • AgentPanePeerApprovalsNoticeTests (new): a new reason replaces the notice and a counted answer removes it; Enroll hides the notice once enrolled and says what failed otherwise, does nothing without a daemon, and keeps the daemon it was clicked on through a reconnect; Enroll goes to the daemon the pane talks to; closing the pane takes the notice away; a late reply of the connection before a reconnect says nothing.

Before (this PR's tests on the code before this PR):

✘ Test anAllowForAPeersPromptCarriesThisMacsSignedOneTimeAllow() recorded an issue at AgentPaneRelayedAllowTests.swift:407:9: Expectation failed: (peerOff → []) == ([.onceOnly] ...)
✘ Test aPeersRefusalOffersToEnrollThisMacThere() recorded an issue at AgentPaneRelayedAllowTests.swift:437:9: Expectation failed: (peerOff → []) == ([.notEnrolled(peer: "far")] ...)

Review fixes, each test red on the code before its fix:

✘ Test anUnsignedAllowsRefusalOffersNoEnroll() ... Expectation failed: (peerOff → [.notEnrolled(peer: "far")]).isEmpty → false
✘ Test eachRefusalIsReadAgainstItsOwnRequest() ... (peerOff → [.onceOnly]) == ([.onceOnly, .notEnrolled(peer: "far")])
✘ Test eachRefusalIsReadAgainstItsOwnRequest() ... (peerOff → [.onceOnly, .notEnrolled(peer: "near"), ...])
✘ Test anAnswerRefusedBeforeItGoesLeavesTheSignedOnesHint() ... (peerOff → []) == ([.notEnrolled(peer: "far")])
✘ Test enrollHidesTheNoticeOnceEnrolledAndSaysWhatFailedOtherwise() ... (asked → ["far"]).isEmpty → false
✘ Test aLateReplyFromTheConnectionBeforeSaysNothing() ... (decisions.takeAnswers() → [.counted, .counted]).isEmpty → false

After (head bd02c5f, the same commits as this head before the rebase onto feat-cmux-next):

✔ Test run with 3 tests in 1 suite passed (AcpmuxMacApproverTests)
✔ Test run with 12 tests in 1 suite passed (AgentPaneRelayedAllowTests)
✔ Test run with 5 tests in 1 suite passed (AgentPanePeerApprovalsNoticeTests)

cmux-ci compile-check --app at this head (0c56334): passed (86 s).

Captures

Fleet-built app of this stack at 9492ba8 (cmux-ci build; the review fixes after it change which refusals show the hint, not how it draws), two acpmux daemons on a capture mini: this Mac's daemon A and a peer B named devbox; the chat devbox/demo runs on B, opened through A. Native clicks on the card.

Before (base): "Only the cmux app can approve this", Deny only After Allow, B not enrolled: the hint and its button
peer approvals before base peer approvals after not enrolled
Enroll this Mac clicked: the command ran and says why it failed Enrolled, Allow clicked: counted on B, "chose yes"
peer approvals after enroll click peer approvals after allowed

On the capture mini B is a second daemon on the same machine reached at 127.0.0.1, so devbox is no ssh host and the button's host approvals enroll devbox fails at ssh, as shown (on a real peer it runs over the person's ssh). The capture then enrolled this Mac on B by hand (host approvals add with A's thisMac key) and clicked Allow again; B's pending list was empty afterwards and its turn ended with permission_decision yes. The page's own "Only the cmux app can approve this…" line still shows under the card beside the native notice; that line is the page's, unchanged here.

No capture: cmux-ci capture drives one acpmux daemon, and a peer-route card needs two (this Mac's and the peer's), so it cannot draw this scene; its tour:agent-pane run at this head was refused by the privacy check (the tour shows the capture mini's name). The captures above come from a hand-driven run of a fleet build on a capture mini, as described.

Takes back #26201: on purpose. #26201's flush told the pane "remote approvals are off" on any peer refusal; this PR reads each answer's reply against its own request (takeAnswers), so a signed Allow's refusal shows "Enroll this Mac on HOST" and a counted answer clears it.

Review (Codex gpt-6-astra xhigh): SHIP at 0c56334

Agent session: agent session

🤖 Generated with Claude Code

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

@cmux-bot

cmux-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

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

Review 0c56334: 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=pr26202 -f ref=lane/hb-peer-approvals-hint -f platform=macos -f backend_mode=local

Passes: CI passes on 0c56334143.

CI passes on 0c56334143 (run 40543445267 attempt 1).

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.

@teamleaderleo
teamleaderleo force-pushed the lane/hb-peer-approvals-app branch from f7ae4ce to 74c19a8 Compare October 27, 2026 15:11
@teamleaderleo
teamleaderleo force-pushed the lane/hb-peer-approvals-hint branch from 46b435c to 3976a04 Compare October 27, 2026 15:11
Base automatically changed from lane/hb-peer-approvals-app to feat-cmux-next October 27, 2026 16:41
teamleaderleo and others added 3 commits October 27, 2026 09:41
…d enrolls it (peer-route approvals 5/5)

A peer of this Mac's acpmux that has not enrolled this Mac refuses its signed
one-time Allow and names itself (acpmux `data.peer`). The pane's notice now
says "Enroll this Mac on HOST to allow from here" instead of "Remote approvals
are off on the other machine", with an Enroll this Mac button that runs the
app's own `cmux-tui acp host approvals enroll HOST` (the public key goes to
that machine over the person's ssh). Once it is enrolled the notice goes away
and Allow on the same prompt counts. An always-allow on a peer's prompt says
only a one-time Allow works from this Mac.

docs/remote-agents-and-cloud.md: the peer-route bullet says how it works now.

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

Review of part 5: the pane counted unsigned peer allows in one counter and
read refusals in arrival order, so an Allow that went out unsigned because
signing failed offered a useless Enroll, an unsigned answer that failed some
other way left the counter stale (hiding a later signed Allow's hint), and
replies out of order could name the wrong peer.

Each permission answer now records, by its page request id, whether it went
out with this Mac's signed allow (AcpmuxDecisionContexts.sent); its reply is
read against that and retires it whatever it says. Only a signed Allow's
person_required refusal shows "Enroll this Mac on HOST". The counter is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
teamleaderleo and others added 3 commits October 27, 2026 09:41
… out-of-order reply apart

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
…nroll keeps the daemon it was clicked on

Review of part 5: the proof state was noted before the request id was
admitted, so a second answer under an in-flight page id overwrote the first's,
and a failed send left an entry behind; the Enroll task read the pane's socket
when it ran, so a reconnect could enroll through another daemon, and a pane
with no daemon passed an empty environment.

- AcpmuxDecisionContexts.sent is called after ids.begin and every local
  check, right before the frame goes; a failed send retires it (notSent).
- Enroll snapshots the daemon environment on the click and does nothing
  without a daemon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
…eer notice; docs say which Allow counts

Review of part 5: a permission reply that matched no answer this connection
sent (a late one of the connection before a reconnect) was still read as
counted and could take the new notice away; now only a reply to an answer
recorded since the last reset is read. The docs said the card offers no
Allow for this chat; a single prompt's card may still offer an always-allow,
which this Mac does not sign.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
@teamleaderleo
teamleaderleo force-pushed the lane/hb-peer-approvals-hint branch from bd02c5f to 0c56334 Compare October 27, 2026 16:41
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

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

@github-actions github-actions Bot added the risk:high 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 2 test failures on Blacksmith runners:

Failures

Test 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
test_the_repository_highlights_validate (main.DigestTests.test_the_repository_highl
ights_validate)/
test_the_repository_highlights_validate (main.DigestTests.test_the_repository_highl
ights_validate)
View Logs

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

@teamleaderleo
teamleaderleo marked this pull request as ready for review October 27, 2026 18:05
@teamleaderleo
teamleaderleo merged commit 221e6b4 into feat-cmux-next Oct 27, 2026
235 of 249 checks passed
@teamleaderleo
teamleaderleo deleted the lane/hb-peer-approvals-hint branch October 27, 2026 18:05
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

This branch had an error being deployed

1 failed and 2 inactive deployments
ci-routing-agent — 0c563341 Deployed Oct 27, 2026 by teamleaderleo via namespace autofix #4659
artifacts — 0c563341 Deployed Oct 27, 2026 by teamleaderleo via add the Windows daemon to the tree #65971
sentry-dsyms — 0c563341 Deployed Oct 27, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #65971
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant