Accept ADR 0259 with the operator's two decisions, and amend to-be 46 to it
This commit is contained in:
+6
-6
@@ -443,10 +443,10 @@ it should not. **Recorded by** the controller. **Upstream of** Provisioning and
|
||||
## Operator and conversation — the person the mesh works for
|
||||
|
||||
**Purpose.** To let the mesh and its operator talk: tell, ask, answer, and act only on an answer it can
|
||||
trust. **Recorded by** the router and the controller (authorising asks).
|
||||
trust. **Recorded by** the router (asks, warrants and the operator's identities).
|
||||
**Downstream of** Health and repair, Change and delivery, and Identity and access. An authorising
|
||||
answer acts in whichever domain asked, through that domain's own verb; the conversation owns the
|
||||
asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0272.
|
||||
asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272.
|
||||
**Uses:** operator, person, proof (Identity and access), release (Change and delivery).
|
||||
|
||||
- **mesh MCP server** — the endpoint on a node's loopback through which every agent and person on that
|
||||
@@ -476,12 +476,12 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0272.
|
||||
the condition beside its **summary**, which keeps the plans, commits and verbs for whoever looks closer
|
||||
([ADR 0253](../02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md)).
|
||||
- **ask** — a request for the operator's input, of a declared kind (yes-no, one-of, text, number,
|
||||
date, acknowledge). An **authorising ask** is one whose answer performs an action; the controller
|
||||
holds it and checks its proofs (ADR 0234). *Proposed in ADR 0259:* the router holds every ask, and
|
||||
the asker performs the chosen option on a **warrant**.
|
||||
date, acknowledge). An **authorising ask** is one whose chosen option its asker performs. The router
|
||||
holds every ask, checks each answer's proofs and issues a **warrant**; the asker acts on it with its
|
||||
own grants (ADR 0234, ADR 0259).
|
||||
- **warrant** — the router's record that a person chose one option of one ask: the ask, the option,
|
||||
who chose (the operator, as a channel kind's identity), through which channel, the proofs and when,
|
||||
published for that asker alone. The only thing an asker acts on (ADR 0259, proposed).
|
||||
published for that asker alone. The only thing an asker acts on (ADR 0259).
|
||||
- **operator message / input** — what arrives on `intake`, once the router has checked the sender
|
||||
against the controller's list of the operator's identities: a **trusted** one is an operator message,
|
||||
addressed to an agent by `@name` or thread or else to the **responder** (`@mesh`, the router's own
|
||||
|
||||
@@ -289,6 +289,12 @@ consumers are later work.
|
||||
|
||||
### 8. Asks that authorise
|
||||
|
||||
> **Who holds the ask changed — 2026-10-08, by [ADR 0259](0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md).**
|
||||
> What stands: the tiers, the proofs, the desk never authorising, and every tier completable on the away
|
||||
> channel. What moved: the router, not the controller, holds authorising asks, the operator's identities
|
||||
> and the factors, and judges each answer; the asker performs the option itself on a warrant. The
|
||||
> controller's verbs `authorise request`, `authorise answer` and `authorisations` below are replaced there.
|
||||
|
||||
**An ask whose answer performs an action is held by the controller.** The router carries it like any
|
||||
other ask; only the controller performs.
|
||||
|
||||
|
||||
+56
-12
@@ -1,6 +1,6 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: proposed
|
||||
status: accepted
|
||||
date: 2026-10-08
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
@@ -50,6 +50,15 @@ The operator's directions, 2026-10-08, on the first proposal (a Telegram path th
|
||||
answer is recorded once, the asker reads its request's state from a bucket, and the bus's permissions
|
||||
decide who may publish what. Events carry no secret; request and reply may. No HTTP between modules.
|
||||
|
||||
The operator approved this record on 2026-10-08, with the word *warrant*, and decided two points it had
|
||||
left open:
|
||||
|
||||
- **The `telegram` module runs on the control node under a service account of its own, not the
|
||||
operator's.** Claude Code runs as the operator on every machine; the service account keeps an agent away
|
||||
from the bot token and the channel's state.
|
||||
- **A newly linked identity waits one hour before it can approve**; acknowledge works at once. Every link
|
||||
and unlink is announced on every channel. This holds until a code from an authenticator app exists.
|
||||
|
||||
### Against GENESIS
|
||||
|
||||
- **Core value — sovereignty.** Telegram stays an outside dependency taken deliberately (ADR 0234), and
|
||||
@@ -158,7 +167,7 @@ state bucket, the EVENTS stream). `<asker>` is the asking module's name; `<kind>
|
||||
| a warrant, or an ask's end without one | `mesh.seat.operator-channel.event.decided.<asker>`, message id `<asker>.<id>` | EVENTS (7 days), deduplicated by the message id | the router only | the asker, its own name only, through its durable consumer |
|
||||
| show, edit or say on a channel | `mesh.seat.channel.accept.<show\|edit\|send>.<kind>` | the seat's work queue `SEAT_CHANNEL`, one worker per kind | the router | the holder of that kind only |
|
||||
| an answer chosen, a link asked | `mesh.seat.intake.event.<choice\|link>.<kind>` | EVENTS | the holder of that kind only | the router |
|
||||
| a code typed (a link code; later a TOTP code) | `mesh.seat.intake.proof.<kind>`, core request and reply | nothing: never on a stream | the holder of that kind only | the router, which answers |
|
||||
| a code typed (a link code; later a TOTP code) | `mesh.seat.intake.proof.code.<kind>`, core request and reply | nothing: never on a stream | the holder of that kind only | the router, which answers |
|
||||
|
||||
**The grants, composed by the controller as it composes every bus account:**
|
||||
|
||||
@@ -170,8 +179,11 @@ state bucket, the EVENTS stream). `<asker>` is the asking module's name; `<kind>
|
||||
claims of one kind are refused. A holder subscribes its own kind's accepts, publishes its own kind's
|
||||
events and proofs, and nothing of another kind. Each kind has its own worker on the seat's queue, so a
|
||||
channel that is offline keeps its work and holds up no other.
|
||||
- **A seat's `proof` verbs** are core request and reply on `mesh.seat.<seat>.proof.<kind>`: no stream
|
||||
covers them, so a code is never persisted (the mesh's rule: events carry no secret).
|
||||
- **A seat's `proof` verbs** are core request and reply on `mesh.seat.<seat>.proof.<verb>.<kind>`: no
|
||||
stream covers them, so a code is never persisted (the mesh's rule: events carry no secret). A holder
|
||||
asks with its own kind; the modules that watch the seat answer.
|
||||
- **The seat declares all of it**: `kinded`, the verbs named `by-caller`, its `proofs`, and the holder's
|
||||
`records` its users read under their own name. Only `channel` and `intake` may be kinded.
|
||||
- **A record readable by its asker.** The router's `asks` bucket is read by an asker under its own prefix
|
||||
only: direct get on `$KV.<bucket>.<asker>.>`.
|
||||
- **No `answer` tool.** A tool runner may call any tool, so an answer that arrived as a tool call could
|
||||
@@ -236,11 +248,37 @@ met, and when. It carries no secret.
|
||||
the router links the identity when it matches, and says so on every channel.
|
||||
- **One identity per kind.** A second link of a kind is refused until the first is unlinked. Unlinking
|
||||
(a tool of the router) is said on every channel, the unlinked one included.
|
||||
- **A new link answers `acknowledge` at once and `approve` only after one hour.** The hour runs from the
|
||||
moment the link is made, is kept in the identity's record, and is said in the announcement ("it can
|
||||
approve from 15:40"). An approve answer inside the hour is refused and said on the channel. Decided by
|
||||
the operator on 2026-10-08, until an authenticator-app code exists.
|
||||
- **Once a TOTP factor exists**, linking and unlinking are destroy asks (ADR 0234 §8).
|
||||
- **A residual risk is accepted until then:** on the X11 desk an agent can read the code and send it from
|
||||
an account of its own. It is said loudly on every channel when it happens, and the factor closes it.
|
||||
an account of its own. It is said loudly on every channel when it happens, the hour gives the operator
|
||||
time to unlink it before it can approve anything, and the factor closes it.
|
||||
|
||||
### 8. What is superseded in ADR 0234
|
||||
### 8. The `telegram` module's own account
|
||||
|
||||
Decided by the operator on 2026-10-08. The bot token and the channel's state are kept from every agent,
|
||||
because every agent runs as the operator.
|
||||
|
||||
- **Its process runs as the account `telegram`**, which the module makes (a `user` resource: no shell, no
|
||||
login, home in its state directory). The tool runner, which runs as root, starts the module's bundle
|
||||
as that account when the bundle's environment names it (`MESH_RUN_AS`); it refuses to start a bundle as
|
||||
root or as the operator's account.
|
||||
- **Its state directory is owned by `telegram`, mode 0700, and its own secret `telegram-token` by
|
||||
`telegram`, mode 0600** (the manifest's `owner` and `secrets-owner`). The operator's account can read
|
||||
neither, and cannot attach to a process of another account.
|
||||
- **The bus credential stays the tool runner's**, readable by root only. The bundle reaches the bus only
|
||||
through its standard input and output, and the runner publishes for it only the kind it claims.
|
||||
- **An agent reaches the runner only through the mesh MCP server's five tools**, which make tool calls.
|
||||
No tool publishes an intake event or a proof: those come only from the bundle's own channel.
|
||||
- **What remains if an agent has root on the control node:** nothing here holds. Root reads the token,
|
||||
the runner's bus credential and the router's state, and can publish as anything. The boundary rests on
|
||||
agents not holding root there; whether the operator's account may become root without a person is
|
||||
checked by the self-check, which raises a condition when it can.
|
||||
|
||||
### 9. What is superseded in ADR 0234
|
||||
|
||||
- **§8 "an ask whose answer performs an action is held by the controller", the controller's verbs
|
||||
`authorise request`, `authorise answer` and `authorisations`, and the controller performing:** the
|
||||
@@ -251,20 +289,23 @@ met, and when. It carries no secret.
|
||||
- Everything else in ADR 0234 stands: the tiers and proofs, the kinded benches, the content rule, the
|
||||
desk never authorising, long polling, every tier completable on the away channel.
|
||||
|
||||
### 9. The first channels and the order of work
|
||||
### 10. The first channels and the order of work
|
||||
|
||||
1. **The controller:** the kinded bench with `kind` on a claim; the `channel` and `intake` seats; the
|
||||
caller-named and per-kind grants; `proof` subjects; the asker's read of its own prefix.
|
||||
2. **The shared library (mesh-sdk):** an asker's client (ask, cancel, read its state, take its warrants)
|
||||
2. **The tool runner (mesh-tools)** carries a bundle's seat traffic: publishing an accept or an event
|
||||
with its own name or kind, taking a held seat's work, asking and answering a proof, a compare-and-set
|
||||
on its state, and starting a bundle as its own account.
|
||||
3. **The shared library (mesh-sdk):** an asker's client (ask, cancel, read its state, take its warrants)
|
||||
and a channel's (take show, edit and send for a kind; publish a choice or a link; send a proof).
|
||||
3. **The messenger** becomes the router of this record: asks, tokens, identities, judging, warrants,
|
||||
4. **The messenger** becomes the router of this record: asks, tokens, identities, judging, warrants,
|
||||
expiry, linking. Its Telegram client and its desk adapter leave it.
|
||||
4. **`telegram`**, a new module claiming kind `telegram` on both benches: long polling, buttons carrying
|
||||
5. **`telegram`**, a new module claiming kind `telegram` on both benches: long polling, buttons carrying
|
||||
only a token, `/start` linking, ADR 0234's defects D1 to D10 fixed in the move, its token as its own
|
||||
secret. **`desk-channel`**, a new module claiming kind `desktop`.
|
||||
5. **The controller as an asker**: conditions that need the operator are asked; it acts on warrants and
|
||||
6. **The controller as an asker**: conditions that need the operator are asked; it acts on warrants and
|
||||
records them; the words "from the mesh MCP server" give way to the router's own line, below.
|
||||
6. **The vault** gains a verb by which the operator gives a module's own secret through a hidden prompt
|
||||
7. **The vault** gains a verb by which the operator gives a module's own secret through a hidden prompt
|
||||
on the desk, so a bot's token never passes through an agent's session.
|
||||
|
||||
**Where an ask can be answered is said by the router**, from the channels that qualify: "Answer on
|
||||
@@ -301,6 +342,9 @@ Telegram, or from the mesh MCP server." The asker's words never name a channel.
|
||||
| An authorising ask never defaults | router test: expiry publishes `expired`, never an option |
|
||||
| An ask no ready channel can carry is refused at once, in words | router test |
|
||||
| Linking needs the code shown on the desk, and is said on every channel | router tests: a wrong code, a late code, a second link of a kind |
|
||||
| A new link approves only after one hour; it acknowledges at once | router tests: an approve answer at 59 minutes refused and said, at 60 accepted; an acknowledge at once accepted |
|
||||
| The `telegram` bundle runs as its own account, never root or the operator's | tool runner tests: a bundle naming `MESH_RUN_AS` is started as that account; root and the operator's account refused; the manifest's owners checked by the catalogue's manifest test |
|
||||
| A tool call never publishes an intake event or a proof | tool runner test: only a bundle's own channel reaches the seat publish, for its own kind |
|
||||
| The asker acts once per warrant and records the warrant | shared library test; controller test reading the hand-act after a warrant |
|
||||
| The controller holds no channel code | review; the controller's module imports no channel package |
|
||||
| End to end | live drill: a held delivery released from the phone; the hand-act reads "the operator, via telegram, user id verified" |
|
||||
|
||||
@@ -4,6 +4,7 @@ status: designed
|
||||
code: []
|
||||
updated: 2026-10-08
|
||||
decisions:
|
||||
- 02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md
|
||||
- 02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md
|
||||
- 02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md
|
||||
- 02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md
|
||||
@@ -13,8 +14,8 @@ decisions:
|
||||
|
||||
**The mesh, its modules and its agents tell the operator things and ask them things; the operator
|
||||
answers, or writes first. Each exchange travels on a channel whose declared capabilities can carry it,
|
||||
chosen by where the operator is working. An answer that performs an action is checked and performed by
|
||||
the controller, never by the channel or the asker** ([ADR 0234](../../02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md),
|
||||
chosen by where the operator is working. An answer that performs an action is judged by the router, which
|
||||
issues a warrant; the asker performs it with its own grants, never the channel or the router** ([ADR 0234](../../02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md),
|
||||
from [research 028](../../01-RESEARCH/028-the-meshs-output-channel/00-overview.md)).
|
||||
|
||||
This design grows the minimal output channel of [to-be 45](45-a-core-that-cannot-fail-silently.md) §5.
|
||||
@@ -28,14 +29,13 @@ That form stays the first step; §13 below is the order in which it becomes this
|
||||
controller's conditions ──┐ messages and asks ┌──► channel/telegram
|
||||
modules (notify) ────────┼──► required capability → work context ──┼──► channel/desktop
|
||||
agents (ask) ────────┤ → severity → escalation └──► channel/<next kind>
|
||||
controller (authorise │ presence (current state only)
|
||||
any module (ask) ─────────┤ presence (current state only)
|
||||
request) ────────────────┘ ▲ ▲
|
||||
│ │ answer (ordinary asks) intake bench (in)
|
||||
seen ───┘ └────────────────────────────── intake/telegram
|
||||
intake/desktop
|
||||
CONTROLLER ◄──── authorise answer (authorising asks, intake holders only) ─┘
|
||||
│ checks capabilities, identity, code, digest; performs; records the hand-act
|
||||
└──► ask-answered
|
||||
ASKER (any module) ◄── warrant on decided.<asker> ── ROUTER judges: token, identity,
|
||||
│ acts with its own grants; records the warrant capabilities, level; one warrant per ask
|
||||
|
||||
watcher's watcher (outside the seats, own bot, not on the control node) ──► Telegram directly
|
||||
self-check and watcher ──► outside dead-man service
|
||||
@@ -91,14 +91,16 @@ bot) is held by one module claiming both seats under one kind.
|
||||
| `kind` | the holder's kind |
|
||||
| `what` | `message` (written first), `choice` (a button or reaction), `reply` (written in an ask's thread), `mail`, `call` (a webhook), `seen` (activity, for the work context) |
|
||||
| `sender` | the identity on that service, and whether the service authenticated it |
|
||||
| `trusted` | whether the sender is on the **controller's** list of the operator's identities. A holder never sets it: a raw intake event is untrusted by construction, and only the router stamps it, from the controller's answer (§4) |
|
||||
| `trusted` | whether the sender is on the **router's** list of the operator's identities. A holder never sets it: a raw intake event is untrusted by construction, and only the router stamps it, from the controller's answer (§4) |
|
||||
| `conversation` | an opaque handle; sending on it reaches the same chat, room or thread |
|
||||
| `in-reply-to` | the ask or message it answers, if any |
|
||||
| `payload` | the text, or the option's token. **Never a code** (§10) |
|
||||
| `at` | when |
|
||||
|
||||
**Answers go to the ask's owner by request and reply**: the router's `answer` for an ordinary ask, the
|
||||
controller's `authorise answer` for an authorising one; both refuse an answer that is not trusted.
|
||||
**Answers arrive only as intake events of the holder's own kind** (*amended 2026-10-08, ADR 0259*): a
|
||||
`choice` carries the router's token and the sender as the service authenticated it, and the router judges
|
||||
it (§10). A typed code goes by request and reply on the seat's `proof` subject, never in an event. There
|
||||
is no `answer` tool: any tool runner may call any tool, so a tool call proves nothing about its caller.
|
||||
**Everything else is an event on the seat**, taken by the router, which re-emits it stamped (§4): a
|
||||
trusted `message` as an **operator message**, addressed (§5); anything untrusted as **input**, for
|
||||
consumers that accept untrusted input (a later mail rule takes `mail`). The router takes `seen` for the
|
||||
@@ -109,14 +111,13 @@ grants it (the open gap of [to-be 32](32-what-a-module-declares.md) §1).
|
||||
|
||||
## 4. Who counts as the operator
|
||||
|
||||
- **Only a sender on the controller's list of the operator's identities is the operator.** The list
|
||||
is kept per intake kind in the controller's state. While no factor exists, an identity is enrolled
|
||||
- **Only a sender on the router's list of the operator's identities is the operator**
|
||||
(*amended 2026-10-08, ADR 0259:* the router's, not the controller's). The list is kept per intake kind in
|
||||
the router's state, and changed only by linking and unlinking, never by a setting. While no factor exists, an identity is enrolled
|
||||
only at a terminal; once one does, adding or removing an identity is a destroy ask (§10). Telegram's linking verb
|
||||
(§11) produces such an ask.
|
||||
- **The controller serves an identity check** (kind and sender in, trusted or not out). The router keeps
|
||||
the answers as current state, refreshed when the controller announces a change to the list, and
|
||||
stamps every envelope it re-emits with `trusted`. **No holder's word is taken for it**, and the
|
||||
controller checks again itself before authorising (§10).
|
||||
- **The router checks every sender against its own list** and stamps every envelope it re-emits with
|
||||
`trusted`. **No holder's word is taken for it.**
|
||||
- **An untrusted envelope** may start work for a consumer that declares it accepts untrusted input. It
|
||||
can never answer an ask, authorise, or be delivered to an agent as the operator's words. An untrusted
|
||||
sender writing to the bot is told nothing beyond a fixed line, and the attempt is a warning condition
|
||||
@@ -208,10 +209,10 @@ The event **`ask-answered`**, with the ask's id and any conversation handle. An
|
||||
|
||||
### The router's verbs and events
|
||||
|
||||
`ask`, `ask cancel`, `asks`, `answer` (intake holders only), on the `operator-channel` seat beside its
|
||||
existing `notify`, `open` and `history`. Events `ask-opened`, `ask-answered`, `ask-closed`. The router
|
||||
owns every ask that authorises nothing; an authorising ask is owned by the controller (§10) and carried
|
||||
by the router like any other.
|
||||
*Amended 2026-10-08, ADR 0259:* an ask and its cancel are accepted on the `operator-channel` seat under
|
||||
the asker's name; the answer, a warrant or the ask's end, is the seat's event `decided.<asker>`; the tool
|
||||
`asks` lists them, beside the existing `notify`, `open` and `history`. The router owns every ask,
|
||||
authorising or not (§10).
|
||||
|
||||
## 7. The capability vocabulary, `channel-capabilities/1`
|
||||
|
||||
@@ -234,9 +235,9 @@ withdrawn from routing and reported until it passes. A new word is a new version
|
||||
| | `threads` | an answer is tied to what it answers | a reply to A carries A in `in-reply-to` |
|
||||
| | `operator-first` | the operator can write unprompted | a simulated message yields a `message` envelope |
|
||||
| trusting | `verified-sender` | the answer came from the operator's own account, through a holder no agent shares | a choice from an identity not on the list is dropped and reported; placement checked |
|
||||
| | `exact-render` | the ask is shown as the controller rendered it | rendered text equals the controller's, byte for byte |
|
||||
| | `code-factor` | a typed code reaches the controller by request and reply, never judged by the holder | a code never appears in an event; it is deleted from the conversation where the service allows |
|
||||
| | `key-factor` | a security key's assertion over the controller's challenge reaches the controller | the challenge is the controller's; the signature is checked by the controller |
|
||||
| | `exact-render` | the ask is shown as the router rendered it | rendered text equals the router's, byte for byte |
|
||||
| | `code-factor` | a typed code reaches the router by request and reply, never judged by the holder | a code never appears in an event; it is deleted from the conversation where the service allows |
|
||||
| | `key-factor` | a security key's assertion over the router's challenge reaches the router | the challenge is the router's; the signature is checked by the router |
|
||||
|
||||
What the first holders declare:
|
||||
|
||||
@@ -316,23 +317,60 @@ is not exempted.
|
||||
|
||||
## 10. Asks that authorise
|
||||
|
||||
*Proposed 2026-10-08, [ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md):*
|
||||
the router, not the controller, holds authorising asks, keeps the operator's identities and judges each
|
||||
answer; the asker performs the chosen option on a warrant the router publishes for it alone; asks, warrants
|
||||
and answers travel on the bus under subjects whose last token names their publisher. Once that record is
|
||||
accepted, this section and §3, §4, §11 and §13 are amended to it.
|
||||
*Amended 2026-10-08, [ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md):* **the router holds every ask, authorising or not, and
|
||||
judges each answer; the asker performs the chosen option on a warrant.** The controller keeps no channel,
|
||||
no identity and no factor: it asks and acts like any other module. What follows in this section reads
|
||||
with these replacements.
|
||||
|
||||
### The verb table and the controller's verbs
|
||||
### Asks, warrants and answers on the bus
|
||||
|
||||
- **The controller's verb definition gains `authorises`:** the tier, and the arguments that make up
|
||||
`<asker>` is the asking module's name, `<kind>` a channel's kind. Each subject's last token names its
|
||||
publisher, and the bus grants it to that publisher alone.
|
||||
|
||||
| What | Subject or bucket | Kept in | Publisher | Reader |
|
||||
|---|---|---|---|---|
|
||||
| an ask | `mesh.seat.operator-channel.accept.ask.<asker>` | the seat's work queue, 7 days | the asker, its own name | the router |
|
||||
| a cancel | `mesh.seat.operator-channel.accept.cancel.<asker>` | the same | the asker, its own name | the router |
|
||||
| asks and their state | the router's bucket `asks`, key `<asker>.<id>` | its state; closed records 30 days | the router | the router; each asker its own prefix |
|
||||
| answer tokens | the router's bucket `tokens`, key the token | its state, each key living until its ask expires | the router | the router |
|
||||
| the operator's identities | the router's bucket `identities`, key `<kind>.<identity>` | its state | the router | the router |
|
||||
| a warrant, or an ask's end | `mesh.seat.operator-channel.event.decided.<asker>`, message id `<asker>.<id>` | EVENTS, deduplicated | the router | the asker, its own name |
|
||||
| show, edit, say | `mesh.seat.channel.accept.<show\|edit\|send>.<kind>` | the seat's work queue, one worker per kind | the router | that kind's holder |
|
||||
| a choice, a link asked | `mesh.seat.intake.event.<choice\|link>.<kind>` | EVENTS | that kind's holder | the router |
|
||||
| a code | `mesh.seat.intake.proof.code.<kind>` | nothing: core request and reply | that kind's holder | the router answers |
|
||||
|
||||
**An ask** carries an id the asker chose; the question in plain words; at most four options, each a label,
|
||||
what it does in plain words, its assurance level (`acknowledge`, `approve`, `destroy`) and an id; who may
|
||||
answer (`operator`); an expiry; what happens on expiry, in words; and optionally what it is about (a
|
||||
condition's key), so a newer ask about it replaces the older. It is refused at once, on its asker's
|
||||
`decided` subject, when its words break a rule, when no ready channel can carry one of its options' levels,
|
||||
or when the asker already has three open.
|
||||
|
||||
**A warrant** carries the ask's id and asker, the option, `by` ("the operator, via telegram, user id
|
||||
verified"), the channel module, the proofs, the level met and when. No secret. An ask that ends without one
|
||||
ends `expired`, `cancelled`, `replaced` or `refused`, on the same subject.
|
||||
|
||||
**The router judges a `choice` of kind K with token T from sender S**, refusing at the first failure and
|
||||
saying it on K's channel: T is known, unused, unexpired and issued for K; the ask is open and T's option is
|
||||
its own; S is linked for K; K's capabilities, from the controller's record of its claim, meet the option's
|
||||
level; for `approve`, S was linked at least an hour ago; then the ask is closed by compare-and-set and the
|
||||
warrant published. Every copy is edited to the outcome.
|
||||
|
||||
**The asker** acts once per ask id, only on a warrant from its own subject, with its own grants, and its
|
||||
record of the act names the warrant (`by`, `via`, `ask`, `proofs`). The hand-act log counts it as a person's
|
||||
decision.
|
||||
|
||||
**The verbs the controller was to serve** (`authorise request`, `authorise answer`,
|
||||
`authorisations`) are not built: an ask on the seat replaces the first, an intake `choice` the second, and
|
||||
the router's tool `asks` the third. The tiers, the proofs, the direct-call rules and the factors stand, and
|
||||
the factors are the router's state.
|
||||
|
||||
### The verb table
|
||||
|
||||
- **Each asker's verb definition gains `authorises`:** the tier, and the arguments that make up
|
||||
the exact state a person must see (for `retire approve`, the set of consumers).
|
||||
- **`authorise request`** — callable by anyone (an agent, the router on a condition's behalf). Carries
|
||||
the verb and its exact arguments, why, and optionally a conversation handle. The controller renders
|
||||
the ask, stores it with an opaque id, its expiry and a digest of the state shown, and emits
|
||||
`ask-opened` with itself as owner. **Nothing is performed.**
|
||||
- **`authorise answer`** — granted to intake holders only. Carries the ask's id, the option, the
|
||||
sender's identity, and the code or key assertion where the tier needs them.
|
||||
- **`authorisations`** — open and recent authorising asks.
|
||||
- **An authorising ask** is published by its asker on the seat (above); the router renders it and stores
|
||||
it with its expiry. **Nothing is performed** until the asker holds a warrant.
|
||||
|
||||
### What may be authorised, and its tier
|
||||
|
||||
@@ -349,7 +387,7 @@ accepted, this section and §3, §4, §11 and §13 are amended to it.
|
||||
|
||||
- **P1, a verified sender:** a Telegram tap from the operator's own account, through a holder placed
|
||||
where no agent runs as the operator.
|
||||
- **P2, a TOTP code** from the operator's authenticator app, verified by the controller. Valid for the
|
||||
- **P2, a TOTP code** from the operator's authenticator app, verified by the router. Valid for the
|
||||
current or previous 30-second step; accepted once. **This is the proof everything above acknowledge
|
||||
can rest on.**
|
||||
- **P3, a security key's touch bound to the ask** (the challenge is a hash of the ask's id and the
|
||||
@@ -378,26 +416,27 @@ accepted, this section and §3, §4, §11 and §13 are amended to it.
|
||||
5. **No agent authorises.** An agent asks. Agents' grants hold no authorising verb.
|
||||
6. **Only a trusted sender authorises** (§4); an untrusted answer is refused before any other check.
|
||||
|
||||
### The controller's checks, in order, refusing at the first failure
|
||||
### The router's checks, in order, refusing at the first failure
|
||||
|
||||
1. The ask is open and unexpired.
|
||||
2. The caller holds an intake kind — by the controller's own seat records, never the request's claim.
|
||||
3. That kind's declared capabilities, from the controller's records, satisfy the tier, and the proofs
|
||||
2. The answer came on an intake kind's own subject — which the bus grants that kind's holder alone.
|
||||
3. That kind's declared capabilities, from the controller's record of its claim, satisfy the tier, and the proofs
|
||||
present are enough.
|
||||
4. A P1 answer's sender is on the controller's list of the operator's identities for that kind.
|
||||
4. A P1 answer's sender is on the router's list of the operator's identities for that kind, and for
|
||||
approve was linked at least an hour ago.
|
||||
5. A code is valid for the current or previous step, and unused.
|
||||
6. A key assertion verifies against the enrolled credential, over this ask's challenge, with user
|
||||
presence.
|
||||
7. The state now has the digest it had when shown; otherwise the ask is void and a new one is
|
||||
requested.
|
||||
7. The asker checks that the state still is what it asked about before it acts; otherwise it refuses
|
||||
the warrant and asks again.
|
||||
|
||||
Then it performs as itself, records the hand-act, closes the ask with a compare-and-set so a second
|
||||
answer loses, and emits `ask-answered`. Every copy is edited to the outcome and its buttons removed.
|
||||
Then it closes the ask with a compare-and-set so a second answer loses, and publishes the warrant; the
|
||||
asker performs as itself and records the hand-act. Every copy is edited to the outcome and its buttons removed.
|
||||
|
||||
### Direct calls
|
||||
|
||||
`retire approve|reject`, `cleanup delete`, and `pin` while `binding-kept` names it refuse any caller
|
||||
except through `authorise answer`, or **break-glass** at the mesh MCP server with a code — recorded and
|
||||
except on a warrant, or **break-glass** at the mesh MCP server with a code — recorded and
|
||||
announced on every channel as break-glass. `retire approve` takes **`expect`**, the set it approves,
|
||||
and refuses if the set now differs. `conditions silence` stays callable; a silence an agent sets is
|
||||
said on the away channel, with its why.
|
||||
@@ -409,10 +448,10 @@ id) and **`proofs`** (which of P1, P2, P3). `by` reads "the operator, as <kind>
|
||||
|
||||
### The factors
|
||||
|
||||
The operator's identities, the TOTP seed, its recovery codes and any enrolled key are the controller's
|
||||
own state. The seed is made by the mesh and shown once, as a URI, to a plain terminal — never through a
|
||||
The operator's identities, the TOTP seed, its recovery codes and any enrolled key are the router's
|
||||
own state (ADR 0259). The seed is made by the mesh and shown once, as a URI, to a plain terminal — never through a
|
||||
channel or an event. Re-enrolling a factor is a destroy ask, except by recovery below. A code travels
|
||||
only by request and reply from an intake holder to the controller, and is deleted from the conversation
|
||||
only by request and reply from an intake holder to the router, and is deleted from the conversation
|
||||
where the service allows.
|
||||
|
||||
### Recovering the factor
|
||||
@@ -420,7 +459,7 @@ where the service allows.
|
||||
A lost phone must not lock the mesh's only person out for good, and re-enrolling is a destroy ask that
|
||||
needs the lost factor's code. So:
|
||||
|
||||
- **`factor enrol`**, at a terminal: the controller makes the seed and **ten one-time recovery codes**,
|
||||
- **`factor enrol`**, at a terminal: the router makes the seed and **ten one-time recovery codes**,
|
||||
shows them once together, and stores the codes **only as hashes**.
|
||||
- **A recovery code counts as one P2 proof, once.** It may stand in for the app's code in any ask.
|
||||
- **`factor recover`**, at a terminal, given a recovery code: a new seed and ten new codes replacing the
|
||||
@@ -428,25 +467,31 @@ needs the lost factor's code. So:
|
||||
- **The count left** is shown by `factor status` and in `authorisations`; the self-check warns when
|
||||
three or fewer remain.
|
||||
- **The last resort:** root on the control node — at it, or by its SSH key — runs `factor enrol
|
||||
--break-glass` there. It talks to the controller locally, never over the bus; the same verb over the
|
||||
--break-glass` there. It talks to the router locally, never over the bus; the same verb over the
|
||||
bus is refused. Recorded and announced as break-glass.
|
||||
- **No tier requiring P2 is enabled until recovery codes exist.** The controller refuses the setting.
|
||||
- **No tier requiring P2 is enabled until recovery codes exist.** The router refuses the setting.
|
||||
|
||||
## 11. The first holders
|
||||
|
||||
### Telegram — kind `telegram`, both seats, the away channel
|
||||
|
||||
- **Its own module**, holding the bot token and chat id as its own secrets; the router keeps no
|
||||
Telegram client.
|
||||
- **Its own module**, `telegram`, holding the bot token as its own secret; the router keeps no Telegram
|
||||
client. *Amended 2026-10-08, ADR 0259:* **it runs on the control node under its own account `telegram`**,
|
||||
never root or the operator's: the tool runner starts its bundle as that account, its state directory and
|
||||
token belong to it, and the bus credential stays the runner's. If an agent holds root there, none of
|
||||
this holds; the self-check raises a condition when the operator's account can become root without a
|
||||
person.
|
||||
- **Long polling**, never a webhook; the update offset kept in its own state.
|
||||
- **Buttons** carry only an opaque `a1:<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
|
||||
- **Buttons** carry only the router's one-time token (Telegram allows 64 bytes); everything else is in
|
||||
the router. 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. Linking an
|
||||
identity adds it to the controller's list, so after the first it is a destroy ask (§4).
|
||||
deletes it from the chat, and hands it to the router by request and reply.
|
||||
- **Linking** (*amended 2026-10-08, ADR 0259*): the operator sends `/start`; the router shows a six-digit
|
||||
code on the desk for ten minutes; the operator sends it to the bot, and the router links the account.
|
||||
One identity per kind. **A new link acknowledges at once and approves only after one hour**, and every
|
||||
link and unlink is said on every channel. Once a TOTP factor exists, linking is a destroy ask (§4).
|
||||
- **A sender not on the list** is answered with one fixed line and handed on only as untrusted input.
|
||||
- **Placement:** where no agent runs as the operator. Its `verified-sender` depends on it, and the
|
||||
self-check reads the placement.
|
||||
@@ -462,11 +507,12 @@ needs the lost factor's code. So:
|
||||
*Amended 2026-10-08, [ADR 0258](../../02-DECISIONS/0258-a-desk-click-performs-only-an-acknowledgement-and-an-answer-is-a-one-time-ticket.md):*
|
||||
only an acknowledgement (Details, silencing) is offered so: any process of the account can choose an
|
||||
action, so a click is never taken for the operator's word. Every other answer waits for phase 5.
|
||||
- The router's desktop adapter, holding `channel/desktop` and `intake/desktop`, turns that into a
|
||||
`choice` envelope.
|
||||
- *Amended 2026-10-08, ADR 0259:* a module of its own, **`desk-channel`**, holds `channel/desktop` and
|
||||
`intake/desktop`, sends through each machine's `node-notifier` and turns dunst's `action-chosen` into a
|
||||
`choice` envelope. The desk declares no `verified-sender`, so it answers `acknowledge` options only.
|
||||
- **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.
|
||||
router 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).
|
||||
@@ -506,8 +552,9 @@ Each phase ends at its own *done when*. Owning repositories from [`repos.md`](..
|
||||
| **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`; presence as current state; the operator's identity list and identity check in the controller, the router's `trusted` stamping and untrusted input (§4); the content rule allowing machine names, references and `detail`, refusals naming the offending part (§9) | mesh-controller (seat set, claim fields, registration refusals, grants, the identity list and check); mesh-sdk and mesh-tools (seat-event publishing in the shared library and the tool runner); 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; an envelope from an identity not on the list is re-emitted untrusted; a path is refused to its sender by name, and travels as a reference the desk opens |
|
||||
| **4 — Asks, and operator messages** | `ask`, `ask cancel`, `asks`, `answer`; the events; kinds, life, limits, batching, history; escalation; addressing, the register of agents with kept messages, the responder; the untrusted-input rule in every agent module's instructions (§5) | 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; `@mesh status` answered; a message to a stopped agent kept and said |
|
||||
| **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 TOTP seed as the controller's state, enrolled at a terminal with ten recovery codes; `factor recover`, `factor status`, the local-only break-glass enrolment; no P2 tier before recovery codes exist; optional key enrolment; the self-check probe for the away channel; agents' grants lose the authorising verbs | mesh-controller; mesh-catalog (agents' grants, the desk's code prompt) | one controller test per refusal passes; at the desk, approve needs a code; a recovery drill re-enrols with a recovery code, and is announced |
|
||||
| **6 — Telegram live** | the `telegram` module 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 |
|
||||
| **5a — Asks on the bus** (ADR 0259) | the kinded bench, caller-named verbs, `proof` subjects and the records users read (controller); a bundle's seat traffic, compare-and-set state and its own account (tool runner); the asker's and channel's types (shared library); the router's asks, tokens, identities, judging, warrants, expiry and linking with its hour (messenger); `telegram` and `desk-channel`; the controller asking for its conditions' actions and acting on warrants | mesh-controller, mesh-tools, mesh-sdk, mesh-catalog | a held delivery released from the phone; the hand-act reads "the operator, via telegram, user id verified"; a desk Silence goes through an ask |
|
||||
| **5 — Authorise, with TOTP** | `authorises` in the verb table; the router's checks for the code; direct calls refused, break-glass; `retire approve` takes `expect`; the hand-act fields; the TOTP seed as the controller's state, enrolled at a terminal with ten recovery codes; `factor recover`, `factor status`, the local-only break-glass enrolment; no P2 tier before recovery codes exist; optional key enrolment; the self-check probe for the away channel; agents' grants lose the authorising verbs | mesh-controller; mesh-catalog (agents' grants, the desk's code prompt) | one controller test per refusal passes; at the desk, approve needs a code; a recovery drill re-enrols with a recovery code, and is announced |
|
||||
| **6 — Telegram live** | the `telegram` module (built in 5a) with replies and forced-reply codes, under its own account on the control node; the operator's two bots configured; the watcher assigned off the control node; the dead-man ping | mesh-catalog | the self-check says the away channel carries every tier; 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.
|
||||
|
||||
Reference in New Issue
Block a user