diff --git a/00-META/glossary.md b/00-META/glossary.md index b3eac862..e49d56cd 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -527,6 +527,10 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272. effect only on the operator's warrant, bound to the exact thing shown: a shared instruction section (ADR 0274), a trusted setting's layer ([ADR 0277](../02-DECISIONS/0277-a-trusted-setting-is-proposed-through-a-verb-and-set-on-the-operators-warrant-and-a-modules-own-secret-is-asked-for-and-typed-at-the-desk.md)). A proposal changes nothing by itself; declined, expired or refused, it is discarded. +- **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](../02-DECISIONS/0286-a-tool-or-verb-marked-in-its-manifest-runs-only-on-the-operators-approval-of-that-one-call-with-its-exact-arguments.md)). - **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/01-RESEARCH/039-a-tool-that-needs-a-persons-approval/00-overview.md b/01-RESEARCH/039-a-tool-that-needs-a-persons-approval/00-overview.md index 1ca91419..ed45259e 100644 --- a/01-RESEARCH/039-a-tool-that-needs-a-persons-approval/00-overview.md +++ b/01-RESEARCH/039-a-tool-that-needs-a-persons-approval/00-overview.md @@ -1,8 +1,10 @@ --- -status: active +status: graduated initiated: 2026-10-08 touches: [] -became: [] +became: + - 02-DECISIONS/0286-a-tool-or-verb-marked-in-its-manifest-runs-only-on-the-operators-approval-of-that-one-call-with-its-exact-arguments.md + - 03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md --- # 039 — A tool or verb that needs a person's approval diff --git a/02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md b/02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md index e92ded70..195c7c3a 100644 --- a/02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md +++ b/02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md @@ -278,6 +278,11 @@ met, and when. It carries no secret. chose an act, not that only the operator could have caused it. Said here so no reader takes the warrant for that second guarantee. + > **The mechanism changed — 2026-10-10, by [ADR 0286](0286-a-tool-or-verb-marked-in-its-manifest-runs-only-on-the-operators-approval-of-that-one-call-with-its-exact-arguments.md).** A tool or verb marked `needs-approval` in its manifest now runs + > only on a warrant for that one call, with its exact arguments, checked by the tool runner that serves it (or + > by the controller, for its own verbs). For a marked act the warrant is the only way; for the others this + > bullet stands. `mesh-delivery.release` is among the first marked. + ### 7. Who the operator is on a channel: linking - **An answer from an account that is not the operator's** is refused, and the operator is told on the linked diff --git a/02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md b/02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md index 2efc5c0c..5ce0cf78 100644 --- a/02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md +++ b/02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md @@ -321,6 +321,10 @@ found around it and are closed on the mesh's main line: was not measured; where one does, this is also a way back to root on the control node. ADR 0268 §4 closes it once a call to such a tool needs a person's approval (research 039); until then, withholding `execute` on every node is the operator's choice, at the terminal. + + > **The mechanism changed — 2026-10-10, by [ADR 0286](0286-a-tool-or-verb-marked-in-its-manifest-runs-only-on-the-operators-approval-of-that-one-call-with-its-exact-arguments.md).** A call to a tool marked `needs-approval` runs only on the + > operator's warrant for that call. `execute` is marked as ADR 0268 §4 decided; the desk's two tools are split + > into a form that runs a command, which is marked, and one that does not, as issue 417 tracks. - **Tool servers already on the bus** keep rendering until their own change lands (issue 340, "still open"). Text written straight into the bus state needs a tool runner's credential, which the agent account cannot read. diff --git a/02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md b/02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md index 0a3d68a2..1e51355d 100644 --- a/02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md +++ b/02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md @@ -79,6 +79,11 @@ Option 4. or verb declares that a call needs a person's approval, and the mesh refuses the call without a warrant from ADR 0259's flow. The control-node may then serve `execute` again, under the mark. The other nodes keep it unmarked until then. + + > **The mechanism changed — 2026-10-10, by [ADR 0286](0286-a-tool-or-verb-marked-in-its-manifest-runs-only-on-the-operators-approval-of-that-one-call-with-its-exact-arguments.md).** The mark exists as a decision: `needs-approval`, set on + > `execute` in the seat's definition, enforced by the tool runner of each node. It is set once the reads of §5 + > that have no tool yet (a file, an account) have one on every node, which issue 418 tracks. What §4 decided + > stands. 5. **Narrow read tools replace the common reads first,** each a tool of the module that owns the area, so that an approval is asked for an act and not for a read. Otherwise nearly every approval is for a read, the operator learns to approve without reading, and approval is worth nothing. The reads diff --git a/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md index e428bf15..20dfa106 100644 --- a/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md +++ b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md @@ -76,6 +76,9 @@ the original. ([ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md), ADR 0259). Not now. It is the route for a node whose agents must share the operator's account, and it waits for the approval flow (research 039). + > **The mechanism changed — 2026-10-10, by [ADR 0286](0286-a-tool-or-verb-marked-in-its-manifest-runs-only-on-the-operators-approval-of-that-one-call-with-its-exact-arguments.md).** The approval flow for one call exists as a decision: a + > marked tool or verb runs only on the operator's warrant for that call. This option's choice stands. + ### Which account on which node is the terminal 1. **The operator's account on any node.** Rejected. On every node but the control-node, and on the control-node diff --git a/02-DECISIONS/0286-a-tool-or-verb-marked-in-its-manifest-runs-only-on-the-operators-approval-of-that-one-call-with-its-exact-arguments.md b/02-DECISIONS/0286-a-tool-or-verb-marked-in-its-manifest-runs-only-on-the-operators-approval-of-that-one-call-with-its-exact-arguments.md new file mode 100644 index 00000000..2f326fd4 --- /dev/null +++ b/02-DECISIONS/0286-a-tool-or-verb-marked-in-its-manifest-runs-only-on-the-operators-approval-of-that-one-call-with-its-exact-arguments.md @@ -0,0 +1,298 @@ +--- +topic: what runs on it +status: accepted +date: 2026-10-10 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md +--- + +# 286. A tool or verb marked in its manifest runs only on the operator's approval of that one call, with its exact arguments + +## Context + +[Research 039](../01-RESEARCH/039-a-tool-that-needs-a-persons-approval/00-overview.md) asked whether a module's +tool or a seat's verb can declare that calling it needs a person's approval, so that the mesh refuses the call +without one. The operator's words for this decision, 2026-10-10: "risky agent actions ask them first on +Telegram, showing exactly what will be done, and a yes covers that one action only." + +What the mesh has, on 2026-10-10: + +- **Nothing refuses a call for want of a person.** Any tool runner may call any tool on any node + ([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)), and the runner + cannot tell who called: an agent session and the operator reach the same address the same way + ([issue 340](../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md)). + [ADR 0259](0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md) §6 + says it outright: the acts a warrant authorises are still verbs an agent may call directly, "until research + 039 … withholds them from every caller without a warrant". Some modules end a tool's description with "(a)" + for one that acts and "(r)" for one that reads; that is prose, and nothing reads it. +- **The login shell's `execute` is root on a node in one call.** It runs any command as the operator account, + which has passwordless sudo. [ADR 0268](0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md) + withholds it on the control-node until a call to it needs a person's approval, and keeps it on the other + three nodes, where `mesh_search` finds it on 2026-10-10. [ADR 0266](0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md) + names it, with the desk's `node-terminal-emulator.open` and `rofi_run`, as the way an agent on the control-node + still becomes root on another node. +- **The path from an ask to a warrant is built and used.** The router (`operator-channel.asks`) held nine asks + on 2026-10-10: eight chosen "the operator, via telegram (user id verified), chose Approve", five of them the + agent module's instruction proposals ([ADR 0274](0274-an-agent-proposes-a-shared-instruction-section-and-every-node-takes-it-only-on-the-operators-warrant-which-it-proves-itself.md)) + and three the controller's settings proposals ([ADR 0277](0277-a-trusted-setting-is-proposed-through-a-verb-and-set-on-the-operators-warrant-and-a-modules-own-secret-is-asked-for-and-typed-at-the-desk.md)), + the agent module's five shown as "Part shown". The shared library (mesh-sdk, Go) holds the parts an asker needs: an + option's `binds`, the digest of an **act** over named fields in a canonical encoding (`ActDigest`), a + warrant's check against the ask the asker kept (`Warrant.For`), and the check that the act about to be + performed is the one bound (`Option.Performs`). The controller's verb `warranted` records a warrant in the + hand-act log from the router's own record. The tool runner is TypeScript, and the digest exists in Go only. +- **The reads `execute` carried.** ADR 0268 §5 lists them. On 2026-10-10 `mesh_search` finds a tool for a + container's logs (`docker_logs`, on all four nodes), a unit's state and journal (`node-service-manager`), + the listening sockets (`netcheck_listening`) and the hosts entries (`node-hostname.entries`), and none for + reading or searching a file, and none for looking up an account. Issue 418, in the issue tracker, was opened + for those two. + +The record was searched first ("tool needs a person's approval warrant call refused", "approval"): research +039, ADRs 0259, 0266, 0268, 0272, 0274 and 0277, and nothing that marks a tool. + +### Against GENESIS + +- **Effect — "when something genuinely needs a decision, you are asked — with the context, not a log line."** + The call that needs the operator is put to them as what it will do, on the phone, and only that call. +- **Failure must be loud.** A call refused for want of an approval says so, and says it did nothing. An + approval nothing can carry is refused at once, never left waiting. +- **The mesh owns the truth.** The mark is the tool's own declaration in the catalogue, issued to the runner by + the controller; a module's code cannot drop it. + +## Considered Options + +### Where the call is refused + +1. **In the MCP client** (a permission prompt in the agent's session). Rejected: a client is no boundary, since + any tool runner may call any tool, and an agent controls its own client. +2. **In each module's own code**, as ADR 0274 and ADR 0277 do for their proposals. Rejected as the general way: + every module would rebuild the same check, and a module that forgets is a hole nobody sees. Those two stay + as they are, because their act is not a call but a proposal kept until approved. +3. **In the tool runner that serves the call, for every module it carries, and in the controller for its own + verbs, with one shared check.** Chosen. Every call to a module's tool or a node seat's verb passes through + the runner on the node that serves it (ADR 0175). + +### What one approval covers + +1. **One agent session, or a span of time.** Rejected by the operator's words: "a yes covers that one action + only". A wider approval is fewer taps and more reach for whoever holds the session. +2. **One call, with its exact arguments, on one node, once, soon after the choice.** Chosen. + +### Per tool, or per argument + +1. **The mark on an argument's value**, a rule in the manifest saying which values need a person. Rejected: a + rule written wrong lets harm through unasked, and nobody reads it on the phone. +2. **The tool decides from its arguments.** Rejected: the module's code runs before the check, which is what + the mark exists to stop. +3. **The mark is on the tool or the verb, and covers every call to it.** Chosen. A tool that is harmless for + some arguments and not for others is split by its module into two tools, and the second is marked. + +### How the call waits + +1. **The call blocks until the operator answers.** Rejected: an answer can take an hour, longer than any client + waits, and a call held open holds the runner. +2. **The runner performs the call itself on hearing the warrant**, as ADR 0274's nodes do. Rejected for a call: + the caller wants the call's answer, and nobody would receive it. +3. **The call answers at once that it was asked and did nothing; the same call made again after the approval + runs, once.** Chosen. + +### Who asks + +1. **The tool runner under a name of its own.** Rejected: a new asker, a new grant, and an ask that names the + runner rather than the module whose act it is. +2. **The module whose tool is called, through the runner that carries it.** Chosen. ADR 0259 §3 already grants + a runner the asks of each module it carries that uses the `operator-channel` seat. For its own verbs the + controller asks as `mesh-controller`, as it does today. + +### What the operator reads + +1. **Every argument whole, always.** Rejected: the router's content rule refuses a path, an address or a + secret's shape in anything that leaves the mesh, and a command often carries a path. +2. **A fingerprint only.** Rejected: nobody approves what they cannot read. +3. **Every argument whole where every part of it may leave the mesh; else its shape with markers, the + fingerprint, and where it is read whole.** Chosen, as ADR 0277 §2 shows a setting. + +## Decision + +### 1. The approval mark + +A module's manifest gains **`needs-approval`**: a list of its tools, and of the seat verbs it serves, each of +whose calls needs the operator's approval. A seat's definition may mark one of its verbs the same way, and then +every holder's verb is marked. The marks add up: a holder cannot unmark a verb its seat marks. + +The controller refuses a manifest whose mark names a tool or verb the module does not serve. It issues the +mark in the membership it gives each tool runner for the module, and **the runner reads the mark there and +never from the bundle**, so a bundle cannot drop it. A module with a mark is composed as a user of the +`operator-channel` seat, so its grants to ask, to hear its warrants and to read its own asks are the ones ADR +0259 §3 already composes. No new kind of grant. + +**A runner that does not honour the mark is not issued a marked tool.** A runner declares that it honours it +(`approval-mark/1`) when it joins; on a node whose runner does not, the controller withholds every marked tool +and verb, and the runner's status names them. A node is never left serving a marked tool unasked because its +runner is older than this record. + +### 2. The act that is approved + +The call is turned into an **act** (ADR 0259 §6), digested with the shared library's `ActDigest`: + +- `address`, the address as called; `node`, the node that serves it; `module` and `tool` (or `seat` and + `verb`); +- `build`, the digest of the bundle the runner serves it from, so a call approved before the module moved is + asked again rather than run by code the operator did not approve; +- `arg.` for each argument given: a string as itself, any other value as its JSON in RFC 8785's canonical + form. An argument left out is not in the act, so the same call made again must leave it out too. + +### 3. The first call asks, and does nothing + +On a call to a marked tool with no approval for its act, the runner: + +1. refuses at once, asking nothing, a call whose arguments hold anything shaped like a secret (a secret never + goes to a phone; a marked tool takes its secrets another way), or whose arguments are more than 16 KiB; +2. publishes **one ask** under the module's name, unless an open ask for the same act exists, in which case it + asks nothing new; +3. waits up to 5 seconds for the router's word, as ADR 0274 §5 (its note of 2026-10-10) has it, and answers the call. + +**The ask:** + +- **headline**: "Run on ?", shortened to "Run ?" when the names make it longer than the router + allows, and ending "Part shown" whenever the phone does not show every argument whole; +- **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; then **every argument, one per line, "name: value"**, whole where + every part of it may leave the mesh, else in its shape with ‹path›, ‹address› or ‹withheld› in place of what + may not (ADR 0277 §2); the act's **fingerprint** (the first 24 hexadecimal digits of its digest, in groups of + four); and, when a part is not shown, where the whole is read: the runner's tool `approvals` on that node, + through the mesh MCP server or `mesh-cli`, whose fingerprint must be the one on the phone. 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"; +- **two options, Approve and Decline, both at the level approve**, so only a channel that proves who answered + carries either (Telegram today; the desk answers only `acknowledge`, + [ADR 0258](0258-a-desk-click-performs-only-an-acknowledgement-and-an-answer-is-a-one-time-ticket.md)). Each + binds the act's digest and its answer; +- **who may answer**: the operator, the only role; +- **expiry**: 60 minutes after it is asked. It never defaults; +- **about**: the act's digest, so the same call made again while it is open asks nothing new. + +The runner keeps the waiting call (the ask's id, the act and its whole arguments) in its own record, and its +tool **`approvals`** lists the calls asked on that node in the last day, newest first, each with its state and +fingerprint, and given an ask's id, its whole arguments and the act's digest. + +### 4. The same call again runs, once, on the warrant + +When a call to a marked tool comes and its act has an ask that was chosen, the runner, before anything runs: + +1. reads **the router's own record** of the ask, under the module's name (ADR 0259 §3), never a record of its + own or the caller's; +2. composes the ask again from the act and holds the warrant to it with the shared check (`Warrant.For`): the + same asker and ask, the ask's digest the router took, a person's choice, an option the ask offered, at its + level, before the ask expired; +3. requires the option chosen to be **Approve**, and the act about to be performed to be the one it bound + (`Option.Performs`): the same node, build and every argument; +4. requires the call to come **within 30 minutes of the choice**; +5. claims the ask as used in **the node's own file**, written whole and synced to the disk before it replaces + the old one, and kept for 48 hours. A warrant claimed before is refused, so a call runs once however often it + is made, and a warrant replayed into the bus runs nothing. + +Then it runs the call, answers with the tool's own answer and a line naming the approval ("run on the operator's +approval via telegram at 19:21, ask "), and asks the controller's `warranted` to record it in the hand-act +log, again every ten minutes for a week while the controller has not taken it (ADR 0274 §6). + +A record of the runner's that points at another ask, or a waiting call written into the bus by anyone else, +runs nothing: every check above is against the router's record and the call actually made. + +### 5. What the caller is told + +The call that does not run says so first, as an error, never as a result: "Not run: this needs the operator's +approval. Asked on telegram at 19:21 (ask , fingerprint …); make the same call again once it is approved, +before 20:21." Then, for the same call made again: still waiting; declined (the same act is not asked again for +10 minutes, and the call says so); expired; approved too long ago (asked again); already run; or refused, with +the router's words. `operator-channel.asks` shows the same ask. No answer ever reads as done when nothing ran. + +### 6. Fail closed + +The call is refused, runs nothing and says why, when: no module holds the operator channel; the router refuses +the ask (no ready channel can carry an approval, or the module already has three open asks); the ask cannot be +published; the router's record cannot be read; the node's own file cannot be read, in which case no marked call +runs on that node until it can; or the mark cannot be read from the membership. + +**The controller's terminal is not exempt.** A marked tool called through `mesh-cli` asks like any other call, +because the runner cannot tell who called. A call refused by any check here is recorded in the runner's log +with the ask's id and fingerprint and never the arguments, which may hold what may not be logged. + +### 7. The controller's own verbs + +The controller serves its own verbs and enforces the same mark on them, with the same shared check, asking as +`mesh-controller` and claiming each warrant once in its own record. Its verbs are marked in its own manifest. + +### 8. What is marked first + +- **`node-login-shell.execute`, on every node, in the seat's definition**, as ADR 0268 §4 decided, once the reads + of ADR 0268 §5 that have no tool yet (reading or searching a file, looking up an account) have one on every + node, tracked by issue 418. Until then the control-node keeps withholding it and the others serve it unmarked, + as ADR 0268 has it. Whether the control-node then serves it again, under the mark, stays the operator's + setting. +- **`mesh-delivery.release`**, the glossary's "a person letting a held delivery go on". The controller's own + Release on a condition already goes through an ask, so the operator sees no new kind of question. + +The rest of research 039's candidates, and the split of `rofi_run` and `node-terminal-emulator.open` into a +form that opens an entry or a window and a form that runs a command, which is marked, are tracked by issue 417. +Each is a change to its module's manifest, reviewed like any other. + +### 9. What this changes in the earlier records + +- **ADR 0259 §6**, "a warrant is not yet the only way to these acts": for a marked tool or verb it is now the + only way. The others stay as §6 says. +- **ADR 0268 §4**: the mark is this record's; §4 holds as decided, with §8 above saying when. +- **ADR 0266**, "what still remains": the remote path to root through `execute` closes on each node as the mark + reaches it; the desk's two tools close with their split. +- **ADR 0272**, the second factor (its option 4): a per-call approval on a verified channel now exists to build + that route on. Which node is the terminal is unchanged. + +## Consequences + +- An agent can still call anything, and a marked call does nothing until the operator taps Approve on the phone, + having read the tool, the node and every argument, or its shape and fingerprint. The tap runs that call once. +- A call the module, the controller, the bus or a replayed record could have caused alone now needs the router's + record of the operator's choice. +- **What got harder:** + - an agent that needs a marked tool waits for the operator, and makes the call twice; + - a call whose arguments carry a path is shown on the phone in shape only, with "Part shown"; reading exactly + what will run then means `approvals` at a terminal or the desk. "Showing exactly what will be done" holds + on the phone only for arguments that may leave the mesh; + - each module with a mark asks against its own three open asks, so a fourth waiting call from one module is + refused until one is answered; + - a tool whose risk depends on its arguments is split into two tools; + - a node whose runner predates the mark does not serve marked tools until its runner is updated; + - the tool runner, which is TypeScript, needs the act's digest, which exists in Go only, so the shared + library carries it in both, held to the same vectors. + +## How it is checked + +| Rule | Checked by | +|---|---| +| A mark naming nothing served is refused; the mark reaches the runner from the membership | the controller's tests: a manifest marking an unknown tool is refused; a seat's mark reaches every holder's membership; the membership carries it whatever the bundle lists | +| A runner that does not honour the mark is not issued a marked tool | the controller's test: a membership for a runner without `approval-mark/1` leaves out the marked tool and names it | +| The first call asks once and runs nothing | the runner's test: a marked tool's bundle is not called; one ask is published under the module's name, passing `Check`, Approve and Decline both at approve, each binding the act's digest; the same call again asks nothing new; the answer is an error saying "Not run" | +| The same call runs once on Approve, and only that call | the runner's test against a real bus: after the router's record says Approve, the call runs once; again, it is refused as already run; with one argument changed, another build, another node, after 30 minutes, on Decline, at the level acknowledge, nobody choosing, another ask's warrant, or a record of the runner's pointing at another ask, nothing runs | +| A warrant replayed or a file lost runs nothing | the runner's test: a used claim kept across a restart; an unreadable file refuses every marked call | +| Fail closed | the runner's test: no holder of the operator channel, the router's refusal, a fourth ask, a secret in an argument and more than 16 KiB are each refused, nothing asked or run where so stated, each said | +| The digest is the same in both languages | the shared library's conformance vectors, run in Go and TypeScript | +| The controller's verbs | the controller's test: a marked verb of its own runs only on a warrant, once | +| The arguments are never logged | the runner's test: its log over an ask and a run holds the ask's id and fingerprint and no argument | +| Live, after rollout | an agent calls a marked tool on a node; the phone shows "Run on ?" with the arguments and Approve and Decline; the call made again after Approve runs once, a third call is refused, and `hand-acts` shows the warrant | + +## References + +- [Research 039](../01-RESEARCH/039-a-tool-that-needs-a-persons-approval/00-overview.md) — the effort this + graduates +- [ADR 0259](0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md) — the + ask, the warrant, the act's digest +- [ADR 0274](0274-an-agent-proposes-a-shared-instruction-section-and-every-node-takes-it-only-on-the-operators-warrant-which-it-proves-itself.md), + [ADR 0277](0277-a-trusted-setting-is-proposed-through-a-verb-and-set-on-the-operators-warrant-and-a-modules-own-secret-is-asked-for-and-typed-at-the-desk.md) + — the proposals this reuses: the shape shown, the fingerprint, the warrant proven from the router's record, + the `warranted` verb +- [ADR 0266](0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md), + [ADR 0268](0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md), + [ADR 0272](0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md) + — the records that wait on this one +- [to-be 46](../03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md) §10 — the design +- Issue 417 (the build) and issue 418 (the reads that come first), in the issue tracker diff --git a/03-DESIGN/00-as-is/13-the-console.md b/03-DESIGN/00-as-is/13-the-console.md index 9d2e8564..d68f484f 100644 --- a/03-DESIGN/00-as-is/13-the-console.md +++ b/03-DESIGN/00-as-is/13-the-console.md @@ -2,7 +2,7 @@ layer: as-is status: implemented code: [mesh-catalog modules/mesh-console, mesh-tools src/mesh.ts, mesh-tools src/http.ts, mesh-tools src/runtime.ts, mesh-controller internal/broker, mesh-controller cmd/mesh-controller/check.go, mesh-controller cmd/mesh-controller/seatverbs.go] -updated: 2026-09-30 +updated: 2026-10-10 decisions: - 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md - 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md @@ -31,6 +31,10 @@ mesh records rather than rolls out — and 62 tools from the rest. `tools/call` reaches any tool by `.`, listed or not. The mesh MCP server's grant is `*`, so what it may call is every tool on the mesh; its account may publish nothing else and subscribes nothing. +No call is refused for want of a person's approval: a tool runner serves every call it is issued, whoever made +it, and cannot tell an agent from the operator (ADR 0259 §6, issue 340). Only the agent module's instruction +proposals and the controller's settings proposals wait for the operator's warrant, each in its own code. + ## The mesh's own verbs *Since 2026-09-30 evening ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).* diff --git a/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md b/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md index 705fa4f3..790a1b09 100644 --- a/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md +++ b/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md @@ -1,9 +1,10 @@ --- layer: to-be -status: designed -code: [] +status: in-progress +code: [mesh-controller, mesh-tools, mesh-sdk, mesh-catalog] updated: 2026-10-10 decisions: + - 02-DECISIONS/0286-a-tool-or-verb-marked-in-its-manifest-runs-only-on-the-operators-approval-of-that-one-call-with-its-exact-arguments.md - 02-DECISIONS/0277-a-trusted-setting-is-proposed-through-a-verb-and-set-on-the-operators-warrant-and-a-modules-own-secret-is-asked-for-and-typed-at-the-desk.md - 02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md - 02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md @@ -444,6 +445,40 @@ announced on every channel as break-glass. `retire approve` takes **`expect`**, and refuses if the set now differs. `conditions silence` stays callable; a silence an agent sets is said on the away channel, with its why. +### A call that needs the operator's approval + +*Added 2026-10-10, [ADR 0286](../../02-DECISIONS/0286-a-tool-or-verb-marked-in-its-manifest-runs-only-on-the-operators-approval-of-that-one-call-with-its-exact-arguments.md).* +A module's manifest marks a tool, or a seat verb it serves, as **`needs-approval`**; a seat's definition may mark +a verb for every holder. The controller issues the mark in the runner's membership, never from the bundle, and +withholds a marked tool from a runner that does not declare `approval-mark/1`. + +``` + agent ──call──► tool runner (serving node) ──ask, under the module's name──► router ──► Telegram + ▲ │ "Not run: asked, ask " │ Approve + │ ▼ ▼ + └──same call──► runner reads the router's record ◄────────────── warrant (decided.) + │ Warrant.For + Option.Performs on the act; within 30 min; claimed once + ▼ + runs the call once ──► mesh-controller.warranted (hand-act log) +``` + +- **The act** is the address, the node, the module and tool, the bundle's build and every argument given, digested + with the shared library's `ActDigest` (in Go and TypeScript, one set of vectors). +- **The first call asks and does nothing.** One ask per act (its `about` is the act's digest): "Run on + ?", the tool's first line, every argument whole where it may leave the mesh or in its shape with + ‹path›, ‹address› or ‹withheld› and "Part shown", the fingerprint, and where the whole is read (the runner's + `approvals` on that node). Approve and Decline, both at approve; expiry 60 minutes; never a default. The call + answers as an error: "Not run", with the ask's id and until when. +- **The same call again** runs once on Approve, read from the router's record, within 30 minutes of the choice, + claimed in the node's own file (kept 48 hours); then `warranted` records the hand-act. Anything else is + refused and said: waiting, declined (not asked again for 10 minutes), expired, already run, or the router's + refusal. +- **Fail closed:** a secret's shape in an argument or more than 16 KiB of arguments, no operator channel, a + fourth open ask, an unpublished ask, an unreadable router record, node file or mark: nothing runs. +- **The controller** enforces the same mark on its own verbs, asking as `mesh-controller`. +- **Marked first:** `node-login-shell.execute` on every node once the reads it carried have tools (issue 418), + and `mesh-delivery.release`. + ### The hand-act Gains **`via`** (kind and holder), **`requested-by`** (agent principal or condition key), **`ask`** (the @@ -562,6 +597,7 @@ Each phase ends at its own *done when*. Owning repositories from [`repos.md`](.. | **5a — Asks on the bus** (ADR 0259) | the kinded bench, caller-named verbs, `proof` subjects and the records users read (controller); a bundle's seat traffic, compare-and-set state and its own account (tool runner); the asker's and channel's types (shared library); the router's asks, tokens, identities, judging, warrants, expiry and linking with its hour (messenger); `telegram` and `desk-channel`; the controller asking for its conditions' actions and acting on warrants | mesh-controller, mesh-tools, mesh-sdk, mesh-catalog | a held delivery released from the phone; the hand-act reads "the operator, via telegram, user id verified"; a desk Silence goes through an ask | | **5 — Authorise, with TOTP** | `authorises` in the verb table; the router's checks for the code; direct calls refused, break-glass; `retire approve` takes `expect`; the hand-act fields; the TOTP seed as the router's state, enrolled at a terminal with ten recovery codes; `factor recover`, `factor status`, the local-only break-glass enrolment; no P2 tier before recovery codes exist; optional key enrolment; the self-check probe for the away channel; agents' grants lose the authorising verbs | mesh-controller; mesh-catalog (agents' grants, the desk's code prompt) | one controller test per refusal passes; at the desk, approve needs a code; a recovery drill re-enrols with a recovery code, and is announced | | **6 — Telegram live** | the `telegram` module (built in 5a) with replies and forced-reply codes, under its own account on the control node; the operator's two bots configured; the watcher assigned off the control node; the dead-man ping | mesh-catalog | the self-check says the away channel carries every tier; rehearsals: an approve and a destroy answered on the phone, with the hand-acts read | +| **7 — A call that needs approval** (ADR 0286) | `needs-approval` in the manifest and the seat set, issued in the membership, withheld from a runner without `approval-mark/1` (controller); the act's digest in TypeScript beside Go, with shared vectors (shared library); the check, the ask, the runner's `approvals`, the node's used file and `warranted` (tool runner); the controller's own verbs; `execute` and `mesh-delivery.release` marked (catalogue, the controller's seat set) | mesh-controller, mesh-sdk, mesh-tools, mesh-catalog | a marked call on a node is asked on the phone, does nothing, runs once when made again after Approve, is refused the third time, and `hand-acts` shows the warrant | **Switching phase 5a on: the operator's steps, in order** (amended 2026-10-09, ADR 0259). Each line is typed with `nox` (`mesh-cli`) on the control-node, from the operator's own account, which is the controller's terminal