diff --git a/03-DESIGN/01-to-be/34-milestones/every-event.md b/03-DESIGN/01-to-be/34-milestones/every-event.md new file mode 100644 index 00000000..95408e5e --- /dev/null +++ b/03-DESIGN/01-to-be/34-milestones/every-event.md @@ -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 `.`, optionally followed + by further tokens (`operator-channel.decided.claude-code`); each token matches `[A-Za-z0-9_-]+`. It answers four + subjects: `mesh.seat..event.`, the same followed by `.>`, `mesh.mod..event.`, and + the same followed by `.>`, where `` 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..event.` or + `mesh.mod..event.` it answers the name `.` 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-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, `.`, 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). diff --git a/03-DESIGN/01-to-be/34-the-console.md b/03-DESIGN/01-to-be/34-the-console.md index 2bc703e4..61f897fa 100644 --- a/03-DESIGN/01-to-be/34-the-console.md +++ b/03-DESIGN/01-to-be/34-the-console.md @@ -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
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