Write the operator's answers on the approval mark into milestone 7

Call again stands by the operator's choice; the credential file's reach is
a known limit, as G's shared account is; and the forge's two loosening acts
move to a marked tool of their own, so tightening stays unasked.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 02:48:10 +01:00
parent a6c9b96be3
commit 636b2be7ac
@@ -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]
code: [mesh-sdk, mesh-contracts, mesh-delivery, mesh-tools, mesh-catalog modules/zsh, mesh-catalog modules/gitea]
decisions:
- "ADR 0286"
- "ADR 0259"
@@ -35,8 +35,8 @@ 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. Nothing is built in a controller: no controller issues memberships today, bus permissions are the grants part's
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).
@@ -46,13 +46,16 @@ digest is Go alone (§8), the claim is kept in the runner's bus state and not a
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.** 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: its account may subscribe a marked subject on a machine whose runner honours the mark. Who
may read that file is therefore the bound of the operator's choice; the prepare reads its mode on every machine with
`<node>/node-reads.stat` and writes it in the milestone file, and a mode readable beyond the operator's account is a
design question for the operator before develop. The same limit as design 47 G §385 (one tool-runner account per
machine).
**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. The
prepare reads the file's mode and owner on every node with `<node>/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
@@ -70,14 +73,19 @@ machine).
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 and declares a resource of
type `process`, whose tools its own program answers and no tool runner gates; or names in `needs-approval` one name
that is both one of its tools and one of its events. Each refusal names the entry and the rule. *Why:* a mark
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, and a mark naming none is its finding
`<node>.<module>.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
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). *How it is checked:* unit, one row per refusal in `judge/rules_test.go`, and a row that
accepts the zsh module's manifest of §21.
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
@@ -156,9 +164,9 @@ machine).
run, asked; not yet taken by the router (ask <id>, fingerprint …); make the same call again later." The ask stays
in the seat's stream of accepts, a work queue the router takes it from when it runs, and the `act.` key stays, so
the next call reads the router's record of the same ask and asks nothing new. A mark added since the caller last
called is named in the answer ("marked since <revision>"). *Why:* ADR 0286 §14, §37. The caller calls again: the
held call the operator asked for on 2026-10-16 (issue 417), which runs on the Approve and returns its outcome, is
with the operator; this line is written for "call again" until the operator answers. *How it is checked:* unit,
called is named in the answer ("marked since <revision>"). *Why:* ADR 0286 §14, §37; the caller calls again, as
the operator chose on 2026-11-11 ("Call again (as designed)") over a held call that runs on the Approve and returns
its outcome (issue 417, 2026-10-16). *How it is checked:* unit,
rows of §22's table hold both answers; a row with no router holds that a second call asks nothing new.
## The runner's record, and the same call again
@@ -249,7 +257,17 @@ machine).
the seat, gains `operator-channel` in `uses`. The control-node keeps withholding `execute`, and whether it serves
it again under the mark stays the operator's setting. *Why:* ADR 0286 §44, §45: the reads it carried have their
tools on every node (the seat `node-reads`, ADR 0310; issue 418 resolved 2026-10-12, answering on every node since
2026-10-16). No other tool or verb is marked in this milestone. *How it is checked:* live, as the acceptance says.
2026-10-16). *How it is checked:* live, as the acceptance says.
- **The forge's two acts on a trunk's protection are marked.** The forge module (mesh-catalog `modules/gitea`)
gains a tool `gitea_branch_protection_loosen`, marked in its `needs-approval`, which alone performs the two acts
`gitea_branch_protection_set_full` refuses today: removing a trunk's protection, and allowing a force-push onto
a trunk; `gitea_branch_protection_set_full` keeps refusing both and its refusal names the new tool. The manifest
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.
## Tests, delivery and going back