Graduates research 039. The tool runner (and the controller for its own verbs) asks the operator
through the existing operator-channel asks and runs the exact call once on the warrant, reusing the
act digest and warrant check of ADR 0259 and the shown-in-shape form of ADRs 0274 and 0277, so
risky agent calls stop resting on the agent's restraint. Amends to-be 46 and hands it off.
@@ -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
@@ -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
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
([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
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
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.<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, as ADR 0274 §5 (its note of 2026-10-10) has it, 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. 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 <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 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 <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
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 <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", 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
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.