Run telegram as its own account in a runtime of its own, keep its offset per update, and keep no copy of a secret on the desk (hq ADR 0259 §8)

This commit is contained in:
jochen
2026-10-09 17:08:17 +02:00
committed by jschoubben
parent a412389725
commit 0d9ec84a9c
11 changed files with 49 additions and 26 deletions
+3 -1
View File
@@ -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
@@ -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)
}
@@ -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")
}
}
+1 -1
View File
@@ -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
+2
View File
@@ -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=
+15 -17
View File
@@ -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 <control node> 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 <control node> 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
+3 -3
View File
@@ -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
}
@@ -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 {
+1 -1
View File
@@ -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
+2
View File
@@ -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=
+2 -1
View File
@@ -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"
}
}