Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e710abd9b8 | ||
|
|
479b8fe72d | ||
|
|
e3a5862c5a | ||
|
|
5eadf36937 | ||
|
|
54213ba90c | ||
|
|
7fb59bde98 |
@@ -9,6 +9,11 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
|
||||
|
||||
- **node** — a machine in the mesh. There are 0..n of them, and each runs the host agent. A node is
|
||||
just a machine that has joined; being one implies nothing about what it runs.
|
||||
- **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 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
|
||||
the controller (and, today, the foundation). Lose it and the other nodes keep running what they
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
status: active
|
||||
initiated: 2026-10-02
|
||||
touches:
|
||||
- 02-DECISIONS/0040-what-a-module-is.md
|
||||
- 02-DECISIONS/0011-managed-files-are-generated-never-edited.md
|
||||
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
||||
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
||||
- 02-DECISIONS/0161-what-deserves-a-seat.md
|
||||
- 03-DESIGN/01-to-be/05-the-node-host.md
|
||||
- 03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md
|
||||
- 03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md
|
||||
- 03-DESIGN/01-to-be/34-the-console.md
|
||||
- 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: []
|
||||
---
|
||||
|
||||
# 018 — The operator's machine as modules
|
||||
|
||||
**What.** The mesh owns the whole machine, not only the services on it. Everything a person
|
||||
configures on a node — the login manager, the display server, the window manager, the shell, the
|
||||
terminal, the launcher, the notifier, the audio setup, the boot images, the downloads folder, the
|
||||
agent at the terminal — is a module: a package, the files it owns under `/etc` and under the
|
||||
operator's home, the seat it holds, the tools it serves. One default configuration per module,
|
||||
varied per node only through settings rendered into the file or a kept operator region, never
|
||||
through an edit. The servers take the universal modules (shell, prompt, git, the agent); the
|
||||
workstations take those and the graphical stack, which a capability the machine reports gates.
|
||||
This effort writes that behaviour down, measures what the predecessor's desktop modules actually
|
||||
contain, and settles what the mesh must gain before the first of them can be written.
|
||||
|
||||
**Why.** The predecessor is retired on every node. What it still owned on the two workstations —
|
||||
about thirty modules' worth of dotfiles, user units and `/etc` files — is now owned by nothing:
|
||||
no generator regenerates them, and a fix to one of them is a hand edit that nothing records. The
|
||||
migration scoped these modules out as *the workstation's own environment*, and
|
||||
[to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) names them as the last
|
||||
thing the predecessor was keeping alive. To-be 29 covers one directory, `~/.ssh`, and draws a
|
||||
boundary inside it. The operator wants no boundary: the machine is the mesh's, as far as it makes
|
||||
sense to configure it. That is a wider scope than any design states, and it reaches three records
|
||||
that were written for services: what a module is, where a module's tools run, and what a managed
|
||||
file may be.
|
||||
|
||||
**What it touches.** The module definition ([ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md)),
|
||||
seats and their contracts ([ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md)),
|
||||
where a module's tools run ([ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md),
|
||||
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
|
||||
[to-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §6), the host's vocabulary
|
||||
([to-be 05](../../03-DESIGN/01-to-be/05-the-node-host.md)), managed files and settings
|
||||
([ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md),
|
||||
[issue 168](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)),
|
||||
and the catalogue's shape ([as-is 10](../../03-DESIGN/00-as-is/10-module-catalogue.md)).
|
||||
|
||||
**Documents.**
|
||||
|
||||
- [01 — The intended behaviour](01-the-intended-behaviour.md): the operator's wish, written as
|
||||
how the mesh behaves, in the mesh's own words.
|
||||
- [02 — What exists, and what is missing](02-what-exists-and-what-is-missing.md): the
|
||||
predecessor's desktop modules measured; which records already say what is wanted; the gaps.
|
||||
- [03 — One tool executor per node](03-one-tool-executor-per-node.md): where a module's tools
|
||||
run. The direction the operator set, the evidence for it, and what it supersedes.
|
||||
- [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 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.
|
||||
2. One tool executor per node, host-side, module-agnostic; which records it supersedes and
|
||||
in what form the console continues.
|
||||
3. Per-node variation is a setting rendered into the file or a kept region, never an edit —
|
||||
ADR 0011 stands — and issue 168 is fixed before any environment module carries a setting.
|
||||
4. User-scoped units on the host's `service` shape, and a service-manager seat whose holder
|
||||
serves the tools about them.
|
||||
5. The operator account stated on every node; today no node record carries one.
|
||||
6. The seats of the environment and their verbs, one record per seat, slowly, because a
|
||||
seat's tools bind every future holder.
|
||||
7. Where the environment modules live: this catalogue, or one of their own as the media chain
|
||||
has; and whether a third-party organisation's tooling belongs in a public catalogue at all.
|
||||
@@ -0,0 +1,99 @@
|
||||
# 01 — The intended behaviour
|
||||
|
||||
*Written 2026-10-02 from the operator's words, in the mesh's words. What is wanted, before what
|
||||
exists. Where a sentence restates a record, the record is named; where it goes further, that is
|
||||
said.*
|
||||
|
||||
## The machine is the mesh's
|
||||
|
||||
**Everything configurable on a node is declared by a module.** Not only the services the mesh
|
||||
runs: the login manager, the display server, the window manager, the bar, the launcher, the
|
||||
notifier, the compositor, the lock screen, the terminal emulator, the clipboard, the shell and its
|
||||
prompt, the editor, the audio setup, the boot images, the package manager's configuration, the
|
||||
agent a person runs at a terminal, and the folders a person works in — a downloads folder that is
|
||||
tidied, backed up, distributed to other nodes and asked questions of. System folders and the
|
||||
operator's home alike. The operator is the only person on every node, so the mesh manages the
|
||||
person's machine, not a machine with a person on it.
|
||||
|
||||
This is [ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md)'s definition applied without
|
||||
the service bias its examples carry. A module is one managed thing, named once, described
|
||||
completely by its manifest. It may have a package, files, a container, a unit, a binary, a seat it
|
||||
holds, and tools it serves — any one of these, or all, or two. There is **no kind of module**: zsh
|
||||
has a package, files, a seat claim and the tools that claim obliges it to serve; downloads has a
|
||||
folder, a process and tools; nftables has a package, files, a service, a seat and tools. The
|
||||
difference is what each declares, not what each is.
|
||||
|
||||
**The home has no boundary.** [To-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md)
|
||||
owns one directory under the home and draws a line inside it between the mesh's and the person's.
|
||||
Here the line is drawn only by what the modules declare: every file some module places is the
|
||||
mesh's; what no module declares is found and left alone, exactly as the adoption rules already
|
||||
say for a machine. The reach is bounded by sense, not by a rule — the mesh configures what can
|
||||
be configured, and a person's documents, projects and history are data under
|
||||
[ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md), not configuration.
|
||||
|
||||
**A module names no node and no path.** The operator account is a node fact and the home is
|
||||
derived from it ([to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §1–2,
|
||||
shipped in the controller; its record is proposed in an open change). A module places a file
|
||||
*under the home, owned by the account*, and the same manifest lands on a server and a laptop.
|
||||
|
||||
## One default, varied by settings, never by edits
|
||||
|
||||
**One module, one default configuration.** The window manager module ships the configuration
|
||||
that is right for every node. There are no flavors: the predecessor's one desktop module carried
|
||||
four, one per class of machine, and what differed between them is what settings are for.
|
||||
|
||||
**A node varies a module in exactly two ways.** A **setting**, declared by the module with its
|
||||
type, meaning and default (proposed alongside the container-runtime records), set for the mesh
|
||||
or for one node, and rendered into the file at composition — the value is in the file, not in an
|
||||
environment variable the file reads. Or a **kept region**: a block in a file the mesh writes
|
||||
*into*, where the operator's own lines survive every push
|
||||
([ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md)). An
|
||||
edit to a managed file outside such a region is not a third way; it is overwritten, as
|
||||
[ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md) says, and the
|
||||
predecessor's habit of adopting disk drift back into its database is not carried over.
|
||||
|
||||
The predecessor's theming — some ninety environment variables substituted into templates at sync
|
||||
time, with tools to list and set them — is the same idea with the wrong rendering. The knobs
|
||||
become declared settings; the file carries the value.
|
||||
|
||||
## Roles a machine has once are seats, and seats carry tools
|
||||
|
||||
**A role a machine fills at most once is a node-scoped seat**, declared by a module
|
||||
([ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md),
|
||||
[ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)): the login shell, the
|
||||
display session, the display server, the terminal emulator, the launcher, the notifier, the
|
||||
compositor, the lock screen, the service manager, the boot loader. Several modules may be able to
|
||||
hold one — zsh, fish and bash can all hold the login shell — and the assignment on each node says
|
||||
which does. Installing a shell is installing software; holding the seat is being *the* shell.
|
||||
|
||||
**A seat's contract is its tools** ([ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
|
||||
Every holder of the login-shell seat serves `execute`, which takes one string, the command, and
|
||||
runs it on the node the seat is scoped to. Every holder of the boot seat serves "rebuild the boot
|
||||
images", so *"rebuild your boot images"* is a verb addressed to a machine, not a one-off step in
|
||||
a hook. Every holder of the service-manager seat answers for the units on the machine, system and
|
||||
user scope. A module may serve its own tools beside the seat's
|
||||
([ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) §2): show the rendered
|
||||
configuration, set a theme value, report status.
|
||||
|
||||
**Any tool may be called from any node.** The operator's statement, and the grant model it
|
||||
implies: the executor on each node may call everything, as the console already may. A verb that
|
||||
needs root on the machine is the module's concern — the tool escalates, the executor and the
|
||||
caller do not know.
|
||||
|
||||
## Servers and workstations differ by capability, not by catalogue
|
||||
|
||||
The same catalogue serves every node. A module declares what it needs — a graphical session, a
|
||||
display server, a container runtime — and the machine reports what it has, as the profile already
|
||||
reports eight capabilities today ([issue 160](../../04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md)).
|
||||
Assignment refuses the wrong placement by name
|
||||
([ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md) §3). So every node takes the shell,
|
||||
the prompt, git and the agent; only a node with a graphical session can take the display server,
|
||||
and only a node holding the display server can take a window manager. Nothing in a module says
|
||||
"workstation".
|
||||
|
||||
## What the operator would say to the mesh
|
||||
|
||||
*Set the login shell on the build node to fish. Rebuild the laptop's boot images. Show me the
|
||||
window manager's effective configuration on the desktop and where each value comes from. Give
|
||||
the downloads folder on the laptop to the home server. Run `uptime` on every node.* Each of these
|
||||
is a seat verb or a module tool, addressed to a node, answered by whatever holds the role there.
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
# 02 — What exists, and what is missing
|
||||
|
||||
*Measured 2026-10-02 on one installation: two workstations, two servers, all four converged to
|
||||
the mesh; the predecessor retired on the last workstation the day before. Numbers are from the
|
||||
machines and the repositories, not from memory.*
|
||||
|
||||
## 1. What the predecessor's desktop looks like
|
||||
|
||||
The predecessor's catalogue on the laptop held **34 modules**, of which **28** are the operator's
|
||||
environment rather than services. By what they declare:
|
||||
|
||||
| shape | count | examples |
|
||||
|---|---|---|
|
||||
| package only | 9 | browser, mail client, process monitor, media player, file manager, chat |
|
||||
| package + `/etc` files + system service | 5 | login manager, display server, power and thermal daemons, package manager configuration |
|
||||
| package + files under the home | 6 | shell and prompt, the agent at the terminal, scripts, the sync client, a music player |
|
||||
| files under the home + user units + hooks | 2 | the desktop environment, audio |
|
||||
| third-party organisation tooling | 6 | out of scope here |
|
||||
|
||||
**The desktop module alone** declares **88 files**, **4 flavors** (the window-manager stack, and
|
||||
one per class of machine), **2 user units** with a hook to enable them, 8 files under `/etc`, a
|
||||
wallpaper shipped as an asset, and reads **about 90 environment variables** as theme knobs,
|
||||
substituted into its templates at sync time and set through a theming tool. Its hook exists
|
||||
because *shipping a unit file does not run it*: one unit had been deployed for months and ran on
|
||||
one machine only, because somebody had enabled it there by hand.
|
||||
|
||||
**The shell module** ships `~/.zshrc`, the prompt configuration, an `~/.ssh/config` that the
|
||||
predecessor generated from its registry, and a `LOGIN_SHELL` variable applied with `chsh` by a
|
||||
hook. Two flavors: the prompt theme, and autocompletion.
|
||||
|
||||
**Other modules write into the desktop module's files.** The chat client places i3 and notifier
|
||||
snippets into `config.d` directories the desktop module owns, and its launch flags, window
|
||||
placement and notification colours are each a variable with a default.
|
||||
|
||||
**One-off steps live in hooks** across the set: enable user units, `chsh`, create a swap file,
|
||||
`mkinitcpio`, enable a vendor VPN service the package ships disabled. Every one is state the
|
||||
host could declare or a verb a seat could serve; none is today.
|
||||
|
||||
## 2. What the migration did with them
|
||||
|
||||
The migration's module to-do scoped the whole set out as *desktop / workstation ricing — the
|
||||
workstation's own environment* and *node/OS tooling — managed on the node, never catalogue*. The
|
||||
last workstation's runbook then split the same set three ways: **A**, system scope, which the
|
||||
host's vocabulary can express today (the login manager, the display server, the power daemons,
|
||||
the package manager, the container runtime); **B**, under a home or a user unit, waiting on
|
||||
to-be 29; **C**, package only, the operator's call. The migration log closes the workstation with
|
||||
*the operator's desktop awaiting its design*.
|
||||
|
||||
Two things followed from scoping them out. Nothing regenerates those files now, so a fix is a hand
|
||||
edit — the login manager's session script was fixed this way on the day of writing, and recorded
|
||||
in a repository nothing deploys from. And the one piece of this family written as a mesh module,
|
||||
the ssh client, was closed on hold in the catalogue until the controller carried the account fact.
|
||||
|
||||
## 3. What the records already give
|
||||
|
||||
| wanted | record | state |
|
||||
|---|---|---|
|
||||
| one module per managed thing; every module may have tools | [ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md) | accepted; examples are services, and the shell is named as a *shared* seat |
|
||||
| a module declares its own node-scoped seat | [ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) | accepted |
|
||||
| a seat's contract is its tools; a holder may add its own | [ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) | accepted; one node seat serves verbs live |
|
||||
| a capability the machine reports gates a holder | [ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md) §3 | accepted; the profile already reports `graphical-session` |
|
||||
| the account as a node fact; a file under the home owned by it | [to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §1–2 | built in the controller; its record proposed in an open change |
|
||||
| inside a home: owned, written into, written by the module, found | proposed in the same change | proposed |
|
||||
| a setting declared with type, meaning, default and cost | proposed with the container-runtime records | proposed |
|
||||
| a managed file is derived; an edit is overwritten | [ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md) | accepted |
|
||||
| the mesh writes into a shared file, never over it | [ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md) | accepted |
|
||||
| a module names no path; the host resolves the home | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) | accepted |
|
||||
| the `user` shape: a login shell is declared state | [to-be 05](../../03-DESIGN/01-to-be/05-the-node-host.md) | designed; used by no module |
|
||||
|
||||
## 4. What is missing
|
||||
|
||||
1. **The account is recorded nowhere.** The node record has the column; on all four nodes it
|
||||
is empty. Every home-scoped module is unassignable until the operator states it.
|
||||
2. **User-scoped units.** The host's `service` shape has no user scope. To-be 29 says it
|
||||
plainly: *a workstation's per-user daemons have no form the mesh can send.* The desktop
|
||||
module's two units, the audio masks, the power module's memory guard and the thermal
|
||||
daemon's profile switcher all need it.
|
||||
3. **One-off steps.** `mkinitcpio`, `chsh`, creating a swap file. Each is either declared
|
||||
state the host lacks a shape for, or a verb a seat should serve. An action in a declaration
|
||||
is refused over the link, and rightly.
|
||||
4. **Settings leak** ([issue 168](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)):
|
||||
a setting reaches every mergeable file and every contribution of its module. Ninety theme
|
||||
knobs on that mechanism would reach ninety files. The proposed settings record says a setting
|
||||
names the file it lands in; that has to ship first.
|
||||
5. **Where tools run.** Every module that serves a tool today does so from its own container
|
||||
per node. See [03](03-one-tool-executor-per-node.md).
|
||||
6. **A seat's verbs are undecided for every seat but three.** To-be 33 leaves which verbs each
|
||||
seat serves as *a decision per seat, slowly*. The environment adds a dozen seats.
|
||||
7. **Catalogue placement.** The media chain left this catalogue for its own; whether the
|
||||
environment does the same, and whether a third-party organisation's tooling belongs in a
|
||||
public catalogue, are unasked.
|
||||
@@ -0,0 +1,88 @@
|
||||
# 03 — One tool executor per node
|
||||
|
||||
*The direction the operator set on 2026-10-02, the evidence it rests on, and what it supersedes.
|
||||
A direction, not yet a decision: the record is written when this effort graduates.*
|
||||
|
||||
## Where tools are served today
|
||||
|
||||
[To-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) names three families — a
|
||||
role's tools on the seat, a module's own tools on the module, the mesh's own verbs on the
|
||||
controller seat — and [ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||
says a runtime serves the subjects its membership issues. What *runs* that runtime is
|
||||
[ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md):
|
||||
one supervised process per module, under the module's own account, carrying that module's
|
||||
compiled tools. Measured on the live mesh:
|
||||
|
||||
| who answers | how it runs | count |
|
||||
|---|---|---|
|
||||
| the mesh's own verbs | the controller binary, on its node | 17 verbs |
|
||||
| the store seat | the store's own runtime | 2 verbs |
|
||||
| the packet-filter seat | **a container per node**, built on the tool-runtime base image, with the network namespace and `NET_ADMIN`, on all four nodes | 3 verbs and 1 own tool |
|
||||
| every module's own tools | the module's container, one per node it runs on | 67 tools across the catalogue |
|
||||
| the console | a container per node, loopback MCP, `invokes: *` | serves none, calls all |
|
||||
| the host | — | serves nothing; answers no question about the machine |
|
||||
|
||||
**The packet-filter holder is the case to look at.** The module is a package, three files and a
|
||||
system service. To serve three verbs it also declares a built image and a container on every
|
||||
node whose only job is to answer them. Scaled to the environment — a shell, a prompt, a launcher,
|
||||
a notifier, a compositor, a login manager, a service manager, a boot loader, a downloads folder —
|
||||
that is one container per module per node for software that is itself not a container, and the
|
||||
operator's judgement is that tools should not run inside a container at all.
|
||||
|
||||
## The direction
|
||||
|
||||
**One tool executor per node, on the host side.** A process the host supervises, the way the
|
||||
launcher supervises the host ([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)): not a
|
||||
container, one bus credential for the node, module-agnostic. It loads the tool code of every
|
||||
module assigned to the node and serves each module's tools and each held seat's verbs on the
|
||||
subjects the membership issues — nothing changes in what [ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
|
||||
and [ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||
say about subjects, grants and memberships; what changes is that one process subscribes for the
|
||||
node instead of one per module.
|
||||
|
||||
- **A module brings its tools as a built artifact**, a bundle the pipeline produces, never an
|
||||
image. The executor knows bundles and subjects; it knows nothing of zsh or nftables.
|
||||
- **A tool is code the module wrote**, one function behind an MCP verb. `execute` on the shell
|
||||
seat is a function with a string argument. The executor does not declare, template or
|
||||
interpret tools; it runs them.
|
||||
- **Root is the module's concern.** A tool that must change the packet filter or rebuild boot
|
||||
images escalates itself. The executor does not run as root for everyone, and the caller does
|
||||
not know.
|
||||
- **Any node may call any tool on any node.** The executor's credential may call everything,
|
||||
as the console's already does; per-module grants on the calling side are not kept.
|
||||
- **The mesh's own verbs stay with the controller** ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)),
|
||||
and a mesh-scoped seat's verbs run on the node that holds it
|
||||
([ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)).
|
||||
No hub is added; the controller's node is already one.
|
||||
|
||||
**The console is the executor, renamed.** It already runs on every node with a credential that
|
||||
may call everything, and it already serves the mesh's tools to whoever is on the machine over
|
||||
MCP on loopback ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
|
||||
It moves out of its container into the host's process tree, gains the serving half, and takes a
|
||||
name that says what it is — *the node's tool runtime* or simply *node tools*; "console" names
|
||||
the operator's half only.
|
||||
|
||||
## What it supersedes, and what it keeps
|
||||
|
||||
| record | effect |
|
||||
|---|---|
|
||||
| [ADR 0047](../../02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md), [ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md) | superseded *for tools*: one process per node runs every module's tool code, under one account. A module's long-running service — a daemon, a container — is untouched; the executor runs tools, not services. The record must say why one account for every module's tools is acceptable: every tool may be called from every node anyway, and root is taken by the tool, not granted to the process |
|
||||
| [ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md) | kept in substance — a module, assigned per node, loopback MCP, the machine's login is the authority — changed in form: host-side, not a container; serves as well as calls; renamed |
|
||||
| [to-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §6, [to-be 34](../../03-DESIGN/01-to-be/34-the-console.md) | amended the same way |
|
||||
| [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) §3, *a container may ask for a capability* | moot for that holder: the verbs run on the host side and escalate as they need |
|
||||
| the container-runtime seat, proposed in an open change: *the holder runs as a supervised process and serves the verbs locally to the host and on the bus* | consistent — a supervised process serving verbs is what the executor is; the open question is whether that holder keeps its own process or serves through the executor like everyone else |
|
||||
| the tool-runtime base image | no longer the way tools reach a node; may remain the way a module's *service* is built |
|
||||
|
||||
## What stays open
|
||||
|
||||
- **The executor's language.** The host is a static Go binary and loads no plugins, so the
|
||||
executor is a sibling process, and its language decides the language of every tool bundle.
|
||||
One decision, taken once.
|
||||
- **How a bundle reaches the node.** An artifact of the module's build, delivered as the host
|
||||
delivers everything else; whether it is a file resource in the declaration or a thing the
|
||||
executor fetches by digest.
|
||||
- **Reload.** A push that adds or upgrades a module's bundle reaches a running executor as a
|
||||
reload, not a restart, or every tool on the node blinks on every push.
|
||||
- **The host's own questions.** [Issue 160](../../04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md)
|
||||
wants a machine to say more about itself. With an executor on every node, "what is this
|
||||
machine made of" is a seat verb like any other, served there.
|
||||
@@ -0,0 +1,65 @@
|
||||
# 04 — The seats of the environment
|
||||
|
||||
*Candidates, not decisions. To-be 33 says which verbs a seat serves is a decision per seat,
|
||||
taken slowly, because a seat's tools bind every future holder. This document lists the roles the
|
||||
operator's machine has once, who could hold each, what gates it, and a first verb or two — so
|
||||
each record has a starting point.*
|
||||
|
||||
## The rule for what is a seat here
|
||||
|
||||
A role the machine fills **at most once** is a node-scoped seat, declared by the module family
|
||||
that fills it ([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)). A thing
|
||||
several of which coexist without contention — editors, browsers, media players — is not a seat;
|
||||
each is a module with its own tools, and nothing is singular about it. A seat is held by one
|
||||
assignment per node; other modules of the same family may be installed beside it without
|
||||
holding it ([ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md) §1, read with the sharper
|
||||
distinction: *installed* is not *holding*).
|
||||
|
||||
## Candidate seats
|
||||
|
||||
| seat | holders | gated by | first verbs |
|
||||
|---|---|---|---|
|
||||
| **login shell** | zsh, fish, bash | nothing: universal | `execute(command)`; `show-config`; the holding itself sets the account's login shell through the host's `user` shape |
|
||||
| **service manager** | systemd | the `service-manager` capability the profile reports | units: list, status, start, stop, restart, enable, journal; **user scope** on each |
|
||||
| **boot** | grub, systemd-boot | a machine that boots itself (not a container host) | `rebuild-images`; `entries` |
|
||||
| **package manager** | pacman, apt | the `package-manager` capability | search, installed, upgrade, orphans; today a capability the host uses, not a seat anyone holds |
|
||||
| **display server** | xorg, wayland compositors that are their own server | the `graphical-session` capability | `displays`; `layout` |
|
||||
| **display session** | i3, sway | the display server seat held on the node; i3 needs x11, sway needs wayland | `reload`; `workspaces`; `windows`; `move` |
|
||||
| **terminal emulator** | xterm, alacritty, foot | display session | `open`; `font` |
|
||||
| **launcher** | rofi, dmenu | display session | `show`; `theme` |
|
||||
| **notifier** | dunst, mako | display session | `send`; `history`; `rule` |
|
||||
| **compositor** | picom | display server (x11 only) | `restart`; `effects` |
|
||||
| **lock screen** | i3lock, swaylock | display session | `lock` |
|
||||
| **bar** | i3status-rust, waybar | display session | `reload`; `blocks` |
|
||||
| **login manager** | lemurs, greetd | graphical session | `sessions`; `default-session` |
|
||||
| **audio** | pipewire, pulseaudio | the machine reports a sound device | `sinks`, `sources`, `default`, `volume`, `mute` |
|
||||
| **clipboard** | greenclip, cliphist | display session | `history`; `clear` |
|
||||
|
||||
Not seats, modules with their own tools: the editor, the browser, the mail client, the file
|
||||
manager, the media player, the chat client, the agent at the terminal, the downloads folder, the
|
||||
scripts folder, the sync client, the power and thermal daemons that are specific to one machine's
|
||||
hardware.
|
||||
|
||||
## What the table implies
|
||||
|
||||
**Capabilities come first.** `graphical-session`, `service-manager` and `package-manager` are
|
||||
reported today. *A display server is held* is not a capability but a seat being held, and a
|
||||
module that needs it declares a dependency on the seat, not on a capability: *i3 needs the
|
||||
display server seat held by xorg*. Whether a held seat can gate another's assignment is a
|
||||
question for the controller's resolver, and the first environment module after the shell will
|
||||
ask it.
|
||||
|
||||
**The service manager comes early.** Four of the predecessor's modules ship user units, and the
|
||||
executor itself is a unit. User scope on the host's `service` shape is a host change whichever
|
||||
module holds the seat; the seat's holder answers the questions about units, it does not apply
|
||||
them — the host does, as it does for every declared resource.
|
||||
|
||||
**The shell comes first.** Universal, no capability, one verb that is immediately useful on
|
||||
every node, and the `user` shape already makes the login shell declared state. It is the module
|
||||
that proves the pattern: a package, files under the home owned by the account, a seat claim,
|
||||
tools served by the executor, settings for the few things that vary per node, and a kept region
|
||||
for the operator's own lines.
|
||||
|
||||
**The login manager is the first system-scope one**, because it needs nothing new: a package,
|
||||
two files under `/etc`, a service — the same shape the ssh daemon module has today — and the
|
||||
session script it owns is the file that was hand-fixed the day this effort opened.
|
||||
@@ -204,3 +204,14 @@ 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 0178](0178-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,3 +283,13 @@ 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 0178](0178-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 0178
|
||||
> 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.
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-09-27
|
||||
deciders: jochen
|
||||
reconstructed: true
|
||||
extends: 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||
---
|
||||
|
||||
# 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
|
||||
decision behind it. This record states what was decided, from the code and the design, and adds the
|
||||
two rules the code left implicit — what an empty account means for a module, and that the account is
|
||||
stated rather than discovered. Written 2026-10-02.*
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) took every host path out of a
|
||||
module definition and gave a module's *system* data a place: a directory the mesh resolves under the
|
||||
node's root, owned by the module. It said nothing about the other half of a filesystem — the files
|
||||
that belong under a person's home and are owned by that person. The predecessor wrote several of
|
||||
those: the ssh client configuration, the shell's configuration, an agent's instruction files. It knew
|
||||
whose home it was writing into because each of its node records carried a login name. The mesh took
|
||||
the machine facts over and dropped the human one.
|
||||
|
||||
The loss was found the ordinary way: `ssh <node>` logged into the home-server under the workstation's
|
||||
own login name, because nothing in the mesh said the home-server's account was a different one
|
||||
([to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md),
|
||||
[issue 172](../04-ISSUES/172-the-ssh-client-block-matches-one-spelling-of-a-machine/00-report.md)).
|
||||
|
||||
What the controller does since 2026-09-27: a node record carries an operator account and, optionally,
|
||||
its home; the account and its home are machine facts a definition may name in a resource's path, owner
|
||||
and content; a roster file may say it lives under the home, and is then rendered per node, placed under
|
||||
that node's account's home, owned by the account, and left out on a node with no account. On
|
||||
2026-10-02 **all four nodes of the live mesh carry an empty account**: the fact exists and nobody has
|
||||
stated it, so no home-scoped resource can land anywhere yet.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **The definition names the login.** `owner: <name>` in the module. Rejected: it is the installation
|
||||
written into a definition, which ADR 0112 forbids and
|
||||
[ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md) checks for, and it is
|
||||
wrong on the first machine whose login differs — which is exactly the machine that surfaced this.
|
||||
2. **The host discovers the account.** The first non-system user, or whoever ran the enrolment.
|
||||
Rejected: a guess. A shared machine has several people on it, a server may have none, and a host
|
||||
deciding whose files these are is a decision the mesh then cannot see, state or correct.
|
||||
3. **The account is a fact the operator states on the node record, and the home is derived from it
|
||||
unless stated.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**A node has an operator account: the login name of the person who works on it.** It is stated by the
|
||||
operator on the node record, the way a node's address or mode is held there, and it is empty for a
|
||||
machine nobody logs into. Empty is a real state, not a missing value. The mesh holds the fact because
|
||||
everything below derives from it, and because it is precisely the fact that was lost when the
|
||||
predecessor's records were not carried over.
|
||||
|
||||
**The account's home is derived unless stated.** The superuser's home for the superuser, the
|
||||
distribution's conventional per-user home otherwise; a node whose account lives elsewhere states its
|
||||
home. One place computes the default, so a fact and the record cannot disagree about it.
|
||||
|
||||
**A resource may be placed under the home, owned by the account.** This is ADR 0112's move one level
|
||||
over: as a module's system directory is resolved under the node's root, a file under a person's home is
|
||||
resolved against the account's home, and owned by the account rather than by root or a module's own
|
||||
account. A definition names the account and its home as machine facts, never as a path; a roster fact
|
||||
may say it is a home file and is then placed and owned the same way. The controller resolves both at
|
||||
composition, and the host chowns what it creates.
|
||||
|
||||
**A node with no account cannot carry a home-scoped resource, and says so.** A roster fact that lives
|
||||
under the home is left out of that node's declaration rather than written to nowhere. A resource naming
|
||||
the account fact on such a node is refused at composition, naming the fact the machine does not have.
|
||||
A module that writes a person's files is thereby unassignable to a machine with no person on it, which
|
||||
is the right refusal.
|
||||
|
||||
**One account per node is what this record decides.** Several people on one machine is left open, with
|
||||
the constraint that allowing it must not force the common case — one workstation, one person — to name
|
||||
anything.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **The operator states the account before any home-scoped module lands.** Today none is stated, so the
|
||||
first assignment of such a module begins with four node records.
|
||||
- The roster carries each node's account, so a composed ssh configuration logs in as the right person
|
||||
on every machine — the gap that surfaced this, closed by the same fact.
|
||||
- A family of modules becomes writable: everything the predecessor placed under a home — ssh client,
|
||||
shell, the agent's instruction files — is now a module naming a fact rather than a path
|
||||
([to-be 29 §2](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md)).
|
||||
- **What got harder:** a definition cannot say "my user's home" without the mesh knowing who the user
|
||||
is, so a module of this family is refused on a freshly enrolled machine until a person is named on
|
||||
it. That is a prompt, not an obstacle.
|
||||
- **Not decided here:** several accounts per node; a service unit running as the account rather than
|
||||
as root or a module; a one-off step run as the account. Each is a record of its own.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A resource's path and owner resolve the account and its home | controller tests on machine-fact resolution: a file naming the account facts lands under the account's home, owned by the account |
|
||||
| The home is derived unless stated | a controller test: the superuser's home for the superuser, the conventional home otherwise, the stated home when one is stored |
|
||||
| A home roster fact is left out on a node with no account | a controller test on roster composition: the file is absent from that node's declaration and present on a node with an account |
|
||||
| A resource naming the account on a node with no account is refused by name | a controller test on machine-fact resolution: the refusal names `account` and lists the facts the machine does have |
|
||||
| No definition names a home path | ADR 0112's catalogue test on host paths, which a `/home` or `/root` literal fails |
|
||||
|
||||
## References
|
||||
|
||||
- [to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) — the design this record
|
||||
gives a foundation to, and its "what has shipped" section
|
||||
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) — the system-path placement
|
||||
this mirrors; [ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md) — why
|
||||
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 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)
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-02
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md
|
||||
---
|
||||
|
||||
# 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 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
|
||||
five of them (an instruction file, a conventions rule, a settings file it merged into, two skills); a
|
||||
sibling module placed a sixth (the node's identity rule). The agent itself writes the other
|
||||
twenty-four: its settings, its credentials, its history, the memory of every project it has worked in,
|
||||
its plugins, its session logs. Several of those are what [to-be 15](../03-DESIGN/01-to-be/15-the-agent-session.md)
|
||||
calls memory *written by the session itself and declared by nobody*: a mechanism that regenerated the
|
||||
directory would erase a season of it, silently, while reporting success.
|
||||
|
||||
The predecessor's own module recorded the hazard in the other direction. Its settings file was first
|
||||
shipped as *replace*, and every `/model` choice a person made inside a session was reverted to the
|
||||
template's value on the next synchronisation — on every node, indefinitely, with no indication why. It
|
||||
was changed to *merge*, and the comment explaining why is still in its manifest.
|
||||
|
||||
**And the generator is gone while its output stayed.** The predecessor was retired from the laptop on
|
||||
2026-10-01. Its six files are still on both workstations, with their content telling every session to
|
||||
use tools that no longer exist. Nothing owns them; nothing will ever rewrite or remove them.
|
||||
|
||||
[To-be 29 §3](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) drew the line for one directory,
|
||||
`~/.ssh`: the mesh owns the directory and the files it places; it holds the person's private keys and
|
||||
personal drop-ins as found. That was argued from the lockout `~/.ssh` can cause. The argument here is
|
||||
the same shape with a different stake — the person's work rather than the person's way in — and it has
|
||||
to hold for every directory the family of home-scoped modules will touch, so it is a rule, not a
|
||||
section.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **The module owns the directory whole**, regenerating it from the definition. Rejected: it destroys
|
||||
the memory, history and local settings the agent writes for itself, which is the failure to-be 15
|
||||
names and the predecessor's settings file demonstrated at small scale.
|
||||
2. **The module owns only the files it names, and nothing about the directory.** Rejected: *owning one
|
||||
file beside foreign ones is not owning anything* (to-be 29). The directory must exist, with the right
|
||||
owner and mode, before the tool first runs on a fresh machine; and a credentials file in a
|
||||
world-readable directory is a credentials file in the wrong directory.
|
||||
3. **The module owns the directory and the files it places; a file the tool writes for itself is
|
||||
written into, never over; everything else is held as found.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**A home-scoped module owns the directory it declares: its existence, owner and mode.** The host creates
|
||||
it if absent, owned by the account, and never removes it while it holds anything
|
||||
([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)). Inside it, every path the module touches
|
||||
is in exactly one of four classes, and **the class is visible in the definition from the shape
|
||||
declared**, not inferred from what happened to be on disk:
|
||||
|
||||
| class | declared as | the host's rule |
|
||||
|---|---|---|
|
||||
| **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 0178](0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.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
|
||||
own state file are the tool's; the mesh has one or two facts to state in each. Setting those keys and
|
||||
nothing else is what lets a person's `/model` choice survive a push, and what lets the mesh's keys be
|
||||
taken back cleanly when the module goes.
|
||||
|
||||
**A predecessor's output is found.** A file placed by a generator that no longer exists is, to the
|
||||
mesh, a file it has no record of making. Where the successor module keeps the path, declaring it
|
||||
*adopts* it: the host keeps the original once and writes the mesh's. Where the successor does not keep
|
||||
the path, the mesh does not remove the file, because it removes nothing it did not make; **the operator
|
||||
removes it, once**, and the module's definition names those paths in its own documentation so the step
|
||||
is not forgotten. This is the first instance of the one-off setup step to-be 29 leaves open, and the
|
||||
rule chosen for it is that it is a person's act, listed, not a module's.
|
||||
|
||||
**The rule is the family's.** An ssh client module, a shell module, an agent module each declare their
|
||||
directory and classify their paths this way. A module that cannot say which class a path is in has not
|
||||
finished its definition.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A person's work under their home survives every push and every unassign. The mesh's own files come
|
||||
and go with the module; the mesh's keys in the tool's files come and go with it; the directory stays.
|
||||
- **Stale files survive too.** Two workstations keep three predecessor files each until a person removes
|
||||
them — a visible cost, accepted over a mesh that deletes under a person's home. A module author who
|
||||
renames one of the mesh's own files has the ordinary path: the old resource id is undeclared and the
|
||||
host removes what it made ([ADR 0118](0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md)).
|
||||
- A module's definition is longer by a classification, and a reviewer has one more question per path.
|
||||
That is the point: *which parts are managed must be explicit rather than inferred* (to-be 15).
|
||||
- **What got harder:** a module cannot seed a person's preference once and leave it. A seeded file
|
||||
([ADR 0087](0087-a-seeded-file-is-created-once.md)) is the shape for that, and it is available to
|
||||
this family unchanged; what is refused is a seed the module later wants to change, because what grew
|
||||
in it is the person's.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| The directory is created owned by the account and kept when the module goes | host tests of a directory resource with an owner (ADR 0051's and 0118's), and the family's lab check below |
|
||||
| An owned file found with no record is kept once, then written | host tests of ADR 0102's kept-original rule |
|
||||
| Only the declared keys of a written-into file change, and are given back | host tests of ADR 0102: declared keys set, the rest kept, restored when undeclared |
|
||||
| Nothing found is touched | the family's lab check: a machine with a seeded home holding a person's file beside a predecessor's; after apply the person's file is byte-identical, the predecessor's is kept as the original, the mesh's keys are set and the person's keys in the same file remain; after unassign the mesh's files are gone, the keys are restored, the person's files are untouched and the directory stands |
|
||||
| Every path a home-scoped module touches is classified | a catalogue review rule for this family: each path is a directory, a file, a file written into, a secret-and-step, or absent — the first module written to it is [to-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md) |
|
||||
|
||||
## References
|
||||
|
||||
- [to-be 29 §3](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) — the same boundary drawn for `~/.ssh`
|
||||
- [to-be 15](../03-DESIGN/01-to-be/15-the-agent-session.md) — a session's memory is declared by nobody
|
||||
- [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), [ADR 0087](0087-a-seeded-file-is-created-once.md),
|
||||
[ADR 0118](0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md), [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md) — the mechanics each class rests on
|
||||
- [ADR 0051](0051-shared-data-is-the-operators.md) — the third case the host had no word for: what it neither made nor configured
|
||||
- the predecessor's `claude-code` module manifest, whose comment on `strategy: merge` records the reverted `/model` choice
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
---
|
||||
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 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 37](../03-DESIGN/01-to-be/37-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
|
||||
@@ -273,6 +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)
|
||||
- **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 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](0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
|
||||
|
||||
### How it is built
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ status: in-progress
|
||||
code:
|
||||
- mesh-controller internal/licences
|
||||
- mesh-controller cmd/mesh-controller/licence.go
|
||||
updated: 2026-09-05
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0024-model-access-is-a-provision.md
|
||||
- 02-DECISIONS/0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.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
|
||||
@@ -96,6 +97,14 @@ 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 0178](../../02-DECISIONS/0178-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), [37](37-the-anthropic-licence-manager.md)). This
|
||||
provision stays for the consumers that do not care which vendor answers.
|
||||
|
||||
**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
|
||||
and this reasoning does not extend to them. That belongs with
|
||||
|
||||
@@ -5,8 +5,10 @@ code:
|
||||
- mesh-controller internal/inventory
|
||||
- mesh-controller internal/catalogue
|
||||
- mesh-controller cmd/mesh-controller
|
||||
updated: 2026-10-01
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 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
|
||||
@@ -171,6 +173,14 @@ 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 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 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). 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
|
||||
|
||||
**Why it matters:** when HAL retires, the generators that keep `~/.ssh`, shell config and the
|
||||
|
||||
@@ -28,6 +28,14 @@ module's credential and listens on loopback. It has no state, no provision, no s
|
||||
the bus, which it gets the way every module does: a credential the mesh minted for `<node>.mesh-console`,
|
||||
sealed to the machine, delivered as the module's own secret.
|
||||
|
||||
*2026-10-02:* it gains one provision, at node scope — the MCP endpoint on loopback, serving the port
|
||||
the machine gave it — so that a module whose software must be told where the console is requires that
|
||||
and is coupled to an endpoint rather than to a module's name
|
||||
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)). The first
|
||||
consumer is the operator's agent, [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md) §6;
|
||||
a machine without the console refuses such a module by name. Nothing else above changes: no seat, no
|
||||
state, no tools of its own.
|
||||
|
||||
Its manifest says three things nothing else in the catalogue says together:
|
||||
|
||||
- `invokes: ["*"]` — it calls every tool on the mesh, and the bus grants exactly that publish side;
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 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-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.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/0109-a-package-registry-seat-is-one-per-ecosystem.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 [37 — The Anthropic licence manager](37-the-anthropic-licence-manager.md).
|
||||
|
||||
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.
|
||||
|
||||
**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. Where the mesh's configuration lives: the agent's managed directory, not the home
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
| the predecessor placed | becomes |
|
||||
|---|---|
|
||||
| `~/.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) |
|
||||
|
||||
**The home.** Under [ADR 0177](../../02-DECISIONS/0177-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.
|
||||
|
||||
## 2. What the module declares and what its code writes
|
||||
|
||||
**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`.
|
||||
|
||||
**Written by the module's code**, from the facts file and the manager's hand-over, whenever either
|
||||
changes:
|
||||
|
||||
| path | content |
|
||||
|---|---|
|
||||
| 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) |
|
||||
|
||||
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
|
||||
([research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md)).
|
||||
|
||||
**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.
|
||||
|
||||
## 3. What the instruction file says
|
||||
|
||||
Prose, not a paste; the file is the module's.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
## 4. The console
|
||||
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
## 5. The licence: the consumer side
|
||||
|
||||
[ADR 0178](../../02-DECISIONS/0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
|
||||
decides it; to-be 37 is the manager's half. This module:
|
||||
|
||||
- **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 0176](../../02-DECISIONS/0176-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.
|
||||
|
||||
## 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 0178 |
|
||||
| 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 0177, the host's agnosticism |
|
||||
| on a lab machine with no account, the assignment is refused naming the fact | ADR 0176 |
|
||||
| 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 0178 |
|
||||
| the API-key binding writes nothing under the home and the agent authenticates through the helper | ADR 0178 |
|
||||
| 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 |
|
||||
|
||||
## What this does not settle
|
||||
|
||||
- **Several operator accounts on one node** (ADR 0176 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 37 §5); that is for when workers
|
||||
exist, and nothing here changes for it.
|
||||
- **The package repository seat** (§7).
|
||||
- **How the module's code is run** — a supervised process per module ([ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md))
|
||||
today, one executor per node when research 018 graduates. Nothing here depends on which.
|
||||
|
||||
## References
|
||||
|
||||
- [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-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) — the decisions
|
||||
- [37 — The Anthropic licence manager](37-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
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0178-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
|
||||
---
|
||||
|
||||
# 37 — 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 0178](../../02-DECISIONS/0178-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 0178, 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 0178, to-be 32 §10 |
|
||||
| a worker bound to a dead licence is refused, never answered with another licence's token | ADR 0178, the fallbacks |
|
||||
| a grant offered with a mismatching identity is refused and one notification emitted | ADR 0178, 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 0178](../../02-DECISIONS/0178-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
|
||||
Reference in New Issue
Block a user