From bddf6ce95ad72539cccc21df176b5953536bf188 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 02:51:24 +0100 Subject: [PATCH] Answer round 2 of the domain review of the approval mark A stuck state recovers as a kept call, a node with no or stale readings is denied, the caller-named subjects are withheld too, and the per-machine key form and the client's prefix are named, so no path serves a marked tool unasked; the forge's loosening is marked by the operator's answer. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../01-to-be/34-milestones/every-event.md | 4 +- .../01-to-be/46-milestones/7-approval-mark.md | 72 +++++++++++++------ 2 files changed, 53 insertions(+), 23 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 186f8131..deae12ad 100644 --- a/03-DESIGN/01-to-be/34-milestones/every-event.md +++ b/03-DESIGN/01-to-be/34-milestones/every-event.md @@ -339,7 +339,9 @@ approves, for a marked event or a restricted stream. 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. + 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 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 6f69461d..5f97ca43 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 @@ -53,7 +53,9 @@ them, on a machine whose runner honours the mark. Read on the workstation on 202 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. 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. @@ -78,8 +80,10 @@ file; a node where the file is readable beyond the operator's account is a findi 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, and a mark naming none is its finding - `..mark-unserved` (said as §16's), cleared when the bundle serves it or the mark goes. Each refusal names the entry and the rule. *Why:* a mark + 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, 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 @@ -106,18 +110,25 @@ file; a node where the file is readable beyond the operator's account is a findi `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`, it composes a subscribe deny of every marked subject of every module - the runtime carries (`mesh.mod..tool.`, the same followed by `.*`, and for a marked verb - `mesh.seat..tool.`, the same followed by `.`), which the bus's deny outranks its allow of - `mesh.mod..tool.>` for. `Permissions` gains `SubscribeDeny`, written into the user list as the server's + 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: `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. 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. *How it is checked:* unit, the grants part's - composition table: with readings lacking the capability the deny of each marked subject is composed and named in - `withheld`, with it 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.>`. + 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 @@ -141,7 +152,9 @@ file; a node where the file is readable beyond the operator's account is a findi 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 and whose `State` is the runner's state of §13, unless an ask for the same act is + `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 it writes and reads (empty for every asker today), is + `.`; 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 ?", @@ -178,12 +191,18 @@ file; a node where the file is readable beyond the operator's account is a findi 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: one more subject per per-machine state on each account, and every module's per-machine keys keep their - form. A write the bus refuses (the cap reached, or any other) fails closed under §19 and is said in the answer. + 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 + reached, or any other) fails closed under §19 and is said in the answer; `mesh_approvals` reports the bucket's fill + against its cap. 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. *Why:* ADR 0286 §24, §30 kept the waiting call in the runner's record and claimed a warrant in the node's own file, kept 48 hours, by one writer per node; the client built since keeps asks and marks a warrant acted on by 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 cap and the key form are the design's choices. *How it is + 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 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.` @@ -237,15 +256,18 @@ file; a node where the file is readable beyond the operator's account is a findi refused, runs nothing and says why when: no module holds `operator-channel` (read from the assigner's first view `mesh-delivery-assigner.views.seat-holders`, which the tool runner consumes; the seat list names no holders); the router refuses the ask; the ask cannot be published; the router's record cannot be read; §13's state cannot be read - or written, in which case no marked call runs on that machine until it can; or the marks are not yet read. Before + or written, in which case no marked call runs on that machine until it can, the failed read or write being made + again as a kept call (ADR 0668 §1) until it succeeds; or the marks are not yet read. Before the first view of the catalog half is read, a call waits for it within its 5 seconds and is then judged, asking nothing while the marks are unread; still unread, it is refused, naming `mesh_first_views`. When §13's state has been unreadable or unwritable for five minutes, the runner raises its finding `.approvals.approvals-unreadable` - (said on `node-tools-findings` as §16's), and clears it at the next read and write that succeed. Each refusal is + (said on `node-tools-findings` as §16's), and clears it when the kept call succeeds; its `findings-stated` at a start + judges it anew from a read of the state (design 47 G §384). Each refusal is written in the runner's log with the ask's id and fingerprint, never the arguments. *Why:* ADR 0286 §40, §42; a gate that refuses every call for a reason nobody hears is a silent fault, and the finding and its five minutes are the design's choice. *How it is checked:* unit, one row of §22's table per case, the holder read from a fixture - `views.seat-holders` with none and with one, and the finding raised after five minutes and cleared. + `views.seat-holders` with none and with one; and a row where the state fails, the finding is raised after five + minutes, the state recovers with no further call, and the finding clears. 20. **The terminal is not exempt.** A marked tool called through `mesh-cli` or any caller asks as every call does: the runner cannot tell who called. *Why:* ADR 0286 §41. *How it is checked:* the code review reads that the gate sits on the call's handler, before the bundle, and that no path to a bundle passes around it. @@ -265,9 +287,9 @@ file; a node where the file is readable beyond the operator's account is a findi gains `operator-channel` in `uses`. No other tool or verb is marked in this milestone. *Why:* the forge's own refusal says these two acts are the operator's and waited for this mark (issue 417, 2026-10-28); a mark is on a whole tool (ADR 0286 §1), so the acts move to a tool of their own, as ADR 0286 §48 splits a tool into an unmarked - and a marked form, and tightening a protection stays unasked; marked now pending the operator's answer of - 2026-11-11. *How it is checked:* the forge module's tool test: `set_full` refuses both acts naming the new - tool; the new tool performs each; and live, a loosening asked on the phone runs once on the Approve. + and a marked form, and tightening a protection stays unasked; the operator, 2026-11-11: "Mark them in G". *How it + is checked:* the forge module's tool test: `set_full` refuses both acts naming the new tool; the new tool + performs each; and live, as the acceptance says. ## Tests, delivery and going back @@ -276,7 +298,8 @@ file; a node where the file is readable beyond the operator's account is a findi (`meshtest`), the router's record and the warrant written there as the router writes them; and one integration test that starts the runner against the real bus, with no store, with a fixture marked module: a call, the operator's Approve written as the router's record, the call again, the run, the hand-act, `mesh_approvals`, and a - restart between the run and `warranted`'s answer. *Why:* the operator's test budget (one whole test over many small + restart between the run and `warranted`'s answer. The client's `KeyPrefix` (§10) has a row in mesh-contracts' + client test: every key written and read carries it, and an empty prefix writes today's keys. *Why:* the operator's test budget (one whole test over many small ones). *How it is checked:* the check counts the module's tests against its budget. 23. **In G's delivery group, ordered by the module graph.** The members are pushed and checked in design 47 milestone G's group, with design 34's milestone every-event. The orders are the module graph's: the indexer's manifest @@ -298,6 +321,11 @@ file; a node where the file is readable beyond the operator's account is a findi `node-login-shell.execute` on a node other than the control-node answers "Not run", the ask is on `operator-channel.asks`, and after the operator's Approve the same call runs once, a third identical call is refused as already run, and `mesh-hand-acts.hand-acts` names the warranted act (§10, §12, §14 to §16, §21). +- **The forge's loosening is asked, and the protection is put back** (live, read by tool call): on a scratch repository + of the forge made for this acceptance, `acceptance-approval-mark`, with full protection on its trunk, + `gitea_branch_protection_loosen` removing the trunk's protection answers "Not run" and asks on the phone; after the + Approve the same call runs once; then `gitea_branch_protection_set_full` sets the full protection again and + `gitea_branch_protection_get` reads it back as it was (§21). - **No machine serves `execute` unasked** (live, read by tool call): before this acceptance passes, on every node `execute` is either asked as above or absent, read with each node's `node-engine.composition` and `mesh-grants.grants_user_list`'s `withheld` (§6).