The predecessor's agent module was retired and its six files stayed on both workstations telling every session to use tools that no longer exist. This is its successor's design, revised during review on the operator's directions: the host is module-agnostic, the controller has no part, and a real licence manager hands out the correct licence in every situation. - ADR 0181 (reconstructed): the operator account is a node fact stated by the operator; the home is derived unless stated; a resource may be placed under it owned by the account; a node with no account refuses one. What the controller shipped on 2026-09-27 without a record. - ADR 0182: inside a home the module owns the directory and the files it places, writes into the tool's own files for its few keys, never declares a credential's content, and holds everything else as found; a predecessor's leftovers are the operator's to remove once. - ADR 0183: claude-licence-manager holds the mesh seat anthropic-licence-manager and owns the Anthropic licences, grants (encrypted with a key the vault made for it), bindings per touchpoint, usage and audit; one rotation source under a lease; a token travels module to module sealed to each node's module key on request/reply, never as an event; the agent module alone writes what the agent reads; the host delivers package and state and knows nothing else. A bounded exception to ADR 0113; dated mechanism notes on ADR 0050 and 0113. - To-be 36 (claude-code): the mesh's part of the agent's configuration lives in the agent's machine-wide managed directory, owned whole by the module and written by its code; nothing under the home but the credentials file of a subscription licence; the API-key licence through the key-helper; the console as a node-scoped provision (to-be 34 amended); MCP servers as settings with an mcp_configure tool; one agent directory per machine shared by every session. - To-be 39 (claude-licence-manager): store, the two licence kinds, keeping a grant alive, the hand-over, who gets which licence with the predecessor's fallbacks, adoption with the identity guard, the seat's verbs. Numbers taken across main and every open branch at the time of the merge; to-be 14, 29 and 34 carry dated notes; the glossary gains "operator account".
239 lines
16 KiB
Markdown
239 lines
16 KiB
Markdown
---
|
||
layer: to-be
|
||
status: in-progress
|
||
code:
|
||
- mesh-controller internal/inventory
|
||
- mesh-controller internal/catalogue
|
||
- mesh-controller cmd/mesh-controller
|
||
updated: 2026-10-02
|
||
decisions:
|
||
- 02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
|
||
- 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||
- 02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md
|
||
- 02-DECISIONS/0051-shared-data-is-the-operators.md
|
||
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
|
||
- 02-DECISIONS/0117-a-machines-uplink-is-a-seat.md
|
||
---
|
||
|
||
# 29 — A node has operator accounts, and the mesh owns what lives under a home
|
||
|
||
**The mesh models machines but not the people on them.** A node record holds its name, its
|
||
address, its mode — and nothing about *who a person is* on it: one login name on the build node,
|
||
another on the home-server, a third on both workstations. That username is not incidental. It
|
||
decides who a file under `~` is owned by, who a user service runs as, and — the case that surfaced
|
||
this — which account `ssh <node>` logs in as. The predecessor knew it (its per-node `user:`, and the modules that wrote a
|
||
person's `~/.ssh/config`, `~/.zshrc`, `~/.config`); the mesh, taking those over, kept the machine
|
||
facts and dropped the human one.
|
||
|
||
Several things are missing, and they are one idea.
|
||
|
||
## 1. The account is a node fact
|
||
|
||
A node has one or more **operator accounts**: the human logins on it. At minimum a name; the
|
||
mesh already knows the node and its address, so `<account>@<node>` is then a complete answer to
|
||
"who am I, where." It is the mesh's to hold because everything below is derived from it, and
|
||
because it is exactly the fact that was silently lost — `ssh home-server` logged in under the
|
||
workstation's own name, because nothing in the mesh said the home-server's account is a different
|
||
one.
|
||
|
||
## 2. A resource may live under a home, owned by its account
|
||
|
||
[ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) placed a
|
||
module's *system* data — `<root>/<module>`, owned by the module. It has no analog for the other
|
||
half of the filesystem: the things that belong under a person's home and are owned by that
|
||
person. `~/.ssh/config`, `~/.zshrc`, `~/.config/hal` — every one of these is a resource the mesh
|
||
should be able to place and own, resolved against **the account's home** rather than a system
|
||
root, and chowned to **the account** rather than to root or a module uid.
|
||
|
||
This is the same move as `${dir:…}`, one level over: a resource says `home: <account>` (or names
|
||
an account requirement), and the mesh resolves the home directory and the owning uid on the node
|
||
that account lives on. A module that writes operator config — the eventual replacements for
|
||
`hal/terminal`, `hal/claude-code`, `hal/secrets` — declares its files this way and names no
|
||
`/home/...` path, exactly as a system module now names no `/var/lib` path.
|
||
|
||
These are a **family**, not one module: an `ssh-client` module, a shell module, a `~/.config`
|
||
module, each a *universal-tier* consumer of the account fact — assigned wherever a person logs in,
|
||
which is every node, unlike the graphical stack that a capability gates.
|
||
|
||
## 3. The whole of `~/.ssh` is the mesh's — with one boundary drawn inside it
|
||
|
||
The predecessor owned a single file (`~/.ssh/config`) and left the rest alone; it drifted, because
|
||
owning one file beside foreign ones is not owning anything. The mesh should own **the directory**:
|
||
create `~/.ssh` at `0700`, chown it to the account, and own the files it places there —
|
||
|
||
- **`config`** (or the mesh's region of it): the `Host` blocks for every other node, composed
|
||
from the roster;
|
||
- **`known_hosts`**: authoritative, so the "Host key verification failed / accept-new" dance that
|
||
cost real time during enrolment simply ends;
|
||
- **`authorized_keys`**: who may log into this account, governed centrally rather than by whichever
|
||
key happened to be pasted where.
|
||
|
||
**The boundary — and it is the reason this is safe:** `~/.ssh` is the one directory where a wrong
|
||
declaration locks a person out of their own machine. So the mesh's *found-vs-owned* semantics
|
||
([ADR 0118](../../02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md),
|
||
adoption) apply *inside* the home directory. The mesh **owns** the directory and the files above; it
|
||
**holds as found — never rewrites, never removes** — the operator's own contents: their **private
|
||
keys** and their **personal drop-ins** (`config.d/personal`, the personal `Host` aliases a
|
||
workstation carries, exactly as `hosts.local` is the home the mesh never rewrites for `/etc/hosts`).
|
||
Reconcile removing an unassigned `config.d/mesh` is fine; the same logic aimed at `id_ed25519` or an
|
||
operator's own `authorized_keys` entry is a lockout. This is the login-channel cousin of the rule
|
||
[ADR 0117](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md) draws for the uplink and the sshd
|
||
module draws for the firewall: **the mesh must never be able to arrange the one failure that severs
|
||
its own way back in.** The carve-out is not a convenience; it is that rule, in `~/.ssh`.
|
||
|
||
## 4. Keys are the mesh's to generate — through a CA, and existing keys are adopted, not replaced
|
||
|
||
Key *generation* is the mesh's, not each node's improvising its own. The clean form is an **SSH
|
||
certificate authority as a seat**, the sibling of the TLS internal CA the mesh already runs:
|
||
|
||
- **Host certs.** The mesh signs each node's host key. Every node's `known_hosts` becomes one line
|
||
— `@cert-authority *.<suffix> <mesh-CA-key>` — and nothing is distributed per node; a new node is
|
||
trusted the instant its host key is signed.
|
||
- **User certs.** The mesh signs a cert naming the principals (accounts) allowed. Every node's
|
||
`authorized_keys` / sshd `TrustedUserCAKeys` becomes one trust line — no N×N key spraying — and
|
||
short-lived certs give rotation for free
|
||
([ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md)).
|
||
- The **CA private key is the mesh's**, a secret the vault makes
|
||
([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)).
|
||
|
||
**Three kinds of key, and only one is never minted.** Host keys (server identity) and pure
|
||
machine-to-machine keys the mesh may generate end to end. The operator's **personal** private key —
|
||
possibly on a hardware token, possibly used from an off-mesh laptop — the mesh **signs into a cert
|
||
but never generates**; that, and only that, is the residue of
|
||
[ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md). So "keys are mesh-owned" and
|
||
"the operator's login key is the operator's" reconcile: the mesh owns the CA and the signing; it
|
||
holds the human's private half, never mints it.
|
||
|
||
**Existing keys are not lost.** Taking ownership is *adoption*, not regeneration: a key already on a
|
||
machine is recorded and signed, not overwritten. The mesh gains authority over `~/.ssh` — it does
|
||
not clear it. An enrolling node's host key and the operator's existing key are carried forward; the
|
||
found-vs-owned boundary of §3 is exactly what guarantees nothing already there is destroyed.
|
||
|
||
## 5. How it is distributed: the controller composes, the node applies
|
||
|
||
None of this needs a node to discover the mesh, and none of it needs a control-plane module of its
|
||
own. The ssh files are **roster facts**
|
||
([ADR 0120](../../02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md)): once the
|
||
roster view carries a node's **host key** and its **account** beside its name and address, the
|
||
`ssh-client` module ships a template for `known_hosts`, `config` and `authorized_keys`, and the
|
||
controller renders each node's copy from the full roster and pushes it. The mesh owns the data; the
|
||
module owns ssh's format; the control plane gains no ssh syntax. It is the same act as composing a
|
||
peer list or `/etc/hosts` — which is why there is **no control-node-only "mesh-ssh" module**: the
|
||
centralization is the controller's composition, not a module that runs somewhere. Only non-secret
|
||
facts travel (names, addresses, accounts, host keys, the CA public key); the private key stays the
|
||
operator's, placed as an operator-owned file, referenced by path.
|
||
|
||
## 6. The two modules, and the seat between them
|
||
|
||
- **`sshd`** (server, every node) — manages sshd, owns and **reports** its host key so the roster
|
||
carries it, and trusts the user CA.
|
||
- **`ssh-client`** (client, every node) — owns `~/.ssh` per §3, consumes the roster and the CA
|
||
public key.
|
||
- **`the-ssh-ca`** (a seat, held on the control node) — signs host and user certs.
|
||
|
||
They meet at the account and the CA, not at a bespoke module. The `sshd` server side already exists;
|
||
the client/identity side and the CA are the open pieces.
|
||
|
||
## What has shipped, and what has not
|
||
|
||
*Recorded 2026-10-01 from the controller's main branch, not from intent.*
|
||
|
||
**Built (mesh-controller, merged 2026-09-27):**
|
||
|
||
- **§1, the account as a node fact.** A node record carries an operator account and, optionally,
|
||
its home. Empty is a real state — a freshly enrolled or headless machine has no operator account
|
||
known yet — and an empty home means *derive it* (the superuser's home for the superuser, the
|
||
conventional per-user home otherwise), so the common case needs no entry. The controller's node
|
||
command sets it. One account per node is what exists; "one or several" below is still open.
|
||
- **§2, resources under a home.** The account and its home are offered as machine facts, and a
|
||
resource's *path and owner* resolve placeholders exactly as its content does — so a module places
|
||
a file under a person's home, owned by that person, naming neither. A roster file may say it lives
|
||
under the home: it is rendered per node, placed under that node's account's home, chowned to the
|
||
account, and a node with no account gets none.
|
||
- **§5, the composed ssh config.** The roster rendering carries each node's account, so the
|
||
`ssh-client` template can emit a `Host` block per node with the right login name. Composed
|
||
end-to-end in the controller's tests.
|
||
|
||
**Written but not shipped:** the `ssh-client` catalogue module itself exists on a branch of the
|
||
module repository; its pull request was closed with a hold until this design is deployed, and
|
||
nothing has deployed it since. The predecessor's generator still writes every workstation's ssh
|
||
client blocks today — which is where [issue 172](../../04-ISSUES/172-the-ssh-client-block-matches-one-spelling-of-a-machine/00-report.md)
|
||
was found.
|
||
|
||
**Not built:** the SSH CA and certificates (§4), `known_hosts` and `authorized_keys` as roster files,
|
||
the found-vs-owned boundary inside `~/.ssh` (§3 — the controller has no rule yet that refuses to
|
||
rewrite a private key), adoption of existing keys, the ssh-agent as a user service, and user-scoped
|
||
services in general. The host vocabulary still has no user-scope unit at all; a workstation's
|
||
per-user daemons (a bar watchdog, a config reloader, an audio service masked per user) have no form
|
||
the mesh can send.
|
||
|
||
**A gap this surfaced:** §1 shipped as code before it had a decision record. The account as a node
|
||
fact, the home as a placement root, and what the mesh may and may not do under a home are each a
|
||
decision this document names but no record states. They are the next records to write, before the
|
||
family of §2 modules is built.
|
||
|
||
*2026-10-02:* two of them are written. [ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)
|
||
records the account as a node fact and the home as a placement root, reconstructed from what shipped;
|
||
[ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
||
generalises §3's boundary to every directory under a home. The first member of the §2 family is
|
||
designed in [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md). User-scoped
|
||
units are [ADR 0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md),
|
||
written the same day; still unwritten: several accounts per node, and the CA. On the same day every node of the
|
||
live mesh still carried an empty account.
|
||
|
||
## Why now, and why not yet
|
||
|
||
**Why it matters:** when HAL retires, the generators that keep `~/.ssh`, shell config and the
|
||
operator's `~/.config/hal` current retire with it. Without this, adding a node stops adding its ssh
|
||
alias and its trust, and a fresh machine has no operator dotfiles at all — the mesh would run every
|
||
service and leave the human unable to work on the box.
|
||
|
||
**Why not build it reflexively:** it is a real addition to the node model, the resource model, and
|
||
the seat set, and must be gotten right. The mechanism half is now settled — ADR 0120 is what lets
|
||
the ssh files be templates with no control-plane format — so what remains to decide here is the
|
||
model:
|
||
|
||
- **One account or several per node?** A workstation has one human; a shared box might have more.
|
||
Allow more than one without forcing the common case to name it.
|
||
- **The CA's shape.** Host-cert and user-cert principals, cert lifetime and renewal, where the CA
|
||
runs (a seat on the control node). The one thing fixed: the operator's personal key is signed,
|
||
never minted.
|
||
- **Adoption of existing keys.** How an enrolling node's host key and an operator's existing key are
|
||
recorded and signed rather than replaced — the found-vs-owned boundary, made concrete for keys.
|
||
- **The `sshd` boundary.** Server side exists; this is the client, the identity, and the CA.
|
||
- **The ssh-agent.** An agent is a *user-scoped service running as the account* — the first concrete
|
||
case of the user services §2 anticipates. It holds the operator's private key in memory; the mesh
|
||
declares the unit and sets `AddKeysToAgent`/`IdentityAgent` in `config`, and still never sees the
|
||
private half. Agent *forwarding* wants a policy, not a default: with user certs it is largely
|
||
unnecessary, and forwarding an agent into a node exposes the operator's keys to that node's root —
|
||
so prefer certificates and `ProxyJump` over forwarding.
|
||
|
||
**Now load-bearing.** The migration of every node to the mesh is complete; what remains of the
|
||
predecessor is exactly the user environment this design covers — ssh config, dotfiles, the desktop
|
||
stack and the per-user services of the two workstations. Those generators are the last thing
|
||
keeping the predecessor running, so the model questions above are no longer deferred: the account
|
||
record, the home as a placement root, user-scoped services and the one-off steps a hook used to run
|
||
each need a decision before the modules that replace the generators can be written.
|
||
|
||
## The family beyond `~/.ssh` — 2026-10-02
|
||
|
||
The modules §2 calls *a family* — the shell, the terminal, the desktop, everything under a home that is not `~/.ssh` — are designed in [37 — The operator's machine](37-the-operators-machine.md), under [ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md) to [0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md). This document keeps `~/.ssh`, the CA and the roster files. Two things it listed as not built are decided there: user-scoped services (ADR 0177) and the one-off steps a hook used to run (declared state, or a seat's verb).
|
||
|
||
## References
|
||
|
||
- The gap was found generating `~/.ssh/config` from the *HAL* registry (`hal/terminal`'s
|
||
postConfigure hook), which the nox mesh has no equivalent for.
|
||
- [ADR 0120](../../02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md) — the roster
|
||
fact mechanism that renders the ssh files, format owned by the module.
|
||
- [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) — the
|
||
system-path placement this mirrors for home paths.
|
||
- [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md) — why the operator's personal
|
||
key is signed, never minted.
|
||
- [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md) — the CA key is a secret the
|
||
vault makes; [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md)
|
||
— short-lived certs as rotation.
|
||
- [ADR 0117](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md),
|
||
[ADR 0118](../../02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md) —
|
||
the never-sever-the-channel rule and the found-vs-owned semantics, applied here to `~/.ssh`.
|