Merge pull request 'Designs 34 and 46: sessions wait on any event, and the approval mark gates calls and marked waits, delivered with design 47's milestone G' (#540) from design/34-wait-on-any-event into main

This commit was merged in pull request #540.
This commit is contained in:
2026-11-11 02:08:01 +00:00
5 changed files with 848 additions and 37 deletions
+5 -1
View File
@@ -1117,7 +1117,11 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272, 0383, 03
- **approval mark** — a tool's or verb's declaration in its manifest (`needs-approval`) that each call to it runs
only on the operator's warrant for that one call, with its exact arguments; the tool runner that serves it, or
the controller for its own verbs, asks and checks
(ADR 0286).
(ADR 0286). It covers a module's events and streams too (the operator, 2026-11-11; design 34 every-event §32): a
module may mark one of its events, a restricted stream is marked whole, and a `mesh_wait` on a marked event runs
only on the operator's warrant for that one wait, with the exact names it waits on and its bound, which the tool
runner asks and checks before it subscribes. The tool runner reads every mark from the catalog half on the bus
(design 46 7-approval-mark §5).
- **operator message / input** — what arrives on `intake`, once the router has checked the sender
against its own list of the operator's identities: a **trusted** one is an operator message,
addressed to an agent by `@name` or thread or else to the **responder** (`@mesh`, the router's own
@@ -0,0 +1,381 @@
---
layer: to-be
design: 34
milestone: every-event
updated: 2026-11-11
code: [mesh-sdk, mesh-delivery, mesh-tools, mesh-catalog modules/claude-code]
decisions:
- "ADR 0670"
- "ADR 0671"
- "ADR 0286"
- "ADR 0367"
- "ADR 0043"
- "ADR 0259"
- "ADR 0332"
- "ADR 0363"
- "ADR 0376"
- "ADR 0489"
- "ADR 0571"
- "ADR 0578"
- "ADR 0614"
---
# Design 34, milestone every-event — the tool runner hears every event
**What it builds.** A session's `mesh_wait` waits on any event of any seat or module, by its name, with no list to
keep: the tool runner's bus account is granted the event subjects of the whole mesh, every module's own stream
included (ADR 0671 §1), and the list of families in the tool runner's manifest, the manifest validator's rule about it
and the grants part's computation of subjects from it go (ADR 0670). The whole shape is in the design's main file, [§3b](../34-the-console.md#3b-waiting-on-the-bus); this
part holds every line the milestone builds, cited as "design 34 every-event §n".
**Where.** Four repositories and hq, one delivery group: mesh-sdk (the shared event subjects; the field, type and
membership entry that go), mesh-delivery (the grants part, seat `mesh-grants`; the manifest validator in the indexer),
mesh-tools (the tool runner, module `node-tools`) and mesh-catalog (the claude-code module's Stop hook, §20), with hq
for the playbook and agent lines (§24). Only these reference the list today: no other manifest carries the key
`waits`. The milestone is built on each repository's main as it stands. Design 47's milestone G edits the grants
part's account composition and the tool runner's manifest and manifest test too; its lead rebases it onto this
milestone's heads when G's develop restarts.
**The approval mark on events.** *A marked event waits for the operator's warrant* (§32 to §37) writes the operator's
answers of 2026-11-11: a wait respects the approval mark, and the gate that asks and checks it (design 46's milestone
7, [`7-approval-mark.md`](../46-milestones/7-approval-mark.md)) is built in design 47's milestone G and delivered in one
group with G and this milestone ("Build the gate in G"). So §25's grant of a restricted stream's subjects and the gate
that holds its mark reach the machines together.
## The event subjects, in mesh-sdk
1. **One place names the mesh's event subjects.** mesh-sdk's package `buscompose` gains a file `events.go` with three
exports, used by the grants part (§6) and the tool runner (§13), so neither spells the event namespace itself:
- `EveryEvent`, the subjects the tool runner's account hears: `mesh.seat.*.event.>` and `mesh.mod.*.event.>`, in
that order (ADR 0670 §1);
- `EventSubjects(name)`, the subjects one event name lands on. A name is `<emitter>.<event>`, optionally followed
by further tokens (`operator-channel.decided.claude-code`); each token matches `[A-Za-z0-9_-]+`. It answers four
subjects: `mesh.seat.<emitter>.event.<rest>`, the same followed by `.>`, `mesh.mod.<emitter>.event.<rest>`, and
the same followed by `.>`, where `<rest>` is every token after the first; given a namespace (`seat` or `module`,
§13) it answers only that namespace's two. It refuses, with an error naming the rule, a name of fewer than two
tokens, an empty token, or a token outside that form (so `*` and `>` are refused);
- `EventName(subject)`, the inverse for a heard subject: for `mesh.seat.<emitter>.event.<rest>` or
`mesh.mod.<emitter>.event.<rest>` it answers the name `<emitter>.<rest>` and its namespace; any other subject is
answered as not an event.
Why four subjects: a name does not say whether its emitter is a seat or a module, and both namespaces are heard
(§6); and a seat's event that names its asker or kind after the event (ADR 0259 §3), or a module's that names its
module and branch, is heard by the event's name alone, the `filter` narrowing it by body. How it is checked: one
table-driven unit test of the three, in `events_test.go`, one of whose rows holds that every subject
`EventSubjects` answers for its cases is covered by a subject of `EveryEvent`.
2. **The manifest loses `waits`.** `catalogue.Manifest.Waits` and its comment go from `go/catalogue/manifest.go`;
the comment on `ToolRunner` loses "and the only module that may declare `waits`". `go/catalogue/waits_test.go` is
deleted, and one row is added to the decoder's existing table of unknown fields: a manifest
`{"module":"node-tools","version":"1","tool-runner":true,"waits":["a.b"]}` decodes with `UnknownField()` naming
`waits` (ADR 0670 §2). The decoder itself is unchanged: a stored manifest that still carries `waits` is read
without it (ADR 0578 §3).
3. **The baseline of manifest reads stays as generated.** `go/catalogue/manifest_reads_baseline.txt` keeps
`word:waits`: it is the contract of the engines built before any stated what it reads (ADR 0614 §4), which did read
`waits`. In `own_manifest_test.go` the list of baseline entries this contract no longer reads (today the two
renamed seats) gains `word:waits`, its comment naming ADR 0670 §2 as the record that retired the word.
4. **The wait types go.** `go/buscompose/waits.go` (the type `Wait`) is deleted and `Users.Waits` goes from
`go/buscompose/users.go`; `Membership.Waits` and the type `WaitIssued` go from `go/membership/membership.go`
(ADR 0670 §2). Nothing replaces them: no membership names what its account hears.
## The grant, in the grants part (seat `mesh-grants`)
5. **What the grants part computed from the list goes.** `internal/buscompose/waits.go` is deleted whole (`Wait`,
`WaitSubject`, `waitGrants`, `waitsIssuedFor`); `Declared.Waits` goes from `internal/buscompose/users.go`, and with
it the term `len(d.Waits) > 0` of `runnerServes`; `m.Waits = waitsIssuedFor(d)` goes from `membership.go`, and the
alias `WaitIssued` from `sdk_membership.go`; `waitsOf` and the field `Waits:` of `declaredFor` go from
`internal/grants/records.go` (ADR 0670 §2). `safeSubject` in `nats.go` stays: the bus-state grants use it.
6. **The tool runner's runtime hears every event subject.** In `internal/buscompose/nats.go`, where a runtime's
permissions are composed from the modules it carries, the call of `waitGrants` is replaced by: when one of the
carried modules says `tool-runner` (`Declared.ToolRunner`), `buscompose.EveryEvent` (§1) is appended to the
runtime account's subscribe list. Nothing else is added for it: no publish, no durable consumer, no queue, no other
subject; a runtime carrying no tool runner gets neither subject (ADR 0670 §1, §3; ADR 0043 §2, §3, §5, §8 as ADR
0670 §3 now reads them). The grants part writes them into the bus's user list as every grant (ADR 0571 §9); they
are read with `mesh-grants.grants_user_list`, and composed at once with `mesh-grants.grants_compose_now`.
7. **The tool runner's membership names no events.** The grants part's first composition after its delivery rewrites
every tool runner's membership in its state `memberships` (key `<node>.node-tools`, ADR 0571 §10) without the key
`waits`, since the field is gone (§4); every other part of it is unchanged. It is read with the grants part's
reading of memberships and rewritten at once with `mesh-grants.grants_compose_now`.
8. **A composition test holds the grant.** In `internal/buscompose/buscompose_test.go`, one table-driven test composes
a runtime carrying the tool runner and one other module, and a runtime carrying the other module alone, and holds:
the first's subscribe list contains `mesh.seat.*.event.>` and `mesh.mod.*.event.>`; no subscribe subject of either
is wider than those (none of `>`, `mesh.>`, `mesh.*.>`, `mesh.seat.>`, `mesh.mod.>`, nor any `$JS.` subject beyond
what the carried modules' own consumes give); the first's publish list holds no subject of another module's or
seat's events; the first derives no durable consumer beyond its carried modules' `consumes`; and the second has
neither subject (ADR 0670 §1, §3).
9. **The real bus holds it too.** The grants part's existing real-bus test (`cmd/mesh-grants/composed_integration_test.go`)
gains cases, no new test: the tool runner's account, as composed, subscribes
`mesh.mod.mesh-delivery-view.event.delivered` and receives an event published there; it is refused a publish on
that subject; it is refused a subscription on `mesh.seat.channel.accept.show.<kind>` and on
`mesh.seat.intake.proof.code.<kind>` (an accept and a proof are never events, ADR 0670 §1); and another module's
account is refused a subscription on an event it does not consume (ADR 0670 §1, §3).
## The manifest validator, in the indexer
10. **The rule about the list goes.** `waitProblems` and its call in `judge/rules.go` are deleted, and with them the
regular expression `waitToken` where nothing else uses it; the table rows that judged `waits`
(`judge/rules_test.go`, the `waits` cases) are deleted, and one row is added: a manifest that carries `waits` is
refused by the manifest validator as the checker runs it (`Shelf`), naming the unknown field `waits`, and set aside
to wait as the indexer runs it (`ShelfWaiting`), as any field it does not know (ADR 0578 §1); the tool runner's
manifest without it is accepted (ADR 0670 §2). Only revisions taken in after the indexer's delivery are judged so:
a manifest the validator accepted before keeps its verdict (ADR 0578 §1).
## The tool runner (module `node-tools`)
11. **Its manifest's changes, every one.** `node-tools/module.json` drops the key `waits` and its five entries (ADR
0670 §2); gains `operator-channel` in `uses`, for the asks of marked waits (§35; design 46 7-approval-mark §7); gains
in `requires` the state `approvals` of design 46 7-approval-mark §13; and gains the stream `node-tools-findings` and
the `consumes` of `mesh-conditions.findings-asked` that design 47 G §387 writes. Its `version` stays `2`, a label
no reader judges. Nothing else in it changes.
12. **The publication time is kept.** `bus.Heard` gains `Published`, read from the event's header `x-time`, which
mesh-sdk's emit sets (`go/process/emit.go`) and the status view's own emit too; `HearFirst` reads it from the
message's headers, and leaves it empty for an event that carries none.
13. **`mesh_wait` takes any event name.** `console.Surface.waitAnswer` (`internal/console/wait.go`) no longer reads the
membership. Each name of `subjects` becomes its subjects through `buscompose.EventSubjects` (§1); a refused name is
answered as a failure quoting that error. An optional argument `from`, `seat` or `module`, narrows every name to
that namespace, for an emitter whose name is both a seat's and a module's. A call naming more than 16 names is
refused, saying the bound (each name is four subscriptions for the call; 16 names is 64, far above what a watcher
names). The same subject named twice is subscribed once. The map from subject to name goes: the heard event is
named by `buscompose.EventName` of the subject it arrived on. The timeout, the filter and the timed-out answer are
unchanged (ADR 0367 §1). The refusal of names outside a list, and its words naming the manifest, go (ADR 0670 §2).
14. **A misspelt name is refused, not waited on for ever.** Before it subscribes, `waitAnswer` refuses a name whose
emitter is neither a seat nor a module of the catalog half the tool runner holds from G (its first view by
`mesh-catalog.revision`, followed on `catalog-revision`; design 47 G §196), naming that source and its revision.
A name whose emitter is known but whose event no seat definition and no module's entry in the catalog half
declares (its `emits` and its streams' events, design 46 7-approval-mark §5) is waited on,
and its timed-out answer says that no declaration names that event. While the catalog half is not yet read (the
first views' state, `mesh_first_views`), names are not checked, and the answer says `unchecked` with why; a
marked wait is then held as §34 says.
15. **The answer names the heard event.** A heard event is answered as today, `heard` with `subject`, `body` and
`at`, where `subject` is now the heard event's whole name (`mesh-delivery-view.delivered`,
`operator-channel.decided.claude-code`), with two new fields: `from`, `seat` or `module`, the namespace it
arrived on, and `published`, the event's own time (§12), absent when it carries none. The `read` line stays (an
event only wakes you, ADR 0367 §4); for a name beginning `operator-channel.decided` it says instead that the
operator's answer is read only with `claude_code_answer`, never from the event's body (ADR 0670 §5; ADR 0332 §5,
§6).
16. **A refused subscription is said, and a wait is never a hot loop.** `HearFirst` subscribes every subject of the
call; a subscription the bus refuses is noted, and the others go on waiting. When every subscription of the call is
refused, it waits out the call's timeout (or the caller's going) and then answers the refusal, so a watcher that
loops on it asks once a timeout, never at once again. Its refusal words become: "the bus refused this account a
subscription on <subjects>: the grants part grants this machine's tool runner every event subject (novox/hq ADR
0670 §1), so its grant is not composed yet; read the state from its owner meanwhile"; no words name a wait grant or
ADR 0367 §2. A heard event's answer and a timed-out answer list under `refused` any subject the bus refused while
others stood.
17. **The tool's description says what it hears.** `waitTool`'s description of `subjects` says: one or more event
names, `<seat or module>.<event>`, such as `mesh-delivery-view.delivered`; an event whose subject goes on after
the name (an asker, a kind, a module) is heard by the name too; the filter narrows by the body's fields; `from`
narrows to a seat's or a module's. Its own description keeps "an event only wakes you" and adds that an operator's
answer is read only with `claude_code_answer`. The words "only those the mesh granted this machine's tool runner"
go.
18. **The hearing serves `mesh_wait` alone.** `HearFirst` has one caller, `waitAnswer`. The modules the runtime carries
share its account, so the bus no longer bounds which events their code could hear; the runtime still gives a
carried module's code only what its membership names, and this is accepted under ADR 0670 §1, every session
already hearing every event. How it is checked: the code review of every change to the tool runner reads
`HearFirst`'s callers.
19. **What goes from the tool runner's code, and its tests.** The type `bus.WaitIssued` (`internal/bus/wait.go`) and
`Membership.Waits` (`internal/bus/bus.go`) go. In `console/manifest_waits_test.go` the test of the five families
becomes a test that the manifest has no key `waits`, its comment citing ADR 0670 §2; its tests of what the tool
runner consumes and of the runner's own secrets stay. In `console/wait_test.go`: `waitingRuntime` issues no waits,
and the answering test waits on `mesh-delivery-view.delivered` and on `operator-channel.decided` with no membership
naming either, its rows holding `subject`, `from`, `published` and `read` (a `decided.<asker>` event heard by
`operator-channel.decided`); `TestMeshWaitRefusesAnEventOutsideTheGrantedFamilies` becomes the table of refusals:
a name with `*`, with `>`, of one token, 17 names, an emitter no source knows (§14), and a bus-refused subscription
answered after the timeout with §16's words; `TestMeshWaitWithNoFamiliesIssuedWaitsOnNothing` is deleted. In
`bus/wait_test.go`, `TestTheBusRefusesAWaitOutsideTheGrant` stays as §16's refusal test, with a timeout of a
fraction of a second, its comment citing ADR 0670 §1. The manifest test also holds each change of §11: no `waits`,
`operator-channel` in `uses`, the state `approvals` in `requires` with its options, and the finding stream. No test
function is added: the module stays within its budget.
## The claude-code module's Stop hook
20. **The hook no longer doubts the grant.** In mesh-catalog `modules/claude-code/cmd/claude-code/stop.go`,
`watcherAdvice` says for a wait with an event: "mesh_wait on <event> and reread <read> after every answer, or start
a watcher that does", with no condition on the tool runner's grant; its comment cites ADR 0670 §2 beside ADR 0489
§3. Its row in `stop_test.go` holds the new words.
## Events carry no secret
21. **The prepare lists every event.** The milestone file's prepare (playbook 07, step 0) gains a section *Events and
their fields*: every event each module declares (its `emits` and its streams' events) and each seat's definition
declares, read from the module catalog: the catalog revision of the change's branch (`mesh-catalog.revision`),
which carries the seat definitions too, and nothing else, no manifest and no bus read, so the events G and design
46's milestone 7 add are listed (the finding streams of design 47 G §383 among them), each a holder's event with
its subject form; and every event mesh-sdk publishes on a module's behalf (the announcement's `states` on
`mesh.mod.<module>.event.states`, found by the callers of its subject builders); each with the fields of its body
as the emitting code builds it, by file and line (ADR 0670 §4). *Why:* the operator, 2026-11-11: "only the indexer
uses the manifest to build the module catalog".
22. **The code review confirms none carries a secret, before the grant is delivered.** The milestone's code review
(playbook 07, step 2) reads that list again at the group's heads, and confirms that no event's body carries a
credential, a token that grants an act, code, or a message marked secret; an intake `choice`'s one-use token is not
such a token (ADR 0259 §3, §5). The list is the module catalog's (§21); the review reads no bus. An event found
carrying a secret holds the milestone's delivery and goes to the operator as a design question
(ADR 0670 §4).
23. **The stream events are read too.** §21's list also names every event kept in a module's own stream
(`mesh.stream.<module>.event.<event>`, ADR 0363 §2), the seat holders' events published there among them, and
whether its stream is restricted (its declaration carries a `grant`, ADR 0363 §4). The mail stream's events are
named in it as heard with their content: a mail's own text, a sign-in link or a code someone sent by mail among
it, is the operator's mail and not a secret in §22's sense, which is a credential, token or code the mesh itself
issues or keeps (ADR 0671 §3, which narrows ADR 0670 §4).
24. **From then on every change is asked the same.** In this milestone's hq pull request: playbook 07's review step
gains the question, for any change that adds an event or a field to one, "does it carry a secret?"; and the watcher
lines that describe the list (`.claude/agents/mesh-watcher.md`, playbook 07's *Following and reporting*: "the
families a watcher may wait on are those the mesh granted the tool runner") say instead that `mesh_wait` hears any
event by name (ADR 0670 §2, §4).
## Stream events
*By ADR 0671: the operator chose every module stream, mail too, on 2026-11-11.*
25. **The tool runner also hears every module stream, a restricted one included.** `buscompose.EveryEvent` (§1)
gains a third subject, `mesh.stream.*.event.>`, after the two of §1, and the grants part's composition (§6) appends
it with them. Nothing else is added for it: no durable consumer, no grant to acknowledge, no membership word, no
publish, and no subscribe deny of a restricted stream (ADR 0671 §1). A restricted stream's grant value (ADR 0363
§4) decides who consumes it, not who hears it, so changing or withdrawing that value changes nothing in the tool
runner's grant (ADR 0671 §2); a module that consumes a restricted stream is composed as today (ADR 0376 §2 as ADR
0671 §2 reads it). The bus does not hold the approval mark on a restricted stream; the tool runner's own gate does,
before it subscribes for a wait (§32 to §37; the operator, 2026-11-11), and that gate is delivered in the same
group as this grant (design 46 7-approval-mark §23). *Why:* ADR 0286 §8 has the bus hold a mark, and the bus
cannot grant a subscription for one approved wait alone, so the operator chose the tool runner's gate.
26. **A name hears its stream form.** `EventSubjects` (§1) answers six subjects, adding
`mesh.stream.<emitter>.event.<rest>` and the same followed by `.>`; `from` (§13) takes `stream` too; `EventName`
answers a stream subject with namespace `stream`. A seat's holder event kept in its holder's stream is named by its
holder module (`mesh-delivery-checker.verdict`). The bound of §13 stays 16 names (96 subscriptions).
27. **The tests hold it.** §8's test holds exactly the three subjects for the runtime carrying the tool runner, beside
a fixture module that declares a restricted stream, and that no durable consumer and no subject to acknowledge on
that stream are derived for the tool runner's runtime (ADR 0671 §1). §9's real-bus test gains cases: the tool
runner's account, as composed, receives an event published on a fixture module's stream, and one published on a
fixture restricted stream whose grant value has no entry for the tool runner, and still receives one after that
value is changed and the accounts are composed again; it is refused a durable consumer and an acknowledgement on
that restricted stream; and a module whose `consumes` names the restricted stream with no entry in its grant value
gets no durable consumer and no read grant on it (ADR 0671 §1, §2).
## Delivery
28. **In G's delivery group, no order named beyond the module graph's.** The members are pushed and checked in design
47 milestone G's group, with design 46's milestone 7, whose edges (design 46 7-approval-mark §23) order the gate
before the seat definition that marks `execute`; this milestone adds no order of its own between its members, and no
transitional field is kept (the operator, 2026-11-03: a change is designed as
it should be, its delivery is the delivery system's): a part built with the new mesh-sdk reads a stored manifest of
the tool runner that still carries `waits` without it (ADR 0578 §3), and a node-engine is given a want only for a
manifest whose words it reads (ADR 0614 §4).
29. **What `mesh_wait` answers between the deliveries.** On a node whose tool runner is new and whose grant is not yet
composed, the subjects the old grant names are still heard and the rest are listed under `refused` (§16); on a
node whose tool runner is older and whose membership the new grants part rewrote, the old runner answers at once
that it was issued no event families. Both are loud. A watcher reads its owner after every answer (ADR 0367 §4) and
ends at a call failed twice (its definition), so neither becomes a loop; a laptop asleep through the window gets
its new tool runner when it wakes, and its watchers read their owners meanwhile in the same way.
30. **Going back is going forward** (design 46 7-approval-mark §24 for the gate's part). A fault met after the delivery is fixed by a new change delivered the same way.
Reverting the group instead reverts every member, the indexer first, since the manifest validator delivered here
sets aside the old tool runner's manifest carrying `waits` (§10); until the revert reaches a node, `mesh_wait`
there answers as §29 says.
## Nothing built ahead of the group
31. **No tool is built ahead of the group.** The extension of the nats module's `nats_streams` this line held, for
§22's bus read, is not built: the secrets review reads the module catalog alone (§21, §22). *Why:* the operator,
2026-11-11: "only the indexer uses the manifest to build the module catalog". *How it is checked:* the milestone's
prepare names no member outside the group.
## A marked event waits for the operator's warrant
*By the operator's answers of 2026-11-11: "can it also respect any 'requires approval' config on the topics?", and
"Build the gate in G".* These lines use the gate design 46 7-approval-mark builds for a marked call (its §8 to §19),
delivered in the same group. *Why they depart from the records, said once for the section:* ADR 0286 §1 marks tools
and verbs, and the operator extended the mark to a module's events and streams; ADR 0671 §1 and §3 have every session
hear every module stream, mail included, with no exception, and the operator narrowed that to a wait the operator
approves, for a marked event or a restricted stream.
32. **A module marks an event, and a restricted stream is marked whole.** A module's manifest `needs-approval` (ADR
0286 §1) may also name one of its own events, by the name its `emits` or one of its `streams`' `events` gives it,
with `*` and `**` as there. Every stream a module declares with a `grant` (ADR 0363 §4; `ModuleStream.Restricted`)
is marked whole, every event it carries, with no entry in `needs-approval`; a stream without a `grant` is not, a
work-queue one included. A seat's definition marks no event. The indexer's manifest validator refuses, as the
checker runs it, a manifest whose `needs-approval` names an event that neither its `emits` nor its streams'
`events` carry, naming the event, as ADR 0286 §4 refuses a mark on a tool the module does not serve (design 46
7-approval-mark §4's table gains the row). A module that marks only events needs neither `operator-channel` in
`uses` nor `approval-mark/1`: the tool runner asks for the wait, not the module.
33. **The tool runner reads the marks from the catalog half on the bus.** The tool runner derives each module's marked
subjects from the catalog half it holds (§14; the catalog graph entry carries `needs-approval` and each stream's
`grant`, design 46 7-approval-mark §5): for a marked event `<event>` of module `<module>`,
`mesh.mod.<module>.event.<event>` and `mesh.stream.<module>.event.<event>`, each event pattern as the bus spells it
(`WirePattern`); for a stream with a `grant`, every subject of `ModuleStream.Subjects()`; and each of these also
followed by `.>`, since an event may name its node or asker after its name (design 47 G §383) and
`ModuleStream.Subjects()` answers only the patterns the stream stores. It derives them again on every revision it
hears, and no module's name is written in its code; it is design 46 7-approval-mark §5's derivation, extended to
events, following the running version as that line says. A mark added while a wait waits applies from the next
call, which that call's answer says. *Why:*
ADR 0286 §5 had the mark issued in a membership; no controller issues one now, a membership names no event (ADR
0670 §2), and the catalog half, which the indexer alone builds from the manifests, keeps the mark out of any bundle
as §5 required; from G the catalog half is where the tool runner reads which modules exist (design 47 G §196), and
no part but the indexer reads a manifest (the operator, 2026-11-11).
34. **Which wait is marked.** A wait is marked when any subject it subscribes (§13, §26, after `from`) overlaps a
marked subject of §33, that is, some subject matches both, a wildcard on either side. mesh-sdk's `events.go` (§1)
gains `Overlaps(a, b)` for it. Before the catalog half is read (`mesh_first_views`), a wait with any subject
outside `mesh.seat.` waits, within its own timeout, for the first view and is then judged; nothing is asked while
the marks are unread, and a wait still unjudged when its timeout ends is refused then, saying the marks are not
yet read, since a mark that cannot be read runs nothing (ADR 0286 §40; design 46 7-approval-mark §19). A wait
`from: seat` is never marked.
35. **A marked wait runs only on the operator's warrant for that one wait.** The call is an act as design 46
7-approval-mark §8 makes one: the address `mesh_wait`, the node, the module `node-tools` and the tool `mesh_wait`,
its build (the digest of the tool runner's own artifact), and every
argument given (`subjects`, `from`, `filter`, `timeout`), so the warrant binds the exact names, the filter and the
bound. The tool runner refuses, asks, judges and claims it as that part's §9, §10, §11 and §13 to §17 do for a
marked tool, asking on the seat `operator-channel` under its own module's name, `node-tools`, whose manifest gains
`operator-channel` in `uses` for it (that part's §7). The ask's headline is "Wait on <names> on <node>?"
(shortened as that part's §10 says), and its explanation names every event name one per line, the filter, and the
bound as the timeout in minutes, then the act's fingerprint. On a warrant it accepts, it subscribes, and the wait's
timeout runs from then. One warrant is one wait (that part's §15): a watcher looping on a marked event is approved
call by call. Its answer carries the line naming the approval and the warrant is recorded through
`mesh-hand-acts.warranted` (that part's §16), and `mesh_approvals` (that part's §18) lists the waits asked beside
the calls. *Why:* the operator, 2026-11-11: a marked wait runs only on the operator's warrant for that one wait,
shown with the exact names it waits on and its bound.
36. **A refused marked wait is said, and never a hot loop.** A marked wait with no warrant the tool runner accepts
subscribes to nothing. Its first call publishes the ask at once; every such call answers, as an error, design 46
7-approval-mark §12's or §17's words with "this wait" for "the call", when its timeout ends or its caller goes, as
§16 answers a refused subscription, or as soon as the router's record of its ask says it was chosen: the wait hears
the warrant event `operator-channel.decided.node-tools` for its ask's id and then reads the record, an event only
waking it, and answers at once, so the same call made again runs within the warrant's 30 minutes. *Why:* ADR 0286
§14 answers within 5 seconds, which a watcher's loop would turn into a call every few seconds for the ask's hour;
ending at the choice keeps a long wait from spending the warrant's window. An unmarked wait is heard as §13 to §16
say, with no ask. Every machine's marked waits ask under the one asker `node-tools`, so they share its open asks
and the router's holding back of the oldest beyond three (ADR 0666 §12, as G builds it), and the wait reads the
router's record through `Client.Where` under `node-tools`.
37. **The tests hold it.** Unit, as the budget counts them (`meshtest` runs its bus in the test's process): §19's table
of refusals in `console/wait_test.go` gains rows, no test function: a wait on a fixture module's marked event, and
one on a fixture stream with a `grant`, each with no warrant, subscribes nothing, asks once and answers design 46
7-approval-mark §12's words after its timeout; a wait with the most timeout a wait takes (ten minutes, ADR 0367
§1), its ask chosen at its second minute, answers at the choice, and the same call made then runs; the same wait
with a warrant bound to its exact names, filter and timeout subscribes and answers the event published after it,
so the restricted stream's event is heard only after the warrant; with a warrant for other names, another filter
or another timeout it is refused; the same names `from: seat` ask nothing; a wait named `<module>.<event>.<node>`
under a marked `<module>.<event>` is marked; and before the catalog half is read, a marked wait asks nothing, is
judged when the first view comes within its timeout, and is refused at its timeout when it does not. The warrant is
written on the test bus as the router's record. `Overlaps` gains rows in §1's table (`mesh.stream.mail.event.mail.>` overlaps
`mesh.stream.mail.event.mail.*.message`; `mesh.mod.mail.event.x` does not overlap `mesh.stream.mail.event.x`), and
§32's refusal a row in the indexer's `judge/rules_test.go`.
## Acceptance
- **A watcher wakes on an event no manifest names** (the operator's acceptance; live, read by tool call): at the first
delivery after every node holds the new tool runner, a watcher's `mesh_wait` on `mesh-delivery-view.delivered`
answers the status view's `delivered`, its `at` less than a second after its `published`, with no rereading on a
timer; the tool runner (`node-tools`) is delivered to every node, and the grants part and the indexer to theirs,
read with `mesh-delivery.show` and each node's `node-engine.composition` (§6, §13, §15).
- **The grant is exactly what the record names** (unit, and integration on a real bus): the composition test of §8
and the real-bus cases of §9 pass.
- **The list is gone** (unit): the manifest validator as the checker runs it refuses a manifest carrying `waits`, the
indexer sets it aside, and the tool runner's manifest is accepted without it (§2, §10, §11).
- **`mesh_wait` refuses loudly and never loops** (unit): §19's table of refusals passes.
- **No event carries a secret** (the code review): the milestone file's *Events and their fields* is complete and the
code review's confirmation at the group's heads is written in it before the grants part is delivered (§21, §22).
- **An answer is never taken from an event** (the code review, and unit): in the members nothing reads an operator's
answer from a `decided` event's body, and `mesh_wait`'s answer for one says to read it with `claude_code_answer`
(§15).
- **A stream event wakes a watcher** (live, read by tool call): a watcher's `mesh_wait` on
`mesh-delivery-checker.verdict` wakes on the checker's next verdict (§25, §26; ADR 0671 §1).
- **The grant does not follow a restricted stream's grant value** (integration on a real bus): §27's cases pass
(ADR 0671 §2).
- **A marked wait runs only on the operator's warrant** (unit): §37's rows pass.
- **A wait on mail, as the operator sees it** (live, read by tool call and on the phone): a session's `mesh_wait` on
`mesh-mail.mail`, the mail module's restricted stream, asks "Wait on mesh-mail.mail on <node>?" on the phone with
its bound, and hears nothing before; it ends refused when its timeout ends or as soon as the operator chooses; the
same call made after the Approve hears the next mail, and the next call asks again (§32 to §36).
+90 -3
View File
@@ -1,9 +1,21 @@
---
layer: to-be
status: implemented
code: [mesh-tools, mesh-host, mesh-controller]
updated: 2026-10-17
status: in-progress
code: [mesh-tools, mesh-host, mesh-controller, mesh-sdk, mesh-delivery, mesh-catalog modules/claude-code]
updated: 2026-11-11
decisions:
- "ADR 0670"
- "ADR 0671"
- "ADR 0286"
- "ADR 0043"
- "ADR 0259"
- "ADR 0332"
- "ADR 0571"
- "ADR 0363"
- "ADR 0376"
- "ADR 0489"
- "ADR 0578"
- "ADR 0614"
- "ADR 0197"
- "ADR 0195"
- "ADR 0175"
@@ -119,6 +131,64 @@ listed once per machine does not hold on the live mesh MCP server — postgres a
optional, answered by whichever instance replies. The address replaces that statement rather than
repairing it.
## 3b. Waiting on the bus
*By ADR 0367, as ADR 0670 and ADR 0671 change it.* Beside the five discovery tools of §3a and `mesh_runtimes`, the mesh MCP
server offers a seventh, `mesh_wait`: a session names one or more events, an optional `filter` of body fields and a
`timeout` (four minutes when absent, ten at most), and is answered the first matching event or told it timed out
(ADR 0367 §1). A session calls it in a loop from a background command (ADR 0367 §3), and after every answer or time-out
reads the state once from the verb that owns it: an event only wakes it (ADR 0367 §4).
```mermaid
flowchart LR
E[a seat's, a module's or a module stream's event<br/>mesh.seat.&lt;seat&gt;.event.… / mesh.mod.&lt;module&gt;.event.… / mesh.stream.&lt;module&gt;.event.…] -->|heard for the call| TR[the tool runner's account]
G[seat mesh-grants] -->|grants the three event subjects| TR
M[the catalog half, seat mesh-catalog] -->|the marked events and restricted streams| TR
TR -->|a marked wait only on the operator's warrant| OC[seat operator-channel]
TR -->|mesh_wait answers it by name| S[a session]
S -->|then reads the state| O[the verb that owns it]
```
**The tool runner hears every event of the mesh, and no list says which** (ADR 0670 §1, §2). The grants part (seat
`mesh-grants`) grants the account of the runtime carrying the tool runner a plain subscription on the event subjects
of every seat and every module, and on nothing else of theirs: no accept, proof, call, work queue or bucket, and no
publish. `mesh_wait` subscribes when called and closes the subscription when it answers, so the account keeps no
durable consumer and no queue of its own (kept from ADR 0367 §2), and any event name is accepted: a new event is
heard from its first publication, with no change to a manifest. The tool runner's manifest names no events, the
manifest validator has no rule about them, and no membership lists them. A name whose emitter no seat and no module
bears is refused, so a misspelt wait does not run for ever.
**Who hears.** Whoever may call the mesh MCP server: any process on the machine, through its loopback endpoint (§2),
which is the session there. The tool runner's standing grant of every event is accepted under ADR 0670 §1: what a
session hears through it, it may hear.
**Module streams too, mail included** (ADR 0671 §1, §2). The tool runner also hears the events kept in a module's own
stream (ADR 0363 §2), the seat holders' events published there among them, a restricted stream such as mail's
included. A restricted stream's grant value decides who consumes it, never who hears it, so the tool runner's hearing
needs no `consumes` and no entry in that value, and keeps no consumer of it.
**A marked event is heard only on the operator's warrant** (the operator, 2026-11-11). The approval mark (ADR 0286) covers a module's events and streams as well as its tools and
verbs: a module may mark one of its events in `needs-approval`, and a restricted stream is marked whole. The tool
runner reads the marks from the catalog half on the bus (seat `mesh-catalog`), and a `mesh_wait` on a marked event, or on any subject
of a marked stream, runs only on the operator's warrant for that one wait, asked on the seat `operator-channel` with
the exact names it waits on and its bound, and checked before the tool runner subscribes; without one it is refused
and says so. The bus grants the subjects, and the tool runner's own gate holds the mark, as the operator chose; the
gate is design 46's milestone 7, built in design 47's milestone G and delivered in one group with G and milestone
every-event, so the subjects and the gate reach the machines together. This departs from ADR 0286 §1 and §8 (tools and
verbs only, held by the bus) and from ADR 0671 §1 and §3 (every module stream heard with no exception) by the
operator's answer. An unmarked event is heard with no ask.
**What that makes audible, and what still bounds it.** Every session now hears the operator channel's `decided` of
every asker and the intake bench's events. So no event of a seat or a module carries a secret: no credential, token or
code the mesh itself issues or keeps, and no message marked secret, which the code review asks of every change that
adds an event or a field (ADR 0670 §4 as ADR 0671 §3 narrows it). A mail's own content is the operator's mail and is
heard as it is, a sign-in link or a code someone sent by mail among it (ADR 0671 §3). And a heard event never answers anything: the operator's answer to an ask is read only
with `claude_code_answer`, from the router's record and held to the question's digest (ADR 0670 §5, ADR 0332 §5,
§6), and an asker acts only on a warrant from its own `decided` subject through its own consumer (ADR 0259 §6).
What is built for this, line by line, is milestone every-event's part,
[`34-milestones/every-event.md`](34-milestones/every-event.md).
## 4. Where it runs
On whichever machines an operator sits at, by assignment. It is not on the control node by default and
@@ -145,6 +215,14 @@ address gets a refused connection, which is the truthful answer.
| a call through the mesh MCP server's endpoint reaches a module over the bus and the answer is the module's own, unshaped | ADR 0035, a surface decides nothing |
| on the live mesh: the mesh MCP server assigned to a workstation answers `tools/list` on loopback and a call to the forge returns repositories | the exit of work-order step 3 |
| the composed filter for a machine carrying the mesh MCP server opens no port for it | ADR 0144 |
| on a real bus, `mesh_wait` answers the first matching event within a second of its publication, and after every answer a session reads the owner's state | ADR 0367 §1, §4 |
| live: a watcher's `mesh_wait` on the status view's `delivered`, an event no manifest names, wakes on it with no rereading on a timer | ADR 0670 §2, milestone every-event |
| a composition test: the tool runner's runtime account is granted exactly `mesh.seat.*.event.>`, `mesh.mod.*.event.>` and `mesh.stream.*.event.>` beside what its carried modules declare, with no publish on another's events, no durable consumer for them and no wider subject; on a real bus it is refused a publish on an event subject, and another module's account a subscription on an event it does not consume | ADR 0670 §1, §3; ADR 0671 §1 |
| on a real bus, the tool runner's account hears a restricted stream's event with no entry in its grant value, still after the value changes, and is refused a durable consumer and an acknowledgement on it | ADR 0671 §1, §2 |
| the tool runner's gate refuses a marked wait with no warrant and accepts one with a warrant bound to its exact names, filter and timeout; on the test bus a restricted stream's event is heard only after the warrant | the operator, 2026-11-11; ADR 0286 |
| the manifest validator, as the checker runs it, refuses a manifest that carries `waits` as an unknown field, and accepts the tool runner's without it | ADR 0670 §2 |
| the milestone's prepare lists every event with its body's fields, and its code review confirms none carries a secret before the grant is delivered; every later change adding an event or a field is asked the same in its review | ADR 0670 §4 |
| nothing takes an operator's answer from a `decided` event's body; `claude_code_answer` is its one reader | ADR 0670 §5 |
## What shipped, 2026-09-30
@@ -163,6 +241,15 @@ on the live mesh; and a module registered from the catalog by hand recorded its
than as a path on the git seat, because `--self` takes the forge path form — the rebuild-on-merge still
matched it by URL.
## Milestones
| Milestone | What it builds | State |
|---|---|---|
| the mesh MCP server | §1 to §5: the module, its loopback endpoint, its discovery (ADR 0152) | delivered 2026-09-30 (above) |
| found by address | §3a: five announced tools and addresses (ADR 0195, ADR 0197) | delivered; [as-is 13](../00-as-is/13-the-console.md) |
| waiting on the bus | §3b's `mesh_wait` on a list of five families in the tool runner's manifest (ADR 0367) | delivered (issue 745); [as-is 13](../00-as-is/13-the-console.md) |
| every-event | §3b as ADR 0670 and ADR 0671 change it: the tool runner hears every event, module streams included, a marked one on the operator's warrant, and the list goes; one delivery group of mesh-sdk, mesh-delivery, mesh-tools and mesh-catalog, with hq | designed; its part is [`34-milestones/every-event.md`](34-milestones/every-event.md) |
## What this does not settle
- **The mesh's own verbs on the bus.** Design 33's third family: these now answer directly, through
@@ -0,0 +1,350 @@
---
layer: to-be
design: 46
milestone: 7-approval-mark
updated: 2026-11-11
code: [mesh-sdk, mesh-contracts, mesh-delivery, mesh-tools, mesh-catalog modules/zsh, mesh-catalog modules/gitea]
decisions:
- "ADR 0286"
- "ADR 0259"
- "ADR 0627"
- "ADR 0656"
- "ADR 0666"
- "ADR 0668"
- "ADR 0595"
- "ADR 0597"
- "ADR 0310"
- "ADR 0268"
- "ADR 0564"
---
# Design 46, milestone 7 — a call that needs the operator's approval
**What it builds.** A tool or a seat verb marked in its manifest or its seat's definition runs only on the operator's
approval of that one call, with its exact arguments (ADR 0286). The first call asks the operator on the seat
`operator-channel` and runs nothing; the same call made again within 30 minutes of the operator's Approve runs once.
The tool runner (module `node-tools`) asks, judges and claims; it reads the marks from the catalog half on the bus; and
on a machine whose tool runner does not honour the mark the grants part withholds every marked subject. The shape is in
the design's main file,
[§10, *A call that needs the operator's approval*](../46-the-conversation-with-the-operator.md#a-call-that-needs-the-operators-approval);
this part holds every line the milestone builds, cited as "design 46 7-approval-mark §n".
**When.** It is built and delivered in design 47's milestone G, in G's delivery group, with design 34's milestone
every-event, whose marked waits (design 34 every-event §32 to §37) use this gate (the operator, 2026-11-11: "Build the
gate in G").
**Where.** mesh-sdk (the manifest field, the seat definition's field, the per-machine state's keys), mesh-contracts
(the login shell's seat definition), mesh-delivery (the indexer's manifest validator and catalog graph entry, the grants
part's deny), mesh-tools (the tool runner's gate, its state, its tools, and the mesh MCP server's `mesh_describe` in
it) and mesh-catalog (the zsh module, which holds the login shell, and the forge module, §21). Nothing is built in a controller: no controller issues memberships today, bus permissions are the grants part's
(seat `mesh-grants`), the hand-act log is the seat `mesh-hand-acts`, and the asker's logic is operator-channel's client
in mesh-contracts (ADR 0627 §1).
**What changes from ADR 0286 as written.** Each line says in its *Why* where something built since ADR 0286
(2026-10-10) changes how a clause is met: the mark is read from the catalog half and not from a membership (§5), the
digest is Go alone (§8), the claim is kept in the runner's bus state and not a file (§13), `warranted` is a kept call
and not a retry on a timer (§16), and a fourth open ask is offered as G makes it (§17). The controller's own verbs (ADR
0286 §43) are not built: no controller serves verbs.
**The bound of the gate, a known limit.** The gate holds for every call that reaches the tool runner's handler. It does
not hold for a process that reads the tool runner's bus credential (the file `broker` in its state directory,
`own-secrets`) and speaks to the bus itself: that account may subscribe a marked subject, a restricted stream's among
them, on a machine whose runner honours the mark. Read on the workstation on 2026-11-11 with `node-reads.stat`, the
file is mode 0600, owned by the operator's account, in a directory of mode 0755: every process running as the
operator's account, every module on the machine's shared account and every agent session among them, can read it and
hear a restricted stream without a warrant. This is the same known limit as design 47 G §385 (modules running as the
machine's account, issue 1966 note 193, whose fix carries its check), and this milestone does not change it: that a
restricted stream is heard only on a warrant holds against every caller once that fix runs modules and sessions on
accounts that cannot read the file, and until then against callers that go through the tool runner. The
prepare reads the file's mode and owner on every node with `<node>/node-reads.stat` and writes them in the milestone
file; a node where the file is readable beyond the operator's account is a finding of the prepare.
## The mark
1. **A module marks its tools and verbs in `needs-approval`.** mesh-sdk's `catalogue.Manifest` gains `NeedsApproval`,
the manifest key `needs-approval`: a list of strings, each a tool the module serves (`<tool>`) or a seat verb it
serves (`<seat>.<verb>`), and, under design 34 every-event §32, one of its events. Absent is no mark. *Why:* ADR
0286 §1, §49 (each marking is a manifest change, reviewed like any other). *How it is checked:* unit, a row in the
decoder's table test in `go/catalogue`; the manifest reads baseline gains `word:needs-approval`.
2. **A seat's definition marks a verb for every holder.** mesh-sdk's `seatdef` reads a verb's `needs-approval`, a
boolean, in a seat definition (`seats/<seat>/seat.json` in mesh-contracts); the seat definitions the catalog half
carries (design 47 G §194) carry it on the verb. *Why:* ADR 0286 §2. *How it is checked:* unit, a fixture row in
`seatdef`'s table test.
3. **Marks add up.** A verb is marked when its seat's definition or its holder's `needs-approval` marks it; a holder
cannot unmark what its seat marks. *Why:* ADR 0286 §3. *How it is checked:* unit, a row of §5's table.
4. **The manifest validator refuses a mark it cannot honour.** The indexer's manifest validator (`judge/rules.go`)
refuses, as the checker runs it, and sets aside to wait, as the indexer runs it, a manifest that: marks a tool its
`tools` does not list, or a verb its `claims` do not serve (ADR 0286 §4); marks a tool or a verb, or holds a seat
verb its seat marks, without `operator-channel` in `uses` (§7); marks a tool or a verb while every artifact of kind
`bundle` it builds is run by one of its `process` resources (`artifact`), so its own program answers its tools and
no tool runner loads or gates them (a module such as the forge's, whose tools bundle no process runs, is not
refused); or names in `needs-approval` one name that is both one of its tools and one of its events. A tool mark
is checked against `tools` where the manifest lists its tools; where only its bundle names them, the tool runner
checks each marked tool against the tools the bundle registers at load: a bundle whose marks name a tool it does not
register is not loaded, so no tool of that module is served on that node at all, failing closed rather than serving
unmarked what the manifest meant to mark, and that is its
finding `<node>.<module>.mark-unserved` (said as §16's), cleared when a bundle that serves it, or a manifest without
the mark, is loaded. Each refusal names the entry and the rule. *Why:* a mark
nothing enforces would be a call that runs unasked while saying it is guarded; ADR 0286 §43 had the controller
enforce its own verbs, and no other own-process server has a gate, so such a mark is refused rather than ignored;
and an entry that names two things is read one way by one reader and another way by the next (the design's choices
under ADR 0286 §4, §40, the refusal kept by lead-47 on 2026-11-11). *How it is checked:* unit, one row per refusal
in `judge/rules_test.go`, rows that accept the zsh and forge manifests of §21, and a row of §22's mark table for
`mark-unserved`.
## Where the marks are read, and where they are withheld
5. **From the catalog half on the bus, never from a bundle.** The indexer's catalog graph entry of a module (design 47
G §174) gains its `needs-approval`, its `emits`, and for each stream it declares its events and whether it carries a
`grant`; no part but the indexer reads a manifest (the operator, 2026-11-11: "only the indexer uses the manifest to
build the module catalog"); the seat
definitions of the catalog half carry the verb marks (§2). The tool runner, which from G takes its first view of the
catalog half by `mesh-catalog.revision` and follows `catalog-revision` (design 47 G §196), derives from it, for each
module it serves, its marked tools and verbs, in a package of its own, `node-tools/internal/approval`, again on
every revision it hears. No module's name is written in its code. The marks follow the running version: the runner
applies the marks of the module's version it serves, and, while the newest revision's entry for that module
differs, the union of both, so a mark is never lost before or after the module moves. A mark added while a call
waits applies from the next call, which the answer of §12 says. *Why:* ADR 0286 §5 had the controller issue the mark
in a membership so that a bundle could not drop it; no controller issues memberships now, and the judged catalog
half is the same guarantee: the bundle never carries the mark (ADR 0656 §6, one source for which modules exist).
The union keeps ADR 0286 §3's "marks add up" across a version change. *How it is checked:* unit, a table test in
`approval` derives the marks from fixture catalog halves, §3's row, a version change and the union among them.
6. **A machine whose tool runner does not honour the mark serves no marked tool or verb.** The tool runner states
`approval-mark/1` among its call capabilities (`runtime.CallCapabilities`), which its announcement carries and its
node's node-engine takes into the node's record; the assignment half carries it among the capability readings
(design 47 G §194). The grants part, composing a runtime's account, reads that runtime's node's readings: where the
tool runner does not state `approval-mark/1`, where the node has no readings, or where its readings are older than
the tool runner's version on that node (failing closed as §19), it composes a subscribe deny of every marked
subject of every module the runtime carries, the marks read from the catalog half as §5 has them:
`mesh.mod.<module>.tool.<tool>` and the same followed by `.*`; the
caller-named subject mesh-sdk's `process.Call` sends on first, `mesh.mod.<module>.call.<tool>.>`; and for a marked
verb `mesh.seat.<seat>.tool.<verb>`, the same followed by `.<node>`, and `mesh.seat.<seat>.call.<verb>.<node>.>`.
The bus's deny outranks its allow of `mesh.mod.<module>.tool.>` for each. The tool runner's version on a node is read from the
same assignment half revision, its entry for `node-tools` on that node (the module version placed there, design 47
G §175), against the version the node-engine's `running.<node>.node-tools` last said running (design 47 G §309);
readings that revision names from before that version ran are older. Going back to an older tool runner on a
node leaves the window until its readings update, in which the old deny or none stands; that window is accepted. `Permissions` gains `SubscribeDeny`, written into the user list as the server's
subscribe deny, as `PublishDeny` is for publishing. The withheld tools and verbs are named in the grants part's read
of that account (`mesh-grants.grants_user_list`, a field `withheld` with each subject and "its tool runner does not
state approval-mark/1"), and in the node-engine's `composition` of the module. *Why:* ADR 0286 §7, §8, §9: the bus
enforces the mark; the runner's status of ADR 0286 §7 is read from the grants part's account read, since a runner
that does not honour the mark is a version that cannot say so. A call that reaches no responder because its
subject is withheld is answered by the mesh MCP server naming `withheld` and the grants part's read. *How it is
checked:* unit, the grants part's composition table: readings lacking the capability, no readings, and readings
older than the runner each compose the deny of every subject above, the caller-named ones and a marked seat verb's
`.<node>` form among them, each named in `withheld`; readings with the capability compose no deny; integration, the
grants part's real-bus test: an account composed with the deny is refused delivery of a call on a marked subject
while it holds `mesh.mod.<module>.tool.>`, and so is a call sent through `process.Call` on the caller-named
subject.
7. **The asker's grants are a user's of `operator-channel`.** Because the manifest says `operator-channel` in `uses`
(§4), the grants part composes, on the account of the runtime carrying the module, the publish of
`mesh.seat.operator-channel.accept.ask.<module>` and `.cancel.<module>`, the hearing of its warrants on
`mesh.seat.operator-channel.event.decided.<module>` and the read of the router's record under `<module>.` in the
seat's state `asks`, as for every user (ADR 0259 §3). The runner passes the client its own publish and read on that
runtime's account (`Client.Publish`, `Client.Record`), never the bundle's. *Why:* ADR 0286 §6. *How it is checked:*
unit, a row in the grants part's composition table for a fixture module with a mark.
## The act, and the first call
8. **The act is digested with operator-channel's client.** For a call of a marked tool or verb, the tool runner
builds an `operatorchannelclient.Act` with `address` (as called), `node` (the node that serves it), `module` and
`tool`, or `seat` and `verb`, `build` (the digest of the bundle it serves the module from), and `arg.<name>` for each
argument given, a string as itself and any other value as its JSON in RFC 8785's canonical form; an argument left
out is not in the act. Its digest is `operatorchannelclient.ActDigest`. *Why:* ADR 0286 §10, §11; the digest is in
mesh-contracts since ADR 0627 §1. The TypeScript twin ADR 0286 named is not built: the tool runner is Go, and no
TypeScript asker computes an act. *How it is checked:* unit, rows in `approval` for the act's fields, an omitted
argument, a number, an object and a changed build each giving another digest.
9. **A call refused without asking.** On a call with no approval for its act, the runner refuses at once, asking
nothing, a call whose arguments hold anything `outward.CheckSecret` refuses (mesh-sdk), or whose arguments are more
than 16 KiB. *Why:* ADR 0286 §12; mesh-sdk's `outward` is the content rule ADR 0310 §4 moved there. *How it is
checked:* unit, two rows of §22's table.
10. **One ask, under the module's name.** Otherwise the runner asks through an `operatorchannelclient.Client` whose
`Module` is the marked module, whose `State` is the runner's state of §13, and whose new field `KeyPrefix`, which
the client in mesh-contracts gains and puts before every key of its own `State` it writes and reads (empty for
every asker today), is `<node>.`; never before the router's record it reads through `Record` and `Where`, whose key
stays `<module>.<id>` in the seat state `asks`; unless an ask for the same act is
open there (§13's `act.` key), in which case it asks nothing new. The ask: `about` the act's digest; `who`
`operator`; `expires` 60 minutes after; no `on-expiry`; two options, `approve` (label Approve) and `decline` (label
Decline), both at level `approve`, each binding the act's digest; the headline "Run <tool> on <node>?",
"Run <tool>?" when that is longer than the router allows, ending "Part shown" whenever an argument is not shown
whole; the explanation: the first line of the tool's own description, the node, that it was called through the
mesh's tools, which cannot tell who called, every argument one per line as "name: value", and the act's
fingerprint, its digest's first 24 hexadecimal digits in groups of four. *Why:* ADR 0286 §13, §15, §16, §20 to
§23; the client checks the ask (`Check`) before it publishes. *How it is checked:* unit, a row in `approval` holds
the published ask's fields against a fixture call, and a second identical call publishes nothing.
11. **An argument is shown whole only where it may leave the mesh.** Each argument is shown whole where
`outward.Check` passes it, else in its shape with ‹path›, ‹address› or ‹withheld› in place of what it refuses; the
arguments are shown whole when they are at most 2000 bytes and 40 lines, otherwise their first 20 lines and 1200
bytes, marked "NOT SHOWN IN FULL", and the explanation then says that the whole is read with `mesh_approvals`
(§18) on that node, whose fingerprint must be the one on the phone. *Why:* ADR 0286 §17 to §19. *How it is
checked:* unit, rows of §10's test with a path, an address and a long argument.
12. **The first call answers "Not run", or that the router has not taken it.** The runner waits up to 5 seconds for
the router's record of the ask (§14) and answers the call as an error, never as a result. With the record: "Not
run: this needs the operator's approval. Asked on <channel> at <time> (ask <id>, fingerprint …); make the same call
again within 30 minutes of the approval; the ask expires at <time>." With no record within the 5 seconds: "Not
run, asked; not yet taken by the router (ask <id>, fingerprint …); make the same call again later." The ask stays
in the seat's stream of accepts, a work queue the router takes it from when it runs, and the `act.` key stays, so
the next call reads the router's record of the same ask and asks nothing new. A mark added since the caller last
called is named in the answer ("marked since <revision>"). *Why:* ADR 0286 §14, §37; the caller calls again, as
the operator chose on 2026-11-11 ("Call again (as designed)") over a held call that runs on the Approve and returns
its outcome (issue 417, 2026-10-16). *How it is checked:* unit,
rows of §22's table hold both answers; a row with no router holds that a second call asks nothing new.
## The runner's record, and the same call again
13. **The runner keeps its asks and claims in a state of its own, per machine.** The tool runner's manifest asks in
`requires` for a state `approvals`, template `state`, `per-machine`, `ttl-seconds` 172,800 (48 hours), cap 64 MiB.
Its keys: `<node>.ask.<id>`, the asked call as the client keeps it (the ask, the act, the whole arguments, when
asked, whether acted on, and `recorded` once `warranted` answered, §16), and `<node>.act.<digest>`, the id of the
newest ask for that act. The runner on each machine is the one writer of its machine's keys; a key is deleted by
its time to live, never by hand. A per-machine state reaches today only the key named for its machine; mesh-sdk's
per-machine form and the grants part's `stateAccess` gain the keys below it, `<node>.>`, for every per-machine
state, a bundle's membership (`stateIssuedFor`, ADR 0260 §18) too, in the same form: one more subject per
per-machine state on each account, and every module's per-machine keys keep their form; mesh-sdk's rebuild of it
rides in G's group. The cap is the bucket's, shared by every machine's keys. A write the bus refuses (the cap
reached, or any other) fails closed under §19 and is said in the answer; `mesh_approvals` reports the bucket's fill
against its cap, saying that the fill and the cap are every machine's together. A run still unrecorded (§16) an hour before its key's 48 hours end is written in the runner's log
as an error naming the ask; this is written only while the runner runs, so a machine asleep through that hour keeps
only the `call-unanswered` history of §16, which is accepted.
*Why:* ADR 0286 §24, §30 kept the waiting call in the runner's record and claimed a warrant in the node's own file,
kept 48 hours, by one writer per node; the client built since keeps asks and marks a warrant acted on by
compare-and-set in a module's bus state (ADR 0259 §10, ADR 0627 §1), and a per-machine state (ADR 0595) is one
writer per machine, so the file is not built. The keys below the machine's own depart from ADR 0260 §16 (a
per-machine state reaches only its machine's key), the design's choice following the operator's answers of
2026-11-11 (the gate built in G); the cap and the key form are the design's choices too. *How it is
checked:* unit, the manifest test of `node-tools` holds the entry; the grants part's composition table holds the
`<node>.>` reach and no other node's; integration, §22's whole test reads the keys.
14. **The same call again is judged on the router's record.** When a call of a marked tool comes and `<node>.act.`
names an ask, the runner reads the router's own record of that ask under the module's name (`Client.Where`, the
seat state `asks`, key `<module>.<id>`), never a record of its own or the caller's; it composes the ask again from
the act and holds the warrant to it with `operatorchannelclient.For` (the same asker and ask, the ask's digest, a
person's choice, an option offered, at its level, before it expired); it requires the option to be `approve` and
the act to be the one it bound (`operatorchannelclient.Performs`: the same node, build and every argument); and it
requires the call within 30 minutes of the choice. It judges the newest ask it keeps for the act. *Why:* ADR 0286
§26 to §29, §32. *How it is checked:* unit, rows of §22's table: approved, approved for other arguments, approved
31 minutes ago, declined, expired, another asker's warrant.
15. **One approval runs one call.** Before it runs, the runner marks `<node>.ask.<id>` acted on by compare-and-set
(`Client.Take`); a warrant marked before is refused, so one approval runs one call however often the call is made,
and a warrant replayed onto the bus runs nothing. After "already run", "expired" or "approved too long ago" the next
identical call asks anew, and the used mark stays. *Why:* ADR 0286 §30, §31, §33, §36. *How it is checked:* unit, a
row of §22's table makes the approved call twice and runs it once; two calls at the same moment run it once.
16. **The call runs, and its warrant is recorded, across a restart too.** Once the checks pass, the runner runs the
call and answers with the tool's own answer and a line naming the approval ("run on the operator's approval via
<channel> at <time>, ask <id>"). It then calls `mesh-hand-acts.warranted` with `asker` (the module) and `ask` (the
id) as a kept call (ADR 0668 §1, §2: the verb takes it once per ask), and on its answer writes `recorded` on
`<node>.ask.<id>`. At each start the runner calls `warranted` again for every acted, unrecorded ask its keys hold,
within their 48 hours, each named in its `findings-stated`'s `pending` until it is made (design 47 G §384). One
unanswered after five minutes is the runner's finding `<node>.<ask id>.call-unanswered` in its state `findings`,
said on its stream `node-tools-findings` (design 47 G §383, §387), which the condition keeper hears; it clears when
`warranted` answers. *Why:* ADR 0286 §34, §35; the hand-act log is the seat `mesh-hand-acts`, whose `warranted`
reads the router's own record, and its "again every ten minutes for a week" is ADR 0668's kept call now, kept
across a restart by the `recorded` mark (ADR 0668 §3, ADR 0564). *How it is checked:* integration, §22's whole test
reads the hand-act recorded once, also when the runner is restarted after the run and before `warranted` answers.
17. **What a later call is told.** For the same call made again before it can run, the caller is told, as an error:
still waiting; declined (the same act is not asked again for 10 minutes, and the answer says so); expired,
approved too long ago or already run (each asks again, §15); not yet taken by the router (§12); or refused, with the
router's words. An ask the router holds back as the asker's fourth open one is still waiting, and
`operator-channel.asks` lists it (ADR 0666 §12, as G builds it). *Why:* ADR 0286 §38, §39; ADR 0286 §40's refusal
of a fourth open ask was the router's before G, which now offers a fourth and holds the oldest back. *How it is
checked:* unit, rows of §22's table.
18. **The tools read and steer what was asked.** The mesh MCP server on each machine gains the tool `mesh_approvals`:
with no argument, the calls asked on that machine in the last day, newest first, each with its state and
fingerprint, read from §13's state, and the marks it applies, each module's marked tools and verbs with their
source (its module's entry in the catalog half or its seat's definition) and the catalog revision read; given an ask's id, its whole
arguments and the act's digest; given `cancel` and an ask's id, it takes that open ask back (`Client.Cancel`),
recorded first in the hand-act log through `mesh-hand-acts.hand-act` with who and why, and refused when the record
cannot be written. `mesh_describe` answers, for a marked tool or verb, `needs-approval` with its source. *Why:*
ADR 0286 §25 (the runner's `approvals`), named as the mesh MCP server's own tools are; a tool reads everything a
module holds and does by hand what it does by itself (the operator's instruction of 2026-10-25). *How it is
checked:* unit, rows in the mesh MCP server's tool table test: the list, the marks with their sources, an ask read
whole, a cancel recorded and one refused without a record, and `mesh_describe` of a marked tool.
## Failing closed
19. **Nothing runs when a part of the check is missing, and a stuck gate is said.** A call of a marked tool is
refused, runs nothing and says why when: no module holds `operator-channel` (read from the assigner's first view
`mesh-delivery-assigner.views.seat-holders`, which the tool runner consumes; the seat list names no holders); the
router refuses the ask; the ask cannot be published; the router's record cannot be read; §13's state cannot be read
or written, in which case no marked call runs on that machine until it can, the failed read or write being made
again as a kept call (ADR 0668 §1) until it succeeds; or the marks are not yet read. Before
the first view of the catalog half is read, a call waits for it within its 5 seconds and is then judged, asking
nothing while the marks are unread; still unread, it is refused, naming `mesh_first_views`. When §13's state has
been unreadable or unwritable for five minutes, the runner raises its finding `<node>.approvals.approvals-unreadable`
(said on `node-tools-findings` as §16's), and clears it when the kept call succeeds; its `findings-stated` at a start
judges it anew from a read of the state (design 47 G §384). Each refusal is
written in the runner's log with the ask's id and fingerprint, never the arguments. *Why:* ADR 0286 §40, §42; a
gate that refuses every call for a reason nobody hears is a silent fault, and the finding and its five minutes are
the design's choice. *How it is checked:* unit, one row of §22's table per case, the holder read from a fixture
`views.seat-holders` with none and with one; and a row where the state fails, the finding is raised after five
minutes, the state recovers with no further call, and the finding clears.
20. **The terminal is not exempt.** A marked tool called through `mesh-cli` or any caller asks as every call does: the
runner cannot tell who called. *Why:* ADR 0286 §41. *How it is checked:* the code review reads that the gate sits
on the call's handler, before the bundle, and that no path to a bundle passes around it.
## Marked first
21. **`node-login-shell.execute` is marked in its seat's definition.** mesh-contracts' `seats/node-login-shell/seat.json`
marks the verb `execute` with `needs-approval`; the zsh module's manifest (mesh-catalog `modules/zsh`), which holds
the seat, gains `operator-channel` in `uses`. The control-node keeps withholding `execute`, and whether it serves
it again under the mark stays the operator's setting. *Why:* ADR 0286 §44, §45: the reads it carried have their
tools on every node (the seat `node-reads`, ADR 0310; issue 418 resolved 2026-10-12, answering on every node since
2026-10-16). *How it is checked:* live, as the acceptance says.
- **The forge's two acts on a trunk's protection are marked.** The forge module (mesh-catalog `modules/gitea`)
gains a tool `gitea_branch_protection_loosen`, marked in its `needs-approval`, which alone performs the two acts
`gitea_branch_protection_set_full` refuses today: removing a trunk's protection, and allowing a force-push onto
a trunk; `gitea_branch_protection_set_full` keeps refusing both and its refusal names the new tool. The manifest
gains `operator-channel` in `uses`. No other tool or verb is marked in this milestone. *Why:* the forge's own
refusal says these two acts are the operator's and waited for this mark (issue 417, 2026-10-28); a mark is on a
whole tool (ADR 0286 §1), so the acts move to a tool of their own, as ADR 0286 §48 splits a tool into an unmarked
and a marked form, and tightening a protection stays unasked; the operator, 2026-11-11: "Mark them in G". The
principle: loosening a trunk's protection runs only on the operator's own warrant for that one act, never on a
session's word. Its bound: a process that reads the forge module's own credential speaks to the forge directly
and goes around it, the same known limit as issue 1966 note 193; the prepare reads that credential's mode and
owner on the forge's node with `<node>/node-reads.stat` and writes them in the milestone file. *How it
is checked:* the forge module's tool test: `set_full` refuses both acts naming the new tool; the new tool
performs each; and live, as the acceptance says.
## Tests, delivery and going back
22. **The tests.** The tool runner's tests in `node-tools/internal/approval` are one table-driven unit test of the
refusals and answers (§9, §12, §14, §15, §17, §19) and one of the marks (§5), on the tool runner's test bus
(`meshtest`), the router's record and the warrant written there as the router writes them; and one integration
test that starts the runner against the real bus, with no store, with a fixture marked module: a call, the
operator's Approve written as the router's record, the call again, the run, the hand-act, `mesh_approvals`, and a
restart between the run and `warranted`'s answer. The client's `KeyPrefix` (§10) has a row in mesh-contracts'
client test: every key of its own `State` written and read carries it, an empty prefix writes today's keys, and
with a prefix set `Where` still reads `<module>.<id>`. *Why:* the operator's test budget (one whole test over many small
ones). *How it is checked:* the check counts the module's tests against its budget.
23. **In G's delivery group, ordered by the module graph.** The members are pushed and checked in design 47 milestone
G's group, with design 34's milestone every-event. The orders are the module graph's: the indexer's manifest
validator and catalog graph entry that read `needs-approval` before any manifest or seat definition carrying it (a
reader edge, design 47 G §89); and the grants part that composes the deny of §6 before the seat definition marking
`execute` (an edge of the seat definition's readers), so no machine serves `execute` unasked between the two.
*Why:* the operator, 2026-11-11, "Build the gate in G"; the operator, 2026-11-03, a change is designed as it should
be and its delivery is the delivery system's. *How it is checked:* the delivery plan names both edges.
24. **Going back is going forward.** A fault met after the delivery is fixed by a new change delivered the same way.
Reverting instead reverts G's whole group in the reverse of §23's order: the seat definition's mark first, then the
grants part, then the validator. The `approvals` state is then declared by no running module: it is an orphan
`orphans` lists, its keys aging out within 48 hours, dropped through ADR 0595's `drop` on the operator's word.
*Why:* design 47 G's revert rule; ADR 0595 §4, §5. *How it is checked:* the delivery plan of a revert names the
order.
## Acceptance
- **A marked call is asked on the phone, does nothing, and runs once on the Approve** (live, read by tool call):
`node-login-shell.execute` on a node other than the control-node answers "Not run", the ask is on
`operator-channel.asks`, and after the operator's Approve the same call runs once, a third identical call is refused
as already run, and `mesh-hand-acts.hand-acts` names the warranted act (§10, §12, §14 to §16, §21).
- **The forge's loosening is asked, and the protection is put back** (live, read by tool call): on a scratch repository
`acceptance-approval-mark`, created by the acceptance through the forge's `gitea_create_repo` and kept afterwards,
never deleted, with full protection set on its trunk by `gitea_branch_protection_set_full`,
`gitea_branch_protection_loosen` removing the trunk's protection answers "Not run" and asks on the phone; after the
Approve the same call runs once; then `gitea_branch_protection_set_full` sets the full protection again and
`gitea_branch_protection_get` reads it back as it was (§21).
- **No machine serves `execute` unasked** (live, read by tool call): before this acceptance passes, on every node
`execute` is either asked as above or absent, read with each node's `node-engine.composition` and
`mesh-grants.grants_user_list`'s `withheld` (§6).
- **A marked wait as the operator sees it** (live): design 34 every-event's acceptance of a wait on a mail event.
- **The mark cannot be lost on the way** (unit): §5's and §8's tables pass; a bundle carries no mark.
- **It fails closed** (unit): §19's rows pass.
- **The validator refuses a mark nothing enforces** (unit): §4's rows pass.
@@ -1,9 +1,10 @@
---
layer: to-be
status: in-progress
code: [mesh-controller, mesh-tools, mesh-sdk, mesh-catalog]
updated: 2026-11-10
code: [mesh-controller, mesh-tools, mesh-sdk, mesh-catalog, mesh-contracts, mesh-delivery]
updated: 2026-11-11
decisions:
- "ADR 0668"
- "ADR 0667"
- "ADR 0666"
- "ADR 0347"
@@ -17,6 +18,11 @@ decisions:
- "ADR 0253"
- "ADR 0234"
- "ADR 0227"
- "ADR 0627"
- "ADR 0597"
- "ADR 0595"
- "ADR 0310"
- "ADR 0268"
---
# 46 — The conversation with the operator
@@ -561,44 +567,27 @@ said on the away channel, with its why.
### A call that needs the operator's approval
*Added 2026-10-10, ADR 0286.*
A module's manifest marks a tool, or a seat verb it serves, as **`needs-approval`**; a seat's definition may mark
a verb for every holder. The controller issues the mark in the runner's membership, never from the bundle, and
withholds a marked tool from a runner that does not declare `approval-mark/1`, by leaving its subject out of that
runner's bus permissions.
*Added 2026-10-10, ADR 0286; amended 2026-11-11 to the mesh as built and as design 47's milestone G leaves it.*
A module's manifest marks a tool, or a seat verb it serves, in **`needs-approval`**; a seat's definition may mark
a verb for every holder. The tool runner reads the marks from the catalog half on the bus (seat `mesh-catalog`), never from
the bundle, and on a machine whose tool runner does not state that it honours the mark the grants part withholds every
marked subject from its account.
```
agent ──call──► tool runner (serving node) ──ask, under the module's name──► router ──► Telegram
agent ──call──► tool runner (serving node) ──ask, under the module's name──► seat operator-channel ──► Telegram
▲ │ "Not run: asked, ask <id>" │ Approve
│ ▼ ▼
└──same call──► runner reads the router's record ◄────────────── warrant (decided.<module>)
│ Warrant.For + Option.Performs on the act; within 30 min; claimed once
│ For + Performs on the act; within 30 min; claimed once in its own state
▼
runs the call once ──► mesh-controller.warranted (hand-act log)
runs the call once ──► seat mesh-hand-acts, warranted
```
- **The act** is the address, the node, the module and tool, the bundle's build and every argument given, digested
with the shared library's `ActDigest` (in Go and TypeScript, one set of vectors).
- **The first call asks and does nothing.** One ask per act (its `about` is the act's digest): "Run <tool> on
<node>?", the tool's first line, every argument whole where it may leave the mesh or in its shape with
‹path›, ‹address› or ‹withheld› and "Part shown" (whole when at most 2000 bytes and 40 lines, else the
first 20 lines and 1200 bytes), the fingerprint, and where the whole is read (the runner's
`approvals` on that node). Approve and Decline, both at approve; expiry 60 minutes; never a default. The call
answers as an error: "Not run", with the ask's id, that the call runs within 30 minutes of the approval, and
when the ask expires.
- **The same call again** runs once on Approve, read from the router's record, within 30 minutes of the choice,
claimed in the node's own file (kept 48 hours, one writer per node); the runner judges the newest ask it keeps
for the act, and after "already run", "expired" or "approved too long ago" the next identical call asks anew;
then `warranted` records the hand-act. Anything else is
refused and said: waiting, declined (not asked again for 10 minutes), expired, already run, or the router's
refusal.
- **Fail closed:** a secret's shape in an argument or more than 16 KiB of arguments, no operator channel, a
fourth open ask (until design 47's milestone G is delivered, after which a fourth is offered and the oldest held
back, §6), an unpublished ask, an unreadable router record, node file or mark: nothing runs.
- **The controller** enforces the same mark on its own verbs, asking as `mesh-controller`.
- **Marked first:** `node-login-shell.execute` on every node once the reads it carried have tools (issue 418).
`mesh-delivery.release` waits until a warrant the controller holds passes the mark of a verb it calls (issue
417), so a Release on a condition is not asked twice.
The first call asks and does nothing; the same call made again within 30 minutes of the operator's Approve runs once;
everything else is refused and said, and anything the check cannot read runs nothing. `node-login-shell.execute` is
marked first. Every line of it is in milestone 7's part,
[`46-milestones/7-approval-mark.md`](46-milestones/7-approval-mark.md), built and delivered with design 47's milestone
G (the operator, 2026-11-11).
### The hand-act
@@ -718,7 +707,7 @@ Each phase ends at its own *done when*. Owning repositories from [`repos.md`](..
| **5a — Asks on the bus** (ADR 0259) | the kinded bench, caller-named verbs, `proof` subjects and the records users read (controller); a bundle's seat traffic, compare-and-set state and its own account (tool runner); the asker's and channel's types (shared library); the router's asks, tokens, identities, judging, warrants, expiry and linking with its hour (messenger); `telegram` and `desk-channel`; the controller asking for its conditions' actions and acting on warrants | mesh-controller, mesh-tools, mesh-sdk, mesh-catalog | a held delivery released from the phone; the hand-act reads "the operator, via telegram, user id verified"; a desk Silence goes through an ask |
| **5 — Authorise, with TOTP** | `authorises` in the verb table; the router's checks for the code; direct calls refused, break-glass; `retire approve` takes `expect`; the hand-act fields; the TOTP seed as the router's state, enrolled at a terminal with ten recovery codes; `factor recover`, `factor status`, the local-only break-glass enrolment; no P2 tier before recovery codes exist; optional key enrolment; the self-check probe for the away channel; agents' grants lose the authorising verbs | mesh-controller; mesh-catalog (agents' grants, the desk's code prompt) | one controller test per refusal passes; at the desk, approve needs a code; a recovery drill re-enrols with a recovery code, and is announced |
| **6 — Telegram live** | the `telegram` module (built in 5a) with replies and forced-reply codes, under its own account on the control node; the operator's two bots configured; the watcher assigned off the control node; the dead-man ping | mesh-catalog | the self-check says the away channel carries every tier; rehearsals: an approve and a destroy answered on the phone, with the hand-acts read |
| **7 — A call that needs approval** (ADR 0286) | `needs-approval` in the manifest and the seat list, issued in the membership, withheld from a runner without `approval-mark/1` in its bus permissions (controller); the act's digest in TypeScript beside Go, with shared vectors (shared library); the check, the ask, the runner's `approvals`, the node's used file and `warranted` (tool runner); the controller's own verbs; `execute` marked (the controller's seat list) once issue 418's reads exist | mesh-controller, mesh-sdk, mesh-tools, mesh-catalog | a marked call on a node is asked on the phone, does nothing, runs once when made again after Approve, is refused the third time and asked again, and `hand-acts` shows the warrant |
| **7 — A call that needs approval** (ADR 0286), delivered with design 47's milestone G | the mark in the manifest and the seat definition, read by the tool runner from the bus; the manifest validator's refusals; the capability `approval-mark/1`; the act, the ask, the check, the claim in the runner's own state, `warranted` and `mesh_approvals`; `execute` marked. Its part is [`46-milestones/7-approval-mark.md`](46-milestones/7-approval-mark.md) | mesh-sdk, mesh-contracts, mesh-delivery, mesh-tools, mesh-catalog | its part's acceptance |
| **8 — No ask hidden** (ADR 0666 §12), delivered with design 47's milestone G | a fourth ask offered and the asker's oldest held back, offered again when one ends; `asks` lists the held-back asks; the router's tool `ask-again`; the pipeline runner's `approval` asks and their `about` | mesh-catalog (the router) | a fourth ask and a declined approval each reach the operator; `asks` shows the held-back one |
**Switching phase 5a on: the operator's steps, in order** (amended 2026-10-09, ADR 0259). Each line is typed with