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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+9
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/<next kind>
|
||||
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 <id>`.
|
||||
|
||||
### 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 <kind> identity <id>".
|
||||
|
||||
### 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:<ask>:<option>` (Telegram allows 64 bytes); everything else is
|
||||
in the router or the controller. A tap is dropped and reported unless both the user and the chat are
|
||||
the operator's; otherwise it is acknowledged at once (the phone shows a spinner until then), handed
|
||||
on, and the message edited to the outcome.
|
||||
- **A destroy ask** answers the tap with a forced-reply prompt for the code; the holder reads it,
|
||||
deletes it from the chat, and hands it to the controller by request and reply.
|
||||
- **A linking verb** with a one-time deep-link code replaces reading the chat id by hand.
|
||||
- **Placement:** where no agent runs as the operator. Its `verified-sender` depends on it, and the
|
||||
self-check reads the placement.
|
||||
|
||||
### The desk — kind `desktop`, both seats
|
||||
|
||||
- **`node-notifier.send` gains actions** (token, label). It still answers at once; the dunst holder
|
||||
listens for the chosen action and emits it as an event on its node seat.
|
||||
- The router's desktop adapter, holding `channel/desktop` and `intake/desktop`, turns that into a
|
||||
`choice` envelope.
|
||||
- **For `text`, `number`, `date` and codes**, the notification's single action opens the
|
||||
`node-launcher` seat's prompt; what is typed returns as a `reply` — or, for a code, straight to the
|
||||
controller by request and reply.
|
||||
- **The screen-lock holder emits lock and idle changes** for the work context.
|
||||
- It carries every ordinary ask with no account anywhere, and an authorising one only with a code (or
|
||||
an enrolled key's touch).
|
||||
|
||||
### The watcher's watcher
|
||||
|
||||
Outside the seats, deliberately: it must speak when the bus or the control node is what failed. A
|
||||
minimal sender with its own bot, on a machine that is not the control node; it never reads and never
|
||||
asks. The self-check and the watcher both ping an outside dead-man service, which speaks when both are
|
||||
silent.
|
||||
|
||||
## 9. The defects to fix first
|
||||
|
||||
Found in the built Telegram code of the output seat's holder and the watcher
|
||||
([028/04](../../01-RESEARCH/028-the-meshs-output-channel/04-telegram-as-the-first-holder.md)).
|
||||
|
||||
| # | What | Fix | Weight |
|
||||
|---|---|---|---|
|
||||
| D1 | nothing bounds a message to 4096 characters; one long message wedges the channel | cut at the declared maximum and say so; a refused send never blocks those behind it | high |
|
||||
| D2 | a condition reopened within ten minutes is said by an edit, which rings nothing | a reopening is a new message | high |
|
||||
| D3 | clearing edits only the first message; the newest still says "still open" | clearing reaches the newest message | medium |
|
||||
| D4 | `disable_notification` is never set | warnings and clearings are silent; urgent rings | medium |
|
||||
| D5 | a 429's `retry_after` is ignored | wait as told | low |
|
||||
| D6 | refusals (400, 403) are retried as transport failures; "not modified" on an edit sends a duplicate | refusals are permanent `failed`; an identical edit is a success | low |
|
||||
| D7 | a deprecated preview flag | dropped | low |
|
||||
| D8 | a marshalling error is discarded | reported locally | low |
|
||||
| D9 | the watcher's unsent "silent" is overwritten by its "cleared" | say both, or one line naming how long it was silent | low |
|
||||
| D10 | "ready" means a token and a chat id, not that the bot can reach the operator | `standing` checks the chat at status time | low |
|
||||
|
||||
## 10. The build, in order
|
||||
|
||||
Each phase ends at its own *done when*. Owning repositories from [`repos.md`](../../00-META/repos.md).
|
||||
|
||||
| Phase | What | Owner | Done when |
|
||||
|---|---|---|---|
|
||||
| **1 — Fix the first holder in place** | D1–D4 in the output seat's holder and the watcher, then D5–D10; no change of shape. The operator then makes the two bots and the mesh is given their values through the controller | mesh-catalog | the holder's tests for D1–D4 pass; a live test message reaches the phone from both the holder and the watcher |
|
||||
| **2 — The desk's actions** | `node-notifier.send` gains actions; the dunst holder emits the chosen one; the launcher's prompt returns typed text; the screen-lock holder emits lock and idle changes | mesh-catalog | a notification with two actions returns the chosen token as an event; locking emits an event |
|
||||
| **3 — The seats, and the router** | the kinded bench in the seat set; `kind` and `capabilities` on a claim; `channel-capabilities/1` and its contract tests; publishing on a seat's event subjects and its grant; `channel` and `intake` seats; the output seat's holder becomes the router and holds `channel/desktop` and `intake/desktop`; the content rule and presence as current state | mesh-controller (seat set, claim fields, registration refusals, grants); mesh-sdk and mesh-tools (seat-event publishing in the shared library and the node tools); mesh-catalog (the seats' definitions, the router) | the catalogue refuses an unknown capability and a second holder of one kind; a message is routed to the desk by capability and context |
|
||||
| **4 — Asks** | `ask`, `ask cancel`, `asks`, `answer`; the events; kinds, life, limits, batching, history; escalation; the agent bridge for operator messages | mesh-catalog | the router tests of ADR 0234 pass; live: an agent's question answered at the desk, and with the desk locked, on the phone |
|
||||
| **5 — Authorise, with TOTP** | `authorises` in the verb table; `authorise request`, `authorise answer`, `authorisations`; the seven checks; direct calls refused, break-glass; `retire approve` takes `expect`; the hand-act fields; the operator's identities and the TOTP seed as the controller's state, enrolled at a terminal; optional key enrolment; the self-check probe for the away channel; agents' grants lose the authorising verbs | mesh-controller; mesh-catalog (agents' grants, the desk's code prompt) | one controller test per refusal passes; at the desk, approve needs a code |
|
||||
| **6 — Telegram live** | the `telegram` module holding both seats, with buttons, replies, forced-reply codes and the linking verb, placed where no agent runs as the operator; the operator's two bots configured; the watcher assigned off the control node; the dead-man ping | mesh-catalog | the self-check says the away channel carries every tier; live drills: an approve and a destroy on a test condition answered on the phone, with the hand-acts read |
|
||||
|
||||
**Not touched:** the node engine (mesh-host repository). Presence comes from the screen-lock seat's
|
||||
holder, not the engine; if a later measure shows the engine must relay it, that is a change here.
|
||||
|
||||
## What is not decided here
|
||||
|
||||
- Whether a `private` holder may be exempted from the content rule.
|
||||
- Further holders: Pushover for waking, Matrix once its push is measured, mail out for the digest and
|
||||
mail in as an intake; each is a module claiming a kind, with no change to this design.
|
||||
- Consumers of non-operator input (mail rules, webhooks).
|
||||
- Agents running under an account of their own on a display that isolates clients, which would let the
|
||||
desk earn `verified-sender`.
|
||||
@@ -46,6 +46,7 @@ document is written and this one's status becomes `implemented`.
|
||||
| [`41-the-shell-and-the-accounts-environment.md`](41-the-shell-and-the-accounts-environment.md) | **In progress.** The shell and the account's environment as modules: an environment module every module contributes variables and `PATH` entries to, shell code contributed to the login shell in named slots, the prompt and plugins as modules, the host giving a login back, and the service manager's module finished | [ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md), [ADR 0204](../../02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md), [ADR 0205](../../02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md) |
|
||||
| [`42-the-machines-modules-in-order.md`](42-the-machines-modules-in-order.md) | **In progress.** The order the machines' modules of research 026 and 027 are built and rolled out: every machine's first (sudo, localization, time sync, pacman, logrotate, avahi, systemd, docker, `~/.ssh`, scripts, kernel), then both workstations', then one machine model's; each proven on one workstation before the rest | [ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md), [ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md), [ADR 0205](../../02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md) |
|
||||
| [`45-a-core-that-cannot-fail-silently.md`](45-a-core-that-cannot-fail-silently.md) | **Designed.** The core says when it is wrong, refuses what is stale or unreadable, heals what it knows, upgrades one machine at a time with a witness that rolls it back, and is checked against the real mesh before merge: the writers and signals tables, the condition store, `doctor`, the minimal output channel, healers, the lease and report order, staged upgrades, the facts snapshot and replays, in six phases | [ADR 0227](../../02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md), [ADR 0224](../../02-DECISIONS/0224-a-provider-that-keeps-failing-a-consumer-is-a-problem-the-controller-reports.md), [ADR 0218](../../02-DECISIONS/0218-a-plan-sends-grants-before-code-rolls-out-one-machine-first-and-a-newer-merge-takes-over-an-older-plan.md) |
|
||||
| [`46-the-conversation-with-the-operator.md`](46-the-conversation-with-the-operator.md) | **Designed.** The mesh tells and asks its operator over channels that are holders of two kinded benches, `channel` and `intake`, declaring capabilities from a fixed vocabulary; the router orders them by work context and never lowers the bar; an answer that performs an action is checked and performed by the controller, on a TOTP code or a verified Telegram sender, never on a desk click alone; Telegram first, in six phases | [ADR 0234](../../02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md), [ADR 0227](../../02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md) |
|
||||
|
||||
## Not yet written
|
||||
|
||||
|
||||
Reference in New Issue
Block a user