diff --git a/00-META/glossary.md b/00-META/glossary.md index 2b846b32..c14e2624 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -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 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..490bd251 --- /dev/null +++ b/03-DESIGN/01-to-be/34-milestones/every-event.md @@ -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 `.`, 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; 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..event.` or + `mesh.mod..event.` it answers the name `.` 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-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.` and on + `mesh.seat.intake.proof.code.` (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 : 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, `.`, 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.` 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 and reread 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..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..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..event.` 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 `` of module ``, + `mesh.mod..event.` and `mesh.stream..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 on ?" + (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 `..` + under a marked `.` 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 ?" 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). diff --git a/03-DESIGN/01-to-be/34-the-console.md b/03-DESIGN/01-to-be/34-the-console.md index 2bc703e4..6861be17 100644 --- a/03-DESIGN/01-to-be/34-the-console.md +++ b/03-DESIGN/01-to-be/34-the-console.md @@ -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
mesh.seat.<seat>.event.… / mesh.mod.<module>.event.… / mesh.stream.<module>.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 diff --git a/03-DESIGN/01-to-be/46-milestones/7-approval-mark.md b/03-DESIGN/01-to-be/46-milestones/7-approval-mark.md new file mode 100644 index 00000000..396ad4d8 --- /dev/null +++ b/03-DESIGN/01-to-be/46-milestones/7-approval-mark.md @@ -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-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 (``) or a seat verb it + serves (`.`), 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.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 `..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..tool.` and the same followed by `.*`; the + caller-named subject mesh-sdk's `process.Call` sends on first, `mesh.mod..call..>`; and for a marked + verb `mesh.seat..tool.`, the same followed by `.`, and `mesh.seat..call...>`. + The bus's deny outranks its allow of `mesh.mod..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-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 + `.` 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..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.` and `.cancel.`, the hearing of its warrants on + `mesh.seat.operator-channel.event.decided.` and the read of the router's record under `.` 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.` 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 `.`; never before the router's record it reads through `Record` and `Where`, whose key + stays `.` 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 on ?", + "Run ?" 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 at