Skip to content

acpmux: the provisioning exec keys the running daemon; one owner on Cloud machines (M3) - #36564

Merged
teamleaderleo merged 4 commits into
feat-cmux-nextfrom
lane/acpmux-one-owner
Nov 5, 2026
Merged

teamleaderleo merged 4 commits into
feat-cmux-nextfrom
lane/acpmux-one-owner

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

What

The Cloud VM provisioning exec now gives the person key to the acpmux daemon that is already running (acpmux.service), over its unix socket. Before, it stopped the unit and the daemon and then started a keyed daemon of its own. That took two guest execs, and the unit (Restart=always, daemon run --take-over) then competed with the keyed daemon for the socket. Now there is one owner and no take-over, and agents keep running.

This is M3 in cmuxterm-hq plans/cloud/ARCHITECTURE-2026-11.md ("Session host and keyed acpmux up": no prepare exec). The platform side is manaflow-ai/cmux-platform (linked below).

  • New hidden acpmux daemon enroll --person-key-fd FD, for the root provisioning exec only. It reads one key line, connects to the home's socket (waiting up to 3 s for a unit that was just started), checks the daemon, sends _acpmux/person_enroll, and prints the daemon's status JSON. It never starts a daemon, and it runs before anything touches the home.
  • The daemon accepts _acpmux/person_enroll without the app's signature only on a connection that is the provisioner: SO_PEERCRED uid 0, while the daemon itself is not root and not Team-signed. On accepting such a connection, the daemon makes itself not dumpable (once per run), before it reads any byte. PersonGate::provision installs the key or replaces it, since a new app launch makes a new key, and keeps the run's personInstance.
  • Before the provisioner writes the key, it checks the socket's peer process: not root, the owner of the acpmux home, the same executable file (dev/inode of /proc/PID/exe), not dumpable (/proc/PID/status owned by root), and TracerPid: 0. A squatter on the socket path gets nothing. So does an older daemon, which never made itself not dumpable; the platform then falls back to the keyed restart.

Security properties kept (see hub/person.rs, daemon/provision.rs):

  • Only the person's app (by its signature) or the provisioning exec (root) can set the key. An agent runs as the daemon's user, so its enroll gets person.enroll_unavailable, as before.
  • The key never reaches a child, and the daemon is not dumpable before it holds the key.
  • A signed daemon still enrolls by signature only.
  • A root-layout daemon never takes a provisioned key.

Also fixed: the devloop install deleted the profile it had just made. cp -a kept the image profile's mtime, so ls -t | tail -n +3 pruned the new one and left /opt/cmux/current dangling. It now touches the new profile and never prunes it.

Verified on a prod Cloud machine (devloop hot-swap)

  • The unit daemon (pid 43961, acpmux daemon run --take-over) took the key in 39 ms on the guest. personKey went false to true with the same pid and the same personInstance.
  • _acpmux/person_holds over a fresh nonce matched HMAC(K, ...) computed on the guest, so the daemon holds exactly the enrolled key.
  • A second enroll with a new key replaced it, and holds proved the new key.
  • A same-uid (agent) person_enroll over the socket was refused with person.enroll_unavailable. A same-uid daemon enroll was refused before it read the key.
  • No root-owned file appeared in ~/.acpmux.
  • Fleet compile check on Linux passed. Unit tests for provision and the provisioner rule are added.

Changelog

  • Cloud machines: the keyed acpmux start keys the running daemon instead of restarting it. One daemon owns the machine, agents keep running, and a keyed start takes one guest exec instead of two.

Agent session: agent session

🤖 Generated with Claude Code


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


Summary by cubic

