claude-code: write the agent's home as its own account where it has one
mesh/merge-gate pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request

On the control node agents are to run as an account that cannot become root
(hq ADR 0266). The runtime keeps running as the operator, so the token, the
identity and the home-scope items go to the agent's home through a helper
started as that account; the licence follows the move, what was placed in the
operator's home goes, and the guard refuses a session running as anyone else,
recording to the journal. Unchanged where no agent account is named.
This commit is contained in:
jochen
2026-10-08 18:33:54 +02:00
parent 53c4b47da4
commit 63101f447a
14 changed files with 1227 additions and 89 deletions
+32 -1
View File
@@ -142,11 +142,42 @@ Remove these by hand, once; the mesh removes nothing it did not make (ADR 0182):
- `~/.claude/skills/cleanup/`, `~/.claude/skills/hal-switch-license/`
- the hand-made console entry in `~/.claude.json` under `mcpServers` — it is ignored now anyway
## Where the agent runs as an account of its own (hq ADR 0266)
A machine may name an **agent account** beside the operator's, at the controller's terminal
(`mesh-controller node agent-account <node> <account>`); the control node does. Agents there run as that
account, which cannot become root without a person, and the operator keeps their own account and its sudo.
The runtime hands this module `MESH_AGENT_ACCOUNT` and `MESH_AGENT_HOME` — the operator's where none is
named, and read as the operator's from a controller that sets neither — and everything it writes for the
agent goes to that account's home:
- the licence token (`~/.claude/.credentials.json`) and the account it is logged in as (`~/.claude.json`);
- an API-key licence and its key-helper, in `~/.claude/mesh/`, since the account cannot enter the
module's state;
- the items registered at the home scope, recorded per home (`home-placed-<account>.json`).
The runtime's account cannot enter that home (0700, the agent's), so a path under it is touched by a
**helper running as the agent's account**: this module's binary as the plugin carries it
(`/etc/claude-code/marketplace/plugins/nox-mesh/guard/guard home-fs`, root's), started once through
`sudo -n -u <account>` and kept, one JSON request and answer per line. The escalation goes down only: the
runtime's account may become the agent's, never the other way.
When a machine moves its agent to such an account, the licence follows at once — a binding counts as
applied only while the agent's account holds it — and what this module placed under the operator's home
goes: the access-token-only credentials file (one holding a refresh token is a login the person made, and
stays, said), the home-scope items unless changed by hand, the key kept in the state.
**The guard there refuses every command and file edit of a session that does not run as the agent's
account**, whatever it is, and no override lifts it: the operator's account may become root, and an
agent started in it by habit must do nothing. Its record goes to the machine's journal
(`journalctl -t mesh-claude-code-guard`), which the session's account may add to and not change;
`claude_code_guard` reads it there.
## Escalation
Writing `/etc/claude-code` needs root. The runtime runs as the operator account, and the module uses
that account's passwordless `sudo`; on a machine without it, `claude_code_render` says so and nothing
is written.
is written. The same sudo starts the helper as the agent's account, where there is one.
## Code