The predecessor's agent module was retired and its six files stayed on both workstations telling every session to use tools that no longer exist. This is its successor's design, revised during review on the operator's directions: the host is module-agnostic, the controller has no part, and a real licence manager hands out the correct licence in every situation. - ADR 0181 (reconstructed): the operator account is a node fact stated by the operator; the home is derived unless stated; a resource may be placed under it owned by the account; a node with no account refuses one. What the controller shipped on 2026-09-27 without a record. - ADR 0182: inside a home the module owns the directory and the files it places, writes into the tool's own files for its few keys, never declares a credential's content, and holds everything else as found; a predecessor's leftovers are the operator's to remove once. - ADR 0183: claude-licence-manager holds the mesh seat anthropic-licence-manager and owns the Anthropic licences, grants (encrypted with a key the vault made for it), bindings per touchpoint, usage and audit; one rotation source under a lease; a token travels module to module sealed to each node's module key on request/reply, never as an event; the agent module alone writes what the agent reads; the host delivers package and state and knows nothing else. A bounded exception to ADR 0113; dated mechanism notes on ADR 0050 and 0113. - To-be 36 (claude-code): the mesh's part of the agent's configuration lives in the agent's machine-wide managed directory, owned whole by the module and written by its code; nothing under the home but the credentials file of a subscription licence; the API-key licence through the key-helper; the console as a node-scoped provision (to-be 34 amended); MCP servers as settings with an mcp_configure tool; one agent directory per machine shared by every session. - To-be 39 (claude-licence-manager): store, the two licence kinds, keeping a grant alive, the hand-over, who gets which licence with the predecessor's fallbacks, adoption with the identity guard, the seat's verbs. Numbers taken across main and every open branch at the time of the merge; to-be 14, 29 and 34 carry dated notes; the glossary gains "operator account".
196 lines
12 KiB
Markdown
196 lines
12 KiB
Markdown
---
|
|
layer: to-be
|
|
status: in-progress
|
|
code:
|
|
- mesh-controller internal/licences
|
|
- mesh-controller cmd/mesh-controller/licence.go
|
|
updated: 2026-10-02
|
|
decisions:
|
|
- 02-DECISIONS/0024-model-access-is-a-provision.md
|
|
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
|
- 02-DECISIONS/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
|
|
---
|
|
|
|
# 14 — Model access
|
|
|
|
*[ADR 0024](../../02-DECISIONS/0024-model-access-is-a-provision.md) decided it and listed four
|
|
things the mesh did not have. Written 2026-08-31, when two of them were built. **The other two are
|
|
still gaps and are still written as gaps** — the record's own warning is that pretending otherwise
|
|
is how a plan becomes a surprise.*
|
|
|
|
## What was built
|
|
|
|
**A licence is a record, and the first provision no machine answers.** Everything else the mesh
|
|
brokers is answered by something running on a node. A hosted model is on nobody's machine and is
|
|
reached over the public internet, so the rule that refuses two ends sharing no private network —
|
|
correct everywhere else — must not apply to it. A machine on no private network at all can hold a
|
|
licence, and that is not a special case to remember: it falls out of the answer not being a
|
|
machine.
|
|
|
|
**The name is the operator's.** *The personal account*, *the organisation's account*. Those are
|
|
names a person uses, and the mesh uses them too, because the whole point is saying **which one** a
|
|
consumer uses — and an anonymous credential hanging off a provider cannot be said. Many to many,
|
|
so deliberately **not a claim**: two machines sharing an account is the ordinary case rather than
|
|
a collision.
|
|
|
|
**One provision name for all of them.** A module requires `model-access`, never `anthropic`. A
|
|
module that named a provider could not be moved onto a model the mesh runs itself without editing
|
|
it — and moving it is the point.
|
|
|
|
**A model in a machine's own set answers it locally**, and no record is consulted. That is what
|
|
makes *the mesh's own model* an ordinary answer rather than a parallel arrangement.
|
|
|
|
### Accept: taking a value the mesh did not make
|
|
|
|
Every other credential here the mesh generated, sealed to both ends and discarded. An API key
|
|
arrives from a person, and the missing verb was *accept*: **take a value, seal it to each holder,
|
|
discard the plaintext.** A mesh that kept operator-supplied keys readably is the arrangement this
|
|
project measured and rejected.
|
|
|
|
**It seals to the holders that exist at that moment**, and this has a consequence that must be
|
|
said out loud rather than discovered:
|
|
|
|
> A consumer put on a licence *after* the key was supplied has no key, and **the mesh cannot make
|
|
> one** — it discarded the only copy.
|
|
|
|
So that state is reported at every point a person could meet it: when the consumer is put on the
|
|
licence, in `licence list`, and — decisively — **the declaration is refused** rather than written
|
|
without the file. A machine that resolves cleanly and receives nothing fails later, somewhere that
|
|
names neither the licence nor the mesh.
|
|
|
|
**A key is read from a file or standard input, never an argument.** A key on a command line is a
|
|
key in shell history and in every process listing taken while it ran. It is never echoed back:
|
|
what is stored is unreadable by whoever holds it, the controller included, and printing it
|
|
would put the one copy that matters on a terminal.
|
|
|
|
## Refusing is felt, and that is the design working
|
|
|
|
ADR 0024 predicted it: *a mesh holding three ways to reach a model refuses every consumer that has
|
|
not said which — which is correct and is a great deal of saying-which the first time.*
|
|
|
|
It is correct, and correct is not the same as usable. So the refusal names **the candidates and
|
|
the exact command**. The difference between a mesh that refuses helpfully and one that merely
|
|
refuses is whether anybody can act on it without going and reading something else.
|
|
|
|
## Still gaps
|
|
|
|
Unchanged from [ADR 0024](../../02-DECISIONS/0024-model-access-is-a-provision.md), and deliberately
|
|
not half-built:
|
|
|
|
**A consumer that is not a machine.** *This worker uses that licence* is a binding to an agent, not
|
|
to a node. What is delivered still lands on a machine; what is **chosen** is chosen per agent, and
|
|
the provisions model has no consumer identity other than a node. What exists today is per module
|
|
per machine, which is a step toward it and is not it.
|
|
|
|
*2026-08-31: this gap now has named consumers rather than hypothetical ones.*
|
|
[ADR 0026](../../02-DECISIONS/0026-the-mesh-has-a-session-of-its-own.md) puts two sessions on the
|
|
controller node — the node's own and the mesh's — each bound in its own right. See
|
|
[`15-the-agent-session.md`](15-the-agent-session.md).
|
|
|
|
**And for sessions the gap is already closed, which was not obvious.** A binding is per module per
|
|
machine, and this document called that *a step toward it and not it* — reasoning that a machine
|
|
cannot name an agent. It cannot; but the two sessions are **two modules**, because they are the
|
|
same mechanism started in different context roots and a context root is what a module delivers.
|
|
So `(node, module)` tells them apart, and asking for a licence per session needed no new consumer
|
|
identity. Checked rather than argued: two sessions on one machine hold different licences, each is
|
|
given its own key, and releasing one leaves the other.
|
|
|
|
*2026-10-02:* the operator's own agent at a terminal is **not** a consumer of this provision: it is
|
|
coupled to an Anthropic grant and nothing else, so it uses the `anthropic-licence-manager` seat, whose
|
|
holder owns the Anthropic licences, their bindings and their rotation, and hands each node's agent its
|
|
token over the bus
|
|
([ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md),
|
|
[36](36-the-operators-agent-on-a-machine.md), [39](39-the-anthropic-licence-manager.md)). This
|
|
provision stays for the consumers that do not care which vendor answers.
|
|
|
|
**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
|
|
[ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md), which is unbuilt, and it
|
|
is the reason this section stays open rather than being struck out.
|
|
|
|
**Switching is a reaction, not a declaration.** A licence that hits its limit and must be swapped is
|
|
a response to something observed. Expressing it as a declaration would make the declaration mean
|
|
*whatever is working right now*, which is not a thing anybody declared. It belongs with
|
|
observability, changing a binding — and the binding is then declared as usual. **Saying this
|
|
plainly is what stops the declaration language growing a conditional**, and nothing built here
|
|
grew one.
|
|
|
|
## The vendor-agnostic generalisation
|
|
|
|
*[ADR 0050](../../02-DECISIONS/0050-model-access-is-vendor-agnostic.md) is accepted; this section
|
|
describes what it decides. The section above stands as what is built today.*
|
|
|
|
What runs is one vendor — the mesh's Anthropic feature. A read-only trace asked whether the
|
|
`model-access` provision is Anthropic-shaped or genuinely general, and found that the general layer
|
|
already exists: a licence is a record with a `vendor` and a non-secret `serves`, a holder's
|
|
credential is sealed per holder, `accept` takes an operator-supplied value and discards the
|
|
plaintext, and a locally-run model answers at node scope with no licence. None of that names
|
|
Anthropic. What is Anthropic's is a thin band: the credential is a subscription OAuth grant — an
|
|
hourly access token and a refresh token — and that shape alone drags central rotation, a
|
|
refresh-token-stripping delivery, an identity guard and a `utilization%` usage reading behind it.
|
|
|
|
**A vendor is an adapter.** The vendor-specific lifecycle moves into a per-vendor adapter selected by
|
|
the licence's `vendor` field — the same shape as a registrar-scoped `public-dns` provider behind the
|
|
neutral `public-dns` interface ([ADR 0044](../../02-DECISIONS/0044-a-public-name-is-provisioned-like-any-capability.md)).
|
|
A consumer still requires `model-access` and never names a vendor; the interface is drawn at the
|
|
consumer's real coupling — *reach a model* — per [ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md).
|
|
The field is `vendor` rather than `provider`, because "provider" already means *which node answers a
|
|
brokered provision* and the two facts must not share a word.
|
|
|
|
The adapter declares a `shape` — `static-key` or `refreshable-grant` — and, optionally, the verbs a
|
|
vendor happens to need: `accept` a supplied key, `refresh` a grant, read an `identity` off the
|
|
credential to catch a mis-binding, report `usage`, and `deliver` the value. **A static-key vendor
|
|
implements almost nothing** — a supplied key, sealed to its holders, delivered. The abstraction is
|
|
built so the common vendor is small and the rare one carries its own weight.
|
|
|
|
```
|
|
requires: model-access (the consumer, vendor-blind)
|
|
│
|
|
┌──────┴───────┐
|
|
│ a licence │ vendor: … serves: base URL, model
|
|
└──────┬───────┘
|
|
selected by │ vendor
|
|
┌─────────────────┼──────────────────────────┐
|
|
▼ ▼ ▼
|
|
anthropic-api-key anthropic (another vendor)
|
|
shape: static-key shape: refreshable-grant
|
|
accept, deliver accept, refresh, identity,
|
|
usage, deliver
|
|
```
|
|
|
|
**The crux is one relaxation, stated plainly.** A refreshable credential cannot be both sealed so the
|
|
mesh cannot read it *and* rotated centrally — rotation needs a readable refresh token, and the working
|
|
central rotation is the half [ADR 0024](../../02-DECISIONS/0024-model-access-is-a-provision.md) keeps
|
|
on purpose. So for `refreshable-grant` vendors only, the **manager node holds the refresh token
|
|
encrypted at rest** — a bounded, declared exception. Access tokens stay sealed per holder, and the
|
|
refresh token is stripped on delivery, so *a node never holds a refresh token* remains true for every
|
|
node but the one manager. **Static-key vendors keep the full guarantee**: there is nothing to rotate,
|
|
so `accept` discards the plaintext and the carve-out never fires — and static-key is the majority. The
|
|
exception is written down because a relaxed guarantee that is not stated is indistinguishable from a
|
|
broken one, and it is narrow on three axes at once: refreshable-grant only, the refresh token only,
|
|
the manager node only.
|
|
|
|
Usage is normalised to `(licence, consumer, period, metric, value)` with the raw response kept beside
|
|
it; the metric is vendor-defined, so no false common unit is forced. Anthropic becomes the first
|
|
`refreshable-grant` adapter, and `anthropic-api-key` — the same vendor's plain keys — is the
|
|
`static-key` case that proves the abstraction is more than one vendor in disguise.
|
|
|
|
## How it is checked
|
|
|
|
In the lab, on real machines, in the order a person would meet it: a consumer is refused with both
|
|
candidates named; put on one and still refused because no key exists; the key is given on standard
|
|
input and not echoed; the public half arrives saying it came from a record rather than a machine;
|
|
the key arrives readable only by that machine — and it is **nowhere in the controller's own
|
|
database**, nor in anything that crossed the broker.
|
|
|
|
For the generalisation ([ADR 0050](../../02-DECISIONS/0050-model-access-is-vendor-agnostic.md)): a
|
|
second vendor — `anthropic-api-key`, static-key — is bound to a consumer and exercises the whole path
|
|
with the carve-out switched off, its key sealed per node and absent from the controller's database.
|
|
For a `refreshable-grant` licence, the refresh token is asserted to exist (encrypted) **only on the
|
|
manager node**, to be **absent from every holder's delivery**, and the delivered credential to be
|
|
access-token-only; a `static-key` licence stores no refresh token anywhere. A scenario with two
|
|
vendors confirms each licence's lifecycle runs its own adapter, selected by `vendor`.
|