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