diff --git a/modules/desk-channel/README.md b/modules/desk-channel/README.md index 43d86698..2d759f40 100644 --- a/modules/desk-channel/README.md +++ b/modules/desk-channel/README.md @@ -20,7 +20,9 @@ router sends warnings to the channel that reaches the operator away. ## Where it runs -Held once for the mesh, on the control node beside the router. It asks each machine's notifier through the +Held once for the mesh, on the same machine as the router, which is where it is assigned. A message the +router marks secret (a link's code) is shown and kept nowhere of this module's; the desktop's own notifier +history may still hold it for its length (20 notifications). It asks each machine's notifier through the mesh: nothing new runs on the machines. ## Tools diff --git a/modules/desk-channel/cmd/desk-channel/desk.go b/modules/desk-channel/cmd/desk-channel/desk.go index dcbedb60..4d70a43a 100644 --- a/modules/desk-channel/cmd/desk-channel/desk.go +++ b/modules/desk-channel/cmd/desk-channel/desk.go @@ -152,6 +152,10 @@ func (d *Desk) Take(subject string, body json.RawMessage) error { } return nil } + if m.Secret { + // Shown once and kept nowhere of this module's (a link's code): no edit or re-showing reaches it. + return nil + } if err := d.Store.Put(m.Handle, s); err != nil { d.Logf("[desk-channel] shown, and what was shown could not be kept: an edit of it will not reach it: %v", err) } diff --git a/modules/desk-channel/cmd/desk-channel/desk_test.go b/modules/desk-channel/cmd/desk-channel/desk_test.go index 7c767b70..da887ea2 100644 --- a/modules/desk-channel/cmd/desk-channel/desk_test.go +++ b/modules/desk-channel/cmd/desk-channel/desk_test.go @@ -159,3 +159,15 @@ func TestItPromisesNoVerifiedSender(t *testing.T) { t.Errorf("consumes %v", m.Consumes) } } + +// A message the router marks secret (a link's code) is shown and kept nowhere of this module's. +func TestASecretMessageIsShownAndNotKept(t *testing.T) { + d, shown, _, store := newDesk(t, "laptop") + take(t, d, asks.Send, asks.Message{Handle: "code", Title: "Link a Telegram account as you", Body: "123 456", Secret: true}) + if len(*shown) != 1 { + t.Fatal("not shown") + } + if _, kept := store["code"]; kept { + t.Error("the code was kept") + } +} diff --git a/modules/desk-channel/go.mod b/modules/desk-channel/go.mod index 44601787..ab0fbbbf 100644 --- a/modules/desk-channel/go.mod +++ b/modules/desk-channel/go.mod @@ -2,4 +2,4 @@ module deskchannel go 1.22 -require git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008145004-62367ce15ad6 +require git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008163924-7c32dfbdf969 diff --git a/modules/desk-channel/go.sum b/modules/desk-channel/go.sum index 1d160856..55500767 100644 --- a/modules/desk-channel/go.sum +++ b/modules/desk-channel/go.sum @@ -1,2 +1,4 @@ git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008145004-62367ce15ad6 h1:JT7xM1bnLNInW7/oImV2OlXTrcQ4/GSM0Y8tAb+AhmY= git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008145004-62367ce15ad6/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= +git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008163924-7c32dfbdf969 h1:qivAvJIYHFx3yNA+bt52yNRZkGioHLOw9dqs9zw4OJ8= +git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008163924-7c32dfbdf969/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= diff --git a/modules/telegram/README.md b/modules/telegram/README.md index 17056b21..f1902efe 100644 --- a/modules/telegram/README.md +++ b/modules/telegram/README.md @@ -30,19 +30,18 @@ as the operator. D2 and D3 (a reopening said by an edit; a clearing edits the first message) are the router's, and D9 the watcher's. -## How it keeps the token from the agents (novox/hq ADR 0259 §8) +## How it keeps the token from the agents (novox/hq ADR 0259 §8, as revised) -- **It runs as the account `telegram`**, which the module makes (no shell, no login). The tool runner, which - runs as root, starts this bundle as that account (`MESH_RUN_AS`) and refuses root and the operator's - account. -- **Its state directory is the account's, 0700, and its secret `telegram-token` the account's, 0600.** The - operator's account — which every agent runs as — reads neither, and cannot attach to its process. -- **The bus credential stays the runner's**, root's only. The bundle reaches the bus through its own - standard input and output, and the runner publishes for it only its own kind's subjects. -- **An agent reaches the runner only through the mesh MCP server's tools**, which make tool calls; no tool - says a choice, a link or a proof. -- **If an agent holds root on the machine, none of this holds**: root reads the token and the runner's bus - credential and can publish anything. +- **It runs as the account `telegram`, in a runtime of its own, on a bus account of its own** (`runs-as`). The + machine's runtime runs as the operator's account — which every agent runs as — and never carries it; the + controller refuses to compose it there. +- **Its state directory, its bot token and its bus credential belong to `telegram`**, 0700 and 0600 + (`secrets-owner`). The operator's account reads none of them, and cannot attach to its process. +- **Only this kind's holder says this kind's choices, links and codes**: the bus grants those subjects to its + own account alone. +- **The router believes it proves its sender only so placed**, and only while the controller's self-check + finds no agent able to become root without a person on its machine or the router's (`agent-root`). +- **If an agent holds root on the machine, none of this holds**: root reads the token and every credential. ## Where it runs @@ -54,13 +53,12 @@ never inject one. 1. **A bot**: in Telegram, talk to @BotFather, `/newbot`, a name and a username. BotFather answers with the bot's token. Keep it on the phone; never paste it into a chat with an agent. -2. **The token, to the vault**, as this module's own secret `telegram-token` (novox/hq ADR 0259 §10, the - vault's verb for a secret given at the desk; until it is built, `secret accept telegram - telegram-token` at a terminal on the control node, which reads it without echoing it). Then push the - control node. +2. **The token, to the vault**, as this module's own secret `telegram-token`: the vault's verb for a secret + given at the desk, or `secret accept telegram telegram-token` at a terminal on the control + node, which reads it without echoing it. Then push the control node. 3. **The link**: send the bot `/start`. A six-digit code appears on the desk; send it to the bot within ten minutes. The account answers acknowledgements at once and approves an hour later; every channel says so. - `messenger_identities` shows it, `messenger_unlink` undoes it. + `messenger_identities` shows it. `messenger_unlink` asks the linked account itself whether to unlink it. ## Tools diff --git a/modules/telegram/cmd/telegram/channel.go b/modules/telegram/cmd/telegram/channel.go index 1819c155..c278f3d8 100644 --- a/modules/telegram/cmd/telegram/channel.go +++ b/modules/telegram/cmd/telegram/channel.go @@ -354,8 +354,8 @@ func (c *Channel) Poll(wait time.Duration) error { if u.UpdateID >= offset { offset = u.UpdateID + 1 } - } - if len(updates) > 0 { + // Kept after each update, not after the batch: a restart in the middle takes again only the one it + // was handling (the review of 2026-10-08), and the router acts on an answer once by its id anyway. if err := c.Store.SetOffset(offset); err != nil { c.Logf("[telegram] the update offset could not be kept; a restart may take some updates again: %v", err) } @@ -447,7 +447,7 @@ func (c *Channel) Status() map[string]any { "up to 4096 characters arrive whole; longer is cut and the cut said", "warnings and clearings arrive without sound; urgent messages and questions ring", "a rate limit is waited out; a refusal is said to the router as permanent; an edit to the same text is a success", - "it runs as its own account, which alone reads the bot token", + "it runs as its own account, in a runtime of its own on a bus account of its own, never the machine's runtime; that account alone reads the bot token", } return out } diff --git a/modules/telegram/cmd/telegram/manifest_test.go b/modules/telegram/cmd/telegram/manifest_test.go index 19da5ee7..4baba908 100644 --- a/modules/telegram/cmd/telegram/manifest_test.go +++ b/modules/telegram/cmd/telegram/manifest_test.go @@ -23,6 +23,7 @@ func TestItRunsAsItsOwnAccountWhichAloneHoldsTheToken(t *testing.T) { } Own map[string]string `json:"own-secrets"` Owner string `json:"secrets-owner"` + RunsAs string `json:"runs-as"` Resources []map[string]any Build struct { Artifacts []struct { @@ -44,8 +45,9 @@ func TestItRunsAsItsOwnAccountWhichAloneHoldsTheToken(t *testing.T) { } } env := m.Build.Artifacts[0].Env - if env["MESH_RUN_AS"] != "telegram" || env["MESH_TELEGRAM_TOKEN_FILE"] != m.Own["telegram-token"] || m.Owner != "telegram" { - t.Fatalf("env %v, secret %v, owner %q", env, m.Own, m.Owner) + if m.RunsAs != "telegram" || env["MESH_TELEGRAM_TOKEN_FILE"] != m.Own["telegram-token"] || m.Owner != "telegram" || + m.Own["broker"] == "" { + t.Fatalf("runs as %q, env %v, secrets %v, owner %q", m.RunsAs, env, m.Own, m.Owner) } var account, state bool for _, r := range m.Resources { diff --git a/modules/telegram/go.mod b/modules/telegram/go.mod index 633ef03b..d9a27fcd 100644 --- a/modules/telegram/go.mod +++ b/modules/telegram/go.mod @@ -2,4 +2,4 @@ module telegram go 1.22 -require git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008145004-62367ce15ad6 +require git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008163924-7c32dfbdf969 diff --git a/modules/telegram/go.sum b/modules/telegram/go.sum index 1d160856..55500767 100644 --- a/modules/telegram/go.sum +++ b/modules/telegram/go.sum @@ -1,2 +1,4 @@ git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008145004-62367ce15ad6 h1:JT7xM1bnLNInW7/oImV2OlXTrcQ4/GSM0Y8tAb+AhmY= git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008145004-62367ce15ad6/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= +git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008163924-7c32dfbdf969 h1:qivAvJIYHFx3yNA+bt52yNRZkGioHLOw9dqs9zw4OJ8= +git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008163924-7c32dfbdf969/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= diff --git a/modules/telegram/module.json b/modules/telegram/module.json index 38097f9c..5534476a 100644 --- a/modules/telegram/module.json +++ b/modules/telegram/module.json @@ -2,6 +2,7 @@ "module": "telegram", "version": "1", "slug": "tgrm", + "runs-as": "telegram", "claims": [ { "name": "channel", @@ -33,6 +34,7 @@ } ], "own-secrets": { + "broker": "${dir:state}/broker", "telegram-token": "${dir:state}/telegram-token" }, "secrets-owner": "telegram", @@ -68,7 +70,6 @@ "telegram" ], "env": { - "MESH_RUN_AS": "telegram", "MESH_TELEGRAM_TOKEN_FILE": "${dir:state}/telegram-token" } }