From 9f66452fff41d37e3e71ea46d7a6867f2588b506 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 8 Oct 2026 15:19:08 +0200 Subject: [PATCH 01/16] ADR 0259 (proposed): let any module ask a person and any channel answer, so Telegram is not built into the controller --- 00-META/glossary.md | 6 +- ...wers-it-and-the-asker-acts-on-a-warrant.md | 322 ++++++++++++++++++ .../46-the-conversation-with-the-operator.md | 6 + 3 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md diff --git a/00-META/glossary.md b/00-META/glossary.md index 66991ed4..7892c828 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -477,7 +477,11 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0272. ([ADR 0253](../02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md)). - **ask** — a request for the operator's input, of a declared kind (yes-no, one-of, text, number, date, acknowledge). An **authorising ask** is one whose answer performs an action; the controller - holds it and checks its proofs (ADR 0234). + holds it and checks its proofs (ADR 0234). *Proposed in ADR 0259:* the router holds every ask, and + the asker performs the chosen option on a **warrant**. +- **warrant** — the router's record that a person chose one option of one ask: the ask, the option, + who chose (the operator, as a channel kind's identity), through which channel, the proofs and when, + published for that asker alone. The only thing an asker acts on (ADR 0259, proposed). - **operator message / input** — what arrives on `intake`, once the router has checked the sender against the controller's 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/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 new file mode 100644 index 00000000..d1962753 --- /dev/null +++ b/02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md @@ -0,0 +1,322 @@ +--- +topic: the mesh +status: proposed +date: 2026-10-08 +deciders: jochen +reconstructed: false +supersedes-in-part: + - 0234-the-mesh-holds-a-conversation-with-its-operator.md +extends: 02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md +--- + +# 259. An ask is a message on the bus, any channel answers it, and the asker acts on a warrant + +## Context + +On 2026-10-08 the operator chose Telegram as the next channel for the operator's decisions. "Needs you: +release it, or stop it" must be answerable from the phone, with real authorisation, and without an agent +session. + +What the mesh had on its main branches that day: + +- **The desk performs only an acknowledgement** + ([ADR 0258](0258-a-desk-click-performs-only-an-acknowledgement-and-an-answer-is-a-one-time-ticket.md)). + Release, stop, start and restart are said in words: "from the mesh MCP server; this notification cannot do + it". The messenger (the `operator-channel` seat's holder) calls `conditions silence` itself when the + operator chooses Silence on the desk. +- **The messenger holds the Telegram client in its own process**, says conditions there as words without + buttons, and keeps the bot token as its own secret `telegram-token`. Its status said on 2026-10-08 that the + token held is not a bot token: no bot has been given yet. +- **[ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md) §8 gives authorising asks to the + controller**: it holds the ask, keeps the operator's identities and factors, checks the proofs and + performs the action itself. None of it is built ([to-be 46](../03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md) + phase 5). +- **Any node's tool runner may call any tool on any node** (ADR 0175). A tool call therefore proves + nothing about who made it: the laptop's runner carries the agents' calls. What the bus *does* prove is the + publisher of a module's events and a seat's events: only the principal granted a subject may publish it, + which is why a condition's events need no signature (design 29 §2). + +The operator's directions, 2026-10-08, on the first proposal (a Telegram path through the controller): + +- "I don't think we should mangle Telegram into our controller. Now we're using Telegram, but tomorrow we + could be using Signal. And today the controller needs to receive a human decision, but tomorrow a + completely different module might require human approval." +- **Two generic boundaries.** Asking a person is a mesh-wide service any module uses; the asking module + performs the action itself, with its own grants, and records who decided. Channels are pluggable: a + channel renders the question, receives the answer and verifies the sender by its own means. Nothing + outside the channel module knows which service it talks to. +- **The controller only asks and acts.** +- **The flow is native to the bus**: a request is a message, pending requests survive a restart, each + answer is recorded once, the asker reads its request's state from a bucket, and the bus's permissions + decide who may publish what. Events carry no secret; request and reply may. No HTTP between modules. + +### Against GENESIS + +- **Core value — sovereignty.** Telegram stays an outside dependency taken deliberately (ADR 0234), and + is now one module that can be removed without touching anything that asks. +- **Core value — failure must be loud.** An ask that no channel can carry is refused in words to its asker + at once, never queued in silence. An answer that is refused is said on the channel it came from. +- **Effect — "when something genuinely needs a decision, you are asked, with the context".** Any module + can now ask, not only the controller. + +## Considered Options + +### Where an ask is held + +1. **The controller holds authorising asks** (ADR 0234 §8). Rejected by the operator: the next module + that needs a person (a backup module, the delivery owner) would ask through the controller, and the + controller would perform actions in other domains. +2. **A new seat for decisions, beside `operator-channel`.** Rejected: an ask needs the router's routing, + the work context and the content rule; a second seat would ask the router for each of them, and two + holders would keep two lists of who the operator is. +3. **The `operator-channel` seat's holder, the router, holds every ask, authorising or not, and judges + the answers.** Chosen. ADR 0234 already gives it ordinary asks; this adds the authorising ones. + +### Who performs the action + +1. **The decision holder calls the asker's verb.** Rejected: the router would act in every domain with a + grant to every verb, which is what ADR 0258 took away from the desk. +2. **The asker performs on hearing "yes" from anyone.** Rejected in ADR 0234: a relayed "yes" is not an + answer. +3. **The asker performs on a warrant**: the router's record that the operator chose an option, published + on a subject only the router may publish, for that asker alone. Chosen. The bus proves the warrant's + source as it proves a condition's. A signature was weighed and rejected: it needs a key in every asker + and proves nothing the server's permissions do not. + +### How an ask, a warrant and an answer travel + +1. **Tool calls** (an `ask` verb, an `answer` verb). Rejected: any tool runner may call any tool, so the + router could not tell the controller's ask from an agent's, nor a channel's answer from anybody's. +2. **HTTP between modules.** Rejected by the operator's direction, and it would carry its own + authentication beside the bus's. +3. **Subjects whose last token names the publisher**, granted to that publisher alone, on streams that + survive a restart. Chosen. The mesh already grants a machine its own token on a node seat's events + (ADR 0219); this is the same rule for a caller's module and a channel's kind. + +### Who is the operator on a channel + +1. **A setting naming the operator's account on the service.** Rejected: any principal granted + `mesh-controller.settings` may write a setting, agents included, so the operator's identity would be + anyone's to set. +2. **The channel module's own allow-list.** Rejected in ADR 0234: the channel would decide who the + operator is. +3. **The router's list of the operator's identities, per channel kind, written only by linking.** + Chosen. A channel module passes the sender as the service authenticated it, as an opaque string; the + router alone compares it with its list. + +### How a channel takes its answers (Telegram) + +1. **A webhook.** The control node is the stably reachable node, so it could take one. Rejected all the + same: it needs a public route, a name and a certificate into the control node, guarded only by a + secret header; a stolen token could point it elsewhere. +2. **Long polling**, as ADR 0234 chose. Chosen: outbound only, unaffected by the control node's address, + answered as fast as a webhook, and a stolen token can read updates (visible as a conflict) but not + inject one. + +### Where the desk's buttons live + +1. **Inside the router**, as its desktop adapter (to-be 46 §11). Rejected: the router would know one + channel, and the operator asked that the desk be just another channel. +2. **The dunst module** claims the channel. Rejected: dunst holds a node seat on each machine, and a + channel kind has one holder for the mesh. +3. **A small module of its own, kind `desktop`**, sending through each machine's `node-notifier` and + reading dunst's `action-chosen`. Chosen. + +## Decision + +### 1. The words + +- An **ask** keeps its glossary sense; an **authorising ask** is one whose chosen option the asker + performs. Each option carries its **assurance level**: `acknowledge`, `approve` or `destroy` (ADR 0234's + tiers, the glossary's word). +- A **warrant** (new) is the router's record that a person chose one option of one ask: the ask, the + option, who chose (the operator, as a channel kind's identity), through which channel, the proofs, and + when. It is the only thing an asker acts on. +- **The router** keeps its sense (the `operator-channel` seat's holder). It now holds every ask and + issues warrants. It never performs an option. + +### 2. The boundaries + +| Party | Does | Never | +|---|---|---| +| **an asker** (any module; first the controller) | publishes an ask under its own name; reads its asks' state; acts on its own warrants with its own grants; records the warrant in its own record of the act | performs without a warrant; reads another asker's asks | +| **the router** (`operator-channel` holder, the `messenger` module) | keeps asks, tokens and the operator's identities; routes an ask to the channels whose capabilities meet each option's level; judges each answer; issues one warrant per ask | calls an asker's verb; knows a service's API | +| **a channel** (a module claiming a kind on `channel` and `intake`: `telegram`, `desktop`) | shows what the router sends, with the router's tokens on its buttons; passes back the token chosen and the sender as its service authenticated it | judges whether the sender is the operator; holds another channel's secret | + +### 3. The subjects, streams and buckets + +Every name below follows the mesh's existing shapes (`mesh.seat...…`, a module's +state bucket, the EVENTS stream). `` is the asking module's name; `` a channel's kind. + +| What | Subject or bucket | Kept in | Publisher (granted) | Reader (granted) | +|---|---|---|---|---| +| an ask | `mesh.seat.operator-channel.accept.ask.` | the seat's work queue `SEAT_OPERATOR_CHANNEL` (kept 7 days, removed when the router has recorded it) | the asker, its own name only | the router | +| an ask cancelled by its asker | `mesh.seat.operator-channel.accept.cancel.` | the same | the asker, its own name only | the router | +| the asks and their state | the router's bucket `asks`, key `.` | its own state, closed records kept 30 days | the router | the router; each asker reads its own prefix only (direct get) | +| answer tokens | the router's bucket `tokens`, key the token, each with a time to live equal to its ask's expiry | its own state | the router | the router | +| the operator's identities | the router's bucket `identities`, key `.` | its own state | the router | the router | +| a warrant, or an ask's end without one | `mesh.seat.operator-channel.event.decided.`, message id `.` | EVENTS (7 days), deduplicated by the message id | the router only | the asker, its own name only, through its durable consumer | +| show, edit or say on a channel | `mesh.seat.channel.accept..` | the seat's work queue `SEAT_CHANNEL`, one worker per kind | the router | the holder of that kind only | +| an answer chosen, a link asked | `mesh.seat.intake.event..` | EVENTS | the holder of that kind only | the router | +| a code typed (a link code; later a TOTP code) | `mesh.seat.intake.proof.`, core request and reply | nothing: never on a stream | the holder of that kind only | the router, which answers | + +**The grants, composed by the controller as it composes every bus account:** + +- **A caller-named verb.** A seat may mark an accepted verb or an emitted event as named by its caller + (`ask.*`, `cancel.*`, `decided.*`). A module that uses the seat is granted to publish the accept, and to + subscribe the event, with its own module name as the last token, and no other. A tool runner carrying + modules is granted the same for each module it carries. The controller asks as `mesh-controller`. +- **A kinded bench** (ADR 0234 §2, built here for `channel` and `intake`). A claim carries `kind`; two + claims of one kind are refused. A holder subscribes its own kind's accepts, publishes its own kind's + events and proofs, and nothing of another kind. Each kind has its own worker on the seat's queue, so a + channel that is offline keeps its work and holds up no other. +- **A seat's `proof` verbs** are core request and reply on `mesh.seat..proof.`: no stream + covers them, so a code is never persisted (the mesh's rule: events carry no secret). +- **A record readable by its asker.** The router's `asks` bucket is read by an asker under its own prefix + only: direct get on `$KV...>`. +- **No `answer` tool.** A tool runner may call any tool, so an answer that arrived as a tool call could + come from anyone. An answer arrives only as an intake event of a kind. + +### 4. An ask + +An ask carries: an `id` the asker chose, unique to it; the **question** in plain words, held to the plain +rule and the content rule (ADR 0253, ADR 0234 §6); its **options**, at most four, each a label, what it +does in plain words, its assurance level and an option id; **who may answer** (the role `operator`, the +only role today); an **expiry**; **what happens on expiry**, in words the operator is shown ("the delivery +stays held"); and optionally a subject it is about (a condition's key), so a newer ask about the same +thing replaces the older. + +- **Refused at once, in words, on the decided subject**: an ask whose words break a rule, whose options + include a level no ready channel can carry (`destroy` until a TOTP factor exists), or a fourth open ask + from one asker. +- **An authorising ask never defaults.** At its expiry the router ends it with `expired`; the asker does + what it said it would. +- **An approve option expires within 24 hours; a destroy option within 10 minutes of being shown** + (ADR 0234). + +### 5. The router judges an answer + +On an intake `choice` of kind K carrying token T and sender S, the router refuses at the first failure and +says the refusal on K's channel: + +1. T is known, unused, unexpired, and was issued for kind K; +2. the ask is open, and the option T names belongs to it; +3. S is on the router's list of the operator's identities for K; +4. K's declared capabilities, **read from the controller's record of its claim and never from the + channel**, meet the option's level: `acknowledge` needs `choice` and `exact-render`; `approve` needs + `verified-sender` as well (P1); `destroy` needs two proofs, one of them a code (P2); +5. the ask is closed with a compare-and-set on its record, so a second answer loses. + +Then it publishes the warrant on `decided.`, and sends every channel that showed the ask an edit +naming the outcome and removing its buttons. + +**The desk declares no `verified-sender`**, so it answers only `acknowledge` options, as ADR 0258 decided. +Telegram declares it while placed where no agent runs as the operator, so it answers `approve`. + +### 6. The warrant, and the asker's act + +The warrant carries the ask's id and asker, the option, `by` (the operator, as a kind's identity: "the +operator, via telegram, user id verified"), the channel module, the proofs present, the assurance level +met, and when. It carries no secret. + +- **The asker acts once per ask id**, keeping that in its own record, and only on a warrant from its own + `decided` subject. +- **Its record of the act names the warrant**: a hand-act's `by`, `via`, `ask` and `proofs` (ADR 0234 §8), + a delivery's transition, a silence's why. The hand-act log counts it as a person's decision, not a repair. +- **The controller only asks and acts.** For a condition that needs the operator it asks with the + condition's actions as options, and on the warrant it calls the verb it named, with its own grant. It + holds no channel, no identity and no factor. + +### 7. Who the operator is on a channel: linking + +- **The router's list is the only list.** It is changed only by linking and unlinking, never by a setting. +- **Linking:** the operator sends the bot `/start`. The channel publishes `link.` with the sender + and the name the service shows. The router shows a six-digit code on the desk, with that name, valid + for ten minutes. The operator sends the code to the bot; the channel passes it on the `proof` subject; + the router links the identity when it matches, and says so on every channel. +- **One identity per kind.** A second link of a kind is refused until the first is unlinked. Unlinking + (a tool of the router) is said on every channel, the unlinked one included. +- **Once a TOTP factor exists**, linking and unlinking are destroy asks (ADR 0234 §8). +- **A residual risk is accepted until then:** on the X11 desk an agent can read the code and send it from + an account of its own. It is said loudly on every channel when it happens, and the factor closes it. + +### 8. What is superseded in ADR 0234 + +- **§8 "an ask whose answer performs an action is held by the controller", the controller's verbs + `authorise request`, `authorise answer` and `authorisations`, and the controller performing:** the + router holds the ask and issues a warrant; the asker performs. +- **§3 "the controller's list of the operator's identities"** and §8 "the factors' secrets are the + controller's own": the identities and, when built, the TOTP factor and its recovery codes are the + router's own state, with ADR 0234's rules on showing them once, at a terminal, never on a channel. +- Everything else in ADR 0234 stands: the tiers and proofs, the kinded benches, the content rule, the + desk never authorising, long polling, every tier completable on the away channel. + +### 9. The first channels and the order of work + +1. **The controller:** the kinded bench with `kind` on a claim; the `channel` and `intake` seats; the + caller-named and per-kind grants; `proof` subjects; the asker's read of its own prefix. +2. **The shared library (mesh-sdk):** an asker's client (ask, cancel, read its state, take its warrants) + and a channel's (take show, edit and send for a kind; publish a choice or a link; send a proof). +3. **The messenger** becomes the router of this record: asks, tokens, identities, judging, warrants, + expiry, linking. Its Telegram client and its desk adapter leave it. +4. **`telegram`**, a new module claiming kind `telegram` on both benches: long polling, buttons carrying + only a token, `/start` linking, ADR 0234's defects D1 to D10 fixed in the move, its token as its own + secret. **`desk-channel`**, a new module claiming kind `desktop`. +5. **The controller as an asker**: conditions that need the operator are asked; it acts on warrants and + records them; the words "from the mesh MCP server" give way to the router's own line, below. +6. **The vault** gains a verb by which the operator gives a module's own secret through a hidden prompt + on the desk, so a bot's token never passes through an agent's session. + +**Where an ask can be answered is said by the router**, from the channels that qualify: "Answer on +Telegram, or from the mesh MCP server." The asker's words never name a channel. + +## Consequences + +- **A channel is a module and nothing else changes when one is added.** Signal or mail is a module + claiming a kind and declaring capabilities. +- **Any module may ask a person**, through one seat, and acts on the answer with its own grants. ADR + 0258's desk Silence becomes an ask of the controller's, and the messenger loses its grant to + `conditions silence`. +- **The controller loses three verbs it never built** and gains an asker's client. +- **The router's state grows**: asks, tokens and identities, each a bucket of its own. +- **Harder:** an asker must keep which warrants it acted on; the shared library carries it. +- **Harder:** the controller composes two new grant rules, and an asking module must use the seat in its + manifest to be granted anything. +- **The bot token moves** from the messenger's own secret to the `telegram` module's. +- **Accepted risk:** a linked Telegram account, if taken, can approve (announced, reversible), never + destroy. Linking before a TOTP factor exists carries the X11 risk in §7. + +## How it is checked + +| Rule | Checked by | +|---|---| +| An asker publishes asks only under its own name, and hears only its own warrants | controller grant tests: a module using `operator-channel` is granted `accept.ask.` and `event.decided.`, no wildcard; a live drill publishing under another name is refused by the server | +| Only the router publishes a warrant | controller grant test: `event.decided.*` is in no principal's publish list but the holder's | +| A channel publishes only its own kind's answers, links and proofs | controller grant tests over a kinded claim; a second claim of a kind is refused | +| A code is never on a stream | controller test: no stream's subjects cover `mesh.seat.*.proof.>` | +| A token is used once, expires, and is bound to its ask, option and kind | router tests: the same token twice; a token past its expiry; a token from another kind; a token of a closed ask | +| An answer from an identity not linked is refused and said | router test | +| A level a channel's declared capabilities do not meet is refused | router tests: an approve option answered from `desktop`; capabilities taken from the controller's record, a channel's own claim ignored | +| A second answer to one ask loses | router test: two answers raced; one warrant | +| An authorising ask never defaults | router test: expiry publishes `expired`, never an option | +| An ask no ready channel can carry is refused at once, in words | router test | +| Linking needs the code shown on the desk, and is said on every channel | router tests: a wrong code, a late code, a second link of a kind | +| The asker acts once per warrant and records the warrant | shared library test; controller test reading the hand-act after a warrant | +| The controller holds no channel code | review; the controller's module imports no channel package | +| End to end | live drill: a held delivery released from the phone; the hand-act reads "the operator, via telegram, user id verified" | + +## References + +- [ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md) (the conversation, the tiers and + proofs; §3 and §8 in part superseded here) +- [ADR 0258](0258-a-desk-click-performs-only-an-acknowledgement-and-an-answer-is-a-one-time-ticket.md) + (the desk performs only an acknowledgement; one-time answer tokens) +- [ADR 0253](0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md) (a + condition's actions and plain words) +- [ADR 0246](0246-a-seats-new-verb-is-promised-before-it-is-required.md) (a new seat's verbs are required + from the start) +- [ADR 0255](0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md) + (a generic boundary in front of pluggable modules) +- [To-be 46](../03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md) §3, §4, §10, §11 and §13 +- [Issue 320](../04-ISSUES/320-the-operator-could-not-read-the-meshs-notifications/00-report.md), + [issue 324](../04-ISSUES/324-a-desk-click-could-answer-in-the-operators-name/00-report.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 83e68de5..d7a3d591 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 @@ -316,6 +316,12 @@ is not exempted. ## 10. Asks that authorise +*Proposed 2026-10-08, [ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md):* +the router, not the controller, holds authorising asks, keeps the operator's identities and judges each +answer; the asker performs the chosen option on a warrant the router publishes for it alone; asks, warrants +and answers travel on the bus under subjects whose last token names their publisher. Once that record is +accepted, this section and §3, §4, §11 and §13 are amended to it. + ### The verb table and the controller's verbs - **The controller's verb definition gains `authorises`:** the tier, and the arguments that make up From 1714132a083c44922b0b1c9bbacbd858979d5bae Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 8 Oct 2026 16:18:05 +0200 Subject: [PATCH 02/16] Accept ADR 0259 with the operator's two decisions, and amend to-be 46 to it --- 00-META/glossary.md | 12 +- ...-holds-a-conversation-with-its-operator.md | 6 + ...wers-it-and-the-asker-acts-on-a-warrant.md | 68 +++++-- .../46-the-conversation-with-the-operator.md | 175 +++++++++++------- 4 files changed, 179 insertions(+), 82 deletions(-) diff --git a/00-META/glossary.md b/00-META/glossary.md index 7892c828..388360d1 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -443,10 +443,10 @@ it should not. **Recorded by** the controller. **Upstream of** Provisioning and ## Operator and conversation — the person the mesh works for **Purpose.** To let the mesh and its operator talk: tell, ask, answer, and act only on an answer it can -trust. **Recorded by** the router and the controller (authorising asks). +trust. **Recorded by** the router (asks, warrants and the operator's identities). **Downstream of** Health and repair, Change and delivery, and Identity and access. An authorising answer acts in whichever domain asked, through that domain's own verb; the conversation owns the -asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0272. +asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272. **Uses:** operator, person, proof (Identity and access), release (Change and delivery). - **mesh MCP server** — the endpoint on a node's loopback through which every agent and person on that @@ -476,12 +476,12 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0272. the condition beside its **summary**, which keeps the plans, commits and verbs for whoever looks closer ([ADR 0253](../02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md)). - **ask** — a request for the operator's input, of a declared kind (yes-no, one-of, text, number, - date, acknowledge). An **authorising ask** is one whose answer performs an action; the controller - holds it and checks its proofs (ADR 0234). *Proposed in ADR 0259:* the router holds every ask, and - the asker performs the chosen option on a **warrant**. + date, acknowledge). An **authorising ask** is one whose chosen option its asker performs. The router + holds every ask, checks each answer's proofs and issues a **warrant**; the asker acts on it with its + own grants (ADR 0234, ADR 0259). - **warrant** — the router's record that a person chose one option of one ask: the ask, the option, who chose (the operator, as a channel kind's identity), through which channel, the proofs and when, - published for that asker alone. The only thing an asker acts on (ADR 0259, proposed). + published for that asker alone. The only thing an asker acts on (ADR 0259). - **operator message / input** — what arrives on `intake`, once the router has checked the sender against the controller's 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/02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md b/02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md index 9ef02032..8321d058 100644 --- a/02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md +++ b/02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md @@ -289,6 +289,12 @@ consumers are later work. ### 8. Asks that authorise +> **Who holds the ask changed — 2026-10-08, by [ADR 0259](0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md).** +> What stands: the tiers, the proofs, the desk never authorising, and every tier completable on the away +> channel. What moved: the router, not the controller, holds authorising asks, the operator's identities +> and the factors, and judges each answer; the asker performs the option itself on a warrant. The +> controller's verbs `authorise request`, `authorise answer` and `authorisations` below are replaced there. + **An ask whose answer performs an action is held by the controller.** The router carries it like any other ask; only the controller performs. 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 d1962753..c3e63a12 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 @@ -1,6 +1,6 @@ --- topic: the mesh -status: proposed +status: accepted date: 2026-10-08 deciders: jochen reconstructed: false @@ -50,6 +50,15 @@ The operator's directions, 2026-10-08, on the first proposal (a Telegram path th answer is recorded once, the asker reads its request's state from a bucket, and the bus's permissions decide who may publish what. Events carry no secret; request and reply may. No HTTP between modules. +The operator approved this record on 2026-10-08, with the word *warrant*, and decided two points it had +left open: + +- **The `telegram` module runs on the control node under a service account of its own, not the + operator's.** Claude Code runs as the operator on every machine; the service account keeps an agent away + from the bot token and the channel's state. +- **A newly linked identity waits one hour before it can approve**; acknowledge works at once. Every link + and unlink is announced on every channel. This holds until a code from an authenticator app exists. + ### Against GENESIS - **Core value — sovereignty.** Telegram stays an outside dependency taken deliberately (ADR 0234), and @@ -158,7 +167,7 @@ state bucket, the EVENTS stream). `` is the asking module's name; ` | a warrant, or an ask's end without one | `mesh.seat.operator-channel.event.decided.`, message id `.` | EVENTS (7 days), deduplicated by the message id | the router only | the asker, its own name only, through its durable consumer | | show, edit or say on a channel | `mesh.seat.channel.accept..` | the seat's work queue `SEAT_CHANNEL`, one worker per kind | the router | the holder of that kind only | | an answer chosen, a link asked | `mesh.seat.intake.event..` | EVENTS | the holder of that kind only | the router | -| a code typed (a link code; later a TOTP code) | `mesh.seat.intake.proof.`, core request and reply | nothing: never on a stream | the holder of that kind only | the router, which answers | +| a code typed (a link code; later a TOTP code) | `mesh.seat.intake.proof.code.`, core request and reply | nothing: never on a stream | the holder of that kind only | the router, which answers | **The grants, composed by the controller as it composes every bus account:** @@ -170,8 +179,11 @@ state bucket, the EVENTS stream). `` is the asking module's name; ` claims of one kind are refused. A holder subscribes its own kind's accepts, publishes its own kind's events and proofs, and nothing of another kind. Each kind has its own worker on the seat's queue, so a channel that is offline keeps its work and holds up no other. -- **A seat's `proof` verbs** are core request and reply on `mesh.seat..proof.`: no stream - covers them, so a code is never persisted (the mesh's rule: events carry no secret). +- **A seat's `proof` verbs** are core request and reply on `mesh.seat..proof..`: no + stream covers them, so a code is never persisted (the mesh's rule: events carry no secret). A holder + asks with its own kind; the modules that watch the seat answer. +- **The seat declares all of it**: `kinded`, the verbs named `by-caller`, its `proofs`, and the holder's + `records` its users read under their own name. Only `channel` and `intake` may be kinded. - **A record readable by its asker.** The router's `asks` bucket is read by an asker under its own prefix only: direct get on `$KV...>`. - **No `answer` tool.** A tool runner may call any tool, so an answer that arrived as a tool call could @@ -236,11 +248,37 @@ met, and when. It carries no secret. the router links the identity when it matches, and says so on every channel. - **One identity per kind.** A second link of a kind is refused until the first is unlinked. Unlinking (a tool of the router) is said on every channel, the unlinked one included. +- **A new link answers `acknowledge` at once and `approve` only after one hour.** The hour runs from the + moment the link is made, is kept in the identity's record, and is said in the announcement ("it can + approve from 15:40"). An approve answer inside the hour is refused and said on the channel. Decided by + the operator on 2026-10-08, until an authenticator-app code exists. - **Once a TOTP factor exists**, linking and unlinking are destroy asks (ADR 0234 §8). - **A residual risk is accepted until then:** on the X11 desk an agent can read the code and send it from - an account of its own. It is said loudly on every channel when it happens, and the factor closes it. + an account of its own. It is said loudly on every channel when it happens, the hour gives the operator + time to unlink it before it can approve anything, and the factor closes it. -### 8. What is superseded in ADR 0234 +### 8. The `telegram` module's own account + +Decided by the operator on 2026-10-08. The bot token and the channel's state are kept from every agent, +because every agent runs as the operator. + +- **Its process runs as the account `telegram`**, which the module makes (a `user` resource: no shell, no + login, home in its state directory). The tool runner, which runs as root, starts the module's bundle + as that account when the bundle's environment names it (`MESH_RUN_AS`); it refuses to start a bundle as + root or as the operator's account. +- **Its state directory is owned by `telegram`, mode 0700, and its own secret `telegram-token` by + `telegram`, mode 0600** (the manifest's `owner` and `secrets-owner`). The operator's account can read + neither, and cannot attach to a process of another account. +- **The bus credential stays the tool runner's**, readable by root only. The bundle reaches the bus only + through its standard input and output, and the runner publishes for it only the kind it claims. +- **An agent reaches the runner only through the mesh MCP server's five tools**, which make tool calls. + No tool publishes an intake event or a proof: those come only from the bundle's own channel. +- **What remains if an agent has root on the control node:** nothing here holds. Root reads the token, + the runner's bus credential and the router's state, and can publish as anything. The boundary rests on + agents not holding root there; whether the operator's account may become root without a person is + checked by the self-check, which raises a condition when it can. + +### 9. What is superseded in ADR 0234 - **§8 "an ask whose answer performs an action is held by the controller", the controller's verbs `authorise request`, `authorise answer` and `authorisations`, and the controller performing:** the @@ -251,20 +289,23 @@ met, and when. It carries no secret. - Everything else in ADR 0234 stands: the tiers and proofs, the kinded benches, the content rule, the desk never authorising, long polling, every tier completable on the away channel. -### 9. The first channels and the order of work +### 10. The first channels and the order of work 1. **The controller:** the kinded bench with `kind` on a claim; the `channel` and `intake` seats; the caller-named and per-kind grants; `proof` subjects; the asker's read of its own prefix. -2. **The shared library (mesh-sdk):** an asker's client (ask, cancel, read its state, take its warrants) +2. **The tool runner (mesh-tools)** carries a bundle's seat traffic: publishing an accept or an event + with its own name or kind, taking a held seat's work, asking and answering a proof, a compare-and-set + on its state, and starting a bundle as its own account. +3. **The shared library (mesh-sdk):** an asker's client (ask, cancel, read its state, take its warrants) and a channel's (take show, edit and send for a kind; publish a choice or a link; send a proof). -3. **The messenger** becomes the router of this record: asks, tokens, identities, judging, warrants, +4. **The messenger** becomes the router of this record: asks, tokens, identities, judging, warrants, expiry, linking. Its Telegram client and its desk adapter leave it. -4. **`telegram`**, a new module claiming kind `telegram` on both benches: long polling, buttons carrying +5. **`telegram`**, a new module claiming kind `telegram` on both benches: long polling, buttons carrying only a token, `/start` linking, ADR 0234's defects D1 to D10 fixed in the move, its token as its own secret. **`desk-channel`**, a new module claiming kind `desktop`. -5. **The controller as an asker**: conditions that need the operator are asked; it acts on warrants and +6. **The controller as an asker**: conditions that need the operator are asked; it acts on warrants and records them; the words "from the mesh MCP server" give way to the router's own line, below. -6. **The vault** gains a verb by which the operator gives a module's own secret through a hidden prompt +7. **The vault** gains a verb by which the operator gives a module's own secret through a hidden prompt on the desk, so a bot's token never passes through an agent's session. **Where an ask can be answered is said by the router**, from the channels that qualify: "Answer on @@ -301,6 +342,9 @@ Telegram, or from the mesh MCP server." The asker's words never name a channel. | An authorising ask never defaults | router test: expiry publishes `expired`, never an option | | An ask no ready channel can carry is refused at once, in words | router test | | Linking needs the code shown on the desk, and is said on every channel | router tests: a wrong code, a late code, a second link of a kind | +| A new link approves only after one hour; it acknowledges at once | router tests: an approve answer at 59 minutes refused and said, at 60 accepted; an acknowledge at once accepted | +| The `telegram` bundle runs as its own account, never root or the operator's | tool runner tests: a bundle naming `MESH_RUN_AS` is started as that account; root and the operator's account refused; the manifest's owners checked by the catalogue's manifest test | +| A tool call never publishes an intake event or a proof | tool runner test: only a bundle's own channel reaches the seat publish, for its own kind | | The asker acts once per warrant and records the warrant | shared library test; controller test reading the hand-act after a warrant | | The controller holds no channel code | review; the controller's module imports no channel package | | End to end | live drill: a held delivery released from the phone; the hand-act reads "the operator, via telegram, user id verified" | 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 d7a3d591..eb479543 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 @@ -4,6 +4,7 @@ status: designed code: [] updated: 2026-10-08 decisions: + - 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 - 02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md - 02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md @@ -13,8 +14,8 @@ decisions: **The mesh, its modules and its agents tell the operator things and ask them things; the operator answers, or writes first. Each exchange travels on a channel whose declared capabilities can carry it, -chosen by where the operator is working. An answer that performs an action is checked and performed by -the controller, never by the channel or the asker** ([ADR 0234](../../02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md), +chosen by where the operator is working. An answer that performs an action is judged by the router, which +issues a warrant; the asker performs it with its own grants, never the channel or the router** ([ADR 0234](../../02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md), from [research 028](../../01-RESEARCH/028-the-meshs-output-channel/00-overview.md)). This design grows the minimal output channel of [to-be 45](45-a-core-that-cannot-fail-silently.md) §5. @@ -28,14 +29,13 @@ That form stays the first step; §13 below is the order in which it becomes this controller's conditions ──┐ messages and asks ┌──► channel/telegram modules (notify) ────────┼──► required capability → work context ──┼──► channel/desktop agents (ask) ────────┤ → severity → escalation └──► channel/ - controller (authorise │ presence (current state only) + any module (ask) ─────────┤ presence (current state only) request) ────────────────┘ ▲ ▲ │ │ answer (ordinary asks) intake bench (in) seen ───┘ └────────────────────────────── intake/telegram intake/desktop - CONTROLLER ◄──── authorise answer (authorising asks, intake holders only) ─┘ - │ checks capabilities, identity, code, digest; performs; records the hand-act - └──► ask-answered + ASKER (any module) ◄── warrant on decided. ── ROUTER judges: token, identity, + │ acts with its own grants; records the warrant capabilities, level; one warrant per ask watcher's watcher (outside the seats, own bot, not on the control node) ──► Telegram directly self-check and watcher ──► outside dead-man service @@ -91,14 +91,16 @@ bot) is held by one module claiming both seats under one kind. | `kind` | the holder's kind | | `what` | `message` (written first), `choice` (a button or reaction), `reply` (written in an ask's thread), `mail`, `call` (a webhook), `seen` (activity, for the work context) | | `sender` | the identity on that service, and whether the service authenticated it | -| `trusted` | whether the sender is on the **controller's** list of the operator's identities. A holder never sets it: a raw intake event is untrusted by construction, and only the router stamps it, from the controller's answer (§4) | +| `trusted` | whether the sender is on the **router's** list of the operator's identities. A holder never sets it: a raw intake event is untrusted by construction, and only the router stamps it, from the controller's answer (§4) | | `conversation` | an opaque handle; sending on it reaches the same chat, room or thread | | `in-reply-to` | the ask or message it answers, if any | | `payload` | the text, or the option's token. **Never a code** (§10) | | `at` | when | -**Answers go to the ask's owner by request and reply**: the router's `answer` for an ordinary ask, the -controller's `authorise answer` for an authorising one; both refuse an answer that is not trusted. +**Answers arrive only as intake events of the holder's own kind** (*amended 2026-10-08, ADR 0259*): a +`choice` carries the router's token and the sender as the service authenticated it, and the router judges +it (§10). A typed code goes by request and reply on the seat's `proof` subject, never in an event. There +is no `answer` tool: any tool runner may call any tool, so a tool call proves nothing about its caller. **Everything else is an event on the seat**, taken by the router, which re-emits it stamped (§4): a trusted `message` as an **operator message**, addressed (§5); anything untrusted as **input**, for consumers that accept untrusted input (a later mail rule takes `mail`). The router takes `seen` for the @@ -109,14 +111,13 @@ grants it (the open gap of [to-be 32](32-what-a-module-declares.md) §1). ## 4. Who counts as the operator -- **Only a sender on the controller's list of the operator's identities is the operator.** The list - is kept per intake kind in the controller's state. While no factor exists, an identity is enrolled +- **Only a sender on the router's list of the operator's identities is the operator** + (*amended 2026-10-08, ADR 0259:* the router's, not the controller's). The list is kept per intake kind in + the router's state, and changed only by linking and unlinking, never by a setting. While no factor exists, an identity is enrolled only at a terminal; once one does, adding or removing an identity is a destroy ask (§10). Telegram's linking verb (§11) produces such an ask. -- **The controller serves an identity check** (kind and sender in, trusted or not out). The router keeps - the answers as current state, refreshed when the controller announces a change to the list, and - stamps every envelope it re-emits with `trusted`. **No holder's word is taken for it**, and the - controller checks again itself before authorising (§10). +- **The router checks every sender against its own list** and stamps every envelope it re-emits with + `trusted`. **No holder's word is taken for it.** - **An untrusted envelope** may start work for a consumer that declares it accepts untrusted input. It can never answer an ask, authorise, or be delivered to an agent as the operator's words. An untrusted sender writing to the bot is told nothing beyond a fixed line, and the attempt is a warning condition @@ -208,10 +209,10 @@ The event **`ask-answered`**, with the ask's id and any conversation handle. An ### The router's verbs and events -`ask`, `ask cancel`, `asks`, `answer` (intake holders only), on the `operator-channel` seat beside its -existing `notify`, `open` and `history`. Events `ask-opened`, `ask-answered`, `ask-closed`. The router -owns every ask that authorises nothing; an authorising ask is owned by the controller (§10) and carried -by the router like any other. +*Amended 2026-10-08, ADR 0259:* an ask and its cancel are accepted on the `operator-channel` seat under +the asker's name; the answer, a warrant or the ask's end, is the seat's event `decided.`; the tool +`asks` lists them, beside the existing `notify`, `open` and `history`. The router owns every ask, +authorising or not (§10). ## 7. The capability vocabulary, `channel-capabilities/1` @@ -234,9 +235,9 @@ withdrawn from routing and reported until it passes. A new word is a new version | | `threads` | an answer is tied to what it answers | a reply to A carries A in `in-reply-to` | | | `operator-first` | the operator can write unprompted | a simulated message yields a `message` envelope | | trusting | `verified-sender` | the answer came from the operator's own account, through a holder no agent shares | a choice from an identity not on the list is dropped and reported; placement checked | -| | `exact-render` | the ask is shown as the controller rendered it | rendered text equals the controller's, byte for byte | -| | `code-factor` | a typed code reaches the controller by request and reply, never judged by the holder | a code never appears in an event; it is deleted from the conversation where the service allows | -| | `key-factor` | a security key's assertion over the controller's challenge reaches the controller | the challenge is the controller's; the signature is checked by the controller | +| | `exact-render` | the ask is shown as the router rendered it | rendered text equals the router's, byte for byte | +| | `code-factor` | a typed code reaches the router by request and reply, never judged by the holder | a code never appears in an event; it is deleted from the conversation where the service allows | +| | `key-factor` | a security key's assertion over the router's challenge reaches the router | the challenge is the router's; the signature is checked by the router | What the first holders declare: @@ -316,23 +317,60 @@ is not exempted. ## 10. Asks that authorise -*Proposed 2026-10-08, [ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md):* -the router, not the controller, holds authorising asks, keeps the operator's identities and judges each -answer; the asker performs the chosen option on a warrant the router publishes for it alone; asks, warrants -and answers travel on the bus under subjects whose last token names their publisher. Once that record is -accepted, this section and §3, §4, §11 and §13 are amended to it. +*Amended 2026-10-08, [ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md):* **the router holds every ask, authorising or not, and +judges each answer; the asker performs the chosen option on a warrant.** The controller keeps no channel, +no identity and no factor: it asks and acts like any other module. What follows in this section reads +with these replacements. -### The verb table and the controller's verbs +### Asks, warrants and answers on the bus -- **The controller's verb definition gains `authorises`:** the tier, and the arguments that make up +`` is the asking module's name, `` a channel's kind. Each subject's last token names its +publisher, and the bus grants it to that publisher alone. + +| What | Subject or bucket | Kept in | Publisher | Reader | +|---|---|---|---|---| +| an ask | `mesh.seat.operator-channel.accept.ask.` | the seat's work queue, 7 days | the asker, its own name | the router | +| a cancel | `mesh.seat.operator-channel.accept.cancel.` | the same | the asker, its own name | the router | +| asks and their state | the router's bucket `asks`, key `.` | its state; closed records 30 days | the router | the router; each asker its own prefix | +| answer tokens | the router's bucket `tokens`, key the token | its state, each key living until its ask expires | the router | the router | +| the operator's identities | the router's bucket `identities`, key `.` | its state | the router | the router | +| a warrant, or an ask's end | `mesh.seat.operator-channel.event.decided.`, message id `.` | EVENTS, deduplicated | the router | the asker, its own name | +| show, edit, say | `mesh.seat.channel.accept..` | the seat's work queue, one worker per kind | the router | that kind's holder | +| a choice, a link asked | `mesh.seat.intake.event..` | EVENTS | that kind's holder | the router | +| a code | `mesh.seat.intake.proof.code.` | nothing: core request and reply | that kind's holder | the router answers | + +**An ask** carries an id the asker chose; the question in plain words; at most four options, each a label, +what it does in plain words, its assurance level (`acknowledge`, `approve`, `destroy`) and an id; who may +answer (`operator`); an expiry; what happens on expiry, in words; and optionally what it is about (a +condition's key), so a newer ask about it replaces the older. It is refused at once, on its asker's +`decided` subject, when its words break a rule, when no ready channel can carry one of its options' levels, +or when the asker already has three open. + +**A warrant** carries the ask's id and asker, the option, `by` ("the operator, via telegram, user id +verified"), the channel module, the proofs, the level met and when. No secret. An ask that ends without one +ends `expired`, `cancelled`, `replaced` or `refused`, on the same subject. + +**The router judges a `choice` of kind K with token T from sender S**, refusing at the first failure and +saying it on K's channel: T is known, unused, unexpired and issued for K; the ask is open and T's option is +its own; S is linked for K; K's capabilities, from the controller's record of its claim, meet the option's +level; for `approve`, S was linked at least an hour ago; then the ask is closed by compare-and-set and the +warrant published. Every copy is edited to the outcome. + +**The asker** acts once per ask id, only on a warrant from its own subject, with its own grants, and its +record of the act names the warrant (`by`, `via`, `ask`, `proofs`). The hand-act log counts it as a person's +decision. + +**The verbs the controller was to serve** (`authorise request`, `authorise answer`, +`authorisations`) are not built: an ask on the seat replaces the first, an intake `choice` the second, and +the router's tool `asks` the third. The tiers, the proofs, the direct-call rules and the factors stand, and +the factors are the router's state. + +### The verb table + +- **Each asker's verb definition gains `authorises`:** the tier, and the arguments that make up the exact state a person must see (for `retire approve`, the set of consumers). -- **`authorise request`** — callable by anyone (an agent, the router on a condition's behalf). Carries - the verb and its exact arguments, why, and optionally a conversation handle. The controller renders - the ask, stores it with an opaque id, its expiry and a digest of the state shown, and emits - `ask-opened` with itself as owner. **Nothing is performed.** -- **`authorise answer`** — granted to intake holders only. Carries the ask's id, the option, the - sender's identity, and the code or key assertion where the tier needs them. -- **`authorisations`** — open and recent authorising asks. +- **An authorising ask** is published by its asker on the seat (above); the router renders it and stores + it with its expiry. **Nothing is performed** until the asker holds a warrant. ### What may be authorised, and its tier @@ -349,7 +387,7 @@ accepted, this section and §3, §4, §11 and §13 are amended to it. - **P1, a verified sender:** a Telegram tap from the operator's own account, through a holder placed where no agent runs as the operator. -- **P2, a TOTP code** from the operator's authenticator app, verified by the controller. Valid for the +- **P2, a TOTP code** from the operator's authenticator app, verified by the router. Valid for the current or previous 30-second step; accepted once. **This is the proof everything above acknowledge can rest on.** - **P3, a security key's touch bound to the ask** (the challenge is a hash of the ask's id and the @@ -378,26 +416,27 @@ accepted, this section and §3, §4, §11 and §13 are amended to it. 5. **No agent authorises.** An agent asks. Agents' grants hold no authorising verb. 6. **Only a trusted sender authorises** (§4); an untrusted answer is refused before any other check. -### The controller's checks, in order, refusing at the first failure +### The router's checks, in order, refusing at the first failure 1. The ask is open and unexpired. -2. The caller holds an intake kind — by the controller's own seat records, never the request's claim. -3. That kind's declared capabilities, from the controller's records, satisfy the tier, and the proofs +2. The answer came on an intake kind's own subject — which the bus grants that kind's holder alone. +3. That kind's declared capabilities, from the controller's record of its claim, satisfy the tier, and the proofs present are enough. -4. A P1 answer's sender is on the controller's list of the operator's identities for that kind. +4. A P1 answer's sender is on the router's list of the operator's identities for that kind, and for + approve was linked at least an hour ago. 5. A code is valid for the current or previous step, and unused. 6. A key assertion verifies against the enrolled credential, over this ask's challenge, with user presence. -7. The state now has the digest it had when shown; otherwise the ask is void and a new one is - requested. +7. The asker checks that the state still is what it asked about before it acts; otherwise it refuses + the warrant and asks again. -Then it performs as itself, records the hand-act, closes the ask with a compare-and-set so a second -answer loses, and emits `ask-answered`. Every copy is edited to the outcome and its buttons removed. +Then it closes the ask with a compare-and-set so a second answer loses, and publishes the warrant; the +asker performs as itself and records the hand-act. Every copy is edited to the outcome and its buttons removed. ### Direct calls `retire approve|reject`, `cleanup delete`, and `pin` while `binding-kept` names it refuse any caller -except through `authorise answer`, or **break-glass** at the mesh MCP server with a code — recorded and +except on a warrant, or **break-glass** at the mesh MCP server with a code — recorded and announced on every channel as break-glass. `retire approve` takes **`expect`**, the set it approves, 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. @@ -409,10 +448,10 @@ id) and **`proofs`** (which of P1, P2, P3). `by` reads "the operator, as ### The factors -The operator's identities, the TOTP seed, its recovery codes and any enrolled key are the controller's -own state. The seed is made by the mesh and shown once, as a URI, to a plain terminal — never through a +The operator's identities, the TOTP seed, its recovery codes and any enrolled key are the router's +own state (ADR 0259). The seed is made by the mesh and shown once, as a URI, to a plain terminal — never through a channel or an event. Re-enrolling a factor is a destroy ask, except by recovery below. A code travels -only by request and reply from an intake holder to the controller, and is deleted from the conversation +only by request and reply from an intake holder to the router, and is deleted from the conversation where the service allows. ### Recovering the factor @@ -420,7 +459,7 @@ where the service allows. A lost phone must not lock the mesh's only person out for good, and re-enrolling is a destroy ask that needs the lost factor's code. So: -- **`factor enrol`**, at a terminal: the controller makes the seed and **ten one-time recovery codes**, +- **`factor enrol`**, at a terminal: the router makes the seed and **ten one-time recovery codes**, shows them once together, and stores the codes **only as hashes**. - **A recovery code counts as one P2 proof, once.** It may stand in for the app's code in any ask. - **`factor recover`**, at a terminal, given a recovery code: a new seed and ten new codes replacing the @@ -428,25 +467,31 @@ needs the lost factor's code. So: - **The count left** is shown by `factor status` and in `authorisations`; the self-check warns when three or fewer remain. - **The last resort:** root on the control node — at it, or by its SSH key — runs `factor enrol - --break-glass` there. It talks to the controller locally, never over the bus; the same verb over the + --break-glass` there. It talks to the router locally, never over the bus; the same verb over the bus is refused. Recorded and announced as break-glass. -- **No tier requiring P2 is enabled until recovery codes exist.** The controller refuses the setting. +- **No tier requiring P2 is enabled until recovery codes exist.** The router refuses the setting. ## 11. The first holders ### Telegram — kind `telegram`, both seats, the away channel -- **Its own module**, holding the bot token and chat id as its own secrets; the router keeps no - Telegram client. +- **Its own module**, `telegram`, holding the bot token as its own secret; the router keeps no Telegram + client. *Amended 2026-10-08, ADR 0259:* **it runs on the control node under its own account `telegram`**, + never root or the operator's: the tool runner starts its bundle as that account, its state directory and + token belong to it, and the bus credential stays the runner's. If an agent holds root there, none of + this holds; the self-check raises a condition when the operator's account can become root without a + person. - **Long polling**, never a webhook; the update offset kept in its own state. -- **Buttons** carry only an opaque `a1::