Repository navigation
acpmux: the provisioning exec keys the running daemon; one owner on Cloud machines (M3) - #36564
Conversation
… (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
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
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. |
|
Owned by an agent session · session |
… 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
This comment has been minimized.
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
Bugbot needs on-demand usage enabledBugbot 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. |
|
Prod timings for the app's
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 🤖 Generated with Claude Code |
|
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). |
… 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>
…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
…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>
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).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._acpmux/person_enrollwithout the app's signature only on a connection that is the provisioner:SO_PEERCREDuid 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::provisioninstalls the key or replaces it, since a new app launch makes a new key, and keeps the run'spersonInstance./proc/PID/exe), not dumpable (/proc/PID/statusowned by root), andTracerPid: 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):person.enroll_unavailable, as before.Also fixed: the devloop install deleted the profile it had just made.
cp -akept the image profile's mtime, sols -t | tail -n +3pruned the new one and left/opt/cmux/currentdangling. It now touches the new profile and never prunes it.Verified on a prod Cloud machine (devloop hot-swap)
acpmux daemon run --take-over) took the key in 39 ms on the guest.personKeywent false to true with the same pid and the samepersonInstance._acpmux/person_holdsover a fresh nonce matched HMAC(K, ...) computed on the guest, so the daemon holds exactly the enrolled key.person_enrollover the socket was refused withperson.enroll_unavailable. A same-uiddaemon enrollwas refused before it read the key.~/.acpmux.provisionand the provisioner rule are added.Changelog
Agent session: agent session
🤖 Generated with Claude Code
Open in [code]smith · Need help on this PR? Tag
@codesmith-botwith 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.
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.SO_PEERCREDuid 0).SO_PASSCREDand 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.cp -akept the image's mtime and left/opt/cmux/currentdangling; the prune also exits cleanly under pipefail when only the new profile remains).Written for commit b32ef43. Summary will update on new commits.