acpmux: the provisioning exec keys the running daemon; one owner on Cloud machines (M3)
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_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. - 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/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):
- 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.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.- A second enroll with a new key replaced it, and holds proved the new key.
- A same-uid (agent)
person_enrollover the socket was refused withperson.enroll_unavailable. A same-uiddaemon enrollwas refused before it read the key. - No root-owned file appeared in
~/.acpmux. - Fleet compile check on Linux passed. Unit tests for
provisionand 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](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
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_PEERCREDuid 0). - Before writing the key, the provisioner turns on
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. - 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 -akept the image's mtime and left/opt/cmux/currentdangling; the prune also exits cleanly under pipefail when only the new profile remains).