Merge pull request 'ADR 0286: a tool or verb marked in its manifest runs only on the operator's approval of that one call (issue 417)' (#302) from feat/0286-approval-bound-call into main
This commit was merged in pull request #302.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+6
@@ -278,6 +278,12 @@ 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 not marked first: issue 417 tracks how the controller's own warrant
|
||||
> for it passes the mark.
|
||||
|
||||
### 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
|
||||
|
||||
+4
@@ -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.
|
||||
|
||||
+5
@@ -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
|
||||
|
||||
+3
@@ -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
|
||||
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
---
|
||||
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. **The bus enforces it, not the runner**: the controller leaves each
|
||||
marked tool's and verb's subject out of the bus permissions it composes for that runner, so the bus refuses the
|
||||
runner's subscription and a call there finds no responder. Where a runner's grant names a module's subjects as a
|
||||
whole today, it names a marked tool's subject apart from the rest. 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.<name>` 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 (ADR 0274 §5's note of 2026-10-10 waits a few seconds; this record
|
||||
fixes it at 5), and answers the call.
|
||||
|
||||
**The ask:**
|
||||
|
||||
- **headline**: "Run <tool> on <node>?", shortened to "Run <tool>?" 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, by one writer per node, so two calls at the same moment cannot both claim
|
||||
it. A warrant claimed before is refused, so one approval runs one call however often it is made, and a
|
||||
warrant replayed into the bus runs nothing.
|
||||
|
||||
The runner always judges **the newest ask it keeps for that act**. After "already run", "expired" or "approved
|
||||
too long ago", the next identical call publishes a new ask, and the used claim stays: running the same act a
|
||||
second time takes a second approval.
|
||||
|
||||
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 <id>"), 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 <id>, fingerprint …); make the same call again within 30 minutes of
|
||||
the approval; the ask expires at 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, or already run
|
||||
(each asks again, §4); 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 (owner: mesh-tools, or the module that owns each area). 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", is not marked first. The
|
||||
controller's Release on a condition is already an ask, and the controller then calls `release` on its warrant;
|
||||
marked as it stands, that call would ask the operator a second time for the same act. How a warrant the
|
||||
controller holds passes the mark of a verb it calls, and then marking `release`, are tracked by issue 417
|
||||
(owners: mesh-controller, mesh-tools). 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
|
||||
(owners: mesh-catalog for the modules, mesh-controller for the seat).
|
||||
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 cannot serve a marked tool | the controller's test: a membership for a runner without `approval-mark/1` leaves out the marked tool and names it; against a real bus, that runner's grant cannot subscribe the marked tool's subject, and a call there finds no responder |
|
||||
| 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 and a new ask is published; two calls at once run once; 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 <tool> on <node>?" 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
|
||||
@@ -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,11 @@ mesh records rather than rolls out — and 62 tools from the rest.
|
||||
`tools/call` reaches any tool by `<module>.<tool>`, 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). An act waits for the operator's warrant
|
||||
only where its module asks first, in its own code: the agent module's instruction proposals, and the
|
||||
controller's settings proposals and the actions it offers on a condition.
|
||||
|
||||
## 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)).*
|
||||
|
||||
@@ -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,46 @@ 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`, by leaving its subject out of that
|
||||
runner's bus permissions.
|
||||
|
||||
```
|
||||
agent ──call──► tool runner (serving node) ──ask, under the module's name──► router ──► Telegram
|
||||
▲ │ "Not run: asked, ask <id>" │ Approve
|
||||
│ ▼ ▼
|
||||
└──same call──► runner reads the router's record ◄────────────── warrant (decided.<module>)
|
||||
│ 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 <tool> on
|
||||
<node>?", 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" (whole when at most 2000 bytes and 40 lines, else the
|
||||
first 20 lines and 1200 bytes), 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, that the call runs within 30 minutes of the approval, and
|
||||
when the ask expires.
|
||||
- **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, one writer per node); the runner judges the newest ask it keeps
|
||||
for the act, and after "already run", "expired" or "approved too long ago" the next identical call asks anew;
|
||||
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).
|
||||
`mesh-delivery.release` waits until a warrant the controller holds passes the mark of a verb it calls (issue
|
||||
417), so a Release on a condition is not asked twice.
|
||||
|
||||
### The hand-act
|
||||
|
||||
Gains **`via`** (kind and holder), **`requested-by`** (agent principal or condition key), **`ask`** (the
|
||||
@@ -562,6 +603,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` in its bus permissions (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` marked (the controller's seat set) once issue 418's reads exist | 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 asked again, 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
|
||||
|
||||
Reference in New Issue
Block a user