From 28c22c65cbddb176e732e9225628b6b0267ea6b5 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 03:19:12 +0100 Subject: [PATCH 1/2] Answer the prepare's questions on the approval mark and marked waits Each of the 25 recommendations becomes a design line with its question id, so the builders find every field, test and source named; the live readings of the credentials go into the gate's stated bound. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../01-to-be/34-milestones/every-event.md | 10 +- .../01-to-be/46-milestones/7-approval-mark.md | 105 ++++++++++++------ 2 files changed, 75 insertions(+), 40 deletions(-) diff --git a/03-DESIGN/01-to-be/34-milestones/every-event.md b/03-DESIGN/01-to-be/34-milestones/every-event.md index 490bd251..26cb3010 100644 --- a/03-DESIGN/01-to-be/34-milestones/every-event.md +++ b/03-DESIGN/01-to-be/34-milestones/every-event.md @@ -65,7 +65,8 @@ that holds its mark reach the machines together. `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 + deleted, and one row is added to the nearest table over raw manifests, `go/catalogue/invokes_test.go`'s + `TestAManifestMaySayWhatItInvokes` (Q-T): 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). @@ -182,8 +183,8 @@ that holds its mark reach the machines together. 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. + `operator-channel` in `uses`, the state `approvals` in `requires` with its options, and the finding stream. New + test functions are allowed within the module's budget of 500 unit and 50 integration tests (Q-TB). ## The claude-code module's Stop hook @@ -316,7 +317,8 @@ approves, for a marked event or a restricted stream. `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 + its build (the tool runner's own fingerprint, from its catalog entry of the version served; design 46 7-approval-mark + §8, Q-BD), 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 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 index 396ad4d8..6bd8676c 100644 --- a/03-DESIGN/01-to-be/46-milestones/7-approval-mark.md +++ b/03-DESIGN/01-to-be/46-milestones/7-approval-mark.md @@ -3,7 +3,7 @@ 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] +code: [mesh-sdk, mesh-contracts, mesh-delivery, mesh-tools, mesh-host, mesh-catalog modules/zsh, mesh-catalog modules/gitea] decisions: - "ADR 0286" - "ADR 0259" @@ -50,26 +50,36 @@ and not a retry on a timer (§16), and a fourth open ask is offered as G makes i 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 +file is mode 0600, owned by the operator's account, in a directory of mode 0755, and the prepare of 2026-11-11 read +the same mode and owner on all four nodes: 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. +file; a node where the file is readable beyond the operator's account is a finding of the prepare. The forge module's +own credential (§21) lies in a directory of mode 0700, owned by the forge's account, which `node-reads` cannot enter: +no tool answers that file's own mode, a missing read tool, said here; the directory's mode is what the prepare read. ## 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`. + 0286 §1, §49 (each marking is a manifest change, reviewed like any other). Only the contract's manifest reads + (`ManifestReads`) gain `word:needs-approval`; the baseline of the engines before any stated what it reads + (`manifest_reads_baseline.txt`) is unchanged, since none of them reads the word (Q-BL). *How it is checked:* unit, a + row in `go/catalogue/invokes_test.go`'s `TestAManifestMaySayWhatItInvokes`, the nearest table over raw manifests + (Q-T): a manifest naming a tool, a seat verb and an event in `needs-approval` decodes all three with `UnknownField()` + empty, and one without it has no mark. 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. + carries (design 47 G §194) carry it on the verb. The definition format's own schema in mesh-contracts + (`format/seat-definition.schema.json`) gains a verb's `needs-approval`, a boolean (Q-SC). Every strict reader of a + seat definition reads the word from G's reader release (design 47 G §340), so no reader refuses a definition that + carries it (Q-RR). *Why:* ADR 0286 §2. *How it is checked:* unit, a fixture row in `go/seatdef/seatdef_test.go`'s + `TestADefinitionIsReadStrictlyNamingWhatIsWrong` (Q-T), and the schema validates the login shell's definition of §21. 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`) @@ -89,20 +99,24 @@ file; a node where the file is readable beyond the operator's account is a findi 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 + in `judge/rules_test.go`'s `TestRulesManifests` (Q-T), 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 + G §174) carries the validated manifest whole (design 47 G §443), its `needs-approval`, its `emits` and its streams + (their events as the manifest spells them, and whether each carries a `grant`) among it; mesh-sdk's `buscompose` + exports `WirePattern`, which a reader uses to turn a stream's events into the bus's subjects, the grants part's own + copy going (Q-WP). No part but the indexer reads a manifest: every other part reads the entry of the build concerned + from the catalog half (design 47 G §442; the operator, 2026-11-11: "only the indexer uses the manifest to build the + module catalog"; Q-MR), the grants part's `declaredFor` among them for every grant, not only the marks; 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 + applies the marks of the module's version it serves, read from the assignment half's entry for that module on its + node (design 47 G §175; Q-VS), 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 @@ -111,22 +125,28 @@ file; a node where the file is readable beyond the operator's account is a findi `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 + node's node-engine takes into the node's record; the assignment half carries, for each node, its capability readings + and the time each was taken (`at`), which the assigner keeps from the node-engine's reading (design 47 G §194; + Q-CR). 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...>`. + verb of a seat held per node `mesh.seat..tool.`, the same followed by `.`, and + `mesh.seat..call...>`; for a marked verb of a seat held once for the mesh + `mesh.seat..tool.` and `mesh.seat..call..>`, since its subjects name no node (Q-DL). 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 + readings whose `at` is 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 + subscribe deny, as `PublishDeny` is for publishing; the user list's `User` gains `subscribe_deny` beside + `publish_deny`, in its comparison and in the differences the grants part says (Q-SD). 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 + state approval-mark/1"), and in the node-engine's `composition` of the module, which reads them through a read verb + `withheld` that the seat `mesh-grants`'s definition gains (input `node`; answer each module's withheld subjects and + why), never from the grants part's state (Q-NE). *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 @@ -148,7 +168,7 @@ file; a node where the file is readable beyond the operator's account is a findi 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 + `tool`, or `seat` and `verb`, `build` (the module's fingerprint, from its catalog entry of the version served; Q-BD), 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 @@ -164,9 +184,11 @@ file; a node where the file is readable beyond the operator's account is a findi 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 + `operator`; `expires` 60 minutes after; `on-expiry` "nothing runs; call again to ask anew", since the client's + `Check` refuses an authorising ask without it, and no option is taken on expiry (ADR 0286 §22; Q-OC2); 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 + "Run ?" when that is longer than the router allows (`HeadlineLength`, 60), and, when that too is longer, + the names cut with "…" to fit (Q-HL), 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 @@ -193,17 +215,19 @@ file; a node where the file is readable beyond the operator's account is a findi ## The runner's record, and the same call again 13. **The runner keeps its asks and claims in a state of its own, per machine.** The tool runner's manifest asks in - `requires` for a state `approvals`, template `state`, `per-machine`, `ttl-seconds` 172,800 (48 hours), cap 64 MiB. + `requires` for a state `approvals`, template `state`, `per-machine`, `ttl-seconds` 172,800 (48 hours), and no cap of + its own: the `state` template's fixed size holds (ADR 0595 §2; Q-CAP). Its keys: `.ask.`, the asked call as the client keeps it (the ask, the act, the whole arguments, when - asked, whether acted on, and `recorded` once `warranted` answered, §16), and `.act.`, the id of the + asked, whether acted on, and `recorded` once `warranted` answered, §16; the client's `Kept` gains `act`, `arguments` + and `recorded` for it, Q-KP), and `.act.`, the id of the newest ask for that act. The runner on each machine is the one writer of its machine's keys; a key is deleted by its time to live, never by hand. A per-machine state reaches today only the key named for its machine; mesh-sdk's per-machine form and the grants part's `stateAccess` gain the keys below it, `.>`, for every per-machine state, a bundle's membership (`stateIssuedFor`, ADR 0260 §18) too, in the same form: one more subject per per-machine state on each account, and every module's per-machine keys keep their form; mesh-sdk's rebuild of it - rides in G's group. The cap is the bucket's, shared by every machine's keys. A write the bus refuses (the cap + rides in G's group. That size is the bucket's, shared by every machine's keys. A write the bus refuses (the size reached, or any other) fails closed under §19 and is said in the answer; `mesh_approvals` reports the bucket's fill - against its cap, saying that the fill and the cap are every machine's together. A run still unrecorded (§16) an hour before its key's 48 hours end is written in the runner's log + against the template's size, saying that both are every machine's together. A run still unrecorded (§16) an hour before its key's 48 hours end is written in the runner's log as an error naming the ask; this is written only while the runner runs, so a machine asleep through that hour keeps only the `call-unanswered` history of §16, which is accepted. *Why:* ADR 0286 §24, §30 kept the waiting call in the runner's record and claimed a warrant in the node's own file, @@ -211,12 +235,13 @@ file; a node where the file is readable beyond the operator's account is a findi compare-and-set in a module's bus state (ADR 0259 §10, ADR 0627 §1), and a per-machine state (ADR 0595) is one writer per machine, so the file is not built. The keys below the machine's own depart from ADR 0260 §16 (a per-machine state reaches only its machine's key), the design's choice following the operator's answers of - 2026-11-11 (the gate built in G); the cap and the key form are the design's choices too. *How it is + 2026-11-11 (the gate built in G); the key form is the design's choice too. *How it is checked:* unit, the manifest test of `node-tools` holds the entry; the grants part's composition table holds the `.>` reach and no other node's; integration, §22's whole test reads the keys. 14. **The same call again is judged on the router's record.** When a call of a marked tool comes and `.act.` names an ask, the runner reads the router's own record of that ask under the module's name (`Client.Where`, the - seat state `asks`, key `.`), never a record of its own or the caller's; it composes the ask again from + seat state `asks`, key `.`, whose value in operator-channel's definition gains the `warrant` it already + carries, so the definition names what `Where` reads; Q-OC1), never a record of its own or the caller's; it composes the ask again from the act and holds the warrant to it with `operatorchannelclient.For` (the same asker and ask, the ask's digest, a person's choice, an option offered, at its level, before it expired); it requires the option to be `approve` and the act to be the one it bound (`operatorchannelclient.Performs`: the same node, build and every argument); and it @@ -231,7 +256,7 @@ file; a node where the file is readable beyond the operator's account is a findi 16. **The call runs, and its warrant is recorded, across a restart too.** Once the checks pass, the runner runs the call and answers with the tool's own answer and a line naming the approval ("run on the operator's approval via at