Merge pull request 'ADR 0259: an ask is a message on the bus, any channel answers it, and the asker acts on a warrant' (#211) from decision/asks-answered-on-any-channel into main
This commit was merged in pull request #211.
This commit is contained in:
+13
-5
@@ -389,6 +389,10 @@ becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252.
|
||||
its **budget**, its back-off and its brake. A healer may not withdraw, delete or recreate data.
|
||||
- **drill** — something broken on purpose to see the mesh raise and clear its condition; a drill is
|
||||
never counted as a repair.
|
||||
- **rehearsal** — a question the operator starts at the controller's terminal (`rehearse`) and answers on a
|
||||
channel, to see an answer travel the whole way: its approval performs nothing and is recorded as the
|
||||
operator's decision ([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)).
|
||||
Not a drill: nothing is broken.
|
||||
- **hand-act** — a repair a person made by hand, recorded so the mesh knows it happened.
|
||||
- **witness** — what rolls a core component back when its new build does not become healthy: never the
|
||||
component itself (to-be 45 §8).
|
||||
@@ -443,10 +447,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,10 +480,14 @@ 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).
|
||||
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).
|
||||
- **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,
|
||||
against its own 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
|
||||
participant, which answers from read verbs); anything else is **untrusted input** — data, never
|
||||
instructions.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+589
@@ -0,0 +1,589 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-09
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
supersedes-in-part:
|
||||
- 0234-the-mesh-holds-a-conversation-with-its-operator.md
|
||||
extends: 02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md
|
||||
---
|
||||
|
||||
# 259. An ask is a message on the bus, any channel answers it, and the asker acts on a warrant
|
||||
|
||||
## Context
|
||||
|
||||
On 2026-10-08 the operator chose Telegram as the next channel for the operator's decisions. "Needs you:
|
||||
release it, or stop it" must be answerable from the phone, with real authorisation, and without an agent
|
||||
session.
|
||||
|
||||
What the mesh had on its main branches that day:
|
||||
|
||||
- **The desk performs only an acknowledgement**
|
||||
([ADR 0258](0258-a-desk-click-performs-only-an-acknowledgement-and-an-answer-is-a-one-time-ticket.md)).
|
||||
Release, stop, start and restart are said in words: "from the mesh MCP server; this notification cannot do
|
||||
it". The messenger (the `operator-channel` seat's holder) calls `conditions silence` itself when the
|
||||
operator chooses Silence on the desk.
|
||||
- **The messenger holds the Telegram client in its own process**, says conditions there as words without
|
||||
buttons, and keeps the bot token as its own secret `telegram-token`. Its status said on 2026-10-08 that the
|
||||
token held is not a bot token: no bot has been given yet.
|
||||
- **[ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md) §8 gives authorising asks to the
|
||||
controller**: it holds the ask, keeps the operator's identities and factors, checks the proofs and
|
||||
performs the action itself. None of it is built ([to-be 46](../03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md)
|
||||
phase 5).
|
||||
- **Any node's tool runner may call any tool on any node** (ADR 0175). A tool call therefore proves
|
||||
nothing about who made it: the laptop's runner carries the agents' calls. What the bus *does* prove is the
|
||||
publisher of a module's events and a seat's events: only the principal granted a subject may publish it,
|
||||
which is why a condition's events need no signature (design 29 §2).
|
||||
|
||||
The operator's directions, 2026-10-08, on the first proposal (a Telegram path through the controller):
|
||||
|
||||
- "I don't think we should mangle Telegram into our controller. Now we're using Telegram, but tomorrow we
|
||||
could be using Signal. And today the controller needs to receive a human decision, but tomorrow a
|
||||
completely different module might require human approval."
|
||||
- **Two generic boundaries.** Asking a person is a mesh-wide service any module uses; the asking module
|
||||
performs the action itself, with its own grants, and records who decided. Channels are pluggable: a
|
||||
channel renders the question, receives the answer and verifies the sender by its own means. Nothing
|
||||
outside the channel module knows which service it talks to.
|
||||
- **The controller only asks and acts.**
|
||||
- **The flow is native to the bus**: a request is a message, pending requests survive a restart, each
|
||||
answer is recorded once, the asker reads its request's state from a bucket, and the bus's permissions
|
||||
decide who may publish what. Events carry no secret; request and reply may. No HTTP between modules.
|
||||
|
||||
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.
|
||||
|
||||
The operator accepted this record as revised on 2026-10-09 — every part marked "added" or "revised" on that day,
|
||||
after the security reviews and the confirmation review — and asked that the mesh be able to ask them a question,
|
||||
and ask for an approval, from then on.
|
||||
|
||||
### Against GENESIS
|
||||
|
||||
- **Core value — sovereignty.** Telegram stays an outside dependency taken deliberately (ADR 0234), and
|
||||
is now one module that can be removed without touching anything that asks.
|
||||
- **Core value — failure must be loud.** An ask that no channel can carry is refused in words to its asker
|
||||
at once, never queued in silence. An answer that is refused is said on the channel it came from.
|
||||
- **Effect — "when something genuinely needs a decision, you are asked, with the context".** Any module
|
||||
can now ask, not only the controller.
|
||||
|
||||
## Considered Options
|
||||
|
||||
### Where an ask is held
|
||||
|
||||
1. **The controller holds authorising asks** (ADR 0234 §8). Rejected by the operator: the next module
|
||||
that needs a person (a backup module, the delivery owner) would ask through the controller, and the
|
||||
controller would perform actions in other domains.
|
||||
2. **A new seat for decisions, beside `operator-channel`.** Rejected: an ask needs the router's routing,
|
||||
the work context and the content rule; a second seat would ask the router for each of them, and two
|
||||
holders would keep two lists of who the operator is.
|
||||
3. **The `operator-channel` seat's holder, the router, holds every ask, authorising or not, and judges
|
||||
the answers.** Chosen. ADR 0234 already gives it ordinary asks; this adds the authorising ones.
|
||||
|
||||
### Who performs the action
|
||||
|
||||
1. **The decision holder calls the asker's verb.** Rejected: the router would act in every domain with a
|
||||
grant to every verb, which is what ADR 0258 took away from the desk.
|
||||
2. **The asker performs on hearing "yes" from anyone.** Rejected in ADR 0234: a relayed "yes" is not an
|
||||
answer.
|
||||
3. **The asker performs on a warrant**: the router's record that the operator chose an option, published
|
||||
on a subject only the router may publish, for that asker alone. Chosen. The bus proves the warrant's
|
||||
source as it proves a condition's. A signature was weighed and rejected: it needs a key in every asker
|
||||
and proves nothing the server's permissions do not.
|
||||
|
||||
### How an ask, a warrant and an answer travel
|
||||
|
||||
1. **Tool calls** (an `ask` verb, an `answer` verb). Rejected: any tool runner may call any tool, so the
|
||||
router could not tell the controller's ask from an agent's, nor a channel's answer from anybody's.
|
||||
2. **HTTP between modules.** Rejected by the operator's direction, and it would carry its own
|
||||
authentication beside the bus's.
|
||||
3. **Subjects whose last token names the publisher**, granted to that publisher alone, on streams that
|
||||
survive a restart. Chosen. The mesh already grants a machine its own token on a node seat's events
|
||||
(ADR 0219); this is the same rule for a caller's module and a channel's kind.
|
||||
|
||||
### Who is the operator on a channel
|
||||
|
||||
1. **A setting naming the operator's account on the service.** Rejected: any principal granted
|
||||
`mesh-controller.settings` may write a setting, agents included, so the operator's identity would be
|
||||
anyone's to set.
|
||||
2. **The channel module's own allow-list.** Rejected in ADR 0234: the channel would decide who the
|
||||
operator is.
|
||||
3. **The router's list of the operator's identities, per channel kind, written only by linking.**
|
||||
Chosen. A channel module passes the sender as the service authenticated it, as an opaque string; the
|
||||
router alone compares it with its list.
|
||||
|
||||
### How a channel takes its answers (Telegram)
|
||||
|
||||
1. **A webhook.** The control node is the stably reachable node, so it could take one. Rejected all the
|
||||
same: it needs a public route, a name and a certificate into the control node, guarded only by a
|
||||
secret header; a stolen token could point it elsewhere.
|
||||
2. **Long polling**, as ADR 0234 chose. Chosen: outbound only, unaffected by the control node's address,
|
||||
answered as fast as a webhook, and a stolen token can read updates (visible as a conflict) but not
|
||||
inject one.
|
||||
|
||||
### Where the desk's buttons live
|
||||
|
||||
1. **Inside the router**, as its desktop adapter (to-be 46 §11). Rejected: the router would know one
|
||||
channel, and the operator asked that the desk be just another channel.
|
||||
2. **The dunst module** claims the channel. Rejected: dunst holds a node seat on each machine, and a
|
||||
channel kind has one holder for the mesh.
|
||||
3. **A small module of its own, kind `desktop`**, sending through each machine's `node-notifier` and
|
||||
reading dunst's `action-chosen`. Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. The words
|
||||
|
||||
- An **ask** keeps its glossary sense; an **authorising ask** is one whose chosen option the asker
|
||||
performs. Each option carries its **assurance level**: `acknowledge`, `approve` or `destroy` (ADR 0234's
|
||||
tiers, the glossary's word).
|
||||
- A **warrant** (new) is the router's record that a person chose one option of one ask: the ask, the
|
||||
option, who chose (the operator, as a channel kind's identity), through which channel, the proofs, and
|
||||
when. It is the only thing an asker acts on.
|
||||
- **The router** keeps its sense (the `operator-channel` seat's holder). It now holds every ask and
|
||||
issues warrants. It never performs an option.
|
||||
|
||||
### 2. The boundaries
|
||||
|
||||
| Party | Does | Never |
|
||||
|---|---|---|
|
||||
| **an asker** (any module; first the controller) | publishes an ask under its own name; reads its asks' state; acts on its own warrants with its own grants; records the warrant in its own record of the act | performs without a warrant; reads another asker's asks |
|
||||
| **the router** (`operator-channel` holder, the `messenger` module) | keeps asks, tokens and the operator's identities; routes an ask to the channels whose capabilities meet each option's level; judges each answer; issues one warrant per ask | calls an asker's verb; knows a service's API |
|
||||
| **a channel** (a module claiming a kind on `channel` and `intake`: `telegram`, `desktop`) | shows what the router sends, with the router's tokens on its buttons; passes back the token chosen and the sender as its service authenticated it | judges whether the sender is the operator; holds another channel's secret |
|
||||
|
||||
### 3. The subjects, streams and buckets
|
||||
|
||||
Every name below follows the mesh's existing shapes (`mesh.seat.<seat>.<accept|event|tool>.…`, a module's
|
||||
state bucket, the EVENTS stream). `<asker>` is the asking module's name; `<kind>` a channel's kind.
|
||||
|
||||
| What | Subject or bucket | Kept in | Publisher (granted) | Reader (granted) |
|
||||
|---|---|---|---|---|
|
||||
| an ask | `mesh.seat.operator-channel.accept.ask.<asker>` | the seat's work queue `SEAT_OPERATOR_CHANNEL` (kept 7 days, removed when the router has recorded it) | the asker, its own name only | the router |
|
||||
| an ask cancelled by its asker | `mesh.seat.operator-channel.accept.cancel.<asker>` | the same | the asker, its own name only | the router |
|
||||
| the asks and their state | the router's bucket `asks`, key `<asker>.<id>` | its own state, closed records kept 30 days | the router | the router; each asker reads its own prefix only (direct get) |
|
||||
| answer tickets | in each ask's record in `asks`: one per option and channel kind, used once, dead when the ask closes | its own state | the router | the router |
|
||||
| the operator's identities | the router's bucket `identities`, key `<kind>.<identity>` | its own state | the router | the router |
|
||||
| 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, a message that failed, the channel's standing | `mesh.seat.intake.event.<choice\|link\|failed\|standing>.<kind>` | EVENTS | the holder of that kind only | the router |
|
||||
| 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:**
|
||||
|
||||
- **A caller-named verb.** A seat may mark an accepted verb or an emitted event as named by its caller
|
||||
(`ask.*`, `cancel.*`, `decided.*`). A module that uses the seat is granted to publish the accept, and to
|
||||
subscribe the event, with its own module name as the last token, and no other. A tool runner carrying
|
||||
modules is granted the same for each module it carries. The controller asks as `mesh-controller`.
|
||||
- **A kinded bench** (ADR 0234 §2, built here for `channel` and `intake`). A claim carries `kind`; two
|
||||
claims of one kind are refused. A holder subscribes its own kind's accepts, publishes its own kind's
|
||||
events and proofs, and nothing of another kind. Each kind has its own worker on the seat's queue, so a
|
||||
channel that is offline keeps its work and holds up no other.
|
||||
- **A seat's `proof` verbs** are core request and reply on `mesh.seat.<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 claim of a kinded bench carries `kind` and `capabilities`** (`channel-capabilities/1`, ADR 0234 §2).
|
||||
The controller names every kind, its holder and its capabilities in the membership of each module that
|
||||
uses or watches the bench: that is where the router reads them, never from a channel.
|
||||
- **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
|
||||
come from anyone. An answer arrives only as an intake event of a kind.
|
||||
|
||||
### 4. An ask
|
||||
|
||||
An ask carries: an `id` the asker chose, unique to it; the **question** in plain words, held to the plain
|
||||
rule and the content rule (ADR 0253, ADR 0234 §6); its **options**, at most four, each a label, what it
|
||||
does in plain words, its assurance level and an option id; **who may answer** (the role `operator`, the
|
||||
only role today); an **expiry**; **what happens on expiry**, in words the operator is shown ("the delivery
|
||||
stays held"); and optionally a subject it is about (a condition's key), so a newer ask about the same
|
||||
thing replaces the older.
|
||||
|
||||
- **Refused at once, in words, on the decided subject**: an ask whose words break a rule, one none of whose
|
||||
options any ready channel can carry, or a fourth open ask from one asker. An option no channel can carry
|
||||
now (`destroy` until a TOTP factor exists; `approve` while no account is linked) is not offered, and the
|
||||
words say where it can still be given.
|
||||
- **An authorising ask never defaults.** At its expiry the router ends it with `expired`; the asker does
|
||||
what it said it would.
|
||||
- **An approve option expires within 24 hours; a destroy option within 10 minutes of being shown**
|
||||
(ADR 0234).
|
||||
|
||||
### 5. The router judges an answer
|
||||
|
||||
On an intake `choice` of kind K carrying token T and sender S, the router refuses at the first failure and
|
||||
says the refusal on K's channel:
|
||||
|
||||
1. T is known, unused, unexpired, and was issued for kind K;
|
||||
2. the ask is open, and the option T names belongs to it;
|
||||
3. S is on the router's list of the operator's identities for K;
|
||||
4. K's declared capabilities, **read from the controller's record of its claim and never from the
|
||||
channel**, meet the option's level: `acknowledge` needs `choice` and `exact-render`; `approve` needs
|
||||
`verified-sender` as well (P1); `destroy` needs two proofs, one of them a code (P2); and for `approve`
|
||||
and above, **the controller's `root-free` verb answers, at that moment, that no agent can become root
|
||||
without a person** on the router's machine and on K's channel's machine (§8 rule 2, added on 2026-10-09:
|
||||
an absent condition is no pass);
|
||||
5. the ask is closed with a compare-and-set on its record, so a second answer loses.
|
||||
|
||||
Then it publishes the warrant on `decided.<asker>`, and sends every channel that showed the ask an edit
|
||||
naming the outcome and removing its buttons.
|
||||
|
||||
**The desk declares no `verified-sender`**, so it answers only `acknowledge` options, as ADR 0258 decided.
|
||||
Telegram declares it while placed where no agent runs as the operator, so it answers `approve`.
|
||||
|
||||
### 6. The warrant, and the asker's act
|
||||
|
||||
The warrant carries the ask's id and asker, the option, `by` (the operator, as a kind's identity: "the
|
||||
operator, via telegram, user id verified"), the channel module, the proofs present, the assurance level
|
||||
met, and when. It carries no secret.
|
||||
|
||||
- **The asker acts once per ask id**, keeping that in its own record, and only on a warrant from its own
|
||||
`decided` subject. A warrant that says no time it was given, or for an ask that says no expiry, is refused:
|
||||
neither can be shown to be in time (the confirmation review of 2026-10-09).
|
||||
- **A silence never takes an approval back** (the confirmation review of 2026-10-09, M1). The controller
|
||||
asks a condition's acknowledgements (Silence) apart from its approvals (Release, Stop, Restart), and choosing
|
||||
Silence silences the condition; the first build then took the approval ask back as no longer wanted, so an
|
||||
acknowledgement, which a desk click may give, ended an approval. An approval asked about a condition that is
|
||||
silenced since stays open, unchanged, until it is answered on a channel that proves who answered, or expires.
|
||||
- **A warrant authorises one act, as the person was shown it** (added on 2026-10-09, before the code was
|
||||
merged). Every option above `acknowledge` carries the digest of exactly what the asker performs when it is
|
||||
chosen — the verb, the machine and every argument (`binds`); an ask without it is refused at the router.
|
||||
The router puts the digest of the ask as it took it in the warrant. The asker acts only when that digest
|
||||
is the one of the ask it keeps, and only when the act it is about to perform is the one the option bound:
|
||||
an ask published again under the same id, or a record of the act changed after the ask, is refused and
|
||||
nothing is done. **Both digests are over named fields in a canonical encoding** (each value written as its
|
||||
length, a colon and its bytes, in a fixed order, versioned), never over a language's encoding of a
|
||||
structure, so any language reproduces them and a field added to an ask changes no digest until it is
|
||||
added to the encoding on purpose (the review of 2026-10-09).
|
||||
- **The router publishes each warrant under a message id nobody can guess** (a random one per publish), on
|
||||
the asker's `decided` subject, which only the router may publish; the asker deduplicates on its own record
|
||||
(a compare-and-set claim), never on the bus's duplicate window. A guessed id published first by another
|
||||
principal into the same stream would otherwise have the real warrant dropped as a duplicate (the review of
|
||||
2026-10-09).
|
||||
- **Its record of the act names the warrant**: a hand-act's `by`, `via`, `ask` and `proofs` (ADR 0234 §8),
|
||||
a delivery's transition, a silence's why. The hand-act log counts it as a person's decision, not a repair.
|
||||
- **The controller only asks and acts.** For a condition that needs the operator it asks with the
|
||||
condition's actions as options, and on the warrant it calls the verb it named, with its own grant. It
|
||||
holds no channel, no identity and no factor.
|
||||
- **A warrant is not yet the only way to these acts.** The acts a warrant authorises today —
|
||||
`mesh-delivery.release` and `stop`, the controller's `plans` go and stop, `node-service-manager.restart`,
|
||||
and `conditions silence` — are still verbs an agent may call directly with its own grant. Until
|
||||
[research 039](../01-RESEARCH/039-a-tool-that-needs-a-persons-approval/00-overview.md) (a tool that needs a
|
||||
person's approval) withholds them from every caller without a warrant, a warrant proves that the operator
|
||||
chose an act, not that only the operator could have caused it. Said here so no reader takes the warrant for
|
||||
that second guarantee.
|
||||
|
||||
### 7. Who the operator is on a channel: linking
|
||||
|
||||
- **An answer from an account that is not the operator's** is refused, and the operator is told on the linked
|
||||
account which account tried (by name and number); the other account is told nothing (added on 2026-10-09:
|
||||
a question forwarded, or a phone in other hands, is something the operator must hear of).
|
||||
- **The router's list is the only list.** It is changed only by linking and unlinking, never by a setting.
|
||||
- **Linking:** the operator sends the bot `/start`. The channel publishes `link.<kind>` with the sender
|
||||
and the name the service shows. The router shows a six-digit code on the desk, with that name, valid
|
||||
for ten minutes. The operator sends the code to the bot; the channel passes it on the `proof` subject;
|
||||
the router links the identity when it matches, and says so on every channel.
|
||||
- **A code is shown to a person or not at all** (added on 2026-10-09, after the review). The router issues a
|
||||
code only while the desk channel says it is ready, and tells the sender the desk's reason when it is not;
|
||||
a code is never queued for later. The desk is ready only while a named desk machine's notifier said within
|
||||
five minutes that it is not paused (the notifier's `paused` event), and the code goes only to such a
|
||||
machine; shown nowhere, it is said back to the router as failed. Every message carrying a code is marked secret: no
|
||||
channel and no router record keeps a copy, the desk channel stores none and hands it to the node's
|
||||
notifier as transient, and the notifier keeps it out of its history, where an agent could read it later.
|
||||
- **The desk channel runs as an account of its own**, like the router and Telegram (§8 rule 1): the codes
|
||||
pass through it. Registration holds every holder of a `private` kind to it, as it holds a `verified-sender`
|
||||
holder (the confirmation review of 2026-10-09).
|
||||
- **One identity per kind.** A second link of a kind is refused until the first is unlinked.
|
||||
- **Unlinking is an approve ask, answered on the account that is linked** (revised after the security
|
||||
review of 2026-10-08). It is never a plain tool: whoever unlinks the operator's account could link one of
|
||||
their own in its place. The notice of an unlink, and of any link for an hour after it, is sent to the
|
||||
account being replaced by name, before anything changes, and to every other channel.
|
||||
- **A link within a day of an unlink waits for the replaced account's approval**, asked there. When that
|
||||
account is lost with its phone, the operator acts as root at the router's machine (break-glass, as ADR 0234
|
||||
§8 does for a lost factor): a regular file `unlink.<kind>` or `allow-link.<kind>`, owned by root, in the
|
||||
router's own state directory (mode 0700, owned by the router's account). The router acts on it within a
|
||||
minute, deletes it, ends every open ask of its own, and announces on every channel what root did. A file of
|
||||
any other owner, or not a regular file, is refused and said: an agent without root cannot make one.
|
||||
- **Linking is bounded**: at most three links asked per account an hour and ten per kind, counted in the
|
||||
router's own state so a restart forgets nothing (a count that cannot be read or written refuses the link); the code's notice on the desk names the account by
|
||||
the name and the number the service gives, and only a channel declaring `private` (the desk) may show a
|
||||
code. **A residual risk of the bound, accepted** (the confirmation review of 2026-10-09): anyone may make
|
||||
throwaway accounts and send the bot `/start` ten times in an hour, so the operator's own link is refused
|
||||
for the rest of that hour. It denies a link, never makes one, and every refused link is said; the operator
|
||||
links when the hour has passed. A bound per sender alone would let the same throwaway accounts flood the
|
||||
desk with codes, which is worse.
|
||||
- **The ask to unlink or relink binds the account**, not only the kind (added on 2026-10-09): a warrant for
|
||||
unlinking one account never unlinks the one linked after it. A link re-checks that its kind has no account
|
||||
when it is made, and an account just unlinked cannot link again at once from a code shown before.
|
||||
- **An account still in its first hour can be unlinked without its own approval** (added on 2026-10-09, after
|
||||
the review): it cannot approve yet, so its consent protects nothing. When another account sends `/start`
|
||||
while one is in its first hour, the router asks at the desk, as an acknowledgement bound to that account,
|
||||
whether to unlink it, and tells the sender so. If the hour has passed when the answer arrives, the unlink
|
||||
is refused. After the hour, unlinking needs the linked account's approval, or break-glass.
|
||||
- **An unlink at the desk in the first hour bars nothing** (the confirmation review of 2026-10-09, finding
|
||||
N1-router). A desk click is anyone's at the desk machine — a program there can forge one — and the first
|
||||
build barred the unlinked account for a day and made the next link wait for it. Another account's `/start`
|
||||
offered the desk the unlink, a forged click took it, and the operator's own account stayed barred, again
|
||||
and again, so the operator could never approve. Now such an unlink lets any account, the operator's too,
|
||||
link again at once with a code shown on the desk. **The day's bar is set only by an unlink the account
|
||||
itself approved, or root's**; root's `allow-link` at the router's machine lifts any bar. What is said on
|
||||
every channel names which it was.
|
||||
- **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 running as the operator can read the
|
||||
code and send it from an account of its own. It is said loudly on every channel when it happens; during
|
||||
the hour the account cannot approve, and the operator can unlink it from the desk without its approval (the
|
||||
rule above — corrected on 2026-10-09: before that rule, the hour gave no way to unlink an account that
|
||||
would not consent); and the factor closes it.
|
||||
|
||||
### 8. Who may say what on the bus: the trusted parties' own accounts
|
||||
|
||||
*Revised after the security review of 2026-10-08; the revision accepted by the operator the same day.* The first version of
|
||||
this section rested on the tool runner running as root with a credential only root reads. **On the control
|
||||
node it runs as the operator's account, its bus credential belongs to that account, and that account may
|
||||
become root without a password.** Every agent runs as that account. So an agent there could read the runner's
|
||||
credential and publish anything any carried module may: a warrant, an answer from Telegram, a link. The
|
||||
service account of the first version kept the bot token from nobody.
|
||||
|
||||
The revision rests on three rules.
|
||||
|
||||
**1. The trusted parties hold bus accounts of their own, never a runner's.**
|
||||
|
||||
- The **router** (`operator-channel`'s holder) and **every channel that declares `verified-sender`** run as
|
||||
their own process, as their own service account (`messenger`, `telegram`), with a bus account of their
|
||||
own: an own secret `broker`, owned by that account, mode 0600. The manifest says so with `runs-as`; the
|
||||
controller composes for such a module a runtime of its own — the machine's runtime program, run as that
|
||||
account, serving that module alone on that credential — and the machine's runtime never carries it.
|
||||
- **The controller refuses to compose** into any runner's user, or into any principal whose process runs as
|
||||
the node's operator account:
|
||||
- publishing on a by-caller event of `operator-channel` (a warrant);
|
||||
- writing the router's state (asks, identities);
|
||||
- publishing on, or answering, a kinded bench's subjects for a kind declaring `verified-sender`.
|
||||
A manifest asking for it is refused at registration, and a runner carrying such a module is refused at
|
||||
composition, naming the module.
|
||||
- **Proofs are answered by the holder of `operator-channel` alone**, not by any module watching the bench;
|
||||
and **a kind's accepts are published by that holder alone**, not by every user of the bench.
|
||||
- **The tool runner hands a bundle no bus variable** of its own: the credential and the bus's address are
|
||||
taken out of every bundle's environment.
|
||||
|
||||
**2. `verified-sender` is honoured only where the controller can show who holds it.**
|
||||
|
||||
The controller passes a kind's `verified-sender` to the router only when its holder runs as its own
|
||||
account, with its own bus account, on a machine where **no account an agent runs as can become root without
|
||||
a person**. Otherwise it strips the capability from the membership, and the router answers that kind only
|
||||
as far as acknowledgements, saying why. The same check holds the router: while it fails on the router's
|
||||
machine, the router issues no approve warrant, and the condition says what to change.
|
||||
|
||||
*Revised on 2026-10-09, after the review of the first build.* **Only a positive, recent measure is a pass;
|
||||
the absence of a condition never is.** The first build let the router approve while no `agent-root`
|
||||
condition was open — so before the first self-check, after a probe error, with the conditions view lost, or
|
||||
with the router not knowing its own machine, it approved. Now:
|
||||
|
||||
- **The controller's `root-free` verb** answers, for each machine asked, whether no agent can become root
|
||||
there without a person, judged at the moment it is asked. It is a pass only when the machine names an
|
||||
account its agents run as, the machine's node-engine judged that account unable to become root (ADR 0266:
|
||||
the node-engine reads it as root, on every look, and the controller takes a verdict at most fifteen
|
||||
minutes old), and the login shell's `execute` is not served there (ADR 0268). Anything else — no agent
|
||||
account named, so agents run as the operator; a verdict missing, stale or unhealthy; a store or the bus not
|
||||
answering — is not a pass, and says why.
|
||||
- **The measure is the node-engine's, not the measured account's.** The first build asked the sudo module,
|
||||
served by the machine's runtime — whose account is the one an agent's calls run as — so the account
|
||||
being measured answered its own measure. The node-engine runs as root, which an agent that cannot become
|
||||
root cannot be. The machine's runtime account is counted as able to become root, always: the mesh's
|
||||
acting tools give it passwordless sudo.
|
||||
- **The router asks `root-free`** for its own machine and the answering channel's, when it shows an
|
||||
approve option and again when an approve answer arrives; an error, a missing machine, or a router that does
|
||||
not know its own machine is not a pass. **The controller asks it too when it composes**, and strips
|
||||
`verified-sender` from a kind without a pass; the router's live question covers a pass that ends between
|
||||
compositions. The `root-not-free` condition is raised on the same judgement, so the condition and the
|
||||
verb never disagree.
|
||||
- *Added after the confirmation review of 2026-10-09.* **Only the serving controller answers `root-free`.**
|
||||
Composed from the controller's own manifest, the module principal of the machine running the controller
|
||||
and that machine's runtime were granted the controller seat's verbs too, and that runtime's credential is
|
||||
one an agent there may hold. The controller's own seat is now served by the controller's principal alone,
|
||||
in grants and in memberships, and a grant test composes every principal and finds exactly one that may
|
||||
answer `root-free`. **A machine waiting for its node-engine's first setuid search is not root-free**
|
||||
(ADR 0266 keeps the self-check quiet for that bound after a restart; `root-not-free` keeps the same quiet,
|
||||
and the verb still answers no). The condition has its own key, apart from ADR 0266's
|
||||
`agent-can-become-root`, which a second probe wrote under the same key with other words. **The router asks
|
||||
`root-free` before it takes its work**, never while holding it — a controller slow to answer held every
|
||||
ask, answer and link behind it — and judges an answer with the word it was given then; inside the work it
|
||||
reads only a word under half a minute old, and an ask that failed drops the last word.
|
||||
|
||||
**3. What remains, said plainly.**
|
||||
|
||||
- **Root on the router's or the channel's machine defeats all of this.** The self-check raises an urgent
|
||||
condition while an agent can become root there without a person; on the control node today it can (the
|
||||
operator's account has passwordless sudo, measured on 2026-10-08). **The operator chose, on 2026-10-08,
|
||||
that agents on the control node run under an account of their own without sudo.**
|
||||
- **An agent reaches root through the mesh's own tools too.** Any machine's tool runner may call any tool
|
||||
on any machine, and the login shell seat's `execute` runs a command as the account the runner runs as —
|
||||
on the control node, the operator's account, with passwordless sudo, which the mesh's acting tools rely
|
||||
on (measured on 2026-10-08). So an agent on any machine can run a root command on the control node
|
||||
without touching its own account. The self-check therefore counts both ways in: an account an agent
|
||||
runs as that can become root without a person, **and** a tool an agent may call that runs a command
|
||||
given to it as an account that can. It fails until both are closed; the second is decided separately
|
||||
(the login shell's reach, and what an agent's calls are granted).
|
||||
- **Since 2026-10-09 the second way is closed on the control node**:
|
||||
[ADR 0268](0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md)
|
||||
withholds `execute` there through its holder's setting. The self-check counts the login shell only where
|
||||
`execute` is served, failing closed: served while the holder's setting for that machine is `serve`, or
|
||||
while the bus hears the verb answered there, or while the bus cannot be asked. The first way closes with
|
||||
ADR 0266 (hq pull request 225, not yet merged): agents on the control node run under an account of their
|
||||
own, without sudo.
|
||||
- **What cannot be measured is not a pass** (added on 2026-10-09). A machine where a trusted party runs and
|
||||
the node-engine's verdict on its agent account is missing, stale or not given raises the same urgent
|
||||
condition, saying it was not measured, and `root-free` answers no for it.
|
||||
- **A known gap, accepted by the operator on 2026-10-09: the operator's own Telegram client on a machine
|
||||
where agents run as the operator** (issue 344, hq pull request 244, not yet merged). The operator's Telegram may also run on such a
|
||||
machine — their laptop, with Telegram Desktop. There an agent can read the client's data (found readable by
|
||||
everyone) and drive the X display, and tap Approve as the operator. The router cannot tell such a tap from
|
||||
one on the phone. Closing it needs agents under an account of their own on that machine, without access to
|
||||
the operator's X display, and the client's data readable by the operator alone; none of it is built or
|
||||
measured now, and approvals are not held back for it. Until then, approve from the phone.
|
||||
- **A known gap, accepted on 2026-10-09: the controller's credential can rewrite the router's state**
|
||||
(issue 344, hq pull request 244, not yet merged). The controller composes every account and grant on the bus, so a grant withheld from
|
||||
its own credential — no write to the router's identities or asks — would be withheld by the very principal
|
||||
that writes the grants, and undone by its next composition. Moving the router's buckets where the
|
||||
controller cannot write therefore protects nothing against a controller that is not trusted, and is not
|
||||
done; the controller is trusted with every record, and root on the control node is the mesh.
|
||||
- **The controller holds the whole JetStream API** today. Narrowing it to the streams and buckets it owns is
|
||||
planned work of its own; until then the controller is trusted with every record.
|
||||
- **On the X11 desk an agent can read a code as it is shown or typed** (ADR 0234's accepted risk). The hour
|
||||
after a link, the replaced account's approval and the announcements bound what a stolen code can do.
|
||||
|
||||
### 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
|
||||
router holds the ask and issues a warrant; the asker performs.
|
||||
- **§3 "the controller's list of the operator's identities"** and §8 "the factors' secrets are the
|
||||
controller's own": the identities and, when built, the TOTP factor and its recovery codes are the
|
||||
router's own state, with ADR 0234's rules on showing them once, at a terminal, never on a channel.
|
||||
- Everything else in ADR 0234 stands: the tiers and proofs, the kinded benches, the content rule, the
|
||||
desk never authorising, long polling, every tier completable on the away channel.
|
||||
|
||||
### 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 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 serving one module alone when it runs on that module's 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).
|
||||
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.
|
||||
5. **`telegram`**, a new module claiming kind `telegram` on both benches: long polling, buttons carrying
|
||||
only a token, `/start` linking, the client's defects of to-be 46 §12 fixed in the move (D1, D4 to D8,
|
||||
D10; D2 and D3 are the router's, D9 the watcher's), its token as its own secret. **`desk-channel`**, a new module claiming kind `desktop`.
|
||||
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.
|
||||
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. *As built (2026-10-09):* the
|
||||
controller's `give`, through its `secret accept` path. It refuses `broker` and any secret the mesh makes
|
||||
itself; the prompt is written by the desk's launcher from names alone (module, secret, machine), never
|
||||
from words a caller chose, and says the controller asks; the bus lets only the controller publish the
|
||||
prompt's verb; no secret value is accepted through a verb otherwise; and every value given raises the
|
||||
urgent condition `secret-given` on every channel until the operator silences it.
|
||||
**A trusted party's secret is never given at a desk** (the confirmation review of 2026-10-09, finding
|
||||
N1-give). The desk prompt's answer comes back over the desk machine's bus, on the subject that machine's
|
||||
runtime answers, and on a desk machine agents run as the operator, who holds that runtime's credential:
|
||||
an agent called `give` for the bot token and answered the prompt first with a token of its own, sealed to
|
||||
the call's key, and the channel the operator's answers are proven on became the agent's. Now the secret of
|
||||
a module running as an account of its own (the router, Telegram, the desk channel) is refused at the desk
|
||||
before anybody is asked, naming the line to type at the controller's terminal instead —
|
||||
`nox secret accept <node> telegram telegram-token` (`nox` is `mesh-cli`'s alias; on the control-node,
|
||||
from the operator's own account, it is the controller's terminal, ADR 0272). There it is announced
|
||||
on every channel, the one being replaced among them (it still runs on its old token until the next push),
|
||||
**before** it is kept, and not kept when that announcement fails. A grant test holds that only the desk
|
||||
machine's launcher, carried by that machine's runtime, may answer its prompt; for any other module's
|
||||
secret that remains the accepted risk of a desk where agents run as the operator.
|
||||
|
||||
**Where an ask can be answered is said by the router**, from the channels that qualify: "Answer on
|
||||
Telegram, or from the mesh MCP server." The asker's words never name a channel.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **A channel is a module and nothing else changes when one is added.** Signal or mail is a module
|
||||
claiming a kind and declaring capabilities.
|
||||
- **Any module may ask a person**, through one seat, and acts on the answer with its own grants. ADR
|
||||
0258's desk Silence becomes an ask of the controller's, and the messenger loses its grant to
|
||||
`conditions silence`.
|
||||
- **The controller loses three verbs it never built** and gains an asker's client.
|
||||
- **The router's state grows**: asks, tokens and identities, each a bucket of its own.
|
||||
- **Harder:** an asker must keep which warrants it acted on; the shared library carries it.
|
||||
- **Harder:** the controller composes two new grant rules, and an asking module must use the seat in its
|
||||
manifest to be granted anything.
|
||||
- **The bot token moves** from the messenger's own secret to the `telegram` module's, given at the desk
|
||||
(`give`, §10 step 7). **Nothing the channel or the router trusts is a setting**
|
||||
([issue 342](../04-ISSUES/342-a-password-is-taken-as-a-setting-and-outranks-the-vaults/00-report.md): a
|
||||
password, token or key is not a setting). The operator's Telegram account is the router's identity list,
|
||||
written by linking alone; no chat id is configured anywhere. The two settings these modules keep — the
|
||||
router's time zone and the desk channel's machines — are the terminal's, as every mergeable file's are
|
||||
since [issue 340](../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md).
|
||||
- **Accepted risk:** a linked Telegram account, if taken, can approve (announced, reversible), never
|
||||
destroy. Linking before a TOTP factor exists carries the X11 risk in §7.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| An asker publishes asks only under its own name, and hears only its own warrants | controller grant tests: a module using `operator-channel` is granted `accept.ask.<itself>` and `event.decided.<itself>`, no wildcard; the lab's proof `asks` publishing under another name on a real bus is refused by the server |
|
||||
| Only the router publishes a warrant | controller grant test: `event.decided.*` is in no principal's publish list but the holder's |
|
||||
| A channel publishes only its own kind's answers, links and proofs | controller grant tests over a kinded claim; a second claim of a kind is refused |
|
||||
| A code is never on a stream | controller test: no stream's subjects cover `mesh.seat.*.proof.>` |
|
||||
| A token is used once, expires, and is bound to its ask, option and kind | router tests: the same token twice; a token past its expiry; a token from another kind; a token of a closed ask |
|
||||
| An answer from an identity not linked is refused and said | router test |
|
||||
| A level a channel's declared capabilities do not meet is refused | router tests: an approve option answered from `desktop`; capabilities taken from the controller's record, a channel's own claim ignored |
|
||||
| A second answer to one ask loses | router test: two answers raced; one warrant |
|
||||
| An authorising ask never defaults | router test: expiry publishes `expired`, never an option |
|
||||
| An ask no ready channel can carry is refused at once, in words | router test |
|
||||
| A condition that needs the operator and could not be asked (no router, or the ask refused) is a condition of its own, `asks-undelivered` | controller test (`TestAnAskThatCannotBeDeliveredIsSaid`) |
|
||||
| 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 |
|
||||
| No runner's user, and no principal running as the operator's account, may publish a warrant, write the router's state, or speak for a verified-sender kind | controller composition test over every such grant; registration refuses a manifest asking for it |
|
||||
| `verified-sender` reaches the router only from a holder with its own account on a machine no agent can become root on | controller test stripping the capability; self-check probe raising the condition |
|
||||
| Unlinking and a relink within a day are approve asks answered on the linked account; the notice reaches it first | router tests, with a fake channel that honours the addressee |
|
||||
| No bus variable reaches a bundle | tool runner test over a bundle's environment |
|
||||
| A module of its own account is served by a runtime of its own, as that account, on its own bus credential, and never by the machine's | controller composition test (`runs-as`); registration refuses root, an account the module does not make, secrets it does not own, no broker; the tool runner test that such a runtime serves that module alone |
|
||||
| While an agent can become root without a person where the router or a verified channel runs, it is said and nothing proven there approves | controller probe test (fails with agents as the operator's account, and with the login shell running as an account with passwordless sudo; passes once neither holds); the sudo module's `sudo_escalation` test; router test refusing approval |
|
||||
| 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 |
|
||||
| A warrant authorises only the act its option bound, for the ask as it was shown | shared library tests (`TestAWarrantAuthorisesOnlyTheActItsOptionBound`); controller test (`TestAWarrantPerformsOnlyTheActItsOptionBound`); router test (the warrant names the ask's digest) |
|
||||
| Only the serving controller answers `root-free`; a machine waiting for its first setuid search is not root-free | controller grant test `TestOnlyTheServingControllerMayAnswerRootFree` (one answerer); `TestAMachineWaitingForItsFirstSetuidSearchIsNotRootFree`; `TestRootNotFreeIsQuietWhileTheFirstSearchRunsAndKeyedApartFromDA` |
|
||||
| A desk unlink in the first hour bars nothing; root lifts any bar (N1-router) | router tests `TestAnUnlinkFromTheDeskInTheFirstHourBarsNothing`, `TestBreakGlassLiftsABarOnRelinking` |
|
||||
| A trusted party's secret is never given at a desk; only the desk machine's launcher answers its prompt (N1-give) | controller tests `TestATrustedPartysSecretIsNeverTakenAtADesk`, `TestOnlyTheDeskMachinesLauncherMayAnswerItsPrompt` |
|
||||
| At the controller's terminal, a trusted party's secret is announced on every channel before it is kept, and not kept when the announcement fails; a machine the mesh does not know is refused before anybody types (N1-give) | controller tests `TestATrustedPartysSecretGivenAtTheTerminalIsAnnouncedBeforeItIsKept` (fails when the announcement before keeping is removed), `TestAGiveNamingAMachineTheMeshDoesNotKnowAsksNobody` |
|
||||
| A silence never takes an approval back (M1) | controller test `TestAnAcknowledgementNeverSharesAnAskWithAnApproval`, with the condition silenced as the controller silences it |
|
||||
| The router never waits on the controller while holding its work | router test `TestTheRootFreeWordIsNotAskedWhileTheWorkIsHeld` |
|
||||
| The self-check counts the login shell only where `execute` is served, and what it cannot measure fails | controller probe tests (`TestTheLoginShellCountsOnlyWhereExecuteIsServed`, the unmeasured case) |
|
||||
| The whole flow, before anyone uses it | the lab's proof `asks` (mesh-lab): a real bus with the controller's composed grants, the router and the Telegram channel in runtimes of their own accounts, a fake Telegram API — approve acted once, decline, expiry, a replayed tap, a tap from another account, a forged warrant, an ask under another name, an ask from a module that may not ask |
|
||||
| The controller holds no channel code | review; the controller's module imports no channel package |
|
||||
| End to end | live acceptance after rollout (mesh-lab `asks/live-acceptance.sh`): a **rehearsal** (`rehearse`), started at the controller's terminal alone, approved on the phone, acted on once and performing nothing; the hand-act reads `warrant rehearsal rehearsal=approve`, by "the operator, as telegram identity …", via "telegram (telegram), user id verified", proofs P1 |
|
||||
| An answer from another account is refused and the operator is told which account tried | router test (`TestEveryAnswerThatIsNotTheOperatorsIsRefusedAndSaid`); the lab's proof |
|
||||
| A message carrying an ask's answers is never folded into a digest | router test (`TestAnAsksMessageWithOneAnswerIsNotFoldedIntoADigest`); the lab's proof, where it was found |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md) (the conversation, the tiers and
|
||||
proofs; §3 and §8 in part superseded here)
|
||||
- [ADR 0258](0258-a-desk-click-performs-only-an-acknowledgement-and-an-answer-is-a-one-time-ticket.md)
|
||||
(the desk performs only an acknowledgement; one-time answer tokens)
|
||||
- [ADR 0253](0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md) (a
|
||||
condition's actions and plain words)
|
||||
- [ADR 0246](0246-a-seats-new-verb-is-promised-before-it-is-required.md) (a new seat's verbs are required
|
||||
from the start)
|
||||
- [ADR 0255](0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md)
|
||||
(a generic boundary in front of pluggable modules)
|
||||
- [To-be 46](../03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md) §3, §4, §10, §11 and §13
|
||||
- [Issue 320](../04-ISSUES/320-the-operator-could-not-read-the-meshs-notifications/00-report.md),
|
||||
[issue 324](../04-ISSUES/324-a-desk-click-could-answer-in-the-operators-name/00-report.md)
|
||||
@@ -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:
|
||||
|
||||
@@ -298,7 +299,7 @@ is not exempted.
|
||||
- **Allowed in a message's words:** roles, words, and the mesh's own machine names.
|
||||
- **Refused:** domains, addresses, paths, and anything shaped like a secret — in words, labels and
|
||||
references alike.
|
||||
- **A refusal is said to the sender** — the `notify`, `ask` or `authorise request` call is refused naming
|
||||
- **A refusal is said to the sender** — the `notify` call or the ask is refused naming
|
||||
the offending part, so the asker can rephrase or move the specific into a reference — and raises
|
||||
`channel-refused`. The offending part is never sent to a channel.
|
||||
|
||||
@@ -316,17 +317,61 @@ is not exempted.
|
||||
|
||||
## 10. Asks that authorise
|
||||
|
||||
### The verb table and the controller's verbs
|
||||
*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 controller's verb definition gains `authorises`:** the tier, and the arguments that make up
|
||||
### Asks, warrants and answers on the bus
|
||||
|
||||
`<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 tickets | in each ask's record: one per option and kind, used once | its state | 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, a failed message, standing | `mesh.seat.intake.event.<choice\|link\|failed\|standing>.<kind>` | EVENTS | that kind's holder | the router |
|
||||
| which kinds exist, and what each promises | the router's membership (`seat-traffic.kinds`) | ASSIGNMENTS | the controller, from the claims | 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
|
||||
|
||||
@@ -343,7 +388,7 @@ is not exempted.
|
||||
|
||||
- **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
|
||||
@@ -372,26 +417,27 @@ is not exempted.
|
||||
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.
|
||||
@@ -403,10 +449,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
|
||||
@@ -414,33 +460,43 @@ 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
|
||||
remaining ones. Recorded as a hand-act, and **announced loudly on every channel**.
|
||||
- **The count left** is shown by `factor status` and in `authorisations`; the self-check warns when
|
||||
- **The count left** is shown by `factor status` and in the router's `asks`; 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.
|
||||
- *Revised after the security review of 2026-10-08 (ADR 0259 §8, not yet accepted):* the router and every
|
||||
channel declaring `verified-sender` run as their own process and service account with a bus account of
|
||||
their own, never carried by a tool runner; the controller passes `verified-sender` only for a holder so
|
||||
placed, on a machine where no agent's account can become root without a person.
|
||||
- **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: a runtime of its own serves it as that account, on a bus account of its own
|
||||
(`runs-as`), and its state directory, token and bus credential belong to it. If an agent holds root there,
|
||||
none of this holds; the self-check raises a condition while an agent can become root without a person, by
|
||||
its own account or through a tool that runs its command as one that can.
|
||||
- **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.
|
||||
@@ -456,11 +512,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).
|
||||
@@ -500,8 +557,32 @@ 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 router'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; rehearsals: an approve and a destroy answered on the phone, with the hand-acts read |
|
||||
|
||||
**Switching phase 5a on: the operator's steps, in order** (amended 2026-10-09, ADR 0259). Each line is typed with
|
||||
`nox` (`mesh-cli`) on the control-node, from the operator's own account, which is the controller's terminal
|
||||
(ADR 0272):
|
||||
|
||||
1. The pull requests merge in their order, each after its checks pass on the merge result; nothing is assigned
|
||||
by the merges.
|
||||
2. The agent account (ADR 0266) runs on the control-node and the login shell's `execute` is withheld there
|
||||
(ADR 0268): until both hold, the self-check's `agent-root` or `root-not-free` stays open and Telegram only
|
||||
acknowledges.
|
||||
3. `messenger`, `telegram` and `desk-channel` are assigned to the control-node (`desktop-machines` set on
|
||||
`desk-channel` in the same step), and the control-node is pushed.
|
||||
4. A bot is made with Telegram's BotFather, and its token is given at the controller's terminal:
|
||||
`nox secret accept <control-node> telegram telegram-token`. It is announced on every channel before it is
|
||||
kept. Then the control-node is pushed again, so the channel holds it.
|
||||
5. The operator sends `/start` to the bot from their own account, in a private chat, and sends back the six digits
|
||||
the desk shows. Nobody configures a chat id: the router's identity list is written by linking alone.
|
||||
6. An hour later, `live-acceptance.sh` (mesh-lab `asks/`) runs a rehearsal: a question on the phone, approved
|
||||
there, read back from the hand-act log.
|
||||
|
||||
Until `mesh-cli` carries standard input (ADR 0272, a later step), step 4's line cannot read the token from the
|
||||
terminal; it is given with `--from` a file the controller's own account reads and nobody else does, removed
|
||||
right after.
|
||||
|
||||
**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