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

Merged
TE
1d ago
+698-36

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).