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:
@@ -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).
|
||||
@@ -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.<seat>.event.… / mesh.mod.<module>.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
|
||||
|
||||
Reference in New Issue
Block a user