ADR 0152: the operator's surface is a module, the console
The work order's group-3 question answered: an ordinary module the mesh assigns to the machine a person sits at, holding a minted credential, calling tools under a manifest grant (invokes), serving MCP on loopback. Design 34; pointers in 33, 25 and 0095; module check designed into 12 (issue 148); README stops claiming an indexing nothing provides (issue 006).
This commit is contained in:
@@ -47,6 +47,14 @@ Anything with the control plane in reach can ask any module anything it serves.
|
||||
harder: nothing outside the control plane can, and the control plane's connection is one more
|
||||
thing on the path of every question — a cost accepted for the audit it buys.
|
||||
|
||||
> **The mechanism changed — 2026-09-30, by ADR 0152.** What stands: `ask` on the control plane, and
|
||||
> that every call passes an account whose permission list says what it may ask. What moved: "nothing
|
||||
> outside the control plane can" stopped being true when a person's account gained a publish grant per
|
||||
> tool (design 25 §7, 2026-09-28), and [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)
|
||||
> takes the first option above for a module as well — a manifest declares `invokes`, and the bus grants
|
||||
> exactly that publish side. The audit the second option bought is the bus's permission list, which
|
||||
> derives both.
|
||||
|
||||
## How it is checked
|
||||
|
||||
A tools-only bed asks a served tool through the control plane and asserts an answer arrived —
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-09-30
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
|
||||
---
|
||||
|
||||
# 152. The operator's surface is a module the mesh assigns: the console
|
||||
|
||||
## Context
|
||||
|
||||
**Since the bus moved, nobody can ask the mesh anything without opening a shell on a machine.** Every
|
||||
tool call an operator's assistant makes fails, on every machine including the one the operator sits
|
||||
at, with *AMQP not connected*
|
||||
([issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
|
||||
The program answering is the predecessor's tool server, started on the workstation by hand, with the
|
||||
predecessor's broker address written into the assistant's own configuration. It has no manifest, no
|
||||
assignment, no account on the bus, and the mesh has never known it exists. Nothing regressed: the
|
||||
mesh removed a transport that a program outside the mesh still dials.
|
||||
|
||||
**The mesh has a tool model, and it works.** A module serves each tool on its own subject and its
|
||||
account may serve nothing else ([ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md));
|
||||
a person is issued an account whose only permission is to publish the tool subjects named at issue
|
||||
([25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §7); a client on the
|
||||
runtime repository's main branch speaks that account as a command line and as an MCP server. Measured
|
||||
on the live mesh on 2026-09-28: a call to the forge's `gitea_list_repos` answered with real
|
||||
repositories; the tool list came back empty, because it asks the catalogue for a tool nothing serves
|
||||
([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
|
||||
|
||||
**Two records have already said where the surface belongs.** ADR 0132's consequences: *the MCP
|
||||
surface belongs inside the mesh — a module the mesh assigns to the machine where the agent sits, with
|
||||
a credential the mesh minted and authority derived from what it may call, not a program started by
|
||||
hand with a credential printed to a terminal.* Design [33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)
|
||||
§6 says the same. Neither is a decision about the surface: 0132 decided where a seat's tools live,
|
||||
and named the surface in passing.
|
||||
|
||||
**And one record says the opposite, in the letter.** [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md)
|
||||
made the control plane *the* way to ask a module, deferred "calling is a grant" until something asked
|
||||
for it, and recorded that *nothing outside the control plane can*. A person's account (design 25 §7,
|
||||
built 2026-09-28) is exactly that grant, minted for a person. So 0095's exclusivity has already been
|
||||
widened once without a record saying so; a module that calls tools widens it a second time, and this
|
||||
record is where that is said.
|
||||
|
||||
**What a module's account may do today, counted from the composition** (`internal/broker`): publish
|
||||
its own events, publish the accept subjects of seats it uses, subscribe its own tools and what it
|
||||
consumes. No module principal may publish another module's tool subject. Of 72 modules in the
|
||||
catalogue, 45 serve tools and 0 may call one.
|
||||
|
||||
The question the work order asks before any code: **does the mesh grow its own operator surface, or is
|
||||
the surface an ordinary module that happens to serve tools?**
|
||||
|
||||
## Considered Options
|
||||
|
||||
**1. The control plane serves the agent protocol itself** — a listener on the controller, or a verb
|
||||
its binary runs. Rejected. It puts a surface for tools the control plane does not implement on the one
|
||||
component that must stay answerable while it is itself being replaced, which is the reason 0132
|
||||
rejected the control plane as the answer to discovery. A person on a workstation would reach it over
|
||||
the network, and the networked surface [ADR 0035](0035-one-implementation-several-surfaces.md)
|
||||
reserves for that authenticates through an OAuth2 provider that is not configured — so the controller's
|
||||
`api` verb correctly serves nothing today, and this option would either wait for it or bypass it.
|
||||
|
||||
**2. A program a person installs and starts by hand with a printed credential** — what exists on the
|
||||
runtime repository's main. Rejected as the end state. It is outside the mesh in every way issue 147
|
||||
names: no assignment, no declaration, no seat, no check that it reaches anything, revoked only by a
|
||||
person remembering to. It is the predecessor's arrangement one bus later, and it fails the same way
|
||||
the next time an address moves. It stays as the recovery path, the way the command line does
|
||||
(ADR 0035): a credential from `operator issue` and the `mesh` client work with no console assigned.
|
||||
|
||||
**3. An ordinary module, assigned to the machine the person sits at, holding a credential the mesh
|
||||
minted, serving the mesh's tools on that machine's loopback.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**The console is a module.** `mesh-console` is built by the mesh, registered like any module,
|
||||
assigned to a machine, and given a bus credential sealed to that machine. It serves the mesh's tools to
|
||||
whoever is on that machine: to an agent over MCP, and to a person through the same endpoint. Assigning
|
||||
it to a machine is what makes the mesh reachable from there; unassigning it revokes that, at the next
|
||||
composition, with nothing on the machine to remember to remove.
|
||||
|
||||
**A grant to call is a manifest word: `invokes`.** A module declares the tools it calls, each as
|
||||
`<module>.<tool>`, or the single entry `*` for every tool on the mesh. The bus grants exactly that
|
||||
publish side and nothing beside it — no event, no subscription, no seat. This is ADR 0095's deferred
|
||||
first option, taken now that a consumer asks for it; a person's account already has this shape, and
|
||||
the same composition derives both. The control plane's `ask` stands, and 0095's audit point with it:
|
||||
every call still passes one account whose permission list says what it may ask.
|
||||
|
||||
**Authority is the machine's login.** The console listens on the machine's loopback only, declared
|
||||
`from: machine`, so whoever can open a socket on the machine is whoever owns the machine, and *the
|
||||
account that installed the host owns the mesh on that node*
|
||||
([ADR 0034](0034-the-local-account-owns-the-mesh.md)). Anything on a machine may call anything on it,
|
||||
and that is the whole of local ([ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md)).
|
||||
The mesh knows no person: what the audit sees is which console asked, under the account
|
||||
`<node>.mesh-console`. How a person's identity reaches a session is the question design 15 leaves
|
||||
open, and this record does not close it.
|
||||
|
||||
**What the console lists is asked of the modules.** Design 33 §5: a module's own tools are answered by
|
||||
the module, from the code that defines them. The tool runtime therefore answers one reserved verb for
|
||||
every module it serves — `tools`, the module's tool names, descriptions and argument schemas — and the
|
||||
console assembles its list by asking the catalogue which modules the mesh holds and each module what
|
||||
it answers. A module that is not running is absent from the list and says so; a tool an agent already
|
||||
knows the name of can be called whether or not it was listed. A module may not name a tool of its own
|
||||
`tools`; the runtime refuses the collision at load rather than letting one shadow the other. A
|
||||
role's tools, and the mesh's own verbs, join the list when the `mesh-controller` seat serves them
|
||||
(design 33 §1, third family) — the console reads whatever the mesh can say about itself, and grows as
|
||||
that does.
|
||||
|
||||
**The console holds one credential and one grant: `*`.** It is the operator's surface on a machine the
|
||||
operator owns; narrowing what it may call is a setting on its assignment, which
|
||||
[ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) already provides for
|
||||
and nothing here builds.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **The way a person drives the mesh is inside the mesh.** It is declared, delivered, replaced and
|
||||
revoked by the same machinery as everything else, and `status` says whether the machine carrying it
|
||||
has applied. Issue 147's shape — a surface kept alive by an address in a file — cannot recur, because
|
||||
there is no file: the console's credential names the bus the mesh is on, and moves when it does.
|
||||
- **A module may now call tools, which ADR 0095 had reserved to the control plane.** The grant is
|
||||
explicit, per tool or `*`, and derived by the same composition that grants everything else. A module
|
||||
that declares no `invokes` gains nothing. What got harder: a manifest reviewer has one more field to
|
||||
read for authority, and `*` in it deserves the reader's attention every time.
|
||||
- **A new manifest word ships one release before any manifest uses it**, and must reach both parsers:
|
||||
the build machine's and the running controller's. The console's manifest cannot be registered until
|
||||
the controller and the builder that packages it have been rebuilt with the word.
|
||||
- **Discovery costs a fan-out per list.** One request per module the mesh holds, answered at once by
|
||||
the bus for every module nothing serves, so the cost is bounded by the modules that are up. The list
|
||||
is cached briefly in the console; a module assigned a moment ago appears at the next refresh.
|
||||
- **Every tool runtime must be rebuilt once** to answer `tools`. Until a module is, it is callable and
|
||||
not listed, and the console says which modules did not answer.
|
||||
- **The mesh's own verbs are not in the console yet.** `status`, `push`, `assign` are the
|
||||
`mesh-controller` seat's tools under 0132, and the three prerequisites 0132 names are still not in
|
||||
place. A person asking what a node runs still opens a shell for that question, and that gap is design
|
||||
33's to close, not this record's — recorded here so nobody reads the console as the whole of 147.
|
||||
- **The person's credential is not retired.** `operator issue` and the `mesh` client remain the path
|
||||
when no console is assigned, and the path an operator uses to bring a mesh up far enough to assign
|
||||
one.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A module's `invokes` becomes exactly that publish grant, and nothing else | the bus user composition test: a module invoking `shop.price` may publish that subject and no other tool's; `*` may publish every tool subject; neither may publish an event or subscribe anything it did not consume |
|
||||
| A malformed `invokes` entry is refused at registration | a parser test: an entry naming no tool is a problem named in the manifest's words |
|
||||
| The runtime answers `tools` for every module it serves | the runtime's test: a module registering two tools answers three names, and a module naming one of its own `tools` is refused at load |
|
||||
| The console's list is what the modules answer | the client's test against a real bus: two modules up, a third the catalogue holds and nothing serves, and the list carries the two and names the third as not answering |
|
||||
| A call from the console reaches a module over the bus | the same test, and the live mesh: the console assigned to a workstation answers `tools/list` on its loopback and a call to the forge returns repositories |
|
||||
| The console listens on loopback and nowhere else | its manifest declares `from: machine`, and the filter composed for the machine opens nothing for it |
|
||||
|
||||
## References
|
||||
|
||||
- [issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md) — the surface outside the mesh
|
||||
- [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md) — extended: a grant to call, for a module as for a person
|
||||
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — where a seat's tools live, and the sentence that named the surface
|
||||
- [ADR 0035](0035-one-implementation-several-surfaces.md) — three surfaces over one implementation
|
||||
- [ADR 0034](0034-the-local-account-owns-the-mesh.md), [ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md) — why loopback is the authority boundary
|
||||
- [33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §5, §6 — discovery, and what serves it to an agent
|
||||
- [34 — The console](../03-DESIGN/01-to-be/34-the-console.md) — the design this record authorises
|
||||
- mesh-tools `src/client.ts`, `src/mcp.ts`, `src/mesh.ts` — the client this makes a module of
|
||||
@@ -256,6 +256,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0146** — [Connectivity is checked by name, per hosting form, with a valid certificate](0146-connectivity-is-checked-by-name-per-hosting-form.md)
|
||||
- **0147** — [A module anchors the mesh's authority on a machine, and takes it away again](0147-a-module-anchors-the-meshs-authority.md)
|
||||
- **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)
|
||||
|
||||
### How it is built
|
||||
|
||||
|
||||
Reference in New Issue
Block a user