Compare commits

..
Author SHA1 Message Date
jochen e3a5862c5a The operator's agent is a module: three records and to-be 36 for the claude-code successor
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. Before a successor module is written, the design
needs the decisions it rests on and nothing in the record stated them:

- ADR 0169 (reconstructed) records what the controller shipped on 2026-09-27 without a record: 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, and a node with no account refuses one.
- ADR 0170 generalises to-be 29 §3's found-vs-owned boundary to every directory under 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 included, which the operator removes once.
- ADR 0171 draws the licence line the operator asked to have drawn rather than assumed: the mesh
  binds and delivers (to-be 14 and 15 stand), the module alone writes the credential file, refresh
  stays central (ADR 0050), a switch is the binding changed through a controller seat verb asked
  for via the console, and the token-carrying shell helper is retired. The controller learns
  nothing about the agent; that is what "no part" means.

To-be 36 is the module's design: the ownership map per path, the fate of the six predecessor
files, what the three instruction documents say, the licence tools and skill, the console as a
node-scoped provision, the package gap stated honestly, and the order of the build. To-be 14, 29
and 34 carry dated notes; the glossary gains "operator account".
2026-10-02 16:37:48 +02:00
32 changed files with 377 additions and 1701 deletions
+1 -19
View File
@@ -12,7 +12,7 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
- **operator account** — the login name of the person who works on a node, stated on the node
record; empty for a machine nobody logs into. Everything the mesh places under a person's home is
resolved against this account's home and owned by it
([ADR 0181](../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)).
([ADR 0176](../02-DECISIONS/0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)).
Not "the user" (ambiguous with a module's own account) and not a name a definition carries.
- **control-node** — the one node that also holds the `mesh-controller` seat. There is exactly one
per mesh. "control-node" is not a separate kind of machine — it is a node that additionally runs
@@ -100,21 +100,3 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
A new name for an existing thing lands here first, in the same change that introduces it in code. A
record under `02-DECISIONS/` keeps whatever word it was written with — those are immutable — so a
term retired here may still appear there, and the mapping above is how to read it.
## The operator's machine
- **node tools** — the one tool runtime per node, a host-side process the host supervises, that loads
every assigned module's tools bundle and serves every tool and held seat's verb on the subjects the
memberships issue; its serving mode on loopback is what was called **the console**
([ADR 0175](../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
Replaces **"console"** as the module's name; *console* remains the word for the person's end of it.
- **bundle** — the artifact a module's tools are built into, interpreted or compiled; never an image.
- **kept region** — a marked block in a managed file the mesh writes *into*, where the operator's own
lines survive every push and are given back when the module goes
([ADR 0174](../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)).
One of the two ways a node varies a module; the other is a **setting**.
- **installed / holding** — a module may be assigned (its package installed, its files placed) without
holding the seat its family declares; *holding* is being the one — the login shell, the display
session — on that node ([ADR 0176](../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)).
- ~~flavor~~ — not used. What a flavor varied is a setting or a separate module.
@@ -1,5 +1,5 @@
---
status: graduated
status: active
initiated: 2026-10-02
touches:
- 02-DECISIONS/0040-what-a-module-is.md
@@ -15,13 +15,7 @@ touches:
- 03-DESIGN/00-as-is/10-module-catalogue.md
- 04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md
- 04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md
became:
- 03-DESIGN/01-to-be/37-the-operators-machine.md
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
- 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
- 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
- 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
became: []
---
# 018 — The operator's machine as modules
@@ -69,7 +63,7 @@ and the catalogue's shape ([as-is 10](../../03-DESIGN/00-as-is/10-module-catalog
- [04 — The seats of the environment](04-the-seats-of-the-environment.md): the roles a machine
has once, their candidate contracts, and what gates each.
**What it had to settle, and where each landed.** *(Graduated 2026-10-02.)*
**What this must settle before it graduates.**
1. A module is one *managed thing*, software or not, and every module may serve tools — or ADR
0040 already says this and only its examples are narrow.
@@ -1,55 +0,0 @@
---
status: active
initiated: 2026-10-02
touches: [lab, the lab module, the catalogue, assignments, settings, the controller's store]
became: []
---
# 019 — A warm twin of the running mesh
## What is being investigated
Whether the lab can keep a **warm twin of the mesh as it actually runs**: the same machines, carrying
the same catalogue, the same assignments and the same settings as the live mesh, raised once and kept
ready, so that a change can be tested against the mesh as it is rather than against a scenario
written to resemble it. A run against the twin would go through the lab module like any other run:
a branch per repository, the twin restored from its snapshot, the change applied, the beds run.
## Why
The lab's beds raise meshes from declarations written for the bed. They prove the mechanism. They
do not prove that a change works on the mesh that runs, with its accumulated assignments, its
operator settings, its adopted machines and its modules in their real combinations. The gap showed
on 2026-10-02:
- a change to how a module's settings reach its files was correct in every bed, and would have put a
setting into the container runtime's configuration on every machine running that module. Only the
composed plan for a real machine showed it;
- a firewall change composed cleanly and still left one machine's wired port unfiltered, because
of a link that machine had and no bed did;
- a recovery step was needed on every machine at once, after a change that every bed had passed.
The lab already has a warm mode, a snapshot of a raised scenario restored between attempts. What it
does not have is a scenario that **is** the running mesh, kept current with it.
## What it touches
- **What a twin is made of.** The catalogue and the assignments are records; settings are records;
secrets are sealed to machines and cannot be copied. Which of these can be carried to the lab as
they are, which must be substituted, and how a twin says what it substituted.
- **Data.** A twin with the real catalogue and no real data proves composition and delivery, not a
migration. Whether a twin carries data, a sample of it, or none.
- **Keeping it current.** A twin raised once goes stale with the first merge. Whether it is
re-derived from the live records on each run, refreshed on a schedule, or rebuilt only when asked.
- **Machines.** The live mesh has machines of different kinds: a server on the internet, machines
behind a home router, a laptop that sleeps. Which of their properties a twin must reproduce for a
test to mean anything (reachability, the private network, the found firewall).
- **Cost.** The lab machine's memory and disk, and how long a twin takes to raise from cold.
- **The lab module's tools.** A run against the twin rather than a named bed: one more tool, or an
argument to the run tool.
## Starting point
The lab module (ADR 0172) runs beds through the mesh, and the lab's warm mode already snapshots and
restores a raised scenario. The beds that raise a machine shaped like one live machine from the
catalogue are the nearest existing thing, and the first to compare against.
-2
View File
@@ -78,8 +78,6 @@ capability. The host hardcodes no firewall, supervisor, package manager or runti
generic apply primitives and platform detection, so it runs where none of those exist — an Android
phone has no ufw, systemd, pacman or Docker.
> **The mechanism changed — 2026-10-02, by [ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md).** The shell example above — *bash, zsh and fish all join `shell`; one may be default* — is read as *installed is not holding*: the three may all be installed, and the `login-shell` seat is node-scoped and held by exactly one. The decision — what a module is, and the three relationships — stands; [ADR 0173](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md) applies it to the operator's whole machine.
## Consequences
- **Supersedes the earlier "grouped by domain" decision** (folded in consolidation; see the
@@ -9,8 +9,6 @@ extends: 0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
# 47. A module runs its code as its own process, with its own account
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** A module's *tools* are no longer served by the module's own process under its own account: one tool runtime per node, on the host side, serves every assigned module's bundle. A tool is still served on its own subject and only the module that serves it answers; what moved is the process and the account.
## Context
A module is one self-contained thing ([ADR 0040](0040-what-a-module-is.md)), and it gets a broker
@@ -204,14 +204,3 @@ only, the refresh token only, the manager node only.**
record extends, amended to describe the adapter generalisation.
- The read-only vendor-agnostic analysis, 2026-09-05 (code workspace) — the inventory and the decisions
taken on the open questions this record encodes.
> **The mechanism changed — 2026-10-02, by [ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md).**
> What stands: `model-access` is one vendor-blind provision for the consumers that do not care which
> vendor answers; a vendor's lifecycle is an adapter's; the carve-out that one node holds a refreshable
> grant's refresh token readably. What moved: the Anthropic adapter is no longer a part of the
> controller's licences context but a module, `claude-licence-manager`, holding the seat
> `anthropic-licence-manager`, with the grants in its own store encrypted with a key the vault made for
> it; and the agent at a terminal is not a consumer of `model-access` — it is coupled to an Anthropic
> grant and uses the seat. The consequence above that the three binding columns *become three ordinary
> consumers of `model-access`* therefore no longer describes the agent's bindings; they are the
> manager's. The static-key adapters and the vendor-blind records stay where this record put them.
@@ -283,13 +283,3 @@ modules in the catalogue require it — so a shared secret is a requirement answ
which is what this record asks for. Private keys are still made where they are used and never
travel, which is the other half and was never in question.
> **The mechanism changed — 2026-10-02, by [ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md).**
> What stands: every shared secret the mesh makes is the vault's, a private key is made where it is
> used, and a long-lived value a backend issues enters the vault's custody — here as the key the vault
> makes for the licence manager, which encrypts the vendor's grants at rest with it. What this record
> did not foresee: a credential that lives hours, issued by a vendor to the one module that holds its
> grant, and handed by that module to the agent on each node sealed to that node's module key, on
> request/reply over the bus, never through the vault and never as a file the host writes. ADR 0183
> states that as a bounded exception — one vendor, tokens that live hours, one recipient per message —
> and a second such channel is a decision of its own.
@@ -9,8 +9,6 @@ extends: 02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-ow
# 150. A module's own code runs as supervised processes under the module's one account
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** For a module's *tools*, read that record: one runtime per node, the node's one account, bundles loaded from the memberships. This record still governs a module's long-lived processes — a daemon, a provisioner, a scheduled ingest — and the account invariant for them.
## Context
The repository answers "what runs a module's own code" two ways and reconciles them nowhere
@@ -111,8 +111,6 @@ operator owns; narrowing what it may call is a setting on its assignment, which
[ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) already provides for
and nothing here builds.
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** The surface stays a module assigned per node, on loopback, with the machine's login as the authority. It is no longer a container: it is the node tools runtime's serving mode, host-side, and that runtime also serves every assigned module's tools. The module is renamed `node-tools`.
## Consequences
- **The way a person drives the mesh is inside the mesh.** It is declared, delivered, replaced and
@@ -94,13 +94,6 @@ itself restarts, and the console says so rather than hiding the modules' tools w
**A grant of `*` reaches a role's tools; `seat:<seat>.<verb>` grants one.** The console's `*` needed no
change to reach the mesh's verbs, which is what a grant meaning *every tool* should mean.
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).**
> The seat gains a generic verb beside the named ones: `command`, which takes one command line as the
> controller's binary takes it and answers what it printed. The named verbs stand and keep their
> schemas; `command` is the whole binary, added because the operator decided any node may call any
> tool and a verb per command was the only thing keeping `node account`, `node show` and the rest
> behind a shell on the control node. Additive within the version, as §"additive" above allows.
## Consequences
- **The console answers the mesh's own questions.** Issue 147's first paragraph closes: what a node
@@ -1,108 +0,0 @@
---
topic: what runs on it
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0040-what-a-module-is.md
---
# 173. The operator's machine is the mesh's, and a module is whatever it declares
## Context
[ADR 0040](0040-what-a-module-is.md) says a module is *one self-contained piece of software the
mesh installs and manages*, and every example it gives is a service: a database, an analytics
server, a forge. The catalogue followed the examples. Of the predecessor's 34 modules on one
workstation, 28 are the operator's environment — a login manager, a window manager with 88 files
and four flavors, a shell, a terminal, a launcher, an audio setup, scripts — and the migration
scoped all 28 out as *the workstation's own environment*, to be managed by nobody
([research 018](../01-RESEARCH/018-the-operators-machine-as-modules/02-what-exists-and-what-is-missing.md)).
Since the predecessor retired, nobody is exactly who manages them: a fix is a hand edit that
nothing records and nothing regenerates.
[To-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) reached under the home for
one directory and drew a boundary inside it. The operator's statement is wider: *the mesh manages
my entire machine, all four of them, as far as it makes sense* — system folders and the home
alike, the servers and the workstations from the same catalogue. And the operator refused a
distinction this effort first drew between modules that ship code and modules that ship only
declarations: *a module can have some tools, a seat implementation, some containers, a unit, a
binary, some config files — one of these, or all, or two.*
## Considered Options
1. **Keep 0040's reading and manage the environment outside the catalogue** — dotfiles in a
repository, a script that places them. Rejected: that is the predecessor's first two days, the
origin of every inherited shape [as-is 10](../03-DESIGN/00-as-is/10-module-catalogue.md)
documents, and it puts the one thing a person looks at outside the one mechanism that is
checked.
2. **Add a second kind of module for configuration** — a "config module" with files and no
process. Rejected by the operator: a kind is a distinction the manifest already makes by what
it declares, and a second kind is a second set of rules to keep in step.
3. **One definition: a module is one managed thing, described by what it declares.** Chosen.
## Decision
**1. Everything configurable on a node is declared by a module.** Services, and equally the login
manager, the display server, the window manager, the shell, the terminal, the launcher, the
notifier, the audio setup, the boot images, the package manager's configuration, the agent at the
terminal, and a folder a person works in. The test is *can it be configured on a machine*; if it
can, some module owns it. What no module declares is found and left alone, as adoption already
says of a machine ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
**2. A module is whatever it declares, and there are no kinds of module.** A package, files, a
container, a unit, a binary, a seat claim, tools — any one, or all. 0040's *one self-contained piece
of software* stands; its examples were services, and that was the whole of the bias. A downloads
folder with a process that tidies it, backs it up and answers questions about it is a piece of
software by 0040's own test, and so is a shell that is a package, three files and a seat.
**3. The home has no boundary of its own.** A file under the operator's home is placed and owned
the way [to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §2 built it: by a
module, resolved against the account's home, owned by the account. Which files are the mesh's is
decided by what modules declare, not by a line drawn through a directory. A person's documents,
projects and history are data under [ADR 0051](0051-shared-data-is-the-operators.md) and no module
declares them.
**4. One module ships one default configuration.** No flavors. What differed between the
predecessor's four flavors of one desktop module is what [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)
is for.
**5. Servers and workstations take the same catalogue.** A module declares what it needs; a
machine reports what it has; assignment refuses by name
([ADR 0161](0161-what-deserves-a-seat.md) §3). The shell, the prompt, git and the agent are universal.
A display server needs a graphical session; a window manager needs the display server held. Nothing
in a manifest says *workstation*.
## Consequences
- The catalogue grows by a family of modules that run no service. Each is still built,
registered, assigned, pushed and reported like every other, and `status` says whether a
machine has applied them.
- The account fact becomes load-bearing for every node a person uses. Today it is empty on all
four node records of this mesh; stating it is the first step of the build.
- A module that *installs* a thing is distinct from a module that *holds its role*: zsh, fish and
bash may all be installed, and one holds the login shell
([ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)).
- The host's `package` shape drives the distribution's package manager only. A module whose
package is outside the distribution's repositories — the login manager in use is one — needs
either an official package or a shape the host does not have. Recorded as a gap, not decided.
- The predecessor's hooks go. What they did becomes declared state the host applies, or a verb a
seat serves ([ADR 0177](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)).
## How it is checked
| Rule | Checked by |
|---|---|
| A manifest with no container, no unit and no binary registers and resolves like any other | the catalogue's registration tests, with a package-and-files manifest |
| A file resource under the home resolves against the account and is owned by it | the controller's composition tests (to-be 29 §2, built) |
| A home-scoped module is refused on a node with no account, naming the fact | the same tests |
| A module needing a capability the machine lacks is refused by name | the resolver's tests (ADR 0161 §3) |
## References
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md), documents
01 and 02 — the behaviour wanted and the inventory measured.
- [ADR 0040](0040-what-a-module-is.md), [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md),
[ADR 0051](0051-shared-data-is-the-operators.md), [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)
- [To-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) — the account and the home
as a placement root, built; the records for them are proposed in an open change.
@@ -1,92 +0,0 @@
---
topic: building it
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0011-managed-files-are-generated-never-edited.md
---
# 174. A node varies a module through settings and kept regions, never through an edit
## Context
[ADR 0011](0011-managed-files-are-generated-never-edited.md) says a managed file is derived and an
edit to it is overwritten without warning. The predecessor said the same and then undid it twice:
a `merge` strategy that adopted disk drift back into its database, so a local edit became the
record; and a theming layer of about 90 environment variables substituted into templates at sync
time, with tools to list and set them, so that *nearly every value was a variable* — a second
configuration language laid over the first.
The operator wants both the variation and the rule. One window-manager module with one default
configuration, and each node tweaking it; and the file carrying the wanted value rather than a
variable the file reads. Two mechanisms already exist for exactly this: a **setting**, declared by
the module and set per mesh or per node, rendered at composition
(`${setting:…}` is live in the resolver's manifest); and a **kept region**, a block in a file the
mesh writes *into* where the operator's own lines survive every push
([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), used by the ssh-client module
for the operator's own `Host` blocks).
What stands in the way is [issue 168](../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md):
a setting today reaches every mergeable file and every contribution of its module. Ninety theme
knobs on that mechanism would reach ninety files. The record that fixes it — a setting declared
with its type, meaning, default and the file it lands in — is proposed in an open change alongside
the container-runtime records.
## Considered Options
1. **Carry the predecessor's merge strategy.** A local edit is adopted into the node's layer.
Rejected: two writers and no arbiter, which is the option 0011 removed, and the reason a
`/model` choice was silently reverted on every node for weeks before anyone found the cause.
2. **Carry the environment-variable theming.** Rejected by the operator: the value belongs in
the file; a variable the file reads is a second place for the same fact.
3. **A per-node file override** — a whole file replaced for one node. Rejected: it is a flavor
under another name, and a module update then misses that node entirely.
4. **Settings rendered into the file, and kept regions, and nothing else.** Chosen.
## Decision
**A node varies a module in exactly two ways.**
- **A setting.** Declared by the module with a default, set for the mesh or for one node, rendered
into the file at composition. The value is in the file. Asked, the mesh lists every setting
with its effective value and where it came from.
- **A kept region.** A marked block in a file the mesh writes into, in which the operator's own
lines are kept across every push and given back when the module goes (ADR 0102).
**An edit outside a kept region is overwritten, as ADR 0011 says, and never adopted.** Nothing
reads a managed file back into the record.
**The predecessor's theme knobs become settings** of the modules whose files they render — the
window manager's colours are the window manager's settings, the bar's are the bar's — each
landing in the file that reads it and no other.
**Issue 168 is fixed before any environment module declares a setting.** A setting must name the
file it lands in; until that ships, the environment modules carry their defaults in their files
and no settings.
## Consequences
- No flavors, no per-node file copies, no environment layer. A module's definition is one set of
files; a node's difference is data in its layer, visible by asking.
- The settings record proposed alongside the container-runtime records is on the critical path
of every module with a knob, and this record depends on it shipping as proposed.
- A kept region is the only place a person edits a managed file, and the file says where it is.
The operator's own prompt customisations, aliases and window rules live there.
- What got harder: a change that is neither a setting the module declared nor the operator's own
lines has no home, and is refused by the mechanism rather than silently kept. That is the point.
## How it is checked
| Rule | Checked by |
|---|---|
| A setting reaches only the file its declaration names | the controller's settings tests, once the proposed record ships; issue 168 closes on it |
| A kept region survives a push with its content and is given back on undeclare | the host's write-into tests (ADR 0102), with a region declared by an environment module |
| An edit outside a region does not survive a push | the same tests, asserting the file equals the composed content outside the region |
| Every effective value names its source | `mesh-controller.settings` and the module's own `show-config` tool |
## References
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/01-the-intended-behaviour.md) §"One default, varied by settings, never by edits"
- [ADR 0011](0011-managed-files-are-generated-never-edited.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md),
[issue 168](../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)
@@ -1,122 +0,0 @@
---
topic: what runs on it
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
---
# 175. One tool runtime per node serves every module's tools, on the host side
## Context
A module's tools are code the module wrote, one function behind each verb, served on the subjects
the controller issues in the module's membership
([ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)).
What *runs* that code is [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md):
a supervised process per module under the module's own account, and in the catalogue as built,
that process is a container per module per node, built on the tool runtime's base image.
Measured on the live mesh ([research 018](../01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md)):
67 module tools, each served from its module's container; the packet-filter seat's three verbs
served by a container with `NET_ADMIN` on every one of four machines, for a module that is
otherwise a package, three files and a service; and the console, a container per node, calling
everything and serving nothing. The operator's environment adds a dozen modules of the
packet-filter shape, and the operator's judgement is plain: *I would never run MCP tools inside
a container; that is a very bad design.* And: *I don't care about permissions or account per
module, that just complicates things for no good reason. Just a node-level tool executor. If a
command needs root, that's the module's concern.*
The tool runtime itself was written for this. Its own description: *the per-node process that
makes a module's tools actually serve — imports the assigned modules' compiled tool entrypoints,
each of which registers its tools as it loads; on a node the host resolves the list and starts it
like any other supervised workload.* What the catalogue did instead was build one image per module
around it.
## Considered Options
1. **Keep a process per module.** Rejected: one container per module per node for software that
is not a container, and the account-per-module invariant it exists to protect is one the
operator declines to pay for.
2. **The host executes tools itself.** Rejected: the host is a static Go binary that loads no
plugins; a module's tools are TypeScript on the SDK, and building a second SDK in Go for the
host's sake is the cost ADR 0039 refuses.
3. **One tool runtime per node, a sibling of the host, loading every assigned module's bundle.**
Chosen. It is what the runtime was written to be.
## Decision
**1. One tool runtime per node, supervised by the host, on the host side — never a container.**
The host starts it the way the launcher starts the host
([ADR 0005](0005-the-node-host.md)): a process on the machine, restarted when it dies. It holds one
bus credential, the node's. It is module-agnostic: it knows bundles and subjects, nothing of what
any module does.
**2. It serves every assigned module's tools and every held seat's verbs** on the subjects the
memberships issue. ADR 0159 and ADR 0160 are unchanged in what they say about subjects, grants
and memberships; what changes is that one process on the node subscribes to all of them instead of
one process per module. A module that runs a long-lived service of its own — a daemon, a
container — keeps it; this record is about tools.
**3. A module brings its tools as a bundle**, the artifact kind the catalogue already has for
interpreted code, built by the pipeline and delivered to the node by the host as it delivers any
artifact. Never an image. The runtime loads each bundle as the membership names it, and a push
that adds or replaces a bundle reaches a running runtime as a reload.
**4. Root is the module's concern.** A tool that must change the packet filter or rebuild boot
images escalates itself. The runtime does not run as root for everyone's sake; the caller does not
know and need not.
**5. Any node may call any tool on any node.** The runtime's credential may call everything, as
the console's already may. A per-module calling grant is not kept.
**6. The console is this runtime's serving mode, renamed.** [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)
stands in substance — a module assigned per node, MCP on the machine's loopback, the machine's
login is the authority — and changes in form: host-side, serving as well as calling, and named for
what it is: **node tools**. The mesh's own verbs stay with the controller
([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)); a mesh-scoped seat's verbs
run on the node that holds it ([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)).
**Where ADR 0047 and ADR 0150 say a module's tools are served by the module's own process under
the module's own account, read this record.** Everything else they decided stands: a tool is served
on its own subject, only the module that serves it answers, a module's long-lived processes are the
machine's to supervise. The invariant 0150 kept — one account per module — no longer holds for
tools, and the reason is stated above: every tool is callable from everywhere by decision 5, so the
account no longer scopes anything a caller cannot already reach.
## Consequences
- The packet-filter module's container goes; its verbs run on the host side and escalate as they
need. [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md) §3's container capability is moot
for it.
- The tool runtime's base image stays the way a module's *service* may be built; it is no longer
the way tools reach a node.
- The node tools runtime needs an interpreter on the machine. The module that is the runtime
declares it as a package.
- The container-runtime seat proposed in an open change says its holder *runs as a supervised
process and serves the verbs locally to the host and on the bus*. A supervised process serving
verbs is what this runtime is; whether that holder keeps a process of its own or serves through
the runtime is for that record's build to say.
- What got harder: one process carries every module's tool code on a node, so one module's
faulty bundle can take down the node's tools. The runtime loads each bundle guarded and names
the one that failed; the others serve.
## How it is checked
| Rule | Checked by |
|---|---|
| The runtime loads every bundle its memberships name and serves each tool on its subject | the runtime's tests against a real bus: two bundles, three tools, each answers |
| A bundle that fails to load is named and the others serve | the same tests, with one bundle that throws on load |
| The host supervises the runtime and restarts it | the host's tests over the launcher's shape |
| A push that replaces a bundle reloads it without a restart | the runtime's tests: a bundle replaced on disk, the membership re-read, the new tool answers |
| No module in the catalogue declares a container whose only purpose is tools | a catalogue check: a manifest with `tools` and an image artifact built on the tool runtime's base is refused once the runtime is live |
| Live | `login-shell.execute@<node>` answers on every node from the node tools runtime; `docker ps` shows no per-module tool container |
## References
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md)
- [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md), [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md),
[ADR 0152](0152-the-operators-surface-is-a-module-the-console.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md),
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
- [To-be 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md), [to-be 34](../03-DESIGN/01-to-be/34-the-console.md)
@@ -7,9 +7,7 @@ reconstructed: false
extends: 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md
---
# 180. The found front end is uninstalled once a machine is converged
> **Renumbered 2026-10-02.** Written and merged as 0175 while another record already held that number on main (one tool runtime per node, merged minutes earlier); `cycle.py` refused main. The branch that lands last renumbers: 0178 and 0179 are claimed by open changes, so this is 0180. Nothing cited it by number.
# 175. The found front end is uninstalled once a machine is converged
## Context
@@ -1,81 +0,0 @@
---
topic: what runs on it
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
---
# 176. The login shell is a node seat held by one shell module, and `execute` is its contract
## Context
[ADR 0040](0040-what-a-module-is.md) names the shell as its example of a *shared* seat: bash, zsh
and fish all join `shell`, and one may be default. The operator's reading is sharper, and it
matches [ADR 0126](0126-a-module-declares-its-own-seats.md) better: *installing* a shell is
installing software, and several may be installed; *holding* the seat is being the login shell,
which a node has exactly one of. A definition says which seats a module can hold; the assignment
says which it does.
A seat carries the tools its holder must serve ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)),
and [to-be 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) leaves which verbs each
seat serves as a decision per seat, taken slowly. This is the first seat of the operator's
environment, and the one every node has.
## Considered Options
1. **A shared `shell` seat with a default**, as 0040's example reads. Rejected: *default* is a
second concept beside *holder* for the same fact, and the `user` shape already makes the
login shell declared state ([to-be 05](../03-DESIGN/01-to-be/05-the-node-host.md)).
2. **No seat; each shell module sets the login shell for itself.** Rejected: two assigned shell
modules would fight over `chsh`, and nothing would say which won.
3. **An exclusive node-scoped seat, `login-shell`, declared by the shell modules, held by one
per node.** Chosen.
## Decision
**1. `login-shell` is a node-scoped seat declared by the shell modules.** zsh, fish and bash each
declare that they can hold it; a node's assignment says which does; the controller refuses a
second holder by name as for every seat. A shell module that is assigned without holding the seat
is installed and nothing more.
**2. Holding the seat sets the account's login shell.** The holder's declaration carries the
`user` shape with the shell it provides, so the login shell is declared state the host applies and
gives back when the holding moves — `chsh` stops being a hook.
**3. The seat's contract is `execute`.** One verb, one argument, the command, run on the node the
seat is scoped to as the operator account, answering with what it printed and how it exited.
Every holder serves it; a holder may serve its own tools beside it
([ADR 0170](0170-the-firewall-seat-serves-its-verbs.md) §2) — show the rendered configuration, list
the plugins, set a prompt value.
**4. Any node may call it on any node.** The grant is the node tools runtime's
([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) §5):
*run `uptime` on every node* is five calls to one verb.
## Consequences
- The first environment module is a shell: a package, files under the home owned by the account,
a seat declaration and claim, a `user` shape, and one tool. It proves the whole pattern on every
node, servers included, before anything graphical is written.
- ADR 0040's shell example is read as *installed is not holding*; a dated note in that record says
so. Its decision is untouched.
- `execute` is a shell on every machine, addressed over the bus. That is the point, and it is
the widest verb the mesh serves; it exists because the operator decided every node may call
every tool, and this record does not narrow that.
## How it is checked
| Rule | Checked by |
|---|---|
| Two shell modules assigned to one node, one holding: one `user` shape in the declaration, naming the holder's shell | the controller's composition tests |
| A second claimant is refused by name | the catalogue's seat tests |
| `execute` runs as the account and answers output and exit status | the module's tool tests over a fake runner, and live on every node |
| The seat's verb appears with its scope and machine in the node tools listing | the runtime's tests (to-be 33 §4) |
## References
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md)
- [ADR 0040](0040-what-a-module-is.md), [ADR 0126](0126-a-module-declares-its-own-seats.md),
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)
@@ -7,7 +7,7 @@ reconstructed: true
extends: 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
---
# 181. The operator account is a node fact, and a home is a placement root
# 176. The operator account is a node fact, and a home is a placement root
*Reconstructed. The controller shipped this on 2026-09-27 and
[to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) recorded it as built without a
@@ -112,7 +112,7 @@ anything.
a login name may not be in a definition
- [ADR 0120](0120-a-roster-fact-carries-its-format-as-a-template.md) — the roster fact a home file
may be
- [ADR 0182](0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md) — what
- [ADR 0177](0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md) — what
the mesh may and may not do inside the home this record lets it reach
- mesh-controller `internal/inventory/nodes.go` (the account and its home on the node record),
`internal/catalogue/machine_into_files.go` and `roster.go` (resolution and the home fact)
@@ -1,80 +0,0 @@
---
topic: what runs on it
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
---
# 177. A unit may be user-scoped, and the service manager is a node seat whose holder answers for the units
## Context
The host's `service` shape puts a system unit into a state. It has no user scope.
[To-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) states the gap: *a
workstation's per-user daemons have no form the mesh can send.* Four of the predecessor's
environment modules ship user units — the desktop's reload watcher and bar watchdog, the audio
module's masks, the power module's memory guard, the thermal daemon's profile switcher — and the
predecessor needed a hook to enable them because *shipping a unit file does not run it*; one unit
was deployed for months and ran on one machine only.
[ADR 0040](0040-what-a-module-is.md) says the host hardcodes no supervisor, and a swappable
machine mechanism is a module implementing a capability — which is what the nftables module is for
the packet filter ([ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)). The service manager is
reported today as a capability, `service-manager`, and held by nobody. The operator's proposal: a
systemd module that holds the seat and serves the tools about units, system and user.
## Considered Options
1. **Keep user units as a module concern** — each module runs `systemctl --user` in a hook.
Rejected: that is the hook that silently never ran, and an action over the link is refused.
2. **The service-manager module applies units** on behalf of others, as a provision. Rejected by
the operator: provisioning is for resources a provider creates for a consumer; a unit is
declared state the host applies, as every resource is.
3. **The host's `service` shape gains a user scope; a systemd module holds the service-manager
seat and serves the verbs about units.** Chosen.
## Decision
**1. The `service` shape gains `scope`: `system` (the default) or `user`.** A user-scoped unit
is applied as the operator account through the account's own service manager: enabled, started,
stopped, reloaded on its triggers, exactly as a system unit is, and refused on a node with no
account, naming the fact. The host applies it; no module does.
**2. `node-service-manager` is a seat of the mesh's own, node-scoped**, seeded by the controller
under this record, as ADR 0121 requires of a `node-*` name. The `systemd` module claims it and is
assigned to every machine whose profile reports `service-manager`.
**3. The seat's verbs answer for every unit on the machine**, each taking an optional `scope`:
`units`, `status`, `start`, `stop`, `restart`, `enable`, `disable`, `journal`. The host applies what
is declared; the holder answers questions and operator acts about it, and says, for a mesh-held
unit, that the host will restore what its declaration says.
## Consequences
- The host's vocabulary grows by one field on one shape, asserted by its count test
([to-be 05](../03-DESIGN/01-to-be/05-the-node-host.md)); an older host refuses a declaration
carrying it, so the host rolls before the first module that uses it.
- The predecessor's four user-unit modules become declarable without a hook.
- The seat's holder is the first system seat held by a module that runs nothing of its own: its
verbs are served by the node tools runtime ([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
- What got harder: `journal` and `status` on a user unit need the account's manager reachable
from the runtime's process, which runs as the node's account; the holder's tool escalates or
switches user as it needs, which is ADR 0175 §4 applied.
## How it is checked
| Rule | Checked by |
|---|---|
| A `service` with `scope: user` is enabled and started under the account, and refused with no account | the host's tests with a fake service manager |
| The seat declares its verbs; a claim serving fewer is refused by name | the catalogue's seat tests |
| The verbs act on a named unit in the named scope and name the unit's holder when the mesh declares it | the module's tests over a fake runner |
| Live | the desktop's reload watcher declared `scope: user` on a workstation; `node-service-manager.status@<node>` reports it active |
## References
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md)
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md),
[ADR 0040](0040-what-a-module-is.md), [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)
- [To-be 05](../03-DESIGN/01-to-be/05-the-node-host.md), [to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md)
@@ -7,11 +7,11 @@ reconstructed: false
extends: 02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md
---
# 182. Inside a home, the mesh owns the directory and the files it places, writes into the tool's own files, and holds everything else as found
# 177. Inside a home, the mesh owns the directory and the files it places, writes into the tool's own files, and holds everything else as found
## Context
[ADR 0181](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md) lets a module
[ADR 0176](0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md) lets a module
place files under a person's home. A home is unlike any directory the mesh has written into so far:
it is shared with the person, and with every program the person runs. The agent's configuration
directory on the laptop makes the point. On 2026-10-02 it holds thirty entries. The predecessor placed
@@ -62,7 +62,7 @@ declared**, not inferred from what happened to be on disk:
|---|---|---|
| **owned** | a file with content, or a roster fact | written whole, regenerated, removed when undeclared; a file found there with no record of the mesh making it is kept once before it is written over ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)) |
| **written into** | a file written *into* a structured document | only the keys the definition names are set, every other key is kept, and each set key is given back when undeclared (ADR 0102). The key list is the module's and is short |
| **written by the module's own process** | nothing the host applies: the module's code writes it from what it was handed | the file's content is never a declared file's content, because a declaration travels in the clear on the bus and the host records it; the module's code writes it, owned by the account, atomically. [ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) says how for a credential |
| **written by the module's own process** | a secret the mesh delivers to the module, and a step that writes the file from it | the file's content is never a declared file's content, because a declaration travels in the clear on the bus and the host records it; the module's process writes it, owned by the account, atomically. [ADR 0178](0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md) says how for a credential |
| **found** | nothing | never read, never rewritten, never removed. The person's memory, history, projects, local settings, their own rules and skills |
**A file the tool writes for itself is written into, never over.** The agent's settings file and its
@@ -0,0 +1,142 @@
---
topic: what runs on it
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0024-model-access-is-a-provision.md
---
# 178. The mesh binds and delivers a licence; the module alone writes the tool's credential; a switch is the binding changed, asked for through the console
## Context
**The operator's stance, set on 2026-10-02:** the controller has no part in the agent module. The
module owns the agent's directory under the operator's home and every related file, handles them
itself, and carries a licence-switching function as the predecessor's did.
**What the predecessor's switching actually was.** A registry of accounts held server-side; a tool,
callable from a session, that decrypted the chosen account's token on the server, refreshed it if near
expiry, and wrote the agent's credentials file on the target node — never returning the token. Beside
it, a shell helper that ran the agent with a token read from a plaintext file in the operator's own
configuration directory, one token per account, on every workstation; and an enrolment helper that
logged in once in a throwaway home and registered what came out. So the central half did the
refreshing and the writing; the node held nothing it could refresh with; and the convenience path kept
every account's token readable on disk wherever it was wanted.
**What the mesh has.** [To-be 14](../03-DESIGN/01-to-be/14-model-access.md) is built as far as it goes:
a licence is a named record with a vendor; a consumer is a module on a node and is put on one licence;
the access token is sealed per holder and delivered to the holder's machine; for a refreshable grant
the manager node alone holds the refresh token, encrypted, and refreshes centrally — the one stated
carve-out of [ADR 0050](0050-model-access-is-vendor-agnostic.md). The controller has commands to add a
licence, put a consumer on it, release it, accept a key, set a manager, set and refresh a grant. **None
of them is a verb on the `mesh-controller` seat**, so none can be asked for through the console
([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md) exposes eighteen commands and
not these). The catalogue has the manager module and a consumer module that already writes an
access-token-only credentials file at a path it is told; both are assigned to nothing.
**Why refresh is central and must stay so.** A refreshable grant rotates its refresh token on use. Two
machines each refreshing one account's grant race: the second refresh presents a token the first
retired. The predecessor refreshed centrally for this reason, and ADR 0024 kept that half on purpose
(*the hard half of this already — and it works*). ADR 0050 narrowed the consequence to one node.
**The two designs are not in conflict, and the line has to be drawn in a record.** The controller
resolving *whose* home a file lands in ([ADR 0176](0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md))
and *which* licence a consumer holds (to-be 14) is what the controller does for every module. "No
part" cannot mean that, or the module could not be assigned. It can mean — and this record says it
means — that **the controller learns nothing about the agent**: no file shape, no path, no key, no
word beyond the vendor adapter it already has.
## Considered Options
1. **The module keeps its own registry of accounts and tokens**, the stance read literally. Rejected: a
second secret store outside the vault ([ADR 0113](0113-the-vault-makes-every-secret.md)); a refresh
token on every workstation, widening ADR 0050's one-node carve-out to every machine a person sits
at; and two records of one licence, which drift.
2. **The agent refreshes itself**: the mesh delivers a full grant once at a switch and the agent's own
refresh keeps it alive. Rejected: the refresh race above, between the agent and the manager and
between two machines on one account; and every node then holds a refresh token, which ADR 0050
decided no node does.
3. **The mesh binds and delivers; the module writes; a switch is the binding changed, asked for through
the console.** Chosen.
## Decision
**The consumer is the module on the machine: the operator's interactive sessions on that node, under
that account, hold one licence at a time.** That is to-be 15's `(node, module)` identity, with the
agent module as the module. Two machines may hold different licences, the ordinary case. The mesh's own
sessions on a machine are other modules and hold theirs in their own right.
**The mesh delivers; the module writes.** The module requires `model-access`. The mesh resolves the
licence the consumer is on, delivers the access token sealed to the machine as a secret in the module's
own state, and delivers the non-secret facts — the licence's name, what it serves — beside it. **The
module's own process writes the agent's credentials file** from the delivered secret: under the
account's home, owned by the account, readable by nobody else, written atomically, and access-token-only
— a refresh token found there is removed, because a node never holds one (ADR 0050). The file's content
is never a declared file's content ([ADR 0177](0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)).
The step runs when the delivered secret changes and on a schedule as a backstop, so a refreshed token
reaches the file without anyone asking.
**The controller learns nothing about the agent.** Where the file is, what shape it has, what the
agent calls its keys, how it is told about the console — all of that is the module's definition and
code. The controller contributes the facts it contributes to every module: the account, the home, the
licence, the delivery.
**A switch is the binding changed.** Putting the consumer on another licence is the mesh's existing
act — *use this licence, for this consumer* — and it becomes a verb on the `mesh-controller` seat the
way the other verbs did (ADR 0154): the command it already has, served on the bus, listed by the
console. The module serves two tools of its own: one that reports which licence the machine holds and
when its token expires, and one that invokes the seat's verb for a named licence and then waits until
the credentials file carries the new licence's token, answering with the licence's name — **never the
token, in any answer, log or event**. A skill in the agent's directory wraps the second so a person
asks in a sentence. Switching remains a reaction, not a declaration (ADR 0024): a person asks for it,
and nothing in the declaration language grows a conditional.
**Enrolling an account is the mesh's act on the manager node.** A new licence is added by name, its
grant obtained by a login in a throwaway home on the manager node and adopted sealed to that node's
key, as the manager module already does. No token is pasted into a prompt, printed, or passed as an
argument (to-be 14's rule for keys).
**The shell helper that read tokens from a file is retired, not replaced.** A second concurrent
session on the same machine under a different licence would need a second consumer identity — the
unbuilt half of to-be 14's gap — and is not provided here. Stated so it is not rediscovered as a bug.
**A licence the mesh no longer grants is withdrawn** at the binding (ADR 0024). The credentials file
the module wrote is the module's own output: unassigning the module leaves it, like the agent's other
files, and the access token in it expires within hours. Releasing the consumer from the licence is the
act that ends its access.
## Consequences
- The agent on a workstation authenticates with a token the mesh delivered and refreshes centrally,
and no workstation holds a refresh token or any other account's token.
- **The manager must run.** The refresh path exists in the catalogue and is assigned to nothing; it is
a prerequisite of this record, on the control node, and the first thing the build proves.
- **The controller gains a verb, not knowledge.** The licence commands become seat verbs, each
running the command it names, as ADR 0154 did for the others; nothing in them is about the agent.
- A switch is a round trip — binding, composition, push, apply — rather than the predecessor's direct
write: seconds to a minute, and reported when done rather than assumed.
- **What got harder:** running two sessions on one machine under two accounts at once, which the
retired helper allowed by keeping tokens readable. The price of not keeping them so.
## How it is checked
| Rule | Checked by |
|---|---|
| The module's definition declares no secret in a file's content, and requires `model-access` | a catalogue test on the module's definition |
| The credentials file is access-token-only, owned by the account, atomic | the consumer module's existing unit tests on the strip and the write, carried into this module; a live check that the file names no refresh token |
| A switch through the console changes the licence and the token, and no answer carries a token | a live check: the bound facts name the new licence, the file's fingerprint changes, the tool's answer and the module's log contain neither token |
| The controller's licence verbs run the commands they name and carry no agent vocabulary | the seat verb's test, as for the eighteen before it |
| The refresh path is live before the module is | the manager assigned on the control node and a refresh observed in the licence's record, before the module's first assignment |
| No workstation holds a refresh token | the live check above, on every machine the module is assigned to |
## References
- [ADR 0024](0024-model-access-is-a-provision.md), [ADR 0050](0050-model-access-is-vendor-agnostic.md),
[ADR 0055](0055-model-access-is-answered-by-a-licence-or-a-node.md) — what a licence is, who refreshes, what answers
- [to-be 14](../03-DESIGN/01-to-be/14-model-access.md), [to-be 15](../03-DESIGN/01-to-be/15-the-agent-session.md) — the consumer identity and the gap this leaves where it is
- [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md), [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — how a verb reaches a person
- [ADR 0113](0113-the-vault-makes-every-secret.md) — why there is no second registry
- [ADR 0177](0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md) — the class the credentials file is in
- the predecessor's `claude-code` module: its switch tool, its shell helpers and the rules of its skill
- mesh-catalog `modules/anthropic-manager`, `modules/anthropic-consumer` — the refresh and the write, as built
@@ -1,163 +0,0 @@
---
topic: what runs on it
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0024-model-access-is-a-provision.md
---
# 183. The Anthropic licence manager is a module holding a seat; it hands each node's agent its token over the bus, sealed; the controller and the host have no part
## Context
**The operator's stance, set on 2026-10-02 and sharpened during the day.** The controller has no part in
the agent module. The host is module-agnostic: it knows no vendor, no agent, no path under a home. The
agent module owns its own files. And there must be a *real* licence manager — a module that doles out
the correct licence in every situation the mesh has: two subscription accounts and one API key today,
used by a person's interactive agent on each workstation, by the mesh's own sessions, and by workers.
**What the predecessor built, read from its code the same day.** Two modules, split after an incident.
A *manager* on exactly one node held every account's full OAuth grant encrypted, rotated each grant
under a per-licence lease on a cadence and an expiry floor, published each rotation over its bus with
the tokens encrypted, collected the vendor's usage figures per licence, and alerted once a day on
repeated failure or on a refresh token within three days of its own expiry. A *consumer* on every node
was the single writer of the agent's credentials file: it applied a published rotation, stripped the
refresh token so a node could never rotate, pulled when stale, refused a stale grant by comparing
expiries within one lineage, and mirrored a local login back to the manager only after checking the
account's identity against the licence's record — because an unchecked mirror had once written one
account's grant into another's row and published it mesh-wide. Three **touchpoints** with fallbacks: the
node's interactive agent; the mesh's own sessions on the node, falling back to the node's licence; a
worker's own account, falling back to the node's, and refusing to spawn when assigned a licence that
could not be served. The split exists because four nodes refreshing one grant destroyed it: an OAuth
refresh rotates the refresh token, and the predecessor's own code records both that a reused token
killed a licence and that a malformed client id was once misdiagnosed as the same fault. **Whether a
refresh token is single-use is not documented by the vendor**; the predecessor treated it as so, and
this record keeps one rotation source for that reason while leaving the fact to be measured.
**What the mesh has.** [ADR 0050](0050-model-access-is-vendor-agnostic.md) put a per-vendor adapter
inside the controller's licences context, with the carve-out that the manager node holds the refresh
token readably; the catalogue has a manager and a consumer module built on it, assigned to nothing. The
controller's licence commands are not seat verbs and cannot be asked for through the console
([to-be 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)). `model-access` is a vendor-blind
provision ([ADR 0024](0024-model-access-is-a-provision.md)), and the operator's judgement is that the
agent is not a vendor-blind consumer: it is coupled to an Anthropic subscription grant and nothing else,
so a name that hides the vendor misdescribes the coupling
([ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md)).
**The bus's rule for a secret** ([to-be 32 §10](../03-DESIGN/01-to-be/32-what-a-module-declares.md)): the
bus is not trusted with one; a secret travels sealed to its recipient, on core request/reply, never
through a stream that persists it.
## Considered Options
1. **Keep the lifecycle in the controller** ([ADR 0050](0050-model-access-is-vendor-agnostic.md) as
built), and make the agent module a consumer of `model-access` delivered by the host as a sealed
file. Rejected by the operator: the controller and the host would both carry a part of an
Anthropic-specific mechanism, and the agent's coupling is misnamed.
2. **The manager delivers each short-lived token through the vault**, as a backend-issued secret the
vault provides to each consumer ([ADR 0113](0113-the-vault-makes-every-secret.md)). Rejected: every
hourly rotation becomes a vault delivery, a composition and a push to every node, and the host
ends up writing a vendor's credential as a file — the module-agnostic host, carrying a vendor's
traffic.
3. **A seat-holding manager module that talks to the agent module on every node over the bus.**
Chosen.
## Decision
**The Anthropic licence manager is a module, `claude-licence-manager`, holding the mesh-scoped seat
`anthropic-licence-manager`.** The seat's contract is the licence verbs: list the licences and their
health, list the bindings, bind or switch a consumer, release one, refresh now, read usage, adopt a
grant, register a node's key, answer a consumer's current token. One holder, on a node the operator
assigns, is what makes rotation happen once ([ADR 0126](0126-a-module-declares-its-own-seats.md),
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)). The seat is named for the vendor,
because what it manages is one vendor's grants and nothing else is coupled to it. The vendor-blind
`model-access` provision stands for the consumers that do not care which vendor answers; the agent is
not among them.
**The manager owns the licences.** The records, the grants, the bindings per touchpoint, the usage
readings and the audit of every switch live in the manager's own store, not in the controller's
licences context, which keeps only what it already serves to vendor-blind consumers. The manager is the
one rotation source: it alone calls the vendor's token endpoint, under a lease per licence, on an expiry
floor and a cadence it declares as a setting.
**The long-lived grants are encrypted at rest with a key the vault made for the manager.** The vault
keeps custody of that one key as the manager's own secret ([ADR 0113](0113-the-vault-makes-every-secret.md));
the grants themselves — a refresh token per subscription account, the API key — are the manager's
rows, readable only by it. This is [ADR 0050](0050-model-access-is-vendor-agnostic.md)'s carve-out,
moved with the manager: *one module, one node, the long-lived grants only.*
**The short-lived tokens travel module to module, sealed, on request/reply.** The agent module on each
node makes a keypair of its own when it first runs — a private key made where it is used, never leaving
([ADR 0113](0113-the-vault-makes-every-secret.md)) — and registers its public half with the seat. The
manager hands a node its token by calling that node's agent module (`<module>.<tool>@<node>`,
[ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)) with the token
sealed to that key, and the module answers *applied* or *refused* and why. An agent module that starts,
or finds its token near expiry, asks the seat for its current token the same way. **A token is never
published as an event**: what the manager emits — rotated, switched, failing, usage read — names the
licence and nothing secret, and the audit logger records it. This is a second channel for a secret
beside the vault's, and it is bounded as 0050's carve-out is: this vendor, tokens that live hours, sealed
to one recipient, request/reply only.
**The agent module alone writes what the agent reads.** For a subscription licence it writes the
agent's credentials file under the operator's home, as the operator, access-token-only, atomically. For
the API-key licence it serves the key through the agent's own key-helper setting, so nothing is written
under the home at all. For the mesh's own sessions and workers on that node, it is the local source of
their token. **The host delivers the module's package and its state directory and knows nothing else**:
no path under the home, no vendor, no file shape.
**A binding is explicit, and a switch is a reaction.** Every consumer — a node's interactive agent, the
mesh's session on a node, a worker — is bound to a licence by the operator through the seat's verb, with
the predecessor's fallbacks: a session inherits its node's licence, a worker inherits its node's, and a
worker assigned a licence that cannot be served is refused rather than lent another. Exhaustion is
observed and warned about once per crossing of a declared threshold; moving a consumer to another
licence is a person's act through the seat's verb, as [ADR 0024](0024-model-access-is-a-provision.md)
says, and the declaration language grows no conditional. An automated policy is not decided here.
**A login is attributed only to the account it belongs to.** When a person logs in on a node, the
agent module reads the account's identity from the agent's own state and offers the grant to the
manager sealed to the manager's key; the manager adopts it only when the identity matches the licence
the node is bound to, and refuses with a notification otherwise.
## Consequences
- One module decides which licence every consumer gets, one module writes what each agent reads, and
neither the controller nor the host carries a word of the vendor.
- **A second sealed channel exists** beside the vault's, bounded as stated. A record that widens it to
another vendor or a longer-lived secret is a new decision, not an application of this one.
- The catalogue's `anthropic-manager` and `anthropic-consumer` modules, built on ADR 0050's placement,
are retired once the manager runs; the controller's licences context stops holding Anthropic licences.
- The console lists the seat's verbs, so a person switches a licence in a sentence, and the controller
gains no `licence` verb.
- **What got harder:** a manager that is down leaves every node on its last token until it expires;
the agent module keeps the last token and says so. And a node whose agent module has not registered
its key cannot be handed a token, which the manager reports by name.
- Every interactive session on a machine shares the node's one agent directory, and so its licence;
twenty sessions share it as one does. A consumer with a licence of its own on the same machine is a
worker running from a home of its own with its own agent directory — the worker touchpoint above, for
when workers exist ([ADR 0003](0003-agents-are-persistent-employees.md)); the predecessor ran its
agents that way.
- **Not decided here:** an automated switch on exhaustion; whether a refresh token is single-use, to be
measured in the lab.
## How it is checked
| Rule | Checked by |
|---|---|
| Only the seat's holder calls the vendor's token endpoint | a catalogue test: no module but the manager names it; the manager's refresh runs under a lease per licence, tested with two concurrent runs |
| A token crosses the bus only sealed, only on request/reply | a bus test: every message the manager publishes as an event carries no token; the hand-over is a request whose payload opens only with the receiving module's key |
| The agent module's private key never leaves the node | the per-key test of ADR 0113, extended to this module's key |
| The host writes nothing under a home and names no vendor | a catalogue test on the agent module's definition: no file resource under a home, no vendor word in anything the host applies |
| A grant is attributed only to a matching identity | a manager test: a grant whose account identity differs from the bound licence's is refused and a notification emitted |
| An unservable binding refuses rather than lends | a manager test: a worker bound to a dead licence is answered with a refusal, never another licence's token |
| A switch through the console changes the token on the node and nothing in the answer is a token | a live check on one workstation |
## References
- [ADR 0024](0024-model-access-is-a-provision.md), [ADR 0050](0050-model-access-is-vendor-agnostic.md) — the licence as a named thing, the carve-out this moves with the manager
- [ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md) — why the seat is named for the vendor
- [ADR 0113](0113-the-vault-makes-every-secret.md) — the vault's custody of the manager's key, and the exception stated here
- [ADR 0126](0126-a-module-declares-its-own-seats.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md) — a module's seat, its verbs, a call addressed to one machine
- [to-be 32 §10](../03-DESIGN/01-to-be/32-what-a-module-declares.md) — a secret on the bus
- [to-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md), [to-be 39](../03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md) — the two modules
- the predecessor's `claude-licences` and `claude-code` modules, read 2026-10-02: the lease, the floor, the lineage comparison, the identity guard, the touchpoints
+4 -9
View File
@@ -182,7 +182,7 @@ python3 00-META/checks/index.py fail if stale
- **0169** — [A machine joins through the tunnel, and the bus is never public](0169-a-machine-joins-through-the-tunnel-and-the-bus-is-never-public.md)
- **0170** — [The firewall seat serves its verbs, and a foreign rule set is removed through one of them](0170-the-firewall-seat-serves-its-verbs.md)
- **0172** — [The lab is a module, and runs a bed when the mesh asks](0172-the-lab-is-a-module-and-runs-a-bed-when-the-mesh-asks.md)
- **0180** — [The found front end is uninstalled once a machine is converged](0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md)
- **0175** — [The found front end is uninstalled once a machine is converged](0175-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md)
### Its tiers, from the bottom up
@@ -273,13 +273,9 @@ python3 00-META/checks/index.py fail if stale
- **0150** — [A module's own code runs as supervised processes under the module's one account](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)
- **0152** — [The operator's surface is a module the mesh assigns: the console](0152-the-operators-surface-is-a-module-the-console.md)
- **0155** — [A definition names no installation: how that is checked, and the three ways a value that did gets out](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
- **0173** — [The operator's machine is the mesh's, and a module is whatever it declares](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md)
- **0175** — [One tool runtime per node serves every module's tools, on the host side](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)
- **0176** — [The login shell is a node seat held by one shell module, and `execute` is its contract](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)
- **0177** — [A unit may be user-scoped, and the service manager is a node seat whose holder answers for the units](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)
- **0181** — [The operator account is a node fact, and a home is a placement root](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)
- **0182** — [Inside a home, the mesh owns the directory and the files it places, writes into the tool's own files, and holds everything else as found](0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
- **0183** — [The Anthropic licence manager is a module holding a seat; it hands each node's agent its token over the bus, sealed; the controller and the host have no part](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
- **0176** — [The operator account is a node fact, and a home is a placement root](0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)
- **0177** — [Inside a home, the mesh owns the directory and the files it places, writes into the tool's own files, and holds everything else as found](0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
- **0178** — [The mesh binds and delivers a licence; the module alone writes the tool's credential; a switch is the binding changed, asked for through the console](0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md)
### How it is built
@@ -301,7 +297,6 @@ python3 00-META/checks/index.py fail if stale
- **0107** — [Persistent data is a directory bind, never a named volume](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)
- **0111** — [A build source is on the mesh's git seat, or it is an external repository](0111-a-build-source-is-on-the-git-seat-or-external.md)
- **0149** — [The live mesh is the test bed](0149-the-live-mesh-is-the-test-bed.md)
- **0174** — [A node varies a module through settings and kept regions, never through an edit](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)
### How it is checked
+2 -2
View File
@@ -9,7 +9,7 @@ code:
- mesh-host internal/apply (the service that reflects a rule set)
updated: 2026-10-02
decisions:
- 02-DECISIONS/0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md
- 02-DECISIONS/0175-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md
- 02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md
- 02-DECISIONS/0169-a-machine-joins-through-the-tunnel-and-the-bus-is-never-public.md
- 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md
@@ -773,7 +773,7 @@ the host reports as *other* is reached through the packet filter seat's `remove`
by name on the bus; the seat also serves `rules` and `reload`, and its holder's runtime declares the
`NET_ADMIN` capability on the machine's network. See design 33.
*2026-10-02, [ADR 0180](../../02-DECISIONS/0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md):*
*2026-10-02, [ADR 0175](../../02-DECISIONS/0175-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md):*
once a machine is converged, the front end it was found with is uninstalled, not merely disabled — the
packet filter's holder declares its package absent after the mesh's filter is loaded, and a return to
adopted then enables nothing. The rollback path ADR 0100 kept on disk is given up on purpose.
+7 -8
View File
@@ -7,7 +7,7 @@ code:
updated: 2026-10-02
decisions:
- 02-DECISIONS/0024-model-access-is-a-provision.md
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
- 02-DECISIONS/0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md
- 02-DECISIONS/0009-modules-and-the-graph.md
- 02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md
- 02-DECISIONS/0055-model-access-is-answered-by-a-licence-or-a-node.md
@@ -97,13 +97,12 @@ So `(node, module)` tells them apart, and asking for a licence per session neede
identity. Checked rather than argued: two sessions on one machine hold different licences, each is
given its own key, and releasing one leaves the other.
*2026-10-02:* the operator's own agent at a terminal is **not** a consumer of this provision: it is
coupled to an Anthropic grant and nothing else, so it uses the `anthropic-licence-manager` seat, whose
holder owns the Anthropic licences, their bindings and their rotation, and hands each node's agent its
token over the bus
([ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md),
[36](36-the-operators-agent-on-a-machine.md), [39](39-the-anthropic-licence-manager.md)). This
provision stays for the consumers that do not care which vendor answers.
*2026-10-02:* the operator's own interactive agent on a workstation is a consumer the same way —
`(node, claude-code)`, one licence at a time per machine, delivered by the mesh and written by the
module, switched through the console
([ADR 0178](../../02-DECISIONS/0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md),
[36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md)). The licence commands
become verbs on the controller's seat for it; none was one before.
**What is still open is the rest of the gap, and it is the harder half.** A *worker* is not one
per machine — many can run on one, from one module — so `(node, module)` cannot name them apart
@@ -7,8 +7,8 @@ code:
- 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/0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
- 02-DECISIONS/0177-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
@@ -173,13 +173,12 @@ fact, the home as a placement root, and what the mesh may and may not do under 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)
*2026-10-02:* two of them are written. [ADR 0176](../../02-DECISIONS/0176-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)
[ADR 0177](../../02-DECISIONS/0177-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
designed in [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md). Still
unwritten: user-scope units, 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
@@ -216,10 +215,6 @@ keeping the predecessor running, so the model questions above are no longer defe
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
@@ -122,8 +122,6 @@ module-specific names that changes the day the forge is replaced.
asked of the module, through a `tools` verb every runtime answers) and lists a role's tools when the
records carry them.
*Amended 2026-10-02 by [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md):* the module that serves this to an agent is the node tools runtime — one per node, host-side, serving every assigned module's tools as well as answering the person on loopback. The console is its serving mode, renamed. See [37 — The operator's machine](37-the-operators-machine.md) §3.
## 7. Versioning
A seat's tools are an interface and change like one. Additive within a version. A change that would
+1 -3
View File
@@ -2,7 +2,7 @@
layer: to-be
status: implemented
code: [mesh-catalog, mesh-tools, mesh-controller]
updated: 2026-10-02
updated: 2026-10-01
decisions:
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
@@ -20,8 +20,6 @@ An agent reaches them over MCP on the machine's loopback; a person reaches the s
installed by hand, nothing is configured with an address, and the mesh knows the surface exists because
it put it there ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
> **Amended 2026-10-02 by [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** What this document describes stays true in substance and changes in form: the console becomes the serving mode of the node tools runtime, a host-side process the host supervises rather than a container, which also serves every assigned module's tools from their bundles. The module is renamed `node-tools`. [37 — The operator's machine](37-the-operators-machine.md) §3 is where it now lives.
## 1. What it is
A module, `mesh-console`, in the catalogue. Its image is the tool runtime's own — the client that
@@ -4,209 +4,254 @@ status: designed
code: []
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/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
- 02-DECISIONS/0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
- 02-DECISIONS/0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
- 02-DECISIONS/0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md
- 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
- 02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
- 02-DECISIONS/0050-model-access-is-vendor-agnostic.md
- 02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
---
# 36 — The operator's agent on a machine: the `claude-code` module
**The agent a person runs at a terminal, put on the machine by the mesh, instructed by the mesh, pointed
at the console, and holding the licence the manager hands it.** It is a member of the family
[to-be 29 §2](29-a-node-has-operator-accounts.md) names, the modules that touch a person's machine, and
its counterpart is [39 — The Anthropic licence manager](39-the-anthropic-licence-manager.md).
**The agent a person runs at a terminal, put on the machine by the mesh, instructed by the mesh,
pointed at the console, and authenticated with a licence the mesh delivers.** It is the first member of
the family [to-be 29 §2](29-a-node-has-operator-accounts.md) names — the modules that place files under
an operator's home — and the smallest, so it is where the pattern is proven before the shell, the
terminal and the desktop follow.
What it replaces: the predecessor's module of the same name and a sibling, which placed six files under
the operator's home. The predecessor is retired; the six files are still on both workstations telling
every session to use tools that no longer exist.
What it replaces: the predecessor's module of the same name, which installed the agent's package and
placed five files under the operator's home, and a sibling that placed a sixth. The predecessor is
retired; those six files are still on both workstations telling every session to use tools that no
longer exist. That is the symptom this design answers, and it answers it by making the files a module's
again rather than by editing them.
**Three rules shape everything below.** The host is module-agnostic: it installs the package and gives
the module a state directory, and knows no vendor, no agent, no path under a home. The controller has no
part beyond resolving what it resolves for every module. And the module handles its own files: the
mesh's part of the agent's configuration is written by the module's own code, from what the mesh
delivered it and what the manager handed it.
## 1. What it is
## 1. Where the mesh's configuration lives: the agent's managed directory, not the home
A module, `claude-code`, universal tier: assigned to every node a person logs into, which is every node
with an operator account ([ADR 0176](../../02-DECISIONS/0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)).
It declares the agent's package, owns the agent's configuration directory under the account's home, and
requires two things: `model-access`, for the licence
([ADR 0178](../../02-DECISIONS/0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md)),
and the console on the same machine, for the tools
([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)). It names no
node, no path and no login: the account and its home are machine facts, the node's name is a machine
fact, the node's role is a setting on the assignment, and the console's address is what the console
serves.
The agent reads a machine-wide, administrator-owned configuration directory under `/etc`, documented
by the vendor: a managed settings file that outranks every user and project setting; a key in it that
adds HTTP tool servers *beside* a person's own without blocking them; and a managed instruction file every
session reads before the user's and the project's. The agent has **no** machine-wide directory for
rules, skills, slash commands or hooks; those exist only under a home or a project.
**The controller has no part in it beyond what it has in every module.** It resolves the account, the
home, the licence and the console's port, and delivers them. It holds nothing about the agent: no file
shape, no key name, no path. The one controller change this design asks for is not about the agent at
all — the licence commands become verbs on the controller's seat, as the other commands did
([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).
So the mesh's part of the agent's configuration lives there, **owned whole by the module**, and the home
is left alone. What the predecessor shipped as two rule files and two skills folds into the managed
instruction file and the manager's tools:
## 2. What it owns under the home, and what it leaves alone
| the predecessor placed | becomes |
Every path the module touches is in one of the four classes
[ADR 0177](../../02-DECISIONS/0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
draws, and the class is visible from the shape the definition declares. The agent's directory is
`~/.claude`; its own state file is `~/.claude.json` beside it.
| path | class | declared as |
|---|---|---|
| `~/.claude/` | owned directory | a directory, owner the account, readable by the account alone |
| `~/.claude/CLAUDE.md` | owned | a file: how a session on this mesh works (§4) |
| `~/.claude/rules/00-mesh.md` | owned | a file: this node's identity (§4) |
| `~/.claude/rules/conventions.md` | owned | a file: the rules of the repositories (§4) |
| `~/.claude/skills/mesh-licence/SKILL.md` | owned | a file: the licence skill (§5) |
| `~/.claude/settings.json` | written into | the agent's settings; the mesh's key is `attribution`, and only that (below) |
| `~/.claude.json` | written into | the agent's own state; the mesh's key is the console's entry under the servers the agent speaks to (§3) |
| `~/.claude/.credentials.json` | written by the module's process | a delivered secret and a step (§5) |
| everything else | found | nothing — the person's memory, history, projects, local settings, plugins, their own rules and skills |
**Which keys of the settings file are the mesh's.** A key is the mesh's when it encodes a rule of the
mesh, and the person's when it is a preference. `attribution` — the trailers the agent adds to commits
and pull requests — encodes the repositories' convention and is the mesh's. The model, the spinner, the
drafts, the automation mode and everything else are the person's, and the predecessor's experience with
the model key is the evidence: a mesh that sets a preference reverts a person's choice on every push. A
preference the operator wants on every machine belongs to the family's dotfiles module, not here.
**The agent's own state file is written into for one key.** The agent is told about the console as one
entry among the servers it speaks to, in the file where it keeps that list. Everything else in that
file — the account it is logged in as, its caches, its history of projects — is the agent's, and
ADR 0102's rule is exactly what keeps it: the mesh sets one key and gives it back on undeclare.
## 3. The predecessor's six files
They were placed by a generator that no longer exists; to the mesh they are found. ADR 0177 says what
happens to each kind, and this is the list:
| file | fate |
|---|---|
| `~/.claude/CLAUDE.md` | the managed instruction file: how a session on this mesh works (§3) |
| `~/.claude/rules/00-hal-mesh.md`, `~/.claude/rules/conventions.md` | sections of the same file: this node's identity, the repositories' conventions |
| `~/.claude/settings.json`, merged | the managed settings file: the mesh's keys only, outranking nothing a person did not also set |
| `~/.claude/skills/hal-switch-license/SKILL.md` | the manager seat's `switch` verb, listed by the console, and a sentence in the instruction file saying to use it |
| `~/.claude/skills/cleanup/SKILL.md` | nothing; it named the predecessor's forge |
| the console's entry in the agent's user-scope state | the managed settings' tool-server key, from the console's provision (§4) |
| `CLAUDE.md`, `rules/conventions.md` | **adopted.** The module declares the same paths; the host keeps the found original once and writes the mesh's content ([ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md)) |
| `settings.json` | **written into.** The values the predecessor merged and the person changed since — the model among them — stay; the mesh sets its one key |
| `rules/00-hal-mesh.md` | **removed by the operator, once.** Its successor is `rules/00-mesh.md`; the old name carries the predecessor's and stays otherwise |
| `skills/hal-switch-license/SKILL.md` | **removed by the operator, once.** Its successor is `skills/mesh-licence/SKILL.md` |
| `skills/cleanup/SKILL.md` | **removed by the operator, once.** A repository hygiene skill naming the predecessor's forge and repository; not the mesh's |
**The home.** Under [ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
every path under `~/.claude` is *found*, with one exception: the agent's credentials file, which the
module's own code writes for a subscription licence (§5). The person's memory, history, projects, local
settings, their own rules, skills and tool servers are never read or written by the mesh. **The six
predecessor files are the operator's to remove, once, on each workstation**; the module's documentation
lists them, and until they go the agent reads stale instructions beside the mesh's.
The module's documentation names the three removals, so a person assigning it on a workstation that
carried the predecessor knows the step. On a fresh machine there is nothing to remove.
## 2. What the module declares and what its code writes
**The console's entry changes name.** The agent on both workstations today reaches the console under
an entry named after this installation. A definition names no installation
([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)), so
the module writes the entry as `mesh`, and every tool an agent sees is prefixed accordingly. The
hand-made entry is the person's to remove; until they do, the agent sees the mesh's tools twice.
**Declared, applied by the host:** the agent's package (§7); the module's state directory; a facts file
in that directory carrying the node's name, the operator account, the console's endpoint, the module's
settings; the bus, the console's provision, and that it uses the `anthropic-licence-manager` seat.
Nothing under the home, nothing under `/etc`.
## 4. What the three documents say
**Written by the module's code**, from the facts file and the manager's hand-over, whenever either
changes:
**Prose, not a paste** — the files are the module's; this is what they are for.
| path | content |
**`CLAUDE.md` — how a session on this mesh works.** The console is the only path to the mesh, and its
tools are the vocabulary: the record is asked through the records module's tools, symptom first — the
literal error text before a hypothesis — and that is the *search before you dig* rule rewritten for a
knowledge base that is now the record itself ([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md));
the mesh is asked and changed through the controller seat's verbs — status, plan, assign, push,
settings, and licence once it exists; the forge through the forge module's tools. The hard rules are the
same rules in new words: a file the mesh manages is changed through the verb that owns it or through
the catalogue, never on disk, and `plan` says what the mesh would write; a store's database is never
written by hand; main is never pushed; the mesh creates no symlinks and nobody else does either; a
package is declared, not installed by hand. It uses the glossary's words — controller, foundation,
node, seat, console — and none of the predecessor's.
**`rules/00-mesh.md` — who this node is.** Two facts and one pointer: the node's name, from the
machine; the node's role, from the assignment's settings on this node; and that the other nodes are
asked of the controller's `nodes` verb rather than listed here. The predecessor's rule carried a table
of every node with its public domain and role; a table is a copy that drifts, and the live answer is
one tool call away. No address, no public domain.
**`rules/conventions.md` — the rules of the repositories.** Concise commit messages in the imperative,
focused on why; a branch, a pull request and a human approval for every merge; test before pushing,
because nodes update unattended; follow the playbooks in the record; shared logic in the SDK; the
module repository's rules on manifests. Nothing that names a tool of the predecessor's.
**Where the module gets the name and the role.** The name is a machine fact the controller already
offers a definition. The role is a value a person chooses per node — *the laptop*, *the home-server* —
and is an operator value on the assignment's node layer, refused by name when unset
([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)).
So assigning the module to a node is two acts: the assignment, and the node's role in its settings.
## 5. The licence
[ADR 0178](../../02-DECISIONS/0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md)
decides it; this is the shape.
**The consumer** is `(node, claude-code)`: the operator's interactive sessions on that machine, under
that account, on one licence at a time. The module requires `model-access` and is put on a licence like
the consumer module already in the catalogue.
**Delivery and the write.** The mesh delivers the access token sealed to the machine, as a secret in
the module's own state directory, and the bound facts beside it. A step in the module's own process —
the consumer module's existing write, carried over — reads the secret and writes
`~/.claude/.credentials.json`: owned by the account, readable by the account alone, atomically,
access-token-only. The step names the secret file as what it reads and runs again when it changes
([ADR 0099](../../02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md)), and on a schedule as
a backstop, so a refreshed token reaches the file unasked. It runs in the module's own context and
never as the person.
**Refresh** is the manager module's on the control node, as [ADR 0050](../../02-DECISIONS/0050-model-access-is-vendor-agnostic.md)
built it. It is assigned to nothing today and is the first prerequisite of the build.
**The two tools** the module serves, listed by the console under the module's name:
| tool | answers |
|---|---|
| the managed settings file | the mesh's keys: the tool servers (the console, plus any the operator declared as settings), the attribution trailers, and — for an API-key binding only — the key-helper that serves the key |
| the managed instruction file | §3 |
| the agent's credentials file under the operator's home | for a subscription binding only: the access token the manager handed over, as the operator, readable by the operator alone, atomic, no refresh token |
| the module's keypair in its state | made once, the private half never leaves (§5) |
| `licence_status` | which licence this machine's agent holds, from the bound facts; when its access token expires; whether the file on disk matches what was delivered — by fingerprint, never by value |
| `licence_switch` | asks the controller seat's `licence` verb to put this consumer on the named licence, waits until the credentials file carries the new licence's token, and answers with the licence's name and expiry. Refuses with the mesh's own words when the licence does not exist or the consumer cannot be put on it |
Writing under `/etc` and as the operator under the home are two escalations the module's code performs
for itself; the mesh does not run the module as root for everyone, and the caller does not know
([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
Neither tool, nor the module's log, nor any event it emits, ever carries a token. The module declares
that it invokes the controller seat's `licence` verb, and nothing else.
**Which settings keys are the mesh's.** A key is the mesh's when it encodes a rule of the mesh: the tool
servers that reach the mesh, the attribution convention of its repositories, the key-helper a binding
requires. The model, the spinner, the drafts and every other preference are the person's, and the
predecessor's experience with the model key is the evidence: a mesh that sets a preference reverts a
person's choice on every push.
**The skill** — `skills/mesh-licence/SKILL.md` — wraps `licence_switch` so a person asks in a sentence,
and carries the predecessor's rules unchanged in substance: never ask for or print a token; never edit
the credentials file by hand; the tool writes the file and the record together; with no licence named,
ask rather than guess.
## 3. What the instruction file says
**Enrolling an account** happens on the manager node: a licence added by name, its grant obtained by a
login in a throwaway home and adopted sealed to that node's key, as the manager module does. **The
shell helper** that ran the agent with a token from a plaintext file is retired and not replaced
([ADR 0178](../../02-DECISIONS/0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md)
says why).
Prose, not a paste; the file is the module's.
## 6. The console
**How a session on this mesh works.** The console is the only path to the mesh, and its tools are the
vocabulary: the record is asked through the records module, symptom first — the literal error text before
a hypothesis ([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md));
the mesh is asked and changed through the controller seat's verbs; the forge through the forge module's
tools; a licence through the `anthropic-licence-manager` seat's verbs, never by editing a file. The hard
rules in new words: a file the mesh manages is changed through the verb that owns it or through the
catalogue, never on disk; a store's database is never written by hand; main is never pushed; the mesh
creates no symlinks and nobody else does; a package is declared, not installed by hand. The glossary's
words, none of the predecessor's.
The agent reaches the mesh through the console on the machine's loopback
([to-be 34](34-the-console.md)). The module must tell the agent the console's address, and the port is
the console's to say: today the console's manifest declares it and the host assigns it, and nothing but
the console knows what was assigned. So **the console provides a node-scoped provision** — the MCP
endpoint on loopback — serving its port, and the module requires it. A requirement names what the
consumer is coupled to ([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)):
the agent is coupled to an MCP endpoint on its own machine, not to a module name. Co-location resolves
it, and a machine without the console refuses the agent module by name — which is right, because an
agent without the console is the predecessor's situation again.
**Who this node is.** The node's name, from the facts file; the node's role, from the module's settings
on the node's layer; and that the other nodes are asked of the controller's `nodes` verb rather than
listed here, because a table is a copy that drifts.
This is a change to the console's definition, not to the controller. [To-be 34 §1](34-the-console.md)
says the console has *no provision*; this is the one it gains, at node scope, and the design is amended
in the same change.
**The repositories' conventions.** Concise commit messages in the imperative, about why; a branch, a
pull request and a human approval for every merge; test before pushing, because nodes update unattended;
the playbooks in the record.
## 7. Scope, settings and the order of assignment
## 4. The console
**Every node with an operator account.** None has one today; the operator states them first. A node
with no account refuses the module, naming the fact.
The module tells the agent where the console is, and the port is the console's to say. **The console
provides a node-scoped provision** — its MCP endpoint on loopback — serving the port the machine gave
it, and the module requires it. A requirement names what the consumer is coupled to
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)); co-location
resolves it; a machine without the console refuses the module by name. [To-be 34](34-the-console.md) is
amended in the same change; issue 192 (open) found the gap.
**Per node:** the role, in the module's settings on the node layer. **Per mesh:** nothing.
**Other tool servers** a person wants on every machine, or on one, are a declared setting of this module
— mesh layer or node layer — rendered into the same managed key. A module tool, `mcp_configure`,
validates a server and sets the setting through the controller's settings verb, so the list stays
declared state. The agent's own HTTP-only constraint for managed servers applies; a person's local
command-based servers stay their own, in their own file.
**Order:** the manager on the control node and a refresh observed; the licences the operator uses,
enrolled; the console's provision and the module in the catalogue; one workstation assigned, the three
predecessor files removed there, and a new session read to confirm it sees the mesh's instructions and
the console's tools; then the rest.
**The entry's name is `mesh`.** The hand-made entry both workstations carry today is named after this
installation, which a definition may not be ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md));
it is the person's to remove, and until then the agent sees the mesh's tools twice.
## 8. The package
## 5. The licence: the consumer side
The module declares the agent's package. The distribution every node of the live mesh runs does not
carry it in its repositories: the two workstations have it from a build the predecessor's helper made
from the community repository, and nothing updates it since the predecessor retired. On those two the
declaration is satisfied — the package is present. **On a fresh machine the host's package manager
refuses it, in its own words, and the module is not applied there.** That is correct and is a gap.
[ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
decides it; to-be 39 is the manager's half. This module:
The answer the mesh already has a shape for is a package repository for this ecosystem as a seat
([ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md)), fed by the
builder with a package it builds from the vendor's release, and trusted by every node's package manager.
Then `package: claude-code` is answered the way every package is, and updates arrive the way every
update does. It is not built, and it is not this module's to build: it is a seat and a provider module
of its own, needed by every package the distribution does not carry.
- **makes a keypair** in its state the first time it runs and registers the public half with the seat;
- **serves `apply`**: the manager's hand-over, a token sealed to the module's key, with the licence's
name and kind. A rotation of the same licence is applied only if newer within one lineage; a switch is
applied regardless, because across licences the expiries are unrelated. The answer says applied or
refused and why, and never echoes a token;
- **pulls** at start and when its token nears expiry, by the seat's `current` verb, and keeps the last
token when the manager does not answer, saying so;
- **writes** for a subscription licence the credentials file as the operator, access-token-only; for the
API-key licence sets the key-helper in the managed settings to a small program that prints the key
from the module's state, so no file under the home is touched;
- **offers a login to the manager**: when the credentials file changes by a person's login, it reads the
account's identity from the agent's state file and offers the grant to the seat, sealed to the manager's
key, for adoption; the manager decides;
- **serves `licence_status`**: which licence and kind this node holds, when the token expires, whether
the file matches what was handed over — by fingerprint, never by value.
Switching is the seat's `switch` verb, asked through the console; this module only applies what it is
handed.
## 6. Scope, settings and the order of assignment
**Every node with an operator account** ([ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)).
None has one today; the operator states them first. **Per node:** the role. **Per mesh or per node:**
extra tool servers. **Prerequisite:** the manager holds its seat and has adopted the licences.
**Order:** the manager assigned and a refresh observed; the console's provision in the catalogue; this
module on one workstation; the six predecessor files and the hand-made console entry removed there; a
new session read to confirm it sees the mesh's instruction file, the console's tools under `mesh`, and
its licence; then the rest.
## 7. The package
The module declares the agent's package. The distribution every node runs does not carry it in its
repositories: the two workstations have it from a build the predecessor's helper made from the community
repository, and nothing updates it since. On those two the declaration is satisfied. **On a fresh machine
the host's package manager refuses it, in its own words, and the module is not applied there.** The
answer is a package repository for this ecosystem as a seat
([ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md)), fed by the builder
and trusted by every node's package manager; not built, and not this module's to build. The vendor's own
installer is rejected: it puts a self-updating binary under the person's home, invisible to the mesh.
Rejected as the answer: the vendor's own installer, which puts a self-updating binary under the
person's home. It is a hand-installed package the mesh cannot see, reproduce or roll back, and it
updates itself outside the mesh — the arrangement the manifest rule *never install a package by hand*
exists to end.
## How it is checked
| Check | Defends |
|---|---|
| the module's definition names no node, path or login, declares nothing under a home or `/etc`, and no file resource carries a secret | ADR 0112, ADR 0155, ADR 0183 |
| on a lab machine with an account and a seeded home holding a person's rule file and the predecessor's leftovers: after assign, the managed directory holds the mesh's files, the home is byte-identical except the credentials file, which is owned by the operator and names no refresh token; after unassign, the managed directory's files are gone and the home is untouched | ADR 0182, the host's agnosticism |
| on a lab machine with no account, the assignment is refused naming the fact | ADR 0181 |
| a switch asked of the seat through the console changes the licence and the token on the node; no tool answer and no log line holds a token | ADR 0183 |
| the API-key binding writes nothing under the home and the agent authenticates through the helper | ADR 0183 |
| the console's provision resolves by co-location; a machine without the console refuses the module by name | ADR 0027, ADR 0152 |
| a new session on the assigned workstation lists the console's tools under `mesh` and answers "which node am I" from the instruction file | the exit of the build |
| the module's definition names no node, path or login, and declares no secret in a file's content | ADR 0112, ADR 0155, ADR 0178 |
| on a lab machine with an account, a seeded home holding a person's rule file, the predecessor's three leftovers and a settings file with the person's model: after assign, the mesh's files are present and owned by the account, the person's file and model are byte-identical, the leftovers are untouched, the console's entry is set; after unassign, the mesh's files are gone, the two keys are given back, the directory and everything else stand | ADR 0177 |
| on a lab machine with no account, the assignment is refused naming the fact | ADR 0176 |
| the credentials file is owned by the account, readable by it alone, and names no refresh token; a switch through the console changes the licence named in the bound facts and the file's fingerprint; neither the tool's answer nor the module's log holds a token | ADR 0178, ADR 0050 |
| the console's provision resolves by co-location and a machine without the console refuses the module by name | ADR 0027, ADR 0152 |
| a new session on the assigned workstation lists the console's tools under the `mesh` prefix and answers "which node am I" from the identity rule | the exit of the build |
| the package is reported present on the workstations and refused in the package manager's words on a machine without it | §8, honestly |
## What this does not settle
- **Several operator accounts on one node** (ADR 0181 decides one).
- **A worker's own licence on a machine.** Every interactive session shares the node's one agent
directory and its licence, however many run. A worker runs from a home of its own with an agent
directory in it, bound to its own licence through the manager (to-be 39 §5); that is for when workers
exist, and nothing here changes for it.
- **The package repository seat** (§7).
- **How the module's tools are run** is decided: the node's tool runtime, host-side
([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
The managed files and the credential write are tools of this module that runtime serves. Until the
runtime exists on every node, the module's code runs as a supervised process of its own
([ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)),
which changes nothing in what it writes.
- **Several operator accounts on one node.** ADR 0176 decides one; the module follows.
- **A parallel session under another licence on the same machine.** The retired helper allowed it by
keeping tokens readable; a clean form needs a second consumer identity (to-be 14's open half).
- **The package repository seat.** §8 names it and leaves it to its own design.
- **The rest of the family** — shell, terminal, desktop, user-scoped services — each a module of the
same shape, each proving nothing new about ownership and something new about its own tool.
## References
- [ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md),
[ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md),
[ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) — the decisions
- [39 — The Anthropic licence manager](39-the-anthropic-licence-manager.md), [34 — The console](34-the-console.md), [29 — A node has operator accounts](29-a-node-has-operator-accounts.md)
- [research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md) — the operator's machine as modules, and where tools run
- the vendor's documentation on managed settings, managed tool servers, the managed instruction file and the key-helper, read 2026-10-02
- the predecessor's two modules and the six files on the workstations, read 2026-10-02
- [ADR 0176](../../02-DECISIONS/0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md),
[ADR 0177](../../02-DECISIONS/0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md),
[ADR 0178](../../02-DECISIONS/0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md) — the three decisions this rests on
- [to-be 29](29-a-node-has-operator-accounts.md) — the family; [to-be 14](14-model-access.md),
[to-be 15](15-the-agent-session.md) — the licence and the consumer; [to-be 34](34-the-console.md) — the console
- the predecessor's `claude-code` module and its sibling's identity rule — what is replaced, read from the workstations on 2026-10-02
- mesh-catalog `modules/anthropic-consumer` — the write this module carries over; `modules/anthropic-manager` — the refresh it depends on
@@ -1,142 +0,0 @@
---
layer: to-be
status: in-progress
code: [mesh-host, mesh-controller, mesh-tools, mesh-catalog]
updated: 2026-10-02
decisions:
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
- 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
- 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
- 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
- 02-DECISIONS/0040-what-a-module-is.md
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
- 02-DECISIONS/0161-what-deserves-a-seat.md
- 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
---
# 37 — The operator's machine
**Every configurable thing on a node is a module, the home included, and the same catalogue serves
a server and a laptop.** One default configuration per module, varied per node by a setting or a
kept region; roles a machine has once as node-scoped seats with tool contracts; one tool runtime
per node serving every module's tools on the host side
([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 is the design [to-be 29](29-a-node-has-operator-accounts.md) §2 called *a family* and
[research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md) measured.
## 1. What a module of the environment looks like
Worked on the first one, a shell. The `zsh` module declares:
- a **package**, `zsh`;
- **files under the home**, owned by the account: the shell's rc file with the module's default
configuration, carrying a kept region for the operator's own lines, and `${setting:…}`
placeholders for the few values a node varies; the account and its home are machine facts the
controller resolves ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md),
to-be 29 §2);
- a **seat declaration**, `login-shell`, node-scoped, with its one verb; and a **claim** on it;
- a **`user` shape** naming the shell, applied only where the module holds the seat;
- a **tools bundle**, the artifact kind for interpreted code, with `execute` and the module's own
`show-config`.
No container, no unit, no service. It is assigned to every node with an operator account. The
`fish` and `bash` modules are the same with another package and other files; one of the three
holds the seat on each node.
The second shape is **system scope**: the login manager declares a package, two files under
`/etc`, and a service, which is exactly what the ssh daemon module declares today. The third
shape is **graphical**: the window manager declares a package, files under the home, a
user-scoped unit or two, a claim on the display-session seat, a dependency on the display server
being held, and a bundle with its tools. Nothing in any of them says which machine it is for.
## 2. Variation
A node differs from the default in two ways and no other
([ADR 0174](../../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)):
a **setting** the module declared, set in the node's layer and rendered into the file; or lines in
a **kept region** the file marks. The predecessor's ninety theme variables become the settings of
the modules whose files read them. Until the settings record proposed alongside the
container-runtime records ships — a setting names the file it lands in — environment modules carry
defaults in their files and declare no setting; that is the order, not a preference.
## 3. The node tools runtime
One per node, started and restarted by the host as a sibling process, never a container
([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
It is the tool runtime that exists, in the role it was written for: it reads the memberships of
every module assigned to the node, loads each module's tools bundle, and serves every tool and
every held seat's verb on the subjects issued. It holds the node's one bus credential and may call
every tool on the mesh. Its serving mode on the machine's loopback is what the console was
([to-be 34](34-the-console.md)); the module is renamed **node-tools** and declares the interpreter
it needs as a package.
A bundle reaches the node as any artifact does. A push that adds or replaces one is a reload. A
bundle that fails to load is named in the node's report and the others serve. A tool that needs
root escalates itself.
## 4. The seats of the environment
Decided now: **`login-shell`** (module-declared; zsh, fish, bash; verb `execute`) and
**`node-service-manager`** (the mesh's own; systemd; verbs over units in both scopes). The rest
are candidates from [research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md),
one record each when its first holder is written: display server, display session, terminal
emulator, launcher, notifier, compositor, lock screen, bar, login manager, audio, clipboard, boot.
Editors, browsers, media players, the agent, the downloads and scripts folders are modules with
tools and no seat.
A module that needs a role filled depends on **the seat being held** on the node, not on a
capability: the window manager needs the display server seat held, by xorg or by a compositor
that is its own server. Whether a held seat can gate an assignment is the first question the
resolver is asked by the second graphical module; the display server itself is gated by the
`graphical-session` capability the profile already reports.
## 5. What the host gains, and what it does not
- `service` gains `scope: user`, applied as the account
([ADR 0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)).
- The host starts and supervises the node tools runtime as it would any host-side process, and
delivers bundles as artifacts.
- Nothing else. No hooks, no actions: `chsh` is the `user` shape, enabling a unit is the `service`
shape, rebuilding boot images is a verb of the boot seat when that seat is written.
- A gap, recorded: the `package` shape drives the distribution's package manager and nothing
outside its repositories. The login manager in use is such a package; it waits on an official
package or a decision the host does not yet have.
## 6. The order of the build
1. **The operator account on every node** — `mesh-controller node` with the login name; empty on
all four today. Nothing home-scoped composes before it.
2. **The node tools runtime** — mesh-host supervises it; mesh-tools serves bundles from memberships
and reloads; mesh-controller composes the bundle into the declaration and the memberships to one
runtime per node; the catalogue renames the console. Proven when the packet-filter verbs answer
from it and its container is gone.
3. **`zsh`**, the first environment module: seat, `user` shape, home files, `execute`. Proven on a
server first, then every node.
4. **`systemd`** and user scope: the host's field, the seat seeded, the module. Proven by the
desktop's reload watcher declared `scope: user` on a workstation.
5. **The login manager**, system scope, once its package is installable; then the display server,
the window manager, and the rest of the graphical stack, each seat its own record.
6. **Settings** for the theme knobs, after the settings record ships and issue 168 closes.
## How it is checked
| Claim | Checked by |
|---|---|
| A module with a package, home files, a seat and a bundle resolves and composes on a node with an account, and is refused on one without | the controller's composition tests |
| One runtime per node serves every assigned module's tools; a per-module tool container no longer exists | the runtime's tests; `docker ps` on a converged machine |
| A user-scoped unit is applied as the account | the host's tests |
| A node's difference from a module's default is visible as a setting with a source or a kept region | `mesh-controller.settings`; the host's write-into tests |
| The same manifests assign to a server and a workstation; the graphical ones are refused on the server by name | the resolver's tests and the live mesh |
## References
- [Research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md)
- [To-be 29](29-a-node-has-operator-accounts.md) — the account and the home; this design is the
family its §2 names, beyond `~/.ssh`.
- [To-be 33](33-the-tools-the-mesh-answers.md), [to-be 34](34-the-console.md) — the tools and
the console, amended by ADR 0175.
- [To-be 05](05-the-node-host.md) — the host's vocabulary, widened by ADR 0177.
@@ -1,193 +0,0 @@
---
layer: to-be
status: in-progress
code: [mesh-tools, mesh-controller, mesh-host, mesh-catalog]
updated: 2026-10-02
decisions:
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
- 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
- 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
- 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
- 02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md
---
# 38. Building the operator's machine
**The work of [design 37](37-the-operators-machine.md), broken into packages small enough that each
ends at something a person can see run, in the order their dependencies allow.** Design 37 is the
authority on *what* is built; this document holds only the packages, their order, their sizes and
their proofs, and is wrong the moment it disagrees with 37 rather than the other way round. It is
the shape [design 28](28-building-the-bus.md) gave the bus work, applied here.
## How this is built, and where it is run
**On the live mesh, by the operator's decision.** Every package is written with unit tests and
committed on one branch per repository; its proof runs on the four machines, not in the lab.
[ADR 0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md) already says the live mesh is
the test bed; the operator's words on 2026-10-02 were *skip the lab, it is not too bad if something
is broken*. The cost accepted: a package that breaks the runtime breaks every tool on a node until
the next push, and the controller's own verbs stay reachable through the controller seat whatever
happens to a node's runtime — which is the one thing that must hold, and does by construction
([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).
Each package names what proves it. A package that cannot name its proof is divided until it can.
## What exists already, measured
Counted 2026-10-02 in the four repositories, non-test source. The point of the count is the same
as design 28's: nothing here is new ground; every package reshapes something standing.
| Piece | Today | Size | Becomes |
|---|---|---|---|
| the tool runtime | TypeScript: loads `MESH_TOOL_MODULES`, serves one module's tools and its claimed seats' verbs; `serve` is the console | ~1 700 lines over six files | loads every assigned module's bundle; `serve` is node tools |
| the host's `process` shape | Go: fetch a bundle by digest, unpack under the mesh's daemons directory, write the unit, run it | 343 lines | **unchanged** — the runtime is one such process |
| the host's `archive` shape | Go: fetch and unpack an artifact at a path | 185 lines | **unchanged** — a module's tools bundle is one such archive |
| the controller's bus principals | Go: one principal per module per node, grants from what it declares | 132 lines | gains one principal per node for the runtime |
| the controller's memberships | Go: one per assignment, the subjects a runtime serves | 143 lines | **unchanged** in shape; the runtime reads several |
| the controller's declaration composer | Go, one file | 2 053 lines | gains the runtime's process, the bundles' archives, two env words |
| the catalogue | 35 manifests build a per-module tool container on the runtime's base image | — | none do; the runtime is a module of its own |
**Two measurements decide the shape.** The host needs no change: a `process` and an `archive` are
what the runtime and a bundle are, and both are applied today. And the runtime already does
nine-tenths of the job — the loop over entrypoints, the seat verbs, the membership subscription —
for one module; the work is to let it do the same for a list.
## The order the work allows
```
WP1 the runtime serves many modules (mesh-tools) ──┐
WP2 the controller composes one runtime a node (mesh-controller) ──┤ independent, test-proven
│
WP3 the runtime is a module; the console is its serving mode (mesh-tools, mesh-catalog)
│
WP4 the first holder moves: the packet filter (mesh-catalog) ── the live proof
│
WP5 the shell, on a server (mesh-catalog) ── the first environment module live
WP6 the service manager, on a workstation (mesh-host #72, mesh-catalog)
│
WP7 the login manager, the display server, the window manager … ── one record per seat, after this document
WP8 settings for the theme knobs ── after issue 168 closes
```
WP1 and WP2 touch different repositories and meet only at the membership's shape, which neither
changes; they are built in parallel. WP3 needs both. WP4 is the first time anything on a machine
changes, and it is the proof of the whole. WP5 and WP6 are the first environment modules; the
packages after them are design 37 §4's candidates and are not broken down here, because each
begins with a decision record this document cannot anticipate.
## WP1 — The runtime serves many modules
*mesh-tools. About a day.*
**What changes.** `serve` takes a list of modules to serve, each with its entrypoints, rather than
one module and one credential. The runtime reads one membership per module from the subjects
[ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
derives for each, and serves each module's tools on that module's subjects and each held seat's
verbs on the seat's. The filter that drops a registration under any name but the one module goes;
what remains is the rule that a registration under a seat's name is served only where some module
the runtime serves claims that seat. A bundle that throws on import is named in the log and in
what `tools` answers, and the others serve. The runtime reads `MESH_OPERATOR_ACCOUNT` and
`MESH_OPERATOR_HOME` and hands them to every tool's environment.
**What does not change.** The SDK. The broker client. The MCP surface. A module's tool code.
**Proof.** The runtime's test against a real bus: three bundles, one of which throws on import;
five tools and two seat verbs answer on their subjects; `tools` names the failed bundle; a
membership republished mid-run re-subscribes without a restart.
## WP2 — The controller composes one runtime per node
*mesh-controller. Two to three days; the largest package.*
**What changes**, in four pieces, each its own commit:
1. **A node principal.** Beside one principal per module per node, one per node of kind
`node-tools`: its serving grants are the union of every assigned module's tool subjects and every
held seat's verbs on that node, its invoking grant is `*`, and it consumes nothing. The
per-module memberships are composed as today; nothing else on the bus learns a new shape.
2. **Bundle delivery.** For every assigned module whose build produced a `bundle`, the node's
declaration gains an `archive` placed under a directory the controller derives, so the host
fetches and unpacks it as it does any artifact. The bundle's digest is what the build recorded.
3. **The runtime's process.** One `process` per node running the runtime from its own bundle
(WP3), `MESH_TOOL_MODULES` composed from the unpacked entrypoints, `MESH_OPERATOR_ACCOUNT` and
`MESH_OPERATOR_HOME` from the account fact, `restart-on` naming every bundle so a push that
changes one restarts it. A node with no account composes the runtime without the two words.
4. **The gate.** A manifest declaring `tools` and a container built on the runtime's base image is
refused at registration once the runtime module is registered, naming this record. It is the
mechanism that keeps the old pattern from returning by habit.
**Proof.** Composition tests: a node with three assigned modules, one holding a seat, yields one
process, three archives, one node principal whose grants are the union, and the same three
memberships as before. The gate's test: the packet-filter manifest as it is today is refused once
the runtime is registered.
## WP3 — The runtime is a module, and the console is its serving mode
*mesh-tools and mesh-catalog. A day.*
**What changes.** mesh-tools gains a `bundle` artifact of itself beside its images, and its manifest
becomes the `node-tools` module: a package for the interpreter, the loopback listener the console
declared, `invokes: *`, and nothing else — the process is the controller's to compose (WP2). In the
catalogue, `mesh-console` is retired as a module and `node-tools` assigned where it was. The
runtime's `serve` keeps answering MCP on loopback; the person's end of it keeps the name *console*
([glossary](../../00-META/glossary.md)).
**Proof.** On every node: the console's container is gone, `node-tools` runs as a unit the host
wrote, `tools/list` on loopback answers as before, and the controller's verbs answer through it.
This is the first live step, and it is reversible by re-assigning `mesh-console`.
## WP4 — The first holder moves: the packet filter
*mesh-catalog. Half a day. The live proof of ADR 0175.*
**What changes.** The nftables module drops its container, its `NET_ADMIN` and its runtime
artifact; its tools bundle stays and its claim stays. Its `remove` and `reload` escalate inside the
tool where they need root, which they have, since the runtime runs as the node's account.
**Proof.** `node-packet-filter.rules@<node>`, `reload` and `remove` answer from the runtime on all
four machines; `docker ps` shows no `mesh-nftables`; `status` is well. Then the fail2ban holder
proposed in an open change follows the same way when it lands.
## WP5 — The shell, on a server first
*mesh-catalog #224, already written. Half a day to assign and prove.*
**Order.** Assign `zsh` to one server; push; `login-shell.execute@<server> command="uptime"`
answers; the account's login shell reads zsh; its `~/.zshrc` carries the mesh's block with the
operator's lines around it. Then the other three nodes. The two things the manifest cannot say
— the `user` shape applying only where the seat is held, and a second shell module installed
beside the holder — are the first follow-up record after this document.
## WP6 — The service manager, on a workstation
*mesh-host #72 merged first; mesh-catalog #224. Half a day.*
**Order.** Merge the host's user-scope change and let it roll. Assign `systemd` everywhere;
`node-service-manager.units@<node> scope=user` answers on a workstation. Then the first user-scoped
unit the mesh sends: the window manager's reload watcher, declared `scope: user` by the window
manager module when WP7 writes it — until then, the host's change is proven by its tests and by
the verb answering.
## What is deliberately not here
- **The graphical stack's seats** (WP7). Each begins with a record naming its holders and verbs,
and the first graphical module asks the resolver a question this document cannot answer for it:
whether a held seat gates another's assignment.
- **Settings for the theme knobs** (WP8). Blocked on the settings record proposed in an open change
and on [issue 168](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md).
- **Reload without restart.** WP2 restarts the runtime on a bundle change; a reload that keeps the
other modules' tools up during one module's change is a refinement for after WP4 proves the
simple form.
- **Lingering.** A user-scoped unit answers only while the account's manager runs; declaring
lingering for the account is a field on the `user` shape, decided when a server first needs a
user unit.
## How this list is kept true
Each package's proof is run on the live mesh when the package is finished and its line here gains
the date and the commit, the way [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md)
carries *built and proven live*. A package whose proof fails is not reworded; the failure is
recorded under it and the package stays open. When WP6 is proven, design 37's status moves to
`implemented` for what it covers and this document's to the same.
@@ -1,170 +0,0 @@
---
layer: to-be
status: designed
code: []
updated: 2026-10-02
decisions:
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
- 02-DECISIONS/0024-model-access-is-a-provision.md
- 02-DECISIONS/0050-model-access-is-vendor-agnostic.md
- 02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
---
# 39 — The Anthropic licence manager
**One module knows every Anthropic licence the mesh has, keeps each alive, decides which consumer gets
which, and hands every node's agent its token over the bus.**
[ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
decides it; this is the shape. It is the successor of the predecessor's manager module, built from what
that module learned the hard way, and the counterpart of [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md),
which is the consumer on every node.
## 1. What it is
A module, `claude-licence-manager`, holding the mesh-scoped seat **`anthropic-licence-manager`**. One
holder, on the node the operator assigns it to — the control node is the natural one, and nothing in the
definition says so. It requires a database for its own store and the bus; it claims the seat; it serves
the seat's verbs. It has no port, no route, no file under anyone's home.
Its store holds four things:
| table | holds |
|---|---|
| **licences** | name, kind (`subscription` or `api-key`), the account's identity (id, address, organisation) once adopted, the grant encrypted at rest, when the access token expires, when the refresh token expires, consecutive failures, the refresh lease, when a person was last notified |
| **bindings** | one row per consumer: kind (`node-agent`, `node-session`, `worker`), its key (the node, or the node and the worker), the licence, or *inherit* |
| **usage** | the vendor's readings per licence per period, raw beside normalised ([ADR 0054](../../02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md)) |
| **audit** | every switch, adoption, refusal and drift, with who asked |
**The grants are encrypted with a key the vault made for the manager** — its one `secret` requirement.
The vault keeps that key; the manager keeps the grants. That is ADR 0050's carve-out, one module, one
node, the long-lived grants only.
## 2. The licences it manages today
Two subscription accounts and one API key. They differ in kind and the manager treats them so:
| kind | what the grant is | refresh | what a node is handed | how the agent uses it |
|---|---|---|---|---|
| `subscription` | an OAuth grant: an access token that lives hours and a refresh token that lives weeks | the manager rotates it, alone | the access token only | written into the agent's credentials file by the agent module, as the operator |
| `api-key` | a key the operator obtained from the vendor | none; a new key is a new adoption | the key | served to the agent through its key-helper setting; nothing is written under the home |
## 3. Keeping a grant alive
Carried from the predecessor, where each rule was earned by an incident:
- **One rotation source.** Only this module calls the vendor's token endpoint. An OAuth refresh is
presumed to rotate the refresh token, so a second refresher presenting the old one would kill the
grant; whether that presumption holds is to be measured in the lab, and the design is safe either way.
- **A lease per licence**, taken in the store before the row is read. A duplicate run sees the token its
predecessor just wrote, finds hours of life on it, and does nothing.
- **An expiry floor and a cadence.** Within an hour of expiry a refresh must happen; otherwise a grant is
rotated once it is older than a declared setting, so a node that misses one rotation still holds hours
of life and a broken refresh surfaces in minutes rather than the next morning.
- **Failure is counted and escalated once.** Consecutive failures are recorded; past a threshold a
notification is emitted, and at most once a day while it stays broken — the predecessor sent one alarm
411 times in 35 hours and the incident went unnoticed inside its own alarm.
- **A refresh token's own expiry is warned about three days ahead**, because the only remedy is a person
logging in again.
- **The vendor's reason is logged**, never only the status code: a malformed request and a revoked grant
both answer 400, and the predecessor built three concurrency fixes for a bug that was a wrong client id.
## 4. Handing a token to a node
Every node that runs the agent module registers that module's public key with the seat when it first
runs. From then on:
- **On rotation**, the manager calls `claude-code.apply@<node>` on every node bound to the rotated
licence, with the new token sealed to that node's module key. The module answers *applied*, or
*refused* and why, and the manager records it.
- **On a switch**, the same call with the other licence's token, and the binding is the authority: the
module applies a bind without comparing expiries, because across two licences the numbers are
unrelated.
- **On a pull** — the module starting, or finding its token near expiry — the module calls the seat's
`current` verb for its binding and is answered sealed the same way.
- **Never as an event.** What the manager emits names the licence and the outcome and carries no token.
A node whose module has not registered a key cannot be handed a token, and the manager says so by name
rather than falling silent. A node whose module refuses — a wrong identity, a stale grant within one
lineage — is recorded as drift and reported.
## 5. Who gets which licence
Three consumer kinds, the predecessor's touchpoints with their fallbacks:
| consumer | bound by | falls back to | if the bound licence cannot be served |
|---|---|---|---|
| **the node's interactive agent** | the node | nothing: an unbound node has no licence and the agent says so | keeps the last token, which expires within hours; a notification is emitted |
| **the mesh's session on a node** | the node, for that session | the node's agent licence | refused |
| **a worker** | the worker | the node's session licence, then the node's | refused: a worker never borrows a person's account |
**One agent directory per machine, shared by every interactive session**, so a node's binding is the
licence of all its sessions at once. A worker is a consumer of its own because it runs from a home of its
own, with its own agent directory and credentials file, which the agent module on that node writes for
it as it writes the operator's — the predecessor ran its agents exactly so.
**Binding is a person's act through the seat's verbs**, listed by the console: `bind`, `switch`,
`release`. **Exhaustion is observed, not acted on**: usage is read every few minutes, a crossing of a
declared threshold in the five-hour window is notified once per crossing, and moving a consumer is the
operator's call. Switching remains a reaction, not a declaration
([ADR 0024](../../02-DECISIONS/0024-model-access-is-a-provision.md)), and an automated policy — move to the
least-used licence, stay off a dying one — is designed later if wanted, on the readings this module
already keeps.
## 6. Adopting a grant
A licence enters the mesh one of two ways, and the token never passes through a prompt, a terminal or an
argument:
- **From a node's login.** A person logs in on a node, as they always have. The agent module there reads
the account's identity from the agent's own state file, and offers the full grant to the seat sealed
to the manager's key. The manager adopts it into the licence the node is bound to **only if the
identity matches** that licence's recorded account; a licence not yet identified is identified by its
first adoption; a mismatch is refused and notified, because the predecessor once filed one account's
grant into another's row this way.
- **An API key** is delivered to the manager by the operator through the seat's `adopt` verb from a file
on the manager's node, never as an argument.
## 7. What it emits and serves
**Events**, no secret in any: `licence.rotated`, `licence.switched`, `licence.adopted`,
`licence.failing`, `licence.refused`, `usage.read` — the audit logger records them all.
**The seat's verbs**, the contract every future holder must serve: `licences` (each with kind,
identity, expiry, failures, who is bound), `bindings`, `bind`, `switch`, `release`, `refresh` (now, one
or all), `usage` (current and history), `adopt`, `register` (a node's module key), `current` (a
consumer's token, sealed, asked by the consumer's module).
## 8. Settings
The refresh cadence; the usage threshold; the notification cooldown. Each declared with a default, so
one definition serves and one mesh may differ.
## How it is checked
| Check | Defends |
|---|---|
| two refresh runs started together rotate one grant once; the second does nothing and says so | ADR 0183, one rotation source |
| every event the manager emits is free of any token; the hand-over opens only with the receiving module's key | ADR 0183, to-be 32 §10 |
| a worker bound to a dead licence is refused, never answered with another licence's token | ADR 0183, the fallbacks |
| a grant offered with a mismatching identity is refused and one notification emitted | ADR 0183, attribution |
| a failing licence notifies once, and once a day after, not once per tick | §3 |
| the console lists the seat's verbs and `switch` changes a workstation's token end to end | ADR 0132, the exit of the build |
## What this does not settle
- An automated switch on exhaustion (§5).
- Whether an OAuth refresh token is single-use; the lab measures it, and §3 holds either way.
- How the mesh's own session and a worker read their token on a node once those exist
([to-be 15](15-the-agent-session.md), [ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)):
the agent module on that node is their local source, and the reading is theirs to design.
## References
- [ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) — the decision
- [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md) — the consumer on every node
- [14 — Model access](14-model-access.md) — the vendor-blind provision this sits beside
- the predecessor's `claude-licences` module: the lease, the floor, the cadence, the cooldown, the identity guard — read 2026-10-02
@@ -1,226 +0,0 @@
---
layer: to-be
status: designed
code: []
updated: 2026-10-02
decisions:
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
- 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/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
- 02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
---
# 40. Building the operator's agent and its licence manager
**The work of [design 36](36-the-operators-agent-on-a-machine.md) and [design 39](39-the-anthropic-licence-manager.md),
broken into packages that each end at something a person can see run, in the order their
dependencies allow.** The two designs are the authority on *what* is built; this document holds the
packages, their order, their sizes and their proofs, and is wrong the moment it disagrees with them.
It is the shape [design 38](38-building-the-operators-machine.md) gave the operator's machine, applied
to the two modules that make its agent work.
## How this is built, and where it is run
**On the live mesh, by the operator's decision** ([ADR 0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md),
and design 38's words on the same day). Every package is written with unit tests, committed on one
branch per repository ([playbook 07](../../00-META/process/07-feature-branches.md)), and proven on the
machines: the control node first for the manager, one workstation first for the agent, then the rest.
The cost accepted: a broken agent module leaves a workstation's agent without the mesh's instructions
or with a stale token until the next push; the person's own files under the home are never in reach of
the failure, by [ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md).
Each package names what proves it. A package that cannot name its proof is divided until it can.
## What exists already, measured
Counted 2026-10-02 in the repositories and on the machines. Nothing here is new ground; every package
reshapes something standing.
| Piece | Today | Becomes |
|---|---|---|
| the vendor's refresh, the sealed box, the grant file | `anthropic-manager` in the catalogue: a token-endpoint client, a sealed-box primitive, adoption of a grant sealed to a node's key; assigned to nothing | the manager's refresh and adoption, with the lease, the floor and the cadence the predecessor's manager had |
| the credentials write, the refresh-token strip, the identity read | `anthropic-consumer` in the catalogue: tested; assigned to nothing | the agent module's write, unchanged in shape |
| the predecessor's manager and consumer | two modules in the retired system: the lease per licence, the expiry floor, the lineage comparison, the identity guard, three touchpoints with fallbacks, cooldowns on alarms | ported as logic with its tests; nothing of its registry or its bus |
| the host's `process` and `archive` shapes | fetch a bundle by digest and run it supervised; fetch and unpack an artifact | **unchanged** — the manager's daemon is one process; both modules' tools are bundles |
| the manifest's `uses`, `claims`, `invokes seat:<seat>.<verb>`, node-scoped `provides` with `serves: {port}` | all four exist and are used by other modules | **unchanged** — the agent uses the seat and invokes its verbs; the console provides its endpoint |
| the console | a container per node, MCP on loopback, no provision | gains one provision; becomes the node-tools runtime's serving mode under design 38's WP3 |
| the agent's package | present on both workstations from a build the predecessor's helper made; the distribution's repositories do not carry it | declared; satisfied where present, refused where not, until a package repository seat exists |
| the operator account | a column on every node record, **empty on all four** | stated by the operator, before anything home-scoped lands |
**One dependency decides the order.** Both modules serve tools and the agent module's tools write
under `/etc` and, as the operator, under the home. Under [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)
tools run in the node's tool runtime, host-side, which design 38 builds in its WP1–WP3. Writing a
per-module tool container for these two modules would be building the pattern that record retires, so
**the live proofs of WP3 to WP5 below wait for design 38's WP3.** Everything before a live proof —
manifests, code, tests — does not, and is written now.
## The order the work allows
```
WP0 the operator states the facts (the live mesh) ── accounts, roles, licences to adopt
WP1 the console provides its endpoint (mesh-catalog) ── small, independent
WP2 the licence manager, built and tested (mesh-catalog) ──┐ independent of each other;
WP3 the agent module, built and tested (mesh-catalog) ──┘ both wait on design 38 WP3 to run
│
WP4 the manager live on the control node (the live mesh) ── three licences adopted, a refresh seen
WP5 the agent live on one workstation (the live mesh) ── the hand-over, the switch, the instructions
WP6 the rest of the nodes, and the predecessor's remains ── adoption from a login, the retirements
```
WP1, WP2 and WP3 touch different directories of one repository and meet only at the seat's name and
the provision's name; they are built in parallel. WP4 is the first time anything on a machine changes.
WP5 is the proof of the whole.
## WP0 — The operator states the facts
*The live mesh. An hour, and it is the operator's.*
The account on each node record, through the controller's node command — none is stated today, and
[ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)
refuses a home-scoped module without one. The role of each node, as the agent module's setting on the
node layer, once the module exists. Which three licences exist and what each is called.
**Proof.** The controller's node command lists an account for every node.
## WP1 — The console provides its endpoint
*mesh-catalog. Half a day.*
**What changes.** The console's manifest gains a node-scoped provision — working name
`console-endpoint`, fixed when the manifest is written — serving the port the machine gave it, as the
local model server already does for its API. Co-location resolves it
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md),
[to-be 34](34-the-console.md) as amended). When design 38's WP3 moves the console into the node-tools
module, the provision moves with it; it is a line in a manifest either way.
**Proof.** The controller's plan for a workstation shows a consumer of the provision bound to the
console's port; the same consumer on a machine without the console is refused naming the provision;
the catalogue's tests pass.
## WP2 — The licence manager, built and tested
*mesh-catalog. Two to three days; the largest package.*
**What is written**, as design 39 says:
1. **The manifest.** Claims the mesh-scoped seat `anthropic-licence-manager` with its verbs; requires a
database, a `secret` for the key its grants are encrypted with, and the bus; a `bundle` of tools; a
`process` for the daemon that refreshes, collects usage and notifies, on a schedule; declared
settings for the cadence, the usage threshold and the cooldown, each with a default; `invokes` the
agent module's `apply`.
2. **The store.** Migrations for licences, bindings, usage and audit, with the lease and the
notification slot as columns, numbered and idempotent.
3. **The refresh.** The token-endpoint client and the sealed box from `anthropic-manager`; the plan
(floor, cadence, forced, cannot) and the lease from the predecessor, as pure functions with their
tests; the vendor's reason logged on failure; counted failures, one notification per cooldown.
4. **Adoption.** From a file on the manager's node for the API key; from a sealed grant a node offers;
the identity guard that refuses a mismatch and notifies.
5. **Usage.** The vendor's reading per licence on a schedule, stored raw and normalised
([ADR 0054](../../02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md)), one notification
per threshold crossing.
6. **The verbs**: `licences`, `bindings`, `bind`, `switch`, `release`, `refresh`, `usage`, `adopt`,
`register`, `current` — the last answering a consumer's token sealed to the key that consumer
registered.
7. **The hand-over**: on rotation or switch, one call to `claude-code.apply@<node>` per bound node,
the token sealed to that node's key, the answer recorded.
**Proof, before anything runs live.** Unit tests: two refresh runs started together rotate one grant
once; a grant with a mismatching identity is refused; a worker bound to a dead licence is refused and
never lent another; every event the daemon emits is free of a token; the hand-over payload opens only
with the registered key. The catalogue's checks: no installation named, no secret in a declared file.
## WP3 — The agent module, built and tested
*mesh-catalog. Two days.*
**What is written**, as design 36 says:
1. **The manifest.** The agent's package; the state directory; the facts file carrying the node's
name, the operator account and its home, the console's bound port, the role and the extra tool
servers from settings; requires the console's endpoint and the bus; `uses` the seat and `invokes`
its `register`, `current` and `adopt`; a `bundle` of tools. **No file resource under a home or
under `/etc`.**
2. **The renderer.** From the facts file and the current binding, the managed settings file (the tool
servers under the entry `mesh`, the attribution trailers, and the key-helper for an API-key binding)
and the managed instruction file (§3 of design 36), written under the agent's managed directory
with the escalation the tool performs for itself; idempotent; re-run when the facts file changes.
3. **The keypair**, made once in the state directory, the public half registered with the seat at
start and at every start.
4. **The consumer side**: `apply` (a rotation applied only if newer within one lineage, a switch applied
regardless, the answer naming the outcome and never a token); the pull at start and near expiry;
the credentials write as the operator, access-token-only, from `anthropic-consumer` with its tests;
the key-helper program for the API key; the offer of a login to the seat, sealed, after reading the
account's identity.
5. **The tools**: `apply`, `licence_status`, `mcp_configure` (validates a server, sets the module's
setting through the controller's settings verb), `render` (re-render now, for a person).
6. **The documentation**: the six predecessor files and the hand-made console entry a person removes
on a workstation that carried the predecessor.
**Proof, before anything runs live.** Unit tests: the renderer writes only the mesh's keys and leaves
every other key of a seeded settings file; the credentials write strips a refresh token and is atomic;
the lineage comparison from the predecessor, with its cases; a login offer carries the identity it read.
The catalogue's checks pass.
## WP4 — The manager live on the control node
*The live mesh. Half a day, after design 38's WP3.*
**Order.** Assign the manager on the control node; push. Adopt the API key from a file there. Adopt
the two subscription grants: a login in a throwaway home on the control node, offered to the seat the
way a node's agent module will. Bind each node's agent to a licence.
**Proof.** Through the console: `anthropic-licence-manager.licences` lists three licences with
identity and expiry; within the cadence, the audit shows a rotation and `licences` shows a later
expiry; a forced `refresh` on one licence is logged with the vendor's answer; the two retired catalogue
modules are still assigned to nothing.
## WP5 — The agent live on one workstation
*The live mesh. Half a day. The proof of the whole.*
**Order.** Set the workstation's role in the module's settings. Record the checksums of everything
under the person's agent directory. Assign the module; push. Remove the six predecessor files and the
hand-made console entry. Start a new session.
**Proof.** The managed directory holds the settings and instruction files, owned by root. Everything
under the person's agent directory is byte-identical to before except the credentials file, which is
owned by the operator, readable by nobody else, and names no refresh token. The new session lists the
mesh's tools under `mesh` once, answers *which node am I* from the instruction file, and makes a model
request. `anthropic-licence-manager.switch` to the second subscription licence changes the token on
the workstation within a minute, and neither the verb's answer nor either module's log holds a token.
Switched to the API-key licence, the credentials file is left as it was and the agent authenticates
through the key-helper. Switched back.
## WP6 — The rest of the nodes, and the predecessor's remains
*The live mesh and mesh-catalog. One day.*
**Order.** Assign the module on the second workstation and on the servers whose account is stated;
remove the predecessor's files on the second workstation. Log in on a workstation under a licence's
account and watch the offer be adopted — and under the wrong account, and watch it refused and
notified. Retire `anthropic-manager` and `anthropic-consumer` from the catalogue. Set designs 36 and
39 to `implemented` for what runs, with the as-is written
([playbook 02](../../00-META/process/02-graduation.md)).
**Proof.** Every node with an account runs the module and `licence_status` answers on each; the
refused login's notification arrived; the catalogue has no module built on the old placement.
## What is deliberately not here
- **The package repository seat** for a distribution that does not carry the agent's package
(design 36 §7). A fresh node refuses the module in the package manager's words until it exists.
- **An automated switch on exhaustion.** The readings are kept from WP4; the policy is a later record.
- **Workers and the mesh's own sessions as consumers.** The manager's bindings and fallbacks know them
from WP2; the consumers themselves do not exist yet ([to-be 15](15-the-agent-session.md),
[ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)).
- **Whether a refresh token is single-use.** WP4 may measure it on a licence deliberately refreshed
twice; the design holds either way.
## How this list is kept true
Each package's proof is run when the package is finished and its line here gains the date and the
commit, as design 38 does. A package whose proof fails is not reworded; the failure is recorded under
it and the package stays open. When WP5 is proven, designs 36 and 39 move to `in-progress` with their
owning repository, and when WP6 is proven to `implemented`, with the as-is written.
-2
View File
@@ -41,8 +41,6 @@ document is written and this one's status becomes `implemented`.
| [`29-a-node-has-operator-accounts.md`](29-a-node-has-operator-accounts.md) | **In progress.** A node has an operator account and a resource may live under its home — built in the controller; the ssh-client module, the SSH CA, the `~/.ssh` boundary and user-scoped services are not. The account fact still wants its decision record | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md) |
| [`32-what-a-module-declares.md`](32-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md), [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md), superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) |
| [`37-the-operators-machine.md`](37-the-operators-machine.md) | **In progress.** Every configurable thing on a node is a module, the home included; one default per module varied by settings or kept regions; roles a machine has once as seats with tool contracts; one tool runtime per node on the host side | [ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md), [0174](../../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md), [0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), [0176](../../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md), [0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md) |
| [`38-building-the-operators-machine.md`](38-building-the-operators-machine.md) | **In progress.** The work of design 37 as packages: the runtime serves many modules, the controller composes one per node, the console becomes its serving mode, the packet filter moves first, then the shell and the service manager — tested on the live mesh by the operator's decision | [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), [0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md), [0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md) |
## Not yet written