From fb30b75f20502db896ebd63c5da8305f47231ea5 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 6 Oct 2026 16:39:11 +0200 Subject: [PATCH 1/2] ADR 0234, to-be 46: let the operator answer, and let only the controller act on an answer Research 028 graduates: the mesh needs to ask as well as tell, over channels that are seats rather than code inside one notifier, and an action a person must approve cannot rest on a desk click an agent can forge. TOTP verified by the controller is the proof; a key stays optional. Amends to-be 45 section 5 as ADR 0227 left it to. --- 00-META/glossary.md | 11 + .../00-overview.md | 6 +- ...cked-and-is-built-to-them-in-six-phases.md | 9 + ...-holds-a-conversation-with-its-operator.md | 326 ++++++++++++++ 02-DECISIONS/README.md | 1 + .../45-a-core-that-cannot-fail-silently.md | 21 +- .../46-the-conversation-with-the-operator.md | 410 ++++++++++++++++++ 03-DESIGN/01-to-be/README.md | 1 + 8 files changed, 777 insertions(+), 8 deletions(-) create mode 100644 02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md create mode 100644 03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md diff --git a/00-META/glossary.md b/00-META/glossary.md index 93686a3..adba5da 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -82,6 +82,17 @@ another — and a mesh you cannot name precisely is a mesh two people describe d capacity-1 seat is exclusive (one holder); a higher-capacity seat is a **bench** (several holders coexist). The first bench is `mesh-dns-resolver`, a *replicated* mesh seat: one holder per machine, each on record and each answering the same names ([ADR 0223](../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md)). + The second sort is the **kinded** bench: holders are different modules, each claiming one **kind**, + and a verb's subject carries the kind. `channel` and `intake` are the only ones + ([ADR 0234](../02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md)). +- **channel / intake** — the two kinded benches the mesh talks to its operator through: `channel` + sends, `intake` turns what arrives into one envelope. A holder of either declares **capabilities** + from the fixed vocabulary `channel-capabilities/1`. Not "notifier" (that is the desktop's node seat, + one holder of kind `desktop`) and not "bot" (that is one service's account). +- **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** (a verified sender, a TOTP code, optionally a security key's touch) + ([ADR 0234](../02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md)). - **claim** — a module taking a spot on a seat. `claims: [{name, scope}]` in a manifest. A mesh-scoped exclusive claim is how the mesh says "there is one of me". A foundation seat is named after the server it guards: the `mesh-controller`, `postgres` and `lavinmq` modules claim diff --git a/01-RESEARCH/028-the-meshs-output-channel/00-overview.md b/01-RESEARCH/028-the-meshs-output-channel/00-overview.md index 2780521..20d7a90 100644 --- a/01-RESEARCH/028-the-meshs-output-channel/00-overview.md +++ b/01-RESEARCH/028-the-meshs-output-channel/00-overview.md @@ -1,5 +1,5 @@ --- -status: active +status: graduated initiated: 2026-10-04 touches: - 04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md @@ -18,7 +18,9 @@ touches: - 02-DECISIONS/0228-a-value-given-by-hand-lives-only-until-its-modules-first-good-start.md - 02-DECISIONS/0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md - 02-DECISIONS/0232-a-binding-to-a-consumers-data-moves-only-by-a-person.md -became: [] +became: + - 02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md + - 03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md --- # 028 — The mesh's output channel diff --git a/02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md b/02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md index 78ae1c4..f548a38 100644 --- a/02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md +++ b/02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md @@ -144,6 +144,15 @@ answering back except through the mesh's own verbs; a message carries roles and address, a path or a secret, refused by the holder otherwise (028 Q8). Routing by presence, quiet hours, answering back and the external dead-man service stay open in 028, whose graduation amends to-be 45. +> **The mechanism changed — 2026-10-06, by [ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md).** +> What still stands: the output channel is built first in this minimal form, as Phase 1 of to-be 45, and +> everything above about conditions, deduplication, the content rule and the watcher. What moved, as +> this paragraph said it would on 028's graduation: "no answering back" no longer holds. The operator +> answers and is asked over channels that are holders of two kinded benches, `channel` and `intake`, +> not contributions to `operator-channel`, whose holder becomes the router; an answer that performs an +> action is checked and performed by the controller. The design is +> [to-be 46](../03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md). + **ADR 0224's provider standing becomes the first condition kind**, unchanged in what it says and when; its storage moves into the condition store. 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 new file mode 100644 index 0000000..c3d9b84 --- /dev/null +++ b/02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md @@ -0,0 +1,326 @@ +--- +topic: the mesh +status: accepted +date: 2026-10-06 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md +--- + +# 234. The mesh holds a conversation with its operator, over channels that are seats, and an answer that performs an action is authorised by the controller + +## Context + +**The output channel was built in its smallest form** ([ADR 0227](0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md), +[to-be 45](../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md) §5): one mesh seat, +`operator-channel`, whose single holder carries the router, a Telegram client and a desktop adapter in +one process, and **no answering back**. ADR 0227 left routing by presence, quiet hours, answering back +and the outside dead-man service open in [research 028](../01-RESEARCH/028-the-meshs-output-channel/00-overview.md), +"whose graduation amends to-be 45". This record is that graduation. + +The evidence, from 028: + +- **The mesh tells nobody.** 15 of the 236 issue reports say the fault was found because a person + happened to look; 74 describe something failing silently. On the day 028 opened the mesh knew of a + machine refusing every declaration for ninety minutes, a machine out of touch for ten minutes, and a + failure repeated thirteen times, and told nobody ([028/01](../01-RESEARCH/028-the-meshs-output-channel/01-what-the-mesh-already-knows.md)). +- **The built Telegram code has ten defects**, two of which silence exactly what the operator most + needs to hear: nothing bounds a message to Telegram's 4096 characters, so one long message wedges the + channel (D1); and a reopened urgent condition is said by an edit, which on Telegram rings nothing (D2) + ([028/04](../01-RESEARCH/028-the-meshs-output-channel/04-telegram-as-the-first-holder.md)). +- **Actions that need a person are ordinary verbs.** `retire approve`, `cleanup delete` and `pin` while + a binding is kept are callable by any principal granted the verb, agents included, and a hand-act + records the calling bus principal, not the person ([028/08](../01-RESEARCH/028-the-meshs-output-channel/08-asks-that-authorise.md)). + `retire approve` re-reads the set when it runs, so "approve what you were shown" + ([ADR 0230](0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md)) + does not hold end to end. +- **The desk cannot prove who clicked.** The graphical session is X11: any client of the display can + inject input and read keystrokes, `dunstctl action` invokes a notification's action for any program + of the account, and the desktop holder's bus credential is readable by every agent running as the + operator ([028/08](../01-RESEARCH/028-the-meshs-output-channel/08-asks-that-authorise.md)). + +The operator's directions, 2026-10-06: + +- Telegram is one channel among many to come; it simply fills a seat. Input (a mail, a message from the + operator) has the same shape. +- Each channel has capabilities, and they decide what may travel on it. +- Agents and modules must be able to **ask** the operator anything, not only for permission. +- The **work context** chooses the channel: in a Telegram conversation, Telegram; at the desk, the desk. +- Approving and rejecting must be possible through Telegram, and while talking to an agent through + Telegram, everything must be completable there. +- **There is no hardware security key.** The proof the operator gives is a code from the authenticator + app on the phone. A key may be added later and must never be required. + +### Against GENESIS + +- **Mission — intake, process, deliver; agents, some of whom are human.** A human agent "acts through a + shell, a desktop, a message from a phone". The conversation is that modality made a first-class part + of the mesh, for asking as well as telling. The distinction this record draws is **not** human versus + non-human as a category: it is which **identity** can present which **proof**. A spawned agent holds + no enrolled factor, so it cannot authorise; nor can a person without one. +- **Core value — failure must be loud.** A conversation that cannot carry something says so, and the + away channel is checked to carry every tier. +- **Core value — sovereignty.** Telegram is an outside dependency, taken deliberately: free, on both + phone platforms, no server of the mesh's own, and the only candidate that carries every tier today. + It holds a seat, so it is replaceable by a holder of the mesh's own (Matrix, once its push is + measured) without changing anything that speaks to the seat. It never sees a factor's secret. +- **Context — one human, usually asleep; nodes are personal and mobile.** Hence routing by where the + operator is, escalation when unanswered, quiet hours, and a watcher's watcher that does not depend on + the control node. +- **Effect — "when something genuinely needs a decision, you are asked, with the context, not a log + line".** That sentence is this record's purpose. + +Nothing in 028 conflicts with GENESIS. + +## Considered Options + +### Where a channel attaches + +1. **Each channel contributes itself to the output seat** (to-be 45 §5 as written). Rejected: a + contribution is content a holder places ([ADR 0210](0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md)); + a channel is running code with a secret, a connection and answers of its own. Making it fit puts + every channel's client back in the router. +2. **One seat per channel kind** (`telegram-channel`, `desktop-channel`, …). Rejected: the router must + learn every seat, and every new channel is a change to the router. +3. **Channel modules found by a manifest field and called by module address.** Rejected: callers use + seats, never modules ([ADR 0126](0126-a-module-declares-its-own-seats.md)). +4. **One notifier with every channel built in.** Rejected: shared secrets, shared failure, a release + of the router per channel. +5. **A per-service module with its own question or approval path** (a "Telegram module" that decides). + Rejected: it locks the conversation to one service, and the next channel repeats it. +6. **Two kinded benches, `channel` (out) and `intake` (in); a fixed capability vocabulary; the output + seat's holder as the router that orders by work context; an authorising layer held by the + controller.** Chosen. + +### How answers come in + +- **A webhook.** Rejected: it needs a public route into the mesh, and a stolen token can redirect it. +- **Long polling.** Chosen: outbound only; a stolen token can steal updates (visible as a conflict) but + cannot inject one. + +### Who performs an authorised action + +- **The channel module calls the authorising verb itself.** Rejected: the hand-act would name the + module, a compromised channel could act, and every channel would repeat the checks. +- **The asker performs it on hearing "yes".** Rejected: an agent relaying "the operator said yes" is + not an answer to anything. +- **The controller holds the ask, checks the answer and performs.** Chosen. + +### What proves the operator answered + +- **A click at the desk.** Rejected: on X11 an agent on the operator's account can produce it. +- **The screen's unlock or a fingerprint reader.** Rejected: only the local holder sees the result, + and an agent can forge what it reports. +- **A security key's touch (FIDO2), required.** Rejected: the operator has no key. Kept as an + optional proof for whoever enrols one; never required for any tier. +- **A TOTP code from the operator's authenticator, verified by the controller.** Chosen as the proof + every tier above acknowledge can rest on. +- **A verified Telegram sender**, through a holder no agent shares. Chosen as a proof for approve, never + enough alone for destroy. + +### Whether context may lower the bar + +- **Being at the desk, or in a recent conversation, counts as presence and so as proof.** Rejected: + presence proves someone was there, not who answered. +- **Context orders the channels that already qualify, and never makes one qualify.** Chosen. + +### The seat shape, against ADR 0223 + +[ADR 0223](0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md) made `mesh-dns-resolver` +the first bench, of the **replicated** sort (the same module, one holder per machine, every holder +answering the same), and said making another bench is a decision, recorded. + +- **Model channels as a replicated bench.** Rejected: the holders are different modules that answer + differently, and a send must reach one chosen holder, not any. +- **Model each holder as a node seat keyed by machine.** Rejected: a channel's identity is its service, + not where it runs. +- **A second sort of bench, kinded.** Chosen, decided here (below). + +## Decision + +### 1. The conversation + +**The mesh holds a conversation with its operator.** Three things are said: a **message** (the mesh +tells; no answer expected), an **ask** (someone wants the operator's input, of a declared kind), and an +**operator message** (the operator writes first). An input from outside that is not the operator (a +mail, a webhook) is the same envelope with another sender; this record decides its shape only, and its +consumers are later work. + +### 2. Channels and intake are kinded benches + +- **A kinded bench is the mesh's second sort of bench**: a mesh seat whose holders are **different + modules, each claiming one kind**. Two claims of one kind are refused at registration. A verb's + subject carries the kind, as a node seat's carries the machine. +- **`channel`** (out) and **`intake`** (in) are kinded benches, and the only ones. Making another is a + decision, recorded, as ADR 0223 requires of every bench. +- `channel` serves `send`, `edit` and `standing`, and emits `delivered` and `failed` (permanent or + transient). `intake` emits one envelope per input. A service read and written by one program is held + by one module claiming both seats under one kind. +- **A claim on a kinded bench carries `kind` and `capabilities`.** Capabilities come from the fixed, + versioned vocabulary **`channel-capabilities/1`**; a word outside it is refused. Each capability has + a contract test the holder's build runs and a drill its `standing` can run; one that fails its drill + is withdrawn from routing and reported until it passes. +- The vocabulary has three groups: delivering (`deliver`, `reaches-away`, `loud`, `silent`, `edit`, + `max-length:N`, `reaches-when-mesh-down`, `private`), conversing (`choice`, `reply`, `threads`, + `operator-first`) and trusting (`verified-sender`, `exact-render`, `code-factor`, `key-factor`). + +### 3. The router + +- **The holder of `operator-channel` is the conversation's router.** It loses its Telegram client to a + module of its own and keeps no channel's client in its process. +- **It routes by required capability, then by work context, then by severity**, and escalates along a + fixed chain when unanswered. When nothing can carry something, it says so as a condition of its own. +- **Context orders, never qualifies.** The work context chooses among channels whose capabilities + already satisfy the message or ask. It never adds one, and never lowers what an ask requires. +- **The content rule stays the router's**, applied before anything reaches any holder. A holder + declaring `private` is not exempted; exempting one is a later decision. +- **Presence is current state on the bus**: a value per machine and per intake kind, overwritten, never + a history, and never in a message's words. + +### 4. Asks + +- **Kinds:** `yes-no`, `one-of` (at most eight options), `text`, `number`, `date`, `acknowledge`, each + requiring its capabilities (`choice` or `reply`). +- **Life:** open → answered | defaulted | expired | cancelled. The first answer wins; every other copy is + edited to say where it was answered. A default is delivered marked as a default, never as the + operator's answer. +- **An authorising ask never defaults.** It expires. +- An asker holds **at most three open asks**; a fourth is refused in words. Asks count against the + router's hourly cap. Asks burst-batched to one channel go out together under one heading, each its + own message. **History is kept 30 days.** +- **The answer returns to the asker as an event**; an asker may also wait a bounded time, or poll. + +### 5. Asks that authorise + +**An ask whose answer performs an action is held by the controller.** The router carries it like any +other ask; only the controller performs. + +- **Every authorising verb declares its tier** in the controller's verb table (`authorises`: the tier, + and the arguments that make up the exact state the operator must see). +- **The controller's verbs:** `authorise request` (anyone may call it; it renders the ask, stores it + with a digest of the state shown, and performs nothing); `authorise answer` (granted to intake holders + only); `authorisations` (open and recent). +- **The proofs** that the operator, and not someone else, answered: + - **P1, a verified sender:** a tap from the operator's own Telegram 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 current or previous 30-second step, and accepted once; + - **P3, a security key's touch bound to the ask** — **optional**. It counts where a key is enrolled, + and no tier ever requires it. +- **The tiers:** + + | Tier | Required | On the away channel (Telegram) | At the desk | + |---|---|---|---| + | acknowledge | `choice`, `exact-render` | tap | click | + | approve | the above and **one** proof | tap (P1) | click **and a code** (P2) | + | destroy | the above and **two** proofs, at least one of them P2 | tap and a code (P1 + P2) | click, code and key touch (P2 + P3) where a key is enrolled; otherwise carried by the away channel | + + Acknowledge performs only what any granted principal may already do (silencing, announced); it is + not an authorisation, and needs no proof. +- **A desk click alone never authorises.** The desk declares no `verified-sender`; at the desk, approve + and destroy rest on a code the controller verifies (or a key touch, where enrolled). +- **Every tier is completable on the away channel.** The self-check verifies it every run; a setting + that would make a tier possible only at a desk is refused unless the operator chose it for that tier + explicitly. +- **The controller checks from its own records, never the request's claims:** the ask is open and + unexpired; the caller holds an intake kind; that kind's declared capabilities meet the tier; a P1 + sender is on the controller's list of the operator's identities; a code is valid and unused; a key + assertion verifies over this ask's challenge; and the state now has the digest it had when shown. + It refuses at the first failure, then performs as itself, closes the ask with a compare-and-set (a + second answer loses), and emits `ask-answered`. +- **No agent authorises.** An agent asks; the operator answers where they are. Agents' grants lose the + authorising verbs. Those verbs refuse direct calls except through `authorise answer`, or as + **break-glass** at the console with a code, recorded and announced as such. +- **`retire approve` takes `expect`**, the set it approves, and refuses if the set differs. +- **The hand-act records** `via` (kind and holder), `requested-by` (agent principal or condition key), + `ask` (its id) and `proofs` (which were present). `by` names the operator as that kind's identity. +- **The factors' secrets are the controller's own.** The TOTP seed is made by the mesh and shown once, + to a terminal, never through a channel or an event. Re-enrolling a factor, or changing the operator's + identities or the away channel, is a destroy ask. +- **A code travels by request and reply only**, from the holder to the controller; never in an + envelope or an event, and deleted from the conversation where the service allows. + +### 6. First holders + +- **Telegram** is the first holder of both seats and the away channel: its own module, kind + `telegram`, long polling, buttons, replies, codes by a forced reply, a linking verb; placed where no + agent runs as the operator, on which its `verified-sender` depends. +- **The desk** holds both seats as kind `desktop`, through the machine's `node-notifier` (gaining + actions) and `node-launcher` (a prompt for text, numbers, dates and codes). It carries every ordinary + ask with no account anywhere. +- **The watcher's watcher stays outside the seats**, with its own bot, on a machine that is not the + control node. An outside dead-man service is pinged by the self-check and by the watcher. + +### 7. The order of work + +**Fix D1–D4 of the built Telegram code first**, before the channel is configured; then the desk's +actions; then the seats and the router; then asks; then authorising with TOTP; then Telegram live. The +phases and their owners are in [to-be 46](../03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md). + +## Consequences + +- **To-be 45 §5 is amended**: the operator answers. ADR 0227's minimal form is the first step of this + design, not reversed; ADR 0227 left this to 028's graduation and carries a dated note pointing here. +- **The catalogue gains a second sort of bench**, `kind` and `capabilities` on a claim, two seats in + the closed set ([ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)), and the + vocabulary with its contract tests. +- **The shared library must publish on a seat's event subjects** (to-be 32 §1's open gap), and the + permission model must grant it; until then the seats' events cannot be emitted as designed. +- **`node-notifier.send` gains actions**, and the screen-lock holder emits lock and idle changes. +- **The controller grows**: the `authorises` field, three verbs, refusals on direct calls, the + hand-act fields, the operator's identities, the TOTP seed and enrolled keys as its own state, and a + self-check probe for the away channel. +- **Harder:** approving at the desk now costs picking up the phone for a code. That is the price of an + X11 desk shared with agents; it falls if agents run under an account of their own on a display that + isolates clients, which is noted, not decided. +- **A residual risk is accepted:** on X11 an agent can read a code as it is typed at the desk and race + to use it. Each code is accepted once and only within its step; the session leaving X11 closes it. +- **Telegram sees the words**, held to the content rule, and never a factor's secret. If the operator's + Telegram account is taken, approve is reachable (announced, reversible), destroy is not without the + code. +- **The predecessor wording in to-be 45** ("each a module contributing itself to the seat") is replaced + there, not here; this record's option 1 says why. + +## How it is checked + +| Rule | Checked by | +|---|---| +| A capability outside `channel-capabilities/1` is refused | catalogue registration test | +| Two holders claiming one kind on a kinded bench are refused; only `channel` and `intake` are kinded | catalogue registration test over the compiled seat set | +| Each declared capability has a contract test that runs in its holder's build | catalogue lint: a declared capability without its test fails the build | +| A capability failing its drill is withdrawn from routing and reported | router test with a holder whose drill fails | +| An ask goes only to channels whose capabilities satisfy it | router test | +| Context reorders but never adds a channel, and never lowers what an ask requires | router test: the same ask in every context yields subsets of the same qualified set | +| An authorising ask never defaults | router test | +| A cancelled or answered ask's other copies are edited | router test against channel doubles | +| A fourth open ask from one asker is refused | router test | +| Presence is never written as history and never in a message's words | router test over its state buckets; the content rule's test | +| A desk click alone never authorises | controller test: `authorise answer` from kind `desktop` with no code is refused for approve and destroy | +| A TOTP code is verified by the controller, once, within its step | controller tests: wrong code, used code, code from two steps back, all refused | +| Destroy needs two proofs, one of them a code; a key is never required | controller tests: P1 alone refused; P1 + P2 accepted; P2 + P3 accepted; no tier's requirement names P3 | +| `authorise answer` refuses a non-intake caller, a kind below the tier, an identity not on the list, a key assertion over another ask's challenge, a stale digest, a second answer | one controller test per refusal | +| No agent authorises; authorising verbs refuse direct calls | controller test: direct `retire approve`, `cleanup delete`, `pin` (while kept) refused without a code; catalogue check that no agent module's grant names an authorising verb | +| `retire approve` approves only the set shown | controller test with `expect` differing from the current set | +| The hand-act records `via`, `requested-by`, `ask`, `proofs` | controller test reading the hand-act after an authorised action | +| Every tier is completable on the away channel | self-check probe, every run; raises a condition when not | +| A code never appears in an event | intake holder contract test (`code-factor`) | +| D1–D4 are fixed before Telegram is configured | holder tests: 4097 characters arrive cut and said; a reopening is a new message; clearing edits the newest; warnings and clearings are silent | +| End to end | live drills: an agent's question answered at the desk; the same with the desk locked, answered on the phone; an approve and a destroy on a test condition, answered on the phone with a code, hand-acts read | + +## References + +- [Research 028](../01-RESEARCH/028-the-meshs-output-channel/00-overview.md), above all + [06](../01-RESEARCH/028-the-meshs-output-channel/06-a-conversation-with-the-operator.md), + [07](../01-RESEARCH/028-the-meshs-output-channel/07-the-work-context-and-the-desk.md), + [08](../01-RESEARCH/028-the-meshs-output-channel/08-asks-that-authorise.md) and the proposed record in + [09](../01-RESEARCH/028-the-meshs-output-channel/09-a-proposed-decision.md). +- [ADR 0227](0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md): the + minimal output channel this record grows, and the place it left answering back. +- [ADR 0223](0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md): the first bench, and + the rule that another is a recorded decision. +- [ADR 0230](0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md): + approve what you were shown; a timer is the mesh acting alone again. +- [ADR 0208](0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md): the desktop + notifier as a node seat. +- [Issue 187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md): the class. +- [To-be 46](../03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md): the design. diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index eaa86a3..83acc01 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -203,6 +203,7 @@ python3 00-META/checks/index.py fail if stale - **0229** — [The core's order is a lease the store remembers, and an epoch a machine is sent once it reads one](0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md) - **0230** — [A consumer the mesh stops asking for is retired, and deleted only by a person](0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md) - **0231** — [A healer acts on what observation raised, and only observation says it worked](0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md) +- **0234** — [The mesh holds a conversation with its operator, over channels that are seats, and an answer that performs an action is authorised by the controller](0234-the-mesh-holds-a-conversation-with-its-operator.md) ### Its tiers, from the bottom up diff --git a/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md b/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md index 7c5c635..884edbc 100644 --- a/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md +++ b/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md @@ -4,6 +4,7 @@ status: in-progress code: [mesh-controller, mesh-host, mesh-tools, mesh-catalog, mesh-sdk, mesh-lab] updated: 2026-10-06 decisions: + - 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 - 02-DECISIONS/0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md - 02-DECISIONS/0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md @@ -220,16 +221,20 @@ itself — an unanswered probe is never a pass. ## 5. The output channel, minimal form (rule 6) From [research 028](../../01-RESEARCH/028-the-meshs-output-channel/00-overview.md), the smallest form -that works; its open questions stay open and its graduation amends this section. +that works. **Amended 2026-10-06 by [ADR 0234](../../02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md)**, +028's graduation: this form is the first step of [to-be 46](46-the-conversation-with-the-operator.md), +the conversation with the operator, which replaces the points marked below. Phase 1 builds this +section; to-be 46 §10 builds what follows from it. - **One mesh seat, `operator-channel`**, held once. It **accepts** `notify` as a work queue, so a message waits for a holder; it keeps its **open messages** in its own key-value state, so a restart forgets nothing; it **serves** `open` and `history`. - **The holder consumes the controller's condition events** and decides what is sent. The controller calls nobody. -- **Two channels**, each a module contributing itself to the seat: **Telegram** (a bot to the - operator's chat; its token and chat id are the channel module's secrets) and the **desktop - notifier** of the machine the operator is at. +- **Two channels**: **Telegram** (a bot to the operator's chat) and the **desktop notifier** of the + machine the operator is at. *Amended:* a channel is not a contribution to this seat. Each is the + holder of a kind on the kinded benches `channel` and `intake`, a module of its own holding its own + secrets, and this seat's holder becomes the router (to-be 46 §2, §3, §6). - **A message** is: the condition's key, its subject (a machine's role, a module, a plan), kind, severity, the one-line summary, since when, and the verb that shows more. **Deduplicated by the key.** - **When:** on `condition-raised`; once more if still open after 1 hour (urgent) or 12 hours @@ -239,7 +244,10 @@ that works; its open questions stay open and its graduation amends this section. - **Rate:** at most twenty messages an hour; the excess is folded into one message naming them all. - **What may leave the mesh:** roles and words. A message carrying an address, a path or anything shaped like a secret is refused by the holder and raises `channel-refused` instead. -- **No answering back** in this form; acknowledging is `conditions silence`, through the mesh. +- **Answering back.** *Amended:* the minimal form had none, and acknowledging was `conditions + silence`, through the mesh. The operator now answers and is asked over the intake seat; an answer + that performs an action is checked and performed by the controller (to-be 46 §4, §7). `conditions + silence` stays callable. - **The watcher's watcher.** A module, `mesh-watcher`, assigned to one machine that is not the control node, holds the Telegram channel's secret too. It listens for the self-check heartbeat (S10) and for the bus itself; when either has been silent past its bound it sends to Telegram **directly over @@ -533,5 +541,6 @@ a new core issue cannot resolve without a replay or a stated reason. - The bus as a cluster of three, to upgrade it live. - Routing by presence, quiet hours, answering back through a channel, and an external dead-man - service — research 028. + service — decided by [ADR 0234](../../02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md), + designed in [to-be 46](46-the-conversation-with-the-operator.md). - A condition that needs judgement handed to an agent as work — research 017. 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 new file mode 100644 index 0000000..b415c35 --- /dev/null +++ b/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md @@ -0,0 +1,410 @@ +--- +layer: to-be +status: designed +code: [] +updated: 2026-10-06 +decisions: + - 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 +--- + +# 46 — The conversation with the operator + +**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), +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. +That form stays the first step; §10 below is the order in which it becomes this one. + +## The parts + +``` + sources ROUTER (holder of operator-channel) channel bench (out) + ─────── ─────────────────────────────────── ─────────────────── + 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) + 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 + + watcher's watcher (outside the seats, own bot, not on the control node) ──► Telegram directly + self-check and watcher ──► outside dead-man service +``` + +## 1. The three things said + +- **A message:** the mesh tells. A condition raised, a reminder, a clearing, a notice from a module. + No answer is expected. It may be edited later. +- **An ask:** someone wants the operator's input, of a declared kind (§4). The answer goes back to + whoever asked. +- **An operator message:** the operator writes first — to an agent, to the mesh, or answering an ask in + its thread instead of tapping. + +Input from outside that is not the operator (a mail, a webhook) is the same envelope (§3) with +another sender. Only its shape is designed here; its consumers are later work. + +## 2. The `channel` seat (out) + +**A mesh seat, kinded bench.** A kinded bench is the second sort of bench, beside the replicated one +([ADR 0223](../../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md)): its +holders are different modules, each claiming one **kind** (`telegram`, `desktop`, later `matrix`, +`ntfy`, `mail`, …). Two claims of one kind are refused at registration. A verb's subject carries the +kind, as a node seat's carries the machine. A new channel is a new module claiming a new kind, with no +change to the router. `channel` and `intake` are the only kinded benches. + +**A claim on it carries** `kind` and `capabilities` (§5). + +**Served:** + +- **`send`** — the words, a priority, whether silent, an optional ask block (the ask's id, its kind, + the options each with an opaque token) and an optional thread (a conversation handle, or the message + this replies to). Answers the channel's own id for what it sent, or a refusal in words. +- **`edit`** — replace a sent message by its id, where `edit` is declared. +- **`standing`** — ready; not configured (naming what is missing, never a value); or failing (since + when, why); the last delivery; the declared capabilities and the result of each drill. + +**Emitted:** `delivered`; `failed`, marked **permanent** or **transient**. + +**Honoured by every holder:** its declared maximum length, by cutting and saying so; silence where +declared; an identical edit is a success; no secret of its own in any error or event. + +## 3. The `intake` seat (in) + +**A mesh seat, kinded bench**, as `channel`. A service read and written by one program (a Telegram +bot) is held by one module claiming both seats under one kind. + +**A holder turns what arrives into one envelope:** + +| Field | What it is | +|---|---| +| `id` | unique, for deduplication | +| `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 | +| `operator` | whether that identity is on the **controller's** list of the operator's identities — filled from that list, never from the holder's own | +| `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** (§7) | +| `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. **Everything else is an event on the seat**, +taken by `what`: the router takes `seen`, an agent bridge takes `message`, a later mail rule takes +`mail`. + +**A prerequisite:** the shared library publishes on a seat's event subjects, and the permission model +grants it (the open gap of [to-be 32](32-what-a-module-declares.md) §1). + +## 4. Asks + +### Kinds + +| Kind | The operator gives | The channel needs | +|---|---|---| +| `yes-no` | yes or no | `choice`, or `reply` read as yes or no | +| `one-of` | one of at most eight labelled options | `choice`, or `reply` with the option's number | +| `text` | free text | `reply` | +| `number`, `date` | a value within bounds | `reply`; parsed by the router, asked again once if it does not parse | +| `acknowledge` | "seen" | `choice` | + +An **authorising** ask is any of these with the authorising flag; it adds the tier's requirements (§7). +`text`, `number` and `date` never authorise. + +### What an ask carries + +An id; the asker (bus principal and machine); the kind with its options or bounds; the words; a +priority (urgent or normal); optionally a timeout and a default; optionally a conversation handle; +optionally a group, for batching. The words pass the content rule. + +### Life + +**open → answered | defaulted | expired | cancelled.** + +- **Answered:** the first answer wins. Every other copy is edited to say where it was answered. +- **Defaulted:** the timeout passed and a default was declared. The asker receives it marked as a + default, never as the operator's answer. +- **Expired:** the timeout passed with no default; the asker is told. **An authorising ask never + defaults; it expires** (ADR 0230: a timer is the mesh acting alone again). +- **Cancelled:** by the asker (`ask cancel`), or by its owner when moot (the condition behind it + cleared). Copies are edited to "no longer needed". + +### Limits, batching, history + +- An asker holds **at most three open asks**; a fourth is refused in words. +- Asks count against the router's hourly cap, as messages do; answers do not. +- Asks to one channel within the burst window go out together under a heading ("3 questions waiting"), + each as its own message, answerable and editable alone. +- **`asks`** lists open asks and closed ones for **30 days**: asker, kind, outcome, channel, answer. A + free-text answer stays in the router's state and is never forwarded to another channel. + +### How the asker gets the answer + +The event **`ask-answered`**, with the ask's id and any conversation handle. An asker may also call +`ask` with a bounded wait (a few minutes), or poll `asks `. + +### 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 (§7) and carried +by the router like any other. + +## 5. The capability vocabulary, `channel-capabilities/1` + +**Fixed and versioned.** A word outside it is refused at registration. **Each word has a contract test** +the holder's build runs, and a **drill** `standing` can run. A capability that fails its drill is +withdrawn from routing and reported until it passes. A new word is a new version of the vocabulary. + +| Group | Capability | Promise | Contract test / drill | +|---|---|---|---| +| delivering | `deliver` | it arrives, or `failed` says why | against a test double / a live test message | +| | `reaches-away` | it reaches a phone away from the operator's machines | by kind / the operator acknowledges a drill | +| | `loud` | it can break through the phone's quiet hours | the service's override is set for urgent | +| | `silent` | it can arrive without sound | the service's silent flag is set | +| | `edit` | a sent message can be replaced in place | edit and read back, against a double | +| | `max-length:N` | up to N characters arrive whole; longer is cut and the cut said | N+1 characters give a cut message, not a failure | +| | `reaches-when-mesh-down` | delivering needs neither the bus nor the control node | the holder's placement and send path | +| | `private` | the words stay on the operator's machines, or are end-to-end encrypted | by kind; reviewed | +| conversing | `choice` | one offered option in one act, and the pick comes back | a simulated tap yields a `choice` envelope with the option's token | +| | `reply` | free text comes back | a simulated reply yields a `reply` envelope | +| | `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 | + +What the first holders declare: + +| Kind | Declares | +|---|---| +| `telegram` | `deliver`, `reaches-away`, `silent`, `edit`, `max-length:4096`, `choice`, `reply`, `threads`, `operator-first`, `verified-sender` (only while placed where no agent runs as the operator), `exact-render`, `code-factor` | +| `desktop` | `deliver`, `loud`, `silent`, `edit`, `private`, `choice`, `reply` (through the launcher's prompt), `threads`, `exact-render`, `code-factor`; `key-factor` only where a key is enrolled and present. **Never `verified-sender`.** | + +## 6. Routing, the work context and presence + +### The rule + +**A message or ask goes to the most direct channel in the operator's current context, among those whose +capabilities already satisfy it. Unanswered in time, it escalates along a fixed chain. Context orders +the candidates; it never adds one, and never lowers the bar.** When nothing qualifies, the router raises +a condition of its own saying so. + +### The signals + +| Signal | Source | Read as | +|---|---|---| +| A graphical session unlocked with input in the last 5 minutes on machine M | the `node-lock-screen` seat's holder on M, emitting an event on each lock and idle change (logind's lock and idle hints underneath) | at the desk on M | +| That session locked, or idle longer | the same | not at the desk | +| An agent asking from machine M | the ask's asker | strengthens "at the desk on M"; alone it proves nothing | +| A verified intake `message`, `reply` or `choice` in the last 15 minutes | the intake seat | in a conversation on that kind | +| An ask carrying a conversation handle | the ask | that conversation, whatever else is true | +| The hour, against quiet hours | the router's setting | night: only urgent wakes | + +### Where things go + +"The away channel" is the operator's setting, Telegram to begin with. "The loud holder" is an optional +second away holder for waking. + +| Context | An ask | Urgent message | Warning | Unanswered → | +|---|---|---|---|---| +| in a conversation through Telegram | that chat, in the thread | that chat | that chat, silent | after 10 min (urgent) or 1 h: also the desk, if active | +| at the desk on M | the desk on M if it can carry it; else the away channel, and the desk says where it went | the desk on M, and the away channel silently | the desk on M | after 5 min (urgent) or 30 min: the away channel, with sound | +| away | the away channel | the away channel | the away channel, silent | after 15 min (urgent): the loud holder, if configured | +| night, away | non-urgent asks wait for morning; urgent as away | the away channel and the loud holder | the morning digest | as away | +| the desk locks while an ask is shown there | moves at once to the away channel | — | — | — | + +All bounds are provisional, set as to-be 45's are: from what the first live weeks measure. + +### Presence stays in the mesh + +Presence facts are events on the bus. The router keeps them as **current state only** — one value per +machine and per intake kind, overwritten — never as a history, and never in a message's words. A +consumer that wants a timeline of the operator's day is a decision of its own. + +### The content rule + +The router's, applied before anything reaches any holder: roles and words, never an address, a path or +anything shaped like a secret (to-be 45 §5). A `private` holder is not exempted. + +## 7. Asks that authorise + +### The verb table and the controller's verbs + +- **The controller'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. + +### What may be authorised, and its tier + +| Action | Tier | +|---|---| +| approve or reject a waiting retirement set (`retire approve` / `retire reject`) | approve | +| confirm a kept binding moves (`pin`, while `binding-kept` names it) | approve | +| end a stuck plan; send a machine its declaration by hand; reset a bus consumer; retry a healer | approve | +| silence a condition | acknowledge | +| delete a retired consumer's data, one or by age (`cleanup delete`) | destroy | +| change the operator's identities, the away channel, or a factor's enrolment | destroy | + +### Proofs and tiers + +- **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 + 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 + state digest; user presence required). **Optional:** counted where a key is enrolled; required by no + tier. + +| Tier | Required | Telegram (away) | Desk | +|---|---|---|---| +| acknowledge | `choice`, `exact-render` | tap | click | +| approve | + one proof | tap (P1) | click + code (P2) | +| destroy | + two proofs, at least one P2 | tap + code (P1 + P2) | click + code + key touch (P2 + P3), only with an enrolled key; otherwise carried by Telegram | + +- Approve: single use, bound to the exact state shown, expiring when that state changes or after 24 h. +- Destroy: valid 10 minutes after it is shown; at most one destroy answered per 10 minutes. +- **A desk click alone never authorises.** An agent at the operator's terminal answers nothing; the + console offers only break-glass with a code. + +### The rules + +1. Every authorising verb declares its tier in the verb table. +2. An authorising ask is offered only on a channel whose capabilities satisfy its tier. An answer from + any other channel is refused, and the refusal is said there. +3. **The away channel satisfies every tier.** The self-check verifies it every run. A setting making a + tier possible only at a desk is refused, unless the operator chose that for the tier explicitly. +4. The work context chooses among channels that qualify; it never makes one qualify. +5. **No agent authorises.** An agent asks. Agents' grants hold no authorising verb. + +### The controller'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 + present are enough. +4. A P1 answer's sender is on the controller's list of the operator's identities for that kind. +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. + +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. + +### 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 console 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. + +### The hand-act + +Gains **`via`** (kind and holder), **`requested-by`** (agent principal or condition key), **`ask`** (the +id) and **`proofs`** (which of P1, P2, P3). `by` reads "the operator, as identity ". + +### The factors + +The operator's identities, the TOTP seed 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 channel or an +event. Re-enrolling a factor is a destroy ask. A code travels only by request and reply from an intake +holder to the controller, and is deleted from the conversation where the service allows. + +## 8. 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. +- **Long polling**, never a webhook; the update offset kept in its own state. +- **Buttons** carry only an opaque `a1::