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:
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user