Design 34: a session waits on any event, the tool runner hearing every one (ADR 0670)

Watchers reread on a timer because mesh_wait refuses every event outside a list of five,
and the delivery status view's events, which playbooks have watchers wait on, are not on
it. ADR 0670 grants the tool runner every event subject and drops the list; this writes
that as milestone every-event, one delivery group of mesh-sdk, mesh-delivery and
mesh-tools, and puts design 34 in the main-file-and-parts shape.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 02:41:11 +01:00
parent c74546a16f
commit 024627f658
2 changed files with 241 additions and 3 deletions
@@ -0,0 +1,182 @@
---
layer: to-be
design: 34
milestone: every-event
updated: 2026-11-11
code: [mesh-sdk, mesh-delivery, mesh-tools]
decisions:
- "ADR 0670"
- "ADR 0367"
- "ADR 0043"
- "ADR 0259"
- "ADR 0332"
- "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 two event subjects of the whole mesh, 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.** Three repositories, one delivery group: mesh-sdk (the shared event subjects, and the field, type and
membership entry that go), mesh-delivery (the grants part, seat `mesh-grants`; the manifest validator in the indexer)
and mesh-tools (the tool runner, module `node-tools`). The milestone is built on each repository's main as it stands;
design 47's milestone G, which edits the grants part's account composition and the tool runner's manifest and manifest
test too, is rebased onto it.
## 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 (§5) and the tool runner (§12), so neither spells the event namespace itself
(shared code belongs in mesh-sdk):
- `EveryEvent`, the two 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. 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 whether the emitter is a seat; 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
(§5), so both are subscribed; 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`.
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. **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 subjects are written by the grants part into the bus's user list as every grant is
(ADR 0571 §9), read with the grants part's own tools as today.
6. **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.
7. **The tool runner's membership names no events.** The membership the grants part writes for the tool runner in
its state `memberships` (key `<node>.node-tools`, ADR 0571 §10) carries no `waits` key, since the field is gone
(§4); every other part of it is unchanged.
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 `mesh.stream.` or
`$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 three 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; 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).
## The tool runner (module `node-tools`)
11. **Its manifest loses `waits`.** `node-tools/module.json` drops the key `waits` and its five entries; nothing else
in it changes, its `version` stays `2` (ADR 0670 §2).
12. **`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, and 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, and a call is never a
subscription to the whole bus). 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).
13. **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`), and a new field `from`, `seat` or `module`, says which namespace it arrived
on. 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).
14. **A refused subscription is said, and why.** `bus.Conn.HearFirst` is unchanged: it says a subscription the bus
refuses at once, naming the subject. `waitAnswer` answers that failure adding that the grants part grants this
machine's tool runner every event subject (ADR 0670 §1), so a refusal means its account's grant is not composed
yet, and that the state is read from its owner meanwhile.
15. **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. 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.
16. **The hearing serves `mesh_wait` alone.** `HearFirst` has one caller, `waitAnswer`; a carried module's code still
reaches the bus only as its own membership names it, as the runtime keeps it today (ADR 0670 §1). How it is
checked: the code review reads `HearFirst`'s callers.
17. **What goes from the tool runner's code.** The type `bus.WaitIssued` (`internal/bus/wait.go`) and `Membership.Waits`
(`internal/bus/bus.go`) go. `console/manifest_waits_test.go` keeps its test of what the tool runner consumes and
its test of the runner's own secrets; its test of the five families becomes a test that the manifest has no key
`waits`, and its comment cites ADR 0670 §2. `console/wait_test.go`'s `waitingRuntime` issues no waits, and its
tests wait on `mesh-delivery-view.delivered` and `operator-channel.decided.claude-code` with no membership naming
either; the test that a name outside the list is refused becomes a test that a name with `*`, `>` or one token is
refused (ADR 0670 §2). No test is added: the module stays within its budget.
## Events carry no secret
18. **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's manifest `emits` and each seat's definition declares, read from the
indexer's modules (`mesh-delivery-view.indexer_modules`) and the seat list (the seat `mesh-catalog`'s definitions),
each with the fields of its body as the emitting code builds it, by file and line (ADR 0670 §4).
19. **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 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).
An event found carrying one holds the milestone's delivery and goes to the operator as a design question
(ADR 0670 §4).
20. **From then on every change is asked the same.** 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? Written in this milestone's hq pull request
(ADR 0670 §4).
## Delivery
21. **One delivery group, no order named between its members.** The three repositories are pushed and checked as
one group. No order is named between them, 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). Between the grants part's delivery and the tool runner's on a
node, `mesh_wait` there answers the bus's refusal (§14), or an older tool runner's "issued no event families":
both loudly, and a watcher reads its owner after every answer (ADR 0367 §4).
## Acceptance
- **A watcher wakes on an event no manifest names** (the operator's acceptance): on the live mesh, a watcher's
`mesh_wait` on `mesh-delivery-view.delivered` answers the status view's `delivered` within a second of its
publication, 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` (§5, §12).
- **The grant is exactly two subjects**: the composition test of §8 and the real-bus cases of §9 pass.
- **The list is gone**: 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).
- **No event carries a secret**: the milestone file's *Events and their fields* is complete and the code review's
confirmation is written in it before the grants part is delivered (§18, §19).
- **An answer is never taken from an event**: in the three repositories nothing reads an operator's answer from a
`decided` event's body, which the code review confirms, and `mesh_wait`'s answer for one says to read it with
`claude_code_answer` (§13).
+59 -3
View File
@@ -1,9 +1,16 @@
---
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]
updated: 2026-11-11
decisions:
- "ADR 0670"
- "ADR 0043"
- "ADR 0259"
- "ADR 0332"
- "ADR 0571"
- "ADR 0578"
- "ADR 0614"
- "ADR 0197"
- "ADR 0195"
- "ADR 0175"
@@ -119,6 +126,40 @@ 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 changes 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 or a module's event<br/>mesh.seat.&lt;seat&gt;.event.… / mesh.mod.&lt;module&gt;.event.…] -->|heard for the call| TR[the tool runner's account]
G[seat mesh-grants] -->|grants the two event subjects| TR
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.
**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, no token
that grants an act, no code, no message marked secret, which the code review asks of every change that adds an event
or a field (ADR 0670 §4). 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 +186,12 @@ 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.>` and `mesh.mod.*.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 |
| 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 +210,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 changes it: the tool runner hears every event, and the list goes; one delivery group of mesh-sdk, mesh-delivery and mesh-tools | 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