Repository navigation
v0.41.7
First-start installs run nothing they downloaded as root: each agent installs as its own unprivileged user (#623, step 3).
Steps 1 and 2 (#646, #652, #653) pinned every first-start install and committed its digests or lock. What was left is who runs them. On 0.41.6 every install ran as root: root unpacked every npm tarball and release archive, ran Claude Code's install script, built the sdists, ran every installed agent's checks, and an override ran all of its install scripts as root.
Now root only fetches, checks digests, makes directories, copies, links and changes owners. Everything else runs as the agent's own install user (uid 19001 and up, one per agent, below the session uids), with the instance's secrets already removed from its environment and no way to gain privileges:
npm ci,npm rebuild(Claude Code's install script), an override'snpm install(now into$TOOLS/npm/<name>, no longer-ginto the shared tree), the venv,pipand any sdist build,tarfor every release archive, and every post-install check or warm-up.- The install user owns a directory only while it fills it. Afterwards every process of that user is killed, hard links to files it doesn't own are removed, every symbolic link that leads out of the directory is removed (only links inside it, and into the image's own
/usr, stay), setuid and setgid bits are cleared, and the tree is root's and sealed again before root reads or links anything in it. Its HOME is its own under/tmp, removed with the install. - After the last install, nothing of any install user is left anywhere it could have written: no process, and no file or directory in
/tmp,/var/tmp,/dev/shm,/run/lock, a message queue or the tools. This runs at every start, so an install a restart cut short is cleared too. - One step still runs as root, named in
ROOT_RUNS: System One'splaywright install-deps, which calls apt. The check also pins the exact Playwright wheels it runs, so a lock update that changes them fails until the pin changes in the same review. A committed apt list (the design's item 5) replaces the step by 2026-11-24.
Also:
npm_link_binschecks what a package declares before root links it: a plain command name, a file inside that agent's own tree with every link resolved (it used tochmod 755whatever abinentry named, as root), not another agent's command, and not one of the image's own programs.$TOOLS/bingoes onPATHonly after the installs, for the runner, the gateway and the console, in the same order as before. Root's ownnode,npm,tarandpython3during installs are the image's.- Hermes's breaker warning now prints: its note was set inside a subshell, so the warning after the install never could.
- The runner's spool directory (
/var/tmp/harness-spoolby default) is used only if it is the runner's own real directory that no one else can write; it is now created0700, and anything else falls back to the system temporary directory. - The self-hosting guide points the install section at the metadata-address block for cloud VMs: an install can reach the network, as it must to download.
scripts/check-install-pins.pyholds the entrypoint to all of it, and its self-test catches the 33 forms 0.41.6 had.check-root-boundary.shalso checks that no install-user process or HOME is left.
Upgrading: nothing reinstalls. Tools installed by an earlier version stay as they are; the install user only changes how the next install runs.
Checked: on a fresh volume, arm64 and amd64, all 30 default agents install from their pins with no warning (amd64 749 s, 0.41.6 726 s). Every file under the tools is root's, no install-user process or HOME is left, the root-boundary check holds, a second start installs nothing, and the agents run as a session user. Hostile installs (a planted install script, an archive with link and ../ entries, a bin entry pointing outside its package, an override) reached no secret, /app or another agent's files. A hostile install that planted links to a secret, to /app, to /proc/1/environ and to another agent, files in every shared directory and a message queue, a setuid file and a setsid background process ended with only its own node_modules/.bin link left: nothing readable through the rest, no file or process of its user anywhere, nothing setuid. test_install_uid.py and test_npm_locks.py in the image: 68 passed; the spool tests as root and as a session user; the pin check's self-test catches 4 changed locks.
On the release candidate, every agent was reinstalled on a running instance this way, and each was then given one real task: 29 of 31 created the file it was asked for, owned by that session's own user. Qoder (no account configured on the test instance) and System One (its model handed the task back before acting) loaded and ran as a session user.
Design by @eric-epsilla on #623.
Full Changelog: v0.41.6...v0.41.7