The Cloud VM provisioning exec now hands the person key to the already-running acpmux daemon over its unix socket, instead of stopping the daemon and starting a keyed one. One daemon owns the machine, agents keep running, and a keyed start takes one guest exec instead of two.

  • New hidden acpmux daemon enroll --person-key-fd FD (root only): reads one key line, connects to the home's socket, checks the daemon's process, and sends _acpmux/person_enroll. It never starts a daemon.
  • A provisionable daemon (Linux, not root, not signed, on a Cloud machine) makes itself not dumpable at start, before it spawns anything, and accepts the enroll only from a root peer (SO_PEERCRED uid 0).
  • Before writing the key, the provisioner turns on SO_PASSCRED and verifies every reply's writer is the socket's listener, then checks that process is the real daemon: not root, the home's owner, the same executable, every executable mapping an unmodified page of the root-owned file it names (no patched or anonymous code), not dumpable, not traced, and holding the home's daemon lock. A squatter, a relay, or an older daemon gets nothing.
  • The provisioner may replace the key on a new app launch; an agent (the daemon's own user) still cannot enroll.
  • Fixed: the devloop install no longer prunes the profile it just made (cp -a kept the image's mtime and left /opt/cmux/current dangling; the prune also exits cleanly under pipefail when only the new profile remains).

Written for commit b32ef43. Summary will update on new commits.

View guided diff Turn on auto-fix

… (one owner, M3)

`acpmux daemon enroll --person-key-fd 0`, run as root by the Cloud VM
provisioning exec, hands the person key to the daemon that already runs
(acpmux.service) over its unix socket. Nothing stops or restarts the daemon,
so the unit stays the home's one owner and agents keep running.

- The daemon takes `_acpmux/person_enroll` without the app's signature only
  from a connection whose SO_PEERCRED uid is 0 while the daemon is not root
  and not signed, and made itself not dumpable when it accepted that
  connection, before it read a byte.
- Before it writes the key, the provisioner checks the socket's peer: not
  root, the owner of the acpmux home, the same executable file, not dumpable
  and not traced. A squatter or an older daemon gets nothing.
- The provisioner may replace the key (a new app launch makes a new one); an
  agent, the daemon's own user, never can.

Also: the devloop install touches the new profile and never prunes it (cp -a
kept the image profile's mtime, so the prune deleted the profile it had just
made and left /opt/cmux/current dangling).

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

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

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

Review 4adba5d: 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=pr36564 -f ref=lane/acpmux-one-owner -f platform=macos -f backend_mode=local

web and size checks: green

Shipped in:

The feat-cmux-next push run https://github.com/manaflow-ai/cmux/actions/runs/42045816653 at 446a051 failed: cmux-next fleet Release compile.
Those jobs last passed at bb2ff7a. This pull request is one of 5 merged since: #36564, #36567, #36547, #36518, #36503.
If the failure is in your change, fix forward on feat-cmux-next. Pull requests run only the tiers their paths reach (docs/ci/cmux-next-tiers.md); label a batch PR full-ci to run them all before merging.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 5, 2026
… listener

Review of fd291f8: SO_PEERCRED names the process that called listen(), which a
same-uid squatter can fork away from (the listener execs the real binary, a
child accepts and reads the key). And a long-running unit daemon was dumpable
until the first root connection, open to ptrace before it.

- The provisioner turns on SO_PASSCRED and reads every reply with the
  kernel's credentials of the process that wrote it. Before the key goes out,
  every reply must come from the socket's listener, and that process must be
  the real daemon: not root, the home's owner, this executable, every
  executable mapping a root-owned file (no user-owned preloaded library), not
  dumpable and not traced.
- A provisionable daemon (Linux, not root, not signed, on a Cloud machine:
  /etc/cmux/daemon-layout) makes itself not dumpable at start, before it
  spawns anything; only such a daemon treats a root peer as the provisioner.
- devloop: the prune's `grep -v` exits 1 when only the new profile is left;
  under pipefail that stopped the install before the restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
@blacksmith-sh

This comment has been minimized.

…o key

Review of 3b08ee2: a daemon an agent starts itself is dumpable from exec until
at_start, so on a kernel that lets it ptrace its own processes it could patch
the code first. The provisioner now also requires, before it sends the key:

- every executable mapping (from /proc/PID/smaps) to have no private-dirty
  or anonymous page (no patched copy-on-write code, no anonymous executable
  memory), and to be that root-owned file by device and inode;
- the process to hold this home's daemon lock (a descriptor on
  daemon.lock), so a daemon of another home moved onto the socket fails.

The module docs state the boundary that remains, the same one
`_acpmux/person_holds` documents: an agent that can write its user's home and
ptrace its own processes could still tamper with a daemon's data before it is
not dumpable, as it could race the keyed restart this replaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
@cursor

cursor Bot commented Nov 5, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Prod timings for the app's POST /v1/cloud/machines/{id}/acpmux/keyed-start, measured from the client, 6 runs each, with an 8 s settle check after each run:

  • Before (prepare exec, then the keyed restart; image cloud-native-a6d444a): 1659, 1815, 1815, 1660, 1907, 1769 ms. Median 1815, max 1907.
  • After (one root enroll exec into the running unit daemon; cmux-platform#869 on prod; a fresh machine on cloud-native-0cda689 running this PR's acpmux): 939, 785, 1314, 817, 892, 1221 ms. Median 939, max 1314. All 6 held the key. The unit daemon kept pid 13900 and its instance through all 6, so nothing restarted.

On the guest, the enroll alone takes 39 to 58 ms. Most of what remains is the exec round trip itself, which is about 650 to 1000 ms from the client.

Machines on images without daemon enroll fall back to the old two-exec path. That fallback stays until cloud-demo bakes an image from this merge.

🤖 Generated with Claude Code

@lawrencecchen

Copy link
Copy Markdown
Contributor

MERGE QUEUE: this PR broke the macOS build of acpmux: entry.rs called crate::platform::unsupported (a module compiled only for not(unix)) from a #[cfg(not(target_os = "linux"))] arm. Fixed forward by #36647. Please compile a cmux-tui change for macOS before merge (cmux-ci compile-check --recipe-version 2 includes the macOS cmux-tui check).

lawrencecchen pushed a commit that referenced this pull request Nov 5, 2026
… the Windows-only module) (#36647)

#36564 routed `daemon enroll` on every
non-Linux build to `crate::platform::unsupported`, but `platform` compiles
only when the target is not unix. A macOS build of acpmux (the app's bundled
daemon) fails with E0433. The arm now returns its own error.

Red: cmux-ci dev build job 4aed088bf06e589b164bc0f8 (tui-fetch, E0433 at
crates/acpmux/src/cli/entry.rs:214).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Nov 5, 2026
…ceiver policy-read log #36551)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-548f18c0a6d1ebb48240
cmux-bot Bot pushed a commit that referenced this pull request Nov 5, 2026
…ceiver policy-read log #36551) (#36640)

* Cloud VM image: pin cmux-tui 6eabd68 (receiver logs each policy read's admission and duration, #36551)

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

* Cloud VM image: pin cmux-tui cbbeec1 (acpmux daemon enroll #36564, receiver policy-read log #36551)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants