diff --git a/01-RESEARCH/028-the-meshs-output-channel/00-overview.md b/01-RESEARCH/028-the-meshs-output-channel/00-overview.md index 9064fd7..2780521 100644 --- a/01-RESEARCH/028-the-meshs-output-channel/00-overview.md +++ b/01-RESEARCH/028-the-meshs-output-channel/00-overview.md @@ -41,23 +41,30 @@ The effort looks at: silenced by the operator; - **the watcher's watcher:** who tells the operator when the parts that would tell them are the ones that failed; -- **the way back in** (widened 2026-10-06): the operator's answers, and decisions, through a channel; - and inputs from outside (a message from the operator, a mail arriving) as triggers of the same shape; -- **the capabilities** a channel declares, and the decisions that may only travel on channels whose - capabilities satisfy them. +- **the conversation** (widened 2026-10-06): the mesh, its modules and its agents send messages and + **asks** (a question, a choice, a value, an approval), and the operator answers or writes first, over + channels chosen by their declared **capabilities** and by the operator's **work context**. Inputs + from outside (a mail arriving) share the same envelope; +- **asks that authorise:** one layer on top, for the answers that perform an action. These are checked + by the controller, and allowed only on channels whose capabilities prove that the operator answered. ### Why this effort widened rather than a new one opened -On 2026-10-06 the operator asked for approving and rejecting through Telegram, for a generic shape in -which Telegram simply holds a seat, for input triggers alongside output channels, and for -capabilities that decide which actions may travel on which channel. That could have opened a new -effort. It did not, because: +On 2026-10-06 the operator asked for these: +- approving and rejecting through Telegram; +- a generic shape in which Telegram simply holds a seat; +- input triggers alongside output channels; +- capabilities that decide which actions may travel on which channel; +- the work context as a factor in choosing the channel; +- asks that are not about permission at all. + +That could have opened a new effort. It did not, because: - every part of it hangs on this effort's open questions: Q1 (where a channel attaches), Q4 (presence), Q7 (answering back) and Q8 (what may leave); - ADR 0227 kept answering back open **here**, and said this effort's graduation amends to-be 45 §5; -- a decision is an answer to a message this seat sends, and splitting the reply from the message - would leave two efforts each owning half of one conversation. +- an answer belongs to the message or ask this seat sends. Splitting them would leave two efforts each + owning half of one conversation. Input that is not an answer (a mail arriving, a webhook) shares the envelope and the seat shape, and is designed here only as far as that shape. Its consumers are later work. @@ -102,9 +109,14 @@ for the mesh, sources that call it, and channels that deliver. 5. [The other holders, on the same axes](05-the-other-holders-on-the-same-axes.md): ntfy, Matrix, Pushover, Gotify, mail, Signal, SMS and a dead-man service, as away channel and as the watcher's path. -6. [Channels and triggers as seats](06-channels-and-triggers-as-seats.md): the kinded benches - `channel` and `intake`, the capability vocabulary, the router, agents as surfaces, the migration. -7. [Deciding from a channel](07-deciding-from-a-channel.md): the decisions that exist, three tiers, - decisions held by the controller, Telegram's buttons and codes, compromise. -8. [A proposed decision](08-a-proposed-decision.md): the recommendation, the operator's steps, the +6. [A conversation with the operator](06-a-conversation-with-the-operator.md): messages, asks and + operator messages; asks' kinds and life; the kinded benches `channel` and `intake`; the capability + vocabulary; agents as participants; the migration. +7. [The work context, and the desk](07-the-work-context-and-the-desk.md): the signals, the routing + by context and its escalation, presence kept inside the mesh, and the desk as a full participant + (notification actions, the launcher's prompt). +8. [Asks that authorise](08-asks-that-authorise.md): the actions that need a person, the trust + capabilities, the three proofs and the three tiers, the controller's checks, why the desk needs a + factor, and what a compromise can reach. +9. [A proposed decision](09-a-proposed-decision.md): the recommendation, the operator's steps, the tables, and a record ready for graduation. diff --git a/01-RESEARCH/028-the-meshs-output-channel/03-open-questions.md b/01-RESEARCH/028-the-meshs-output-channel/03-open-questions.md index 450814b..1d22157 100644 --- a/01-RESEARCH/028-the-meshs-output-channel/03-open-questions.md +++ b/01-RESEARCH/028-the-meshs-output-channel/03-open-questions.md @@ -1,7 +1,7 @@ # 03 — Open questions Each question names the options seen so far. None is decided here. Q1 and Q7 are taken further in -[06](06-channels-and-triggers-as-seats.md) and [07](07-deciding-from-a-channel.md). +[06](06-a-conversation-with-the-operator.md) and [08](08-asks-that-authorise.md). ## Q1. The seat diff --git a/01-RESEARCH/028-the-meshs-output-channel/04-telegram-as-the-first-holder.md b/01-RESEARCH/028-the-meshs-output-channel/04-telegram-as-the-first-holder.md index f804153..b21bc04 100644 --- a/01-RESEARCH/028-the-meshs-output-channel/04-telegram-as-the-first-holder.md +++ b/01-RESEARCH/028-the-meshs-output-channel/04-telegram-as-the-first-holder.md @@ -5,7 +5,7 @@ the output seat's holder carries a Telegram client, and so does the watcher's wa Neither is configured, because no bot exists yet. This document is what the operator needs to make one, what the mesh's use of the bot API must respect, and what the built code gets wrong against it. -[06](06-channels-and-triggers-as-seats.md) makes Telegram one holder of a generic channel seat rather +[06](06-a-conversation-with-the-operator.md) makes Telegram one holder of a generic channel seat rather than the subject of the design. Everything here stays true under that shape: it is the first holder's analysis. @@ -61,7 +61,7 @@ In a private chat the chat id equals the person's user id. There are two ways to The holder reads it by `getUpdates`, and binds **that** chat and **that** user id only if the code matches and is fresh. This proves the chat belongs to whoever held the code, and it never shows the token to anyone. It needs the holder to read updates, which approvals need anyway - ([07](07-deciding-from-a-channel.md)). + ([08](08-asks-that-authorise.md)). The second is the one to build. The first is the stop-gap until it exists. @@ -88,7 +88,7 @@ The holder and the watcher each have their own secret. They can hold the same to before the request may be repeated. Repeating early prolongs the wait. - **Length.** A text message is at most 4096 characters after entity parsing. Longer is refused with HTTP 400, not cut. -- **Callback data** on a button is 1–64 bytes ([07](07-deciding-from-a-channel.md)). +- **Callback data** on a button is 1–64 bytes ([08](08-asks-that-authorise.md)). ### Editing @@ -129,7 +129,7 @@ Answers reach a bot two ways: carry a secret header (`X-Telegram-Bot-Api-Secret-Token`) that proves the call came from the webhook that was set. -Only one of the two at a time. [07](07-deciding-from-a-channel.md) chooses between them. +Only one of the two at a time. [08](08-asks-that-authorise.md) chooses between them. ## What Telegram sees diff --git a/01-RESEARCH/028-the-meshs-output-channel/05-the-other-holders-on-the-same-axes.md b/01-RESEARCH/028-the-meshs-output-channel/05-the-other-holders-on-the-same-axes.md index c039955..102b6b9 100644 --- a/01-RESEARCH/028-the-meshs-output-channel/05-the-other-holders-on-the-same-axes.md +++ b/01-RESEARCH/028-the-meshs-output-channel/05-the-other-holders-on-the-same-axes.md @@ -1,15 +1,15 @@ # 05 — The other holders, on the same axes Every candidate is judged as a holder of the channel and intake seats in -[06](06-channels-and-triggers-as-seats.md): which capabilities it can honestly declare +[06](06-a-conversation-with-the-operator.md): which capabilities it can honestly declare (the vocabulary is defined there), and what it needs. [02](02-the-channels.md) weighed the same candidates before any of this was measured. This document replaces its reading of them with facts as -of 2026-10-06, and adds the question 02 could not ask: **can the operator decide something through -it?** ([07](07-deciding-from-a-channel.md)). +of 2026-10-06, and adds the question 02 could not ask: **can the operator answer through it, and +authorise an action through it?** ([08](08-asks-that-authorise.md)). Two roles are judged separately, because they want different things: -- **The away channel:** the mesh's urgent messages and its decisions, wherever the operator is. +- **The away channel:** the mesh's urgent messages and its asks, wherever the operator is. - **The watcher's path:** the message that the mesh itself has gone silent. It must not depend on the control node, the bus or the controller. The mesh observed has its controller, its bus and its mail server on the anchor (the control node). Its Matrix server is on the home-server, behind a household @@ -92,7 +92,7 @@ beyond opening the app. **Not pursued:** it covers less than ntfy and nothing nt - **Answers:** a reply, slowly. **Who answered is weak:** a From line can be forged, and checking DKIM only proves the operator's provider sent it. - **Mail as an intake** (a new mail arriving) is a trigger in its own right - ([06](06-channels-and-triggers-as-seats.md)), whatever its weakness as a channel for decisions. + ([06](06-a-conversation-with-the-operator.md)), whatever its weakness as a channel for asks that authorise. ### Signal, through `signal-cli` @@ -128,10 +128,10 @@ also covers "the whole house is offline". ## The table -Capabilities are those of [06](06-channels-and-triggers-as-seats.md). ✓ declared honestly, +Capabilities are those of [06](06-a-conversation-with-the-operator.md). ✓ declared honestly, — not, ~ conditional (the note says on what). -| Holder | reaches-away | loud | silent | edit | choice | reply | verified-sender | exact-render | second-factor | private | off the control node | cost | +| Holder | reaches-away | loud | silent | edit | choice | reply | verified-sender | exact-render | code-factor | private | off the control node | cost | |---|---|---|---|---|---|---|---|---|---|---|---|---| | Telegram | ✓ | — (do-not-disturb wins) | ✓ | ✓ | ✓ | ✓ | ✓ (user id) | ✓ | ✓ (code as a reply) | — | ~ (a holder on another machine) | free | | desktop notifier | — | ~ (critical urgency) | ✓ | ✓ | ~ (actions, read by nobody) | — | — (any program of the account) | ✓ | — | ✓ | — | none | @@ -145,17 +145,17 @@ Capabilities are those of [06](06-channels-and-triggers-as-seats.md). ✓ declar ## Reading it -**For the away channel and for decisions,** Telegram is the only candidate that is all of these at +**For the away channel and for asks,** Telegram is the only candidate that is all of these at once: free, on both phone platforms, without a server of the mesh's own, and able to carry every tier -of decision ([07](07-deciding-from-a-channel.md)), including a second factor. Its price is that +of authorising ask ([08](08-asks-that-authorise.md)), including a code as a second proof. Its price is that Telegram reads the words, which is what the content rule is for. -- **Matrix** is the self-hosted equivalent for decisions. It waits on a measurement of Conduit's push. +- **Matrix** is the self-hosted equivalent for asks. It waits on a measurement of Conduit's push. - **Signal** is the end-to-end-encrypted equivalent, at a maintenance cost that has already broken every installation once this year. **For waking the operator,** Pushover's emergency priority is the only thing that repeats until acknowledged and gets through quiet hours. Telegram cannot. It is a reasonable **second** away holder -for urgent conditions only, if the operator wants to be woken. It cannot carry a decision beyond +for urgent conditions only, if the operator wants to be woken. It cannot carry an answer beyond "acknowledged". **For the watcher's path,** the requirement is independence from what it watches: diff --git a/01-RESEARCH/028-the-meshs-output-channel/06-a-conversation-with-the-operator.md b/01-RESEARCH/028-the-meshs-output-channel/06-a-conversation-with-the-operator.md new file mode 100644 index 0000000..2fa0a45 --- /dev/null +++ b/01-RESEARCH/028-the-meshs-output-channel/06-a-conversation-with-the-operator.md @@ -0,0 +1,332 @@ +# 06 — A conversation with the operator + +The operator's directions, 2026-10-06, in substance: + +- **Telegram is one output channel among many to come.** The setup must be generic, and Telegram + simply fulfils a seat. +- **The same holds for input:** a new mail, a new message from the operator. +- **Each channel has capabilities.** +- **Approval is only an example.** An agent may just as well want to ask a simple question, and "it + doesn't have to be permission related". + +So the core of this effort is not a notifier and not an approval path. It is **a conversation with +the operator, held over channels**: + +- the mesh, its modules and its agents **say** things and **ask** things; +- the operator **answers**, or **writes first**; +- each exchange goes over whichever channel is right for its needs and for where the operator is. + +This document is that general model. [07](07-the-work-context-and-the-desk.md) is how the work context +chooses the channel. [08](08-asks-that-authorise.md) is one layer on top: asks whose answer performs an +action, and the checks that makes necessary. [04](04-telegram-as-the-first-holder.md) and +[05](05-the-other-holders-on-the-same-axes.md) are the first holders. + +## What exists, and how it is shaped + +Read from the catalogue's main branch on 2026-10-06. + +- **The output seat's holder is one module doing three jobs.** It claims `operator-channel` (mesh + scope, serving `open`, `history` and `notify`). It consumes the controller's three condition events. + It holds the Telegram bot token as its own secret. It reaches the desktop through the `node-notifier` + seat's `send`. + - The router, the Telegram client and the desktop adapter are one process. + - `notify` is a served verb, not the work queue to-be 32 §3 sketched. +- **The desktop notifier** is the node seat `node-notifier`, held by the dunst module on each + graphical machine. +- **The watcher's watcher** has its own Telegram client and token, and is not assigned yet. +- **Nothing reads anything back.** The operator speaks to the mesh only through an agent session or a + shell. + +## The three things said + +- **A message:** the mesh tells the operator something. A condition raised, a reminder, a clearing, + a notice from a module. It expects no answer. It may be edited later (a clearing). +- **An ask:** someone wants the operator's input, of a declared kind. The answer goes back to whoever + asked. +- **An operator message:** the operator writes first. It is a message to an agent, a note to the mesh, + or an answer to an ask written in the thread instead of tapped. + +Inputs from outside that are not the operator (a mail arriving, a webhook) are the same kind of +envelope as an operator message, with a sender that is not the operator. This effort designs their +shape only. Their consumers are later work. + +## Asks + +### Kinds + +| Kind | The operator gives | Required capabilities of the channel | +|---|---|---| +| `yes-no` | yes or no | `choice`, or `reply` (read as yes or no) | +| `one-of` | one of up to eight labelled options | `choice`, or `reply` with the option's number | +| `text` | free text | `reply` | +| `number`, `date` | a value of that type, within bounds | `reply`. Parsed and checked by the seat's holder; asked again once if it does not parse. | +| `acknowledge` | "seen" | `choice` | + +An ask whose answer **performs an action** (approve a retirement, delete data) is the same ask with +an **authorising** flag. It adds requirements of trust, which [08](08-asks-that-authorise.md) +defines. Everything else about it is as below. + +### What an ask carries + +- an id; +- the asker (its bus principal and its machine); +- the kind, with its options or bounds; +- the words; +- a priority (urgent or normal); +- optionally a timeout and a default; +- optionally a conversation handle (where the asker is talking with the operator); +- optionally a group, for batching. + +The words pass the content rule like every message. + +### Its life + +**open → answered | defaulted | expired | cancelled** + +- **Answered:** the first answer wins. Copies of the ask shown on other channels are edited to say + where it was answered. +- **Defaulted:** the timeout passed and the ask declared a default. The asker receives the default, + marked as a default, never as the operator's answer. +- **Expired:** the timeout passed with no default. The asker is told. + - **An authorising ask never defaults to performing.** It expires. ADR 0230's rule, "a timer is the + mesh acting alone again, only later", applies to every ask that authorises. +- **Cancelled:** the asker no longer needs it (`ask cancel`), or its owner sees it is moot (the + condition behind it cleared). Its copies are edited to "no longer needed". + +### How the asker gets the answer + +- The answer is emitted as **`ask-answered`**, with the ask's id and, where there is one, the + conversation handle. +- An asker may **wait**: `ask` with a wait of up to a few minutes returns the answer if it comes in + time, and otherwise returns the id. +- An agent working through a long task can **poll** `asks `. + +### Batching + +- **Asks to the same channel within the burst window go out together.** A heading says how many are + open ("3 questions waiting"), and each ask follows as its own message, so each can be answered and + edited alone. +- **An asker may hold at most three open asks.** A fourth is refused to it, in words, so an agent in + a loop cannot flood the operator. +- **Asks count against the router's hourly cap** like messages. Answers do not. + +### History + +**`asks`** lists open asks, and closed ones for 30 days: the asker, the kind, the outcome, the channel, +and the answer. A free-text answer is the operator's own words. It stays in the router's state and is +never forwarded to a channel other than the one it came from. + +## Who holds what + +### The output seat's holder becomes the conversation's router + +It holds `operator-channel` and gains asks: + +- `ask` (create), +- `ask cancel`, +- `asks`, +- `answer` (called by intake holders, below), +- events `ask-opened`, `ask-answered`, `ask-closed`. + +It **owns** every ask that authorises nothing. + +An authorising ask is **owned by the controller** ([08](08-asks-that-authorise.md)). The router carries +it like any other, but its answer goes to the controller, which alone can perform. + +### Where a channel sits: [03](03-open-questions.md) Q1, asked again + +Q1 settled that **one seat speaks for the mesh** and that sources never learn channels. It assumed +each channel attaches by contribution. With many channels to come, and channels that answer, that is +the question to settle. + +The mesh's precedents: + +- A **seat** has one holder at its scope (ADR 0121, ADR 0126). +- A **node seat** has one holder per machine, and a verb's subject carries the machine (design 33 §4). +- A **bench** is a seat with several holders. The only one is `mesh-dns-resolver`: the same module, + one per machine. "Making another one is a decision, recorded" (ADR 0223). +- A **work queue** is shared by a seat's holders (ADR 0190). +- A **contribution** is content another module hands to a seat's holder (ADR 0210, ADR 0212). + +The options: + +- **a. Each channel contributes itself to the output seat** (Q1 a, to-be 45 §5). + - A contribution is content a holder places. + - A channel is running code: it holds a secret, keeps a connection, reads answers, fails on its own. + - Making it fit puts every channel's client back in the router. **Rejected.** +- **b. One seat per channel kind.** The router learns every seat; a new channel is a change to the + router. **Rejected.** +- **c. Channel modules found by a manifest field and called by module address.** Callers use seats, + never modules (ADR 0126). **Rejected.** +- **d. One monolithic notifier,** every channel built into the router. Shared secrets and failures, a + release per channel. **Rejected.** +- **e. A channel module per service, with its own approval or question path** (a "Telegram module" + that decides things). It locks the conversation to one service, and the next channel repeats it. + **Rejected.** +- **f. Kinded benches.** Two mesh seats, **`channel`** (out) and **`intake`** (in). Their holders are + different modules, each claiming a **kind** (`telegram`, `desktop`, `ntfy`, `matrix`, `mail`, …). + - One holder per kind; two claiming one kind is refused at registration. + - A verb's subject carries the kind, as a node seat's carries the machine: + `mesh.seat.channel.tool.send.`. + - A new channel is a new module claiming a new kind, with no change to the router. + - **Chosen.** + +Option f needs a second bench, of a new sort (different modules, keyed by kind), which ADR 0223 says +must be decided. It also needs a claim that carries a kind and capabilities. + +## The channel seat (out) + +**`channel`**, mesh scope, a kinded bench. + +### Served + +- **`send`:** words, a priority, whether silent, an optional **ask block** (the kind, the options each + with an opaque token, the ask id) and an optional thread (the conversation handle, or the message + this replies to). Answers the channel's id for what it sent, or a refusal in words. +- **`edit`:** replace a sent message by its id, where `edit` is declared. +- **`standing`:** ready, not configured (naming what is missing, never a value), or failing (since + when, why); the last delivery; the declared capabilities. + +### Emitted + +- **`delivered`** and **`failed`**, the latter marked **permanent** or **transient** (defect D6 in + [04](04-telegram-as-the-first-holder.md)). + +### Honoured + +- The declared maximum length, by cutting and saying so (D1). +- Silence where declared (D4). +- An identical edit is a success (D6). +- No own secret in any error or event. + +### The content rule + +The content rule is the router's, applied before anything reaches a holder that does not declare +`private`. + +## The intake seat (in) + +**`intake`**, mesh scope, a kinded bench. A service that is read and written by one program (a +Telegram bot, [04](04-telegram-as-the-first-holder.md)) is held by one module claiming both seats under +one kind. + +A holder turns what arrives into **one envelope**: + +| Field | What it is | +|---|---| +| `id` | Unique, for deduplication. | +| `kind` | The holder's kind. | +| `what` | `message` (written first), `choice` (a button or a reaction), `reply` (written in an ask's thread), `mail`, `call` (a webhook), `seen` (activity, for the work context). | +| `sender` | The identity on that service, and whether the service authenticated it. | +| `operator` | Whether that identity is on the **controller's** list of the operator's identities. Filled from that list, never from the holder's own. | +| `conversation` | An opaque handle. Sending on it reaches the same chat, room or mail thread. | +| `in-reply-to` | The ask or message it answers, if any. | +| `payload` | The text, or the option's token. **No secrets:** a code for an authorising ask never travels in an envelope ([08](08-asks-that-authorise.md)). | +| `at` | When. | + +**Answers go to the ask's owner by request and reply:** +- the router's `answer` for an ordinary ask; +- the controller's for an authorising one. + +**Everything else is an event on the seat,** which consumers take by `what`: +- the router takes `seen` for the work context; +- an agent bridge takes `message` from the operator; +- a future mail rule takes `mail`. + +**One gap to close:** the shared library cannot yet publish on a seat's event subjects (design 32 §1). + +## The capability vocabulary + +**Fixed and versioned:** `channel-capabilities/1`. A holder declares capabilities in its claim, and +the catalogue refuses a word outside the vocabulary. **Each capability has a contract test** the +holder's build runs and a **drill** its `standing` can run. One that fails its drill is reported and +withdrawn from routing until it passes. + +The words are in three groups. The first two serve every conversation. The third exists only for +asks that authorise, and is defined in [08](08-asks-that-authorise.md). + +### Delivering + +| Capability | Promise | Test / drill | +|---|---|---| +| `deliver` | It arrives, or `failed` says why. | Against a test double; a live test message. | +| `reaches-away` | It reaches a phone away from the operator's machines. | Declared by kind; the operator acknowledges a drill. | +| `loud` | It can break through the phone's quiet hours. | The service's override is set for urgent. | +| `silent` | It can arrive without sound. | The service's silent flag is set. | +| `edit` | A sent message can be replaced in place. | Edit and read back, against a double. | +| `max-length:N` | Up to N characters arrive whole; longer is cut, and the cut is said. | N+1 characters give a cut message, not a failure. | +| `reaches-when-mesh-down` | Delivering needs neither the bus nor the control node. | Checked by the holder's placement and its send path. | +| `private` | The words stay on the operator's machines, or are end-to-end encrypted. | Declared by kind; reviewed. | + +### Conversing + +| Capability | Promise | Test / drill | +|---|---|---| +| `choice` | The operator can pick one offered option in one act, and the pick comes back. | A simulated tap gives a `choice` envelope with the option's token. | +| `reply` | The operator can answer in free text, and it comes back. | A simulated reply gives a `reply` envelope. | +| `threads` | An answer is tied to the message it answers. | A reply to message A carries A in `in-reply-to`. | +| `operator-first` | The operator can write to the mesh unprompted. | A simulated message gives a `message` envelope. | + +### Trusting + +`verified-sender`, `exact-render`, `code-factor` and `key-factor` are defined in +[08](08-asks-that-authorise.md). A channel without them still converses fully. It just cannot carry +an answer that performs an action. + +## Agents in the conversation + +An agent is a participant. It asks, and it receives. + +- **An agent whose operator is at its own terminal** asks there, in the terminal. The seat is not + needed. +- **An agent working unattended** (in the background, on a schedule, or with the operator stepped + away) asks **through the seat**. The router puts the ask where the operator is now + ([07](07-the-work-context-and-the-desk.md)): the desk if they are at it, Telegram if they are away + or talking through Telegram. The agent waits for, or polls, the answer. +- **An agent the operator talks to through Telegram** receives the operator's messages as intake + envelopes. It answers on the conversation handle. Its asks carry that handle, so they appear in the + same chat. +- **An agent's words reach the operator only as an asker's words,** and the operator's answer reaches + the agent only as the operator's. An agent relaying "the operator said yes" is not an answer to + anything. That is why an answer comes from a channel holder, never from the asker + ([08](08-asks-that-authorise.md)). + +## The watcher, in this shape + +The watcher's watcher stays **outside** the seats, deliberately: +- it must speak when the bus and the control node are what failed; +- the seats live on the bus. + +It is a minimal `deliver` + `reaches-when-mesh-down` sender with its own bot. It never reads, and never +asks. Its sibling outside the mesh is the dead-man service ([05](05-the-other-holders-on-the-same-axes.md)). + +## From today to this shape + +1. **Fix the first holder in place:** D1–D4 of [04](04-telegram-as-the-first-holder.md). No change of + shape. +2. **The operator configures Telegram** ([09](09-a-proposed-decision.md)). The mesh starts telling. +3. **The vocabulary and the kinded bench in the catalogue,** and seat events in the shared library. +4. **Split Telegram out** into its own module holding `channel` and `intake` under `telegram`. The + router keeps the desktop adapter as `channel/desktop` and `intake/desktop`. +5. **Asks in the router,** with buttons and replies on Telegram and actions on the desktop + ([07](07-the-work-context-and-the-desk.md)). Agents can ask from here on. +6. **The work context** as the router's ordering ([07](07-the-work-context-and-the-desk.md)). +7. **Authorising asks in the controller** ([08](08-asks-that-authorise.md)). +8. **Operator-first messages,** and an agent bridge consuming them. +9. **Further holders as wanted:** + - Pushover for waking; + - Matrix once its push is measured; + - mail out for the digest, mail in as an intake. + +The watcher changes only at step 1. + +## What this revisits in [03](03-open-questions.md) + +- **Q1:** channels attach as holders of kinded benches (f), not as contributions. +- **Q3:** the life of a message gains the life of an ask. `edit` and `silent` are declared, and decide + how a clearing is said (D2, D3). +- **Q4:** presence becomes the work context ([07](07-the-work-context-and-the-desk.md)). +- **Q7:** answering back is the conversation itself. Its authorising layer is + [08](08-asks-that-authorise.md). +- **Q8:** the content rule stays, applied by the router. Whether a `private` holder may be exempted + is left to graduation. diff --git a/01-RESEARCH/028-the-meshs-output-channel/06-channels-and-triggers-as-seats.md b/01-RESEARCH/028-the-meshs-output-channel/06-channels-and-triggers-as-seats.md deleted file mode 100644 index 7c4f22f..0000000 --- a/01-RESEARCH/028-the-meshs-output-channel/06-channels-and-triggers-as-seats.md +++ /dev/null @@ -1,271 +0,0 @@ -# 06 — Channels and triggers as seats - -The operator's direction, 2026-10-06, in substance: - -- **Telegram is one form of output channel among many to come.** The setup must be generic, and - Telegram simply fulfils a seat. -- **The same holds for input:** a new mail, a new message from the operator, a button pressed. -- **Each channel has capabilities,** human approval buttons for example, and these force some actions - to be allowed only through some channels. - -This document is the generic shape. [04](04-telegram-as-the-first-holder.md) and -[05](05-the-other-holders-on-the-same-axes.md) are its first holders. -[07](07-deciding-from-a-channel.md) is the first thing it carries that is not a notification: a -decision. - -## What exists, and how it is shaped - -Read from the catalogue's main branch on 2026-10-06. - -- **The output seat's holder is one module doing three jobs.** It claims `operator-channel` (mesh - scope, serving `open`, `history` and `notify`). It consumes the controller's three condition events. - It holds the Telegram bot token as its own secret. It reaches the desktop by invoking the - `node-notifier` seat's `send`. - - So the router, the Telegram client and the desktop adapter are one process with one manifest. - - `notify` is a served verb, not the work queue to-be 32 §3 sketched for a `telegram-sender` seat. - - To-be 45 §5 says each channel is "a module contributing itself to the seat". The code does not do - that, and on a closer look that shape does not fit (below). -- **The desktop notifier** is a node seat, `node-notifier`, held by the dunst module on each graphical - machine (`send`, `history`). -- **The watcher's watcher** is a second module with its own Telegram client and token. It sends - directly, by design, and is not assigned anywhere yet. -- **Nothing reads anything back.** No input from the operator reaches the mesh except through an agent - session or a shell. - -## Where a channel sits: [03](03-open-questions.md) Q1, asked again - -Q1 settled that **one seat speaks for the mesh** and that sources never learn channels. It left open -how a channel attaches to that seat, and assumed contribution. With many channels to come, and with -channels that answer, that is the question to settle. - -The mesh's seat model offers these precedents: - -- A **seat** has one holder at its scope (ADR 0121, ADR 0126). -- A **node seat** has one holder per machine, and a verb's subject carries the machine - (`…tool..`, design 33 §4). -- A **bench** is a seat with several holders (glossary). The only one is `mesh-dns-resolver`: the - same module, one per machine. "Making another one is a decision, recorded" (ADR 0223). -- A **work queue** is shared by a seat's holders, and one of them takes each ask (ADR 0190). -- A **contribution** is content another module hands to a seat's holder, which places it - (ADR 0210, ADR 0212). - -The options: - -- **a. Each channel contributes itself to the output seat** (Q1 a, to-be 45 §5). - - A contribution is content: a rule file, a hotkey, a fragment the holder places (ADR 0212). - - A channel is running code. It holds its own secret, keeps a connection, polls for answers, and - fails on its own. None of that is content. - - To make it fit, the router would have to carry every channel's client, which is today's - monolith again. **Rejected.** -- **b. One seat per channel kind** (`telegram-channel`, `ntfy-channel`, …). - - The router must learn each seat, and every new channel is a change to the router. - - That is Q1 c's objection moved one level down. **Rejected.** -- **c. Channel modules found by a manifest field and called by module address.** - - ADR 0126: callers use the seat, never the module. **Rejected.** -- **d. One monolithic notifier,** with every channel built into the router (today's shape, grown). - - Every channel's secrets, dependencies and failures share one process. - - A new channel is a release of the router. - - The watcher's independence becomes the only exception, instead of the rule's natural case. - - **Rejected.** -- **e. A kinded bench.** One mesh seat, `channel`, whose holders are **different modules**, each - claiming it under a **kind** (`telegram`, `desktop`, `ntfy`, `matrix`, `mail`, …). - - One holder per kind. Two modules claiming one kind is refused at registration, as two modules - claiming one machine of a bench are today. - - A verb's subject carries the kind, as a node seat's carries the machine: - `mesh.seat.channel.tool.send.`. - - The router addresses "the channel seat, kind telegram", never a module. - - A new channel is a new module claiming a new kind, with no change to the router. - - **Chosen.** - -Option **e** needs two things the mesh does not have yet: - -- **A second bench, of a new sort.** Its holders are different modules, keyed by kind instead of - machine. ADR 0223 requires that to be decided. -- **A claim that carries a kind and capabilities.** - -Both are small beside what they buy. The same shape serves input. - -## The channel seat (output) - -**`channel`**, mesh scope, a kinded bench. - -### What a holder serves - -- **`send`:** a message (title, body, severity, silent or not, optional choices — see - [07](07-deciding-from-a-channel.md), optional reply-to handle). Answers the channel's id for the - message, or a refusal in words. -- **`edit`:** replace a sent message by its id. Only for a holder that declares `edit`. -- **`reply`:** say something into a conversation, by the handle an intake envelope carried. Only for - a holder that declares `reply`. -- **`standing`:** - - ready, or not configured (and what is missing, named, never a value), or failing (since when, - why); - - the last delivery; - - the declared capabilities. - -### What a holder emits - -- **`delivered`** and **`failed`**, each carrying the message's key and the channel's id. A refusal - is marked **permanent** or **transient**, so the router never retries a permanent refusal for ever - (defect D6 in [04](04-telegram-as-the-first-holder.md)). - -### What a holder must honour - -- **Its declared maximum length:** cut and say so, never refuse and wedge (D1). -- **`silent`** where it declares it (D4). -- **An identical edit is a success** (D6). -- **The token and every other own secret stay out of every error and event,** as the Telegram - client already does. - -### The content rule moves to the router - -A message bound for a holder that does not declare `private` passes the content rule first. The rule -is the router's, so no holder can forget it. - -## The capability vocabulary - -**Fixed and versioned:** `channel-capabilities/1`. A holder declares capabilities in its claim. The -catalogue refuses a word outside the vocabulary. - -**A declaration is a promise with a test.** For each capability the catalogue holds a contract test -the holder's module must pass at build time, and a live drill the holder's `standing` can run on -request. An undeclared capability is never assumed; a declared one that fails its drill is reported as -`channel-capability-broken` and withdrawn from routing until it passes. - -### Delivering - -| Capability | Promise | Contract test / drill | -|---|---|---| -| `deliver` | A message reaches the operator's device, or `failed` says why. | Send to a test double. Live: the holder's test message. | -| `reaches-away` | It reaches a phone away from the operator's machines. | Declared by kind. Drill: the operator acknowledges a test. | -| `loud` | It can break through the phone's quiet hours. | The holder maps urgent to the service's override. Drill acknowledged. | -| `silent` | It can deliver without sound. | The request carries the service's silent flag. | -| `edit` | A sent message can be replaced in place. | Edit, then read back, against a double. | -| `max-length:N` | Messages up to N characters arrive whole. Longer ones are cut, and the cut is said. | N+1 characters give a cut message, not a failure. | -| `reaches-when-mesh-down` | Delivering needs neither the bus nor the control node. | Declared only by a holder not on the control node, with no bus call on the send path. Checked by the manifest's placement. | -| `private` | The words stay on the operator's machines, or are end-to-end encrypted. | Declared by kind. Reviewed, not tested. | - -### Answering ([07](07-deciding-from-a-channel.md)) - -| Capability | Promise | Contract test / drill | -|---|---|---| -| `choice` | The operator can pick one of the offered options in one act, and the pick comes back as an intake event. | A simulated tap gives a `choice` envelope with the option's token. | -| `reply` | The operator can answer in free text, on the same conversation. | A simulated reply gives a `reply` envelope with the handle. | -| `verified-sender` | The holder proves the act came from the operator's own account on that service, by the service's authentication, with nothing in between that relays words. | A tap from a non-allowlisted identity is dropped and reported. A relayed text never carries the flag. | -| `exact-render` | A decision is shown as the controller rendered it, by the holder itself. No agent or other program composes the words the operator decides on. | The rendered text equals the controller's, byte for byte, against a double. | -| `second-factor` | The holder can carry a short code the operator types, to the controller, which verifies it. The holder never judges the code. | A code reply is handed to the controller by request and reply, never as an event, and is deleted from the conversation where the service allows. | - -## The intake seat (input) - -**`intake`**, mesh scope, a kinded bench, held by the same sort of modules. Telegram claims both -`channel` and `intake` under the kind `telegram`, because one program must read the bot's updates -([04](04-telegram-as-the-first-holder.md)). - -A holder turns something from outside into **one envelope**, and emits it as an event on the seat: - -| Field | What it is | -|---|---| -| `id` | Unique, for deduplication. | -| `kind` | The holder's kind: `telegram`, `mail`, `matrix`, `webhook`, … | -| `what` | `message` (the operator wrote), `choice` (a button or reaction), `reply` (an answer to something the mesh sent), `mail`, `call` (a webhook), `seen` (the operator was active here). | -| `sender` | The identity on that service (a user id, a Matrix id, a mail address), and `verified`: whether the service authenticated it **and** the holder declares `verified-sender`. | -| `operator` | Whether that identity is on the **controller's** list of the operator's identities. The holder fills it from the controller's list, never from a list of its own. | -| `conversation` | An opaque handle. Answering on it (`channel.reply `) goes back to the same chat, room or mail thread. | -| `in-reply-to` | The mesh message or decision it answers, if any. | -| `payload` | The text, the choice's token, the mail's subject and body. It passes the same content discipline as everything on the bus: **no secrets in events.** A second-factor code is never in an envelope. | -| `at` | When. | - -**Consumers subscribe by `what`:** -- the router: `seen`, for where the operator is; -- the controller: `choice` and `reply` to decisions ([07](07-deciding-from-a-channel.md)); -- an agent bridge: `message`, from a verified operator; -- a future mail rule: `mail`. - -**Who "the operator" is, per kind, is the controller's record,** not each holder's setting. A channel -module cannot widen it. Adding an identity is itself a decision ([07](07-deciding-from-a-channel.md)). - -**One gap to close:** design 32 §1 notes the shared library cannot yet publish on a seat's event -subjects (`mesh.seat..event.`). Intake needs it. - -## The router - -The output seat's holder becomes **only** the router. It keeps: -- `operator-channel`: its messages, their life (deduplication, reminders, clearing), the rate cap, - `open` and `history`; -- and gains the decisions to deliver ([07](07-deciding-from-a-channel.md)). - -### How it chooses, per message - -1. **What the message needs.** A notification needs `deliver`. A decision needs what its tier - requires ([07](07-deciding-from-a-channel.md)). Urgent prefers `reaches-away` and `loud`. -2. **Where the operator is.** The most recent verified `seen` or `message` on an intake kind within a - bound (15 minutes, a setting) wins. Next comes the desktop when its session answers. Last is the - operator's chosen **away channel** (a setting naming a kind). -3. **Only channels whose declared capabilities satisfy the message,** filtered by `standing`. -4. **Nothing able to carry it:** the router says so on whatever can deliver, and keeps it open as a - condition of its own ("a decision is waiting and no channel can carry it: telegram is failing - since …"). It never degrades a decision to a channel that cannot carry it. - -## Agents as surfaces - -An agent session is a place the operator talks to the mesh. It is a surface with capabilities like -any other. - -- **An agent in a terminal** relays the operator's words. It declares nothing for deciding: - - not `verified-sender`, because what reaches the mesh is the agent's call, not the operator's act; - - not `exact-render`, because the agent composes what the operator reads. - - An agent must never approve on the operator's behalf because "the user said yes". It **requests** - a decision ([07](07-deciding-from-a-channel.md)); the request goes to a capable channel; the - operator decides there. The agent sees the outcome as an event. Having to open Telegram while - working in a terminal is acceptable to the operator. -- **An agent the operator talks to through Telegram** is reached by the intake: verified `message` - envelopes from the operator go to an agent bridge, and its answers go back by `channel.reply` on the - same conversation. When it needs a decision it requests one with the conversation's handle. The - router puts the decision **into that conversation**, rendered by the Telegram holder, with its - buttons. The operator's tap is the holder's verified act, not the agent's relay. So the decision is - made in place. The operator cannot switch to a desk while working this way, so nothing may require - one ([07](07-deciding-from-a-channel.md)). - -## The watcher, in this shape - -The watcher's watcher stays **outside** the seats, deliberately: -- its whole point is to speak when the bus and the control node are what failed; -- the router and the seats live on the bus. - -It shares the vocabulary only: its sender is the minimal `deliver` + `reaches-when-mesh-down` client -it already is, with **its own bot**. It never reads updates and never carries a decision. - -Its sibling outside the mesh is the dead-man service ([05](05-the-other-holders-on-the-same-axes.md)), -which the self-check and the watcher both ping. - -## From today to this shape - -The steps are ordered so that each one is useful alone. - -1. **Fix the first holder in place:** D1–D4 of [04](04-telegram-as-the-first-holder.md), in the - output seat's holder and the watcher. No change of shape. -2. **The operator configures Telegram:** two bots ([08](08-a-proposed-decision.md)). The mesh starts - telling. -3. **The vocabulary and the kinded bench.** The catalogue learns `kind` and `capabilities` on a claim, - the vocabulary file and its contract tests. Registration refuses two holders of one kind. -4. **Split Telegram out** into its own module, holding `channel` and `intake` under `telegram`. - - The router keeps the desktop adapter as the holder of `channel/desktop`, until a reason appears - to move it next to dunst. - - The bot token moves with the module, as an own secret `issued-by: outside`, accepted again. -5. **Decisions in the controller,** and buttons on Telegram ([07](07-deciding-from-a-channel.md)). -6. **Intake for the operator's messages,** and an agent bridge consuming them. -7. **Further holders as wanted:** Pushover for waking, Matrix once its push is measured, mail out - through an outside relay for the digest, mail in as an intake. - -The watcher changes only at step 1. - -## What this revisits in [03](03-open-questions.md) - -- **Q1:** the channels attach as holders of a kinded bench (e), not as contributions. -- **Q3:** "a channel that can edit" becomes the declared `edit`. Whether a clearing is said by edit - depends on `silent` as well (D2, D3). -- **Q4:** presence gains a source: verified intake activity. -- **Q7:** answering back is designed in [07](07-deciding-from-a-channel.md). -- **Q8:** the content rule stays and moves to the router. A `private` holder may be exempted, a - choice left to graduation. diff --git a/01-RESEARCH/028-the-meshs-output-channel/07-deciding-from-a-channel.md b/01-RESEARCH/028-the-meshs-output-channel/07-deciding-from-a-channel.md deleted file mode 100644 index abf6883..0000000 --- a/01-RESEARCH/028-the-meshs-output-channel/07-deciding-from-a-channel.md +++ /dev/null @@ -1,226 +0,0 @@ -# 07 — Deciding from a channel - -The operator, 2026-10-06: - -- "Make sure I can approve and reject stuff via the Telegram channel." -- In a terminal session with an agent, having to open Telegram to react is acceptable. -- But when talking to an agent **through** Telegram, the operator cannot switch to a desktop session. - -So every decision must be completable on the operator's away channel, including the strongest, and -no agent may ever decide in the operator's place. - -## Where this stands against what was decided - -- To-be 45 §5 says: "No answering back in this form; acknowledging is `conditions silence`, through the - mesh." -- ADR 0227 adopted that minimal form, and kept the rest open on purpose: "routing by presence, quiet - hours, **answering back** and the external dead-man service stay open in 028, whose graduation amends - to-be 45." -- So answering back is not a reversal of ADR 0227. It is this effort's open question Q7, now - answered. Its graduation amends to-be 45 §5. - -## What there is to decide - -Read from the controller's main branch on 2026-10-06. The condition store already marks the -conditions only a person resolves: `resolver: operator`. That is set from the start for retirement, -clean-up and binding conditions, and set when a healer's budget is spent. - -| Decision | The verb today | Asked for by | Reversible | Tier | -|---|---|---|---|---| -| Approve the retirement set waiting | `retire approve --why` | `retire-waiting` (urgent) | yes: asking for a consumer again re-enables it | approve | -| Reject it | `retire reject … --why` | `retire-waiting` | yes: a rejected set can be approved later | approve | -| Approve a set rejected before | `retire approve …` | `retire-rejected` (warning) | yes | approve | -| Confirm that a binding moves, once its data is moved | `pin ` | `binding-kept` (urgent) | the pin, yes (`unpin`). The data, not by the mesh. | approve | -| End a stuck plan | `plans stop` / `plans close --why` | `stalled`, `sent-not-reported` once escalated | no, but it destroys nothing | approve | -| Send a machine its declaration by hand | `push --why` | `sent-not-reported` once escalated | n/a | approve | -| Reset a bus consumer's position | `broker consumer-reset --why` | `consumer-behind` once escalated | no: messages are skipped or redelivered | approve (graduation to confirm) | -| Try a healer's repair once more, by hand | the healer's ordinary path | any healer escalation (H1–H5), `healers-braked` | as the repair is | approve | -| Silence a condition for a while | `conditions silence --for --why` | any | yes: it ends by itself, at most 7 days | acknowledge | -| Delete one retired consumer's data | `cleanup delete --why` | `cleanup-waiting` (warning, after 30 days) | **no** | destroy | -| Delete everything retired longer than N days | `cleanup delete --older-than N --confirm --why` | `cleanup-waiting` | **no** | destroy | -| Add an identity to the operator's list, change the away channel, enrol a second factor | (new) | none | yes, but it changes who may decide | destroy | - -Not offered on a channel in the first form: build queue verbs (`cancel`, `kill`, `clear`, `pause`), -`replay --register`. No condition asks for them, and they remain at the console. - -## Three tiers, each a set of required capabilities - -The capabilities are those of [06](06-channels-and-triggers-as-seats.md). - -| Tier | Required of the channel | And of the decision | -|---|---|---| -| **acknowledge** | `choice`, `verified-sender`, `exact-render` | single use, expires with the condition | -| **approve** | `choice`, `verified-sender`, `exact-render` | single use, bound to the exact state shown, expires when that state changes or after 24 h | -| **destroy** | `choice`, `reply`, `verified-sender`, `exact-render`, `second-factor` | as approve, plus a code from the operator's authenticator verified **by the controller**, valid 10 minutes after it is shown, at most one destroy decision answered per 10 minutes | - -### The rule - -1. **Every decision declares its tier,** in the controller's verb table (below). -2. **A decision is offered only on a channel whose declared capabilities satisfy its tier.** Arriving - from any other channel, it is refused, and the refusal is said on the channel it came from. -3. **The operator's away channel must satisfy every tier.** The self-check verifies it. A design or a - setting that would make a tier possible only at a desk is refused, unless the operator has said so - explicitly for that tier, in a setting the self-check reads. -4. **No agent decides.** An agent requests. It never holds a verb that performs a decision, and a - decision's record names the agent that asked. - -## The controller carries decisions - -Today a grant covers a whole verb (`seat:mesh-controller.retire` allows approve and listing alike). -Hand-acts record `by` from the caller's bus principal, which for a channel module would be the -module, not the person. Both call for a decision to be a thing the controller holds. - -### The verb table gains a field - -The controller's verb definition (its table of the mesh's own verbs) has a name, a description and -input and output schemas. It gains **`decision`**: the tier, and which arguments make up the exact -state a person must see (for `retire approve`, the set of consumers). - -### Three new verbs - -- **`decide request`.** Any principal may call it: an agent, a module, the router on a condition's - behalf. It carries the action (verb and exact arguments), why, and optionally a conversation handle. - - The controller renders the decision's text itself: what is asked, the exact state (for example the - consumers in the set), who asks, what each option does. - - It stores the decision in its own state with an opaque id (10 random base32 characters), the - expiry, and a digest of the state shown. - - It emits `decision-requested`. **It never performs anything.** -- **`decide answer`.** Only intake holders are granted it. It carries the id, the option, the sender's - identity as the service authenticated it, and the code for a destroy decision. The controller - checks, in order, and refuses with the reason at the first failure: - 1. the decision exists, is open, and has not expired; - 2. the **calling principal** is the holder of an intake kind, read from the controller's own seat - records, never from the request; - 3. that kind's **declared** capabilities, from the controller's own records, satisfy the tier; - 4. the sender's identity is on the controller's list of the operator's identities for that kind; - 5. for destroy: the code is valid for the current or previous 30-second step, and not used before; - 6. the state **now** has the digest it had when shown (for `retire approve`, the waiting set is - exactly the one rendered). Otherwise the decision is void and a new one is requested. - - Then it performs the action as itself, records the hand-act, closes the decision with a - compare-and-set on its stored revision (so a second answer from another channel loses), and emits - `decision-answered`. -- **`decisions`:** open and recent decisions, for any reader. - -### The verbs that decide become reachable only this way - -The decision-bearing verbs (`retire approve|reject`, `cleanup delete`, `pin` while a `binding-kept` -names it) refuse a caller unless the call comes through `decide answer`, or carries a valid -second-factor code as **break-glass** at the console. Break-glass is recorded as such, and announced on -every channel. So the console, which agents can drive, never decides without a code only the operator -holds. - -- `retire approve` must accept the set it is approving. Today it re-reads the set at the moment it - runs, so "approve what you were shown" does not hold end to end (ADR 0230). It needs an `expect` - argument the provider compares. -- `conditions silence` stays callable as today (it hides, it destroys nothing). A silence set by an - agent is said on the away channel with its why. - -### Who answered, in the record - -The hand-act gains: -- **`via`:** the kind and the holder module; -- **`requested-by`:** the agent principal or the condition key; -- **`decision`:** the id; -- **`factor`:** whether a code was verified. - -`by` becomes "the operator, as identity ". The decision's messages on every channel are -edited to the outcome: "approved by the operator on telegram at 14:02 UTC". Its buttons are removed. - -## Telegram, as the first holder that decides - -- **Buttons.** Each decision message carries an inline keyboard. A button's `callback_data` is at - most 64 bytes, so it carries only `d1::