Repository navigation
The Telegram connector: a blessed chat that is its owner's bot's DMs - #808
Conversation
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for now at f6a6929a1d79c81684be11211d25ee23729ae188, reviewing this layer against #807 at 51691138de1b1c37434fb91eecd88de5790efac8.
The connector fits the intended template boundary, but two flows violate its identity/routing contract.
Blocking:
-
An editor can substitute the pairing candidate by suppressing the authentic update.
templates/telegram/fragment.jsonexposesproposeandseenas ordinary editor mutations; neither checks its caller (app.mjs:143–175). An editor (including an agent member) can read the incoming/startnonce and update id frominbox, callseenbefore the asynchronous intake reaches that step, then callproposewith their own user/chat ids and the owner's display name. Intake now returns “taken already,” so the claimed two-account conflict protection never runs. The page displays only the supplied name, and the owner's normal confirmation binds the attacker's account, whose later DMs speak as the owner. Directseencalls also let editors suppress ordinary incoming messages. Restrict both state transitions to authenticated connector-internal processing, rather than trusting possession of a readable nonce or an editor role. Test direct editor and agent calls, including this interleaving with the genuine/start; keep genuine duplicate delivery coverage. -
Linking another agent does not change the selected recipient.
door()(templates/telegram/app.mjs:56–63) chooses the first owner-owned agent in membership order rather than the actual door binding. Link A, unlink, then link B using the setup page:bind_doorretains A's membership and adds B, sodoor()still selects A. The platform correctly rejects the resultingrelayed.agentmismatch; intake swallows it as “not linked” and drops the DM even though B is linked. This also affects an initial binding to a non-first agent in a chat with multiple agents. Resolve the authoritative bound agent and test both cases, including delivery of the DM and its reply.
Validation: reproduced both template paths on pinned Node 24.22.1 with disposable in-memory SQLite and mocked job capabilities (the second fixture enforces the platform's binding check). These are focused reproductions, not full workerd/e2e runs. CI check and images check pass; e2e shards and real-Hermes Docker are still pending. No real Telegram bot was used. The prescribed external engineering-style file was unavailable on this machine.
Complexity pass: the template, shared chat assets, fake, and integration coverage are proportionate; these fixes belong at the existing trust and binding boundaries.
Security review of #808 (must-fix 1 and should-fixes 1, 2, 5, 6, 8): - `telegram` is named in `blessed::CONNECTORS`, so a Telegram chat is its owner's alone to edit from its making (#804's rule): its agent, acting for its owner or not, sets none of its secrets, writes it no page and gets no token to push its code, and a fork makes it no door again. - The agent joins as a viewer: the template's `work` takes viewer posts from agents, and a reply whose files a viewer cannot upload now goes with its words, saying so (the bridge refused the whole post before). - `intake` publishes what `job.door` says (no guess among the members) and `to` the bound agent; `mirror` sends nothing while unlinked; another bot's token clears the updates the last one took; `work` lists `outcome` as the chat template's does. - The e2e proves it: the agent's pokes (a secret, a page, a code token) are refused; the DM's turn is asked as the owner; interim words are not sent and a file is named; with Telegram off nothing is published or sent; unlinked from its page the agent's membership goes; a fork lets the binding go for good. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alex's review of #806 and #808, and the security review's tests: the agent, acting for its owner, is refused the connector's `seen` and `propose` as well as its secret, a page and a code token; made an editor member by its owner, as itself, it is refused the secret too; its owner rotates the webhook's secret, and the old one takes nothing; after a fork and its template again, a direct message is published nowhere. The stub's `door poke` asks these, for its owner or as itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Both fixed:
Also from the security review:
🤖 Generated with Claude Code |
alexlwn123
left a comment
There was a problem hiding this comment.
Pass for this stack layer at 6fdba2610b82b9423309fdfbf3762ac309f2c770, against #807 at 47e5b46c30ebb69b2d46ef89a9783c911db0357d.
Both earlier blockers are fixed. Registering Telegram in blessed::CONNECTORS activates the platform's owner-only editing boundary, so other members and agents (including agents acting for the owner) cannot call seen/propose or replace its secrets/code/pages. Intake now publishes the authoritative job.door.relayed and addresses job.door.to, removing membership-order routing. The added integration checks exercise mutation refusals and verify the relayed owner and recipient.
The connector stays within the template boundary, reuses the chat assets, and the bridge's attachment-refusal fallback preserves the reply's words. The implementation and integration coverage are proportionate; I found no blocking regression in this layer.
Validation: cargo test -p fragment-core connectors:: --quiet passed (2 tests). CI check and images check pass; e2e shards and real-Hermes Docker remain pending. Earlier duplicate-run cancellations are not demonstrated code failures. I did not run local workerd/e2e or use a real Telegram bot. The prescribed external engineering-style file is unavailable on this machine.
Non-blocking: add the earlier A → unlink → B and initial non-first-agent cases to the integration lane. The source now uses the correct binding, but the new single-agent assertion does not directly preserve those regression scenarios.
alexlwn123
left a comment
There was a problem hiding this comment.
Pass for this stack layer at 43f2c38488b16dc60c8dfc3a518904e46d6bcbb9, against #807 at 88482d2c239c1ecd775b3e4426aa2698bf269536.
Both original blockers remain fixed: connector editing is owner-only, and intake uses the authoritative door binding. Since my previous approval, mirror now checks the posting principal against job.door.to, so another agent's replies stay in the chat. The updated integration assertions match the inherited viewer-only membership rule. This is a small, appropriate correction at the existing boundary; I found no new blocking regression.
Validation: traced the binding identity and channel-record principal through the platform; a focused fixture on pinned Node 24.22.1 passed for the bound reply, another agent's reply, unlink, direct invocation, and interim reply. This used mocked job capabilities, not workerd. Current CI checks, e2e shards, and real-Hermes Docker are still running; the older duplicate run was cancelled. No local full e2e or real Telegram bot test was run. The prescribed external engineering-style file remains unavailable on this machine.
Non-blocking: retain the earlier request for integration coverage of A → unlink → B and initial non-first-agent binding, including suppression of the other agent's reply. The source uses the correct identity, but the single-agent lane does not preserve those scenarios directly.
alexlwn123
left a comment
There was a problem hiding this comment.
Pass for this stack layer at a04de83cfaeb30e4ad46e99b606e26d4bbdf5b90, against #807 at 27241fa2efbb81552f42d7ab1275ba0286ace0a1.
The earlier identity and routing blockers remain fixed. Since my last approval, this layer adds sharing-permission probes: agents cannot change connector visibility, invite, rotate its share link, or retrieve it, while sharing their owner's ordinary fragment still works. The ordinary-fragment comparison now uses a separate fixture, preserving the lane chat's existing share link for later checks. The small extension to the existing scripted probes is proportionate; I found no new blocking regression.
Validation: cargo check -p fragment-e2e --quiet and git diff --check against the stack base passed. Traced the probes through the sharing guard and status response. Active CI checks, e2e shards and real-Hermes Docker remain pending; the apparent failures in run 41195514941 are cancelled jobs in a cancelled duplicate run, not demonstrated code failures. No local full workerd/e2e or real Telegram bot test was run. The prescribed external engineering-style file is unavailable on this machine.
Non-blocking: the previously requested A → unlink → B and initial non-first-agent integration cases remain useful follow-up coverage, including suppression of the other agent's reply.
alexlwn123
left a comment
There was a problem hiding this comment.
Pass for this stack layer at 886a205558db17f2e69a9ce4a36a6827debfd3df, against #807 at 27241fa2efbb81552f42d7ab1275ba0286ace0a1.
The earlier identity and routing blockers remain fixed. Since my previous approval, this adds Experimental/security labels and browser assertions. The Telegram labels cover its setup states and linked state; the SimpleX tags leave the navigation heading unchanged. The small shared heading/tag helpers are proportionate, and I found no new blocking regression.
Validation: cargo check -p fragment-e2e --quiet and git diff --check against the stack base passed. CI check fails because master now requires step Fragment 4 search_forget 06450019092243ac; this layer's migration tree is unchanged from #807, so that failure is inherited and the stack needs updating before merge. Other CI/e2e/image checks remain pending. I did not run local full workerd/browser e2e or use a real Telegram bot. The prescribed external engineering-style file is unavailable on this machine.
Non-blocking: the previously requested A → unlink → B and initial non-first-agent integration cases remain useful follow-up coverage, including suppression of the other agent's reply.
Security review of #808 (must-fix 1 and should-fixes 1, 2, 5, 6, 8): - `telegram` is named in `blessed::CONNECTORS`, so a Telegram chat is its owner's alone to edit from its making (#804's rule): its agent, acting for its owner or not, sets none of its secrets, writes it no page and gets no token to push its code, and a fork makes it no door again. - The agent joins as a viewer: the template's `work` takes viewer posts from agents, and a reply whose files a viewer cannot upload now goes with its words, saying so (the bridge refused the whole post before). - `intake` publishes what `job.door` says (no guess among the members) and `to` the bound agent; `mirror` sends nothing while unlinked; another bot's token clears the updates the last one took; `work` lists `outcome` as the chat template's does. - The e2e proves it: the agent's pokes (a secret, a page, a code token) are refused; the DM's turn is asked as the owner; interim words are not sent and a file is named; with Telegram off nothing is published or sent; unlinked from its page the agent's membership goes; a fork lets the binding go for good. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
886a205 to
12a9c65
Compare
Alex's review of #806 and #808, and the security review's tests: the agent, acting for its owner, is refused the connector's `seen` and `propose` as well as its secret, a page and a code token; made an editor member by its owner, as itself, it is refused the secret too; its owner rotates the webhook's secret, and the old one takes nothing; after a fork and its template again, a direct message is published nowhere. The stub's `door poke` asks these, for its owner or as itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for now at 12a9c651336e0b9c0d4449c1e53644853f4889a0, reviewing this stack layer against #807 at a94281ee3bceb654ece68a0caecc860b23ec21a9.
The earlier identity and routing blockers remain fixed. All ten patches match the previously approved layer (git range-diff), but the rebase introduces a shared-asset integration failure.
Blocking:
- The Telegram page cannot initialize because
control.jsis missing.templates/telegram/site/chat.jslinks to the shared chat module, which now imports./control.js(line 52). Telegram's site has no such file/link. The release builder indexes only files present under each template, andblessed::site_fileresolves within that template; it does not inherit the chat template's other files. Opening a fresh Telegram page therefore fails module loading at/control.js. The inline module attemplates/telegram/site/index.html:60–64never executes eithermount(document.body)orsetup(document.body), breaking both chat and bot setup in normal use.
Include the shared control.js in Telegram's release assets (as with its other shared modules), and check the complete relative-import closure, excluding platform-provided modules such as __fragment.js and __markdown.js. A browser check should assert successful chat/setup initialization, not just static labels.
Validation: a local import/file check identified the missing ./control.js; traced release indexing and serving through crates/templates/build.rs, blessed::site_file, and cell/src/serve.rs. cargo test -p fragment-core connectors:: --quiet passed (2 tests), cargo check -p fragment-e2e --quiet passed, and the layer's git diff --check passed. CI check and images check pass; e2e shards and real-Hermes Docker remain pending. No local full workerd/browser e2e or real Telegram bot test was run. The prescribed external engineering-style file is unavailable on this machine.
Complexity pass: the existing shared-module approach remains proportionate; this needs its missing dependency packaged, not a new abstraction.
`templates/telegram`, a blessed template (decision 40) of kind chat: a chat with its owner's agent that is also their direct messages with a bot of their own, through the platform's generic pieces alone. - Setup, its owner's, on its page: the bot's token, pasted, and a webhook secret the browser makes, each set as this fragment's secret toward api.telegram.org (`__secrets`: never through its code or a model); then `connect` checks the token (getMe) and points the bot's webhook at the chat's inbox (setWebhook, the secret its `secret_token`, both filled in the URL at the egress point). Its inbox takes a delivery only with that secret in Telegram's header (`inbox`), so no member can forge one. - Pairing: a single-use `/start` link of ten minutes; the account that answers is proposed and its owner confirms it on the page, an owner's operation no agent can call. Its numeric ids are bound; its name only shown. Two accounts answering one link (a member racing the owner's own `/start` with a nonce read in the inbox) propose no one. Another bot forgets the account. - The door: its owner links it to their agent (`__door`). Then `intake`, its inbox's trigger alone, publishes each direct message from the paired account as their words to their agent (`relayed`, which the platform takes only from a connector bound as that door), and `mirror` sends each reply an agent posts here back, in at most four messages. Unlinked, it tells Telegram so and publishes nothing. - Plain text over Telegram's servers, said on its page: not end-to-end encrypted. Direct messages and text only for now; topics later. The page is the chat's (its files linked) with the setup above it. A fake Bot API (crates/fakes) and the computers lane run it end to end on the stub agent: setup, pairing and its refusals, the door, a DM reaching the agent as its owner's and the reply going back, a stranger, a duplicate update, a forged delivery, and an unlink. Telegram is in the deployment's default connectors beside SimpleX (a decision for Paul: a deployment that wants it off names `connectors` without it); #709's check of a connector the deployment does not name uses `signal` now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security review of #808 (must-fix 1 and should-fixes 1, 2, 5, 6, 8): - `telegram` is named in `blessed::CONNECTORS`, so a Telegram chat is its owner's alone to edit from its making (#804's rule): its agent, acting for its owner or not, sets none of its secrets, writes it no page and gets no token to push its code, and a fork makes it no door again. - The agent joins as a viewer: the template's `work` takes viewer posts from agents, and a reply whose files a viewer cannot upload now goes with its words, saying so (the bridge refused the whole post before). - `intake` publishes what `job.door` says (no guess among the members) and `to` the bound agent; `mirror` sends nothing while unlinked; another bot's token clears the updates the last one took; `work` lists `outcome` as the chat template's does. - The e2e proves it: the agent's pokes (a secret, a page, a code token) are refused; the DM's turn is asked as the owner; interim words are not sent and a file is named; with Telegram off nothing is published or sent; unlinked from its page the agent's membership goes; a fork lets the binding go for good. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alex's review of #806 and #808, and the security review's tests: the agent, acting for its owner, is refused the connector's `seen` and `propose` as well as its secret, a page and a code token; made an editor member by its owner, as itself, it is refused the secret too; its owner rotates the webhook's secret, and the old one takes nothing; after a fork and its template again, a direct message is published nowhere. The stub's `door poke` asks these, for its owner or as itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sent The platform keeps a chat's own agent, so an unlink leaves the agent a viewer of the connector's chat; the lane checks it answers there on the web and nothing reaches Telegram (`mirror` reads `job.door`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A chat's own agent is not demoted (the platform keeps its replies and steps an editor's), and on a connector it acts as a viewer whatever its membership says; the lane no longer sets it back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security re-review, leftovers 2 and 4: `mirror` sends a reply only when its poster is the agent `job.door` names (after a rebind, or with another agent in the chat, theirs stay in the chat); and the lane checks the platform refuses making anyone an editor of the connector, the agent staying a viewer, as its members list says. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stub's `door poke` gains visibility, invite, rotate and status, and says when an answer shows the share link. On the Telegram connector the agent acting for its owner is refused each and shown no share link; on its owner's own chat it does each, and is shown it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renewing the lane chat's share link left a later check reading it with the old one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul, 2026-10-31: both connectors are tagged "Experimental"; SimpleX "Secure" (end to end, to the computer) in Settings, beside its heading (which names its nav link, so the tags sit under it), and Telegram "Less secure" on its page, its tooltip the literal reason: Telegram's servers can read these messages. A word each; the page's encryption sentence goes, the tag saying it. The shell lane checks Settings' tags and its nav link's name; the templates lane opens the Telegram page as its owner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12a9c65 to
5426d4d
Compare
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for now at 5426d4db810df6d7984515d7ca80e7ef62a2c109.
The previous missing-asset blocker remains unfixed. All ten PR patches match the last reviewed head in git range-diff; this rebase does not add the missing dependency. The earlier identity and routing fixes remain unchanged.
Blocking:
- Telegram's page still cannot initialize.
templates/telegram/site/chat.js:52(the shared chat module) imports./control.js, buttemplates/telegram/site/contains no such file or link. The release builder indexes each template's files, andblessed::site_fileresolves only within that template. A fresh Telegram page therefore cannot load its module graph; neithermountnorsetupintemplates/telegram/site/index.html:60–64executes, breaking chat and bot setup.
Package the shared control.js alongside Telegram's other shared modules. Check the relative-import closure (excluding platform-provided modules), and assert successful chat/setup initialization in the browser test.
Validation: the local relative-import/file check reproduces the missing dependency; traced release indexing and serving through crates/templates/build.rs, blessed::site_file, and cell/src/serve.rs. git diff --check passes. CI check, images check, and real-Hermes Docker were pending; e2e build/shards were skipped. No local full workerd/browser e2e or real Telegram bot test was run. The prescribed external engineering-style file is unavailable on this machine.
Complexity pass: the shared-module approach remains proportionate; the correction is to package its dependency.
# Conflicts: # docs/api.md # docs/bridge.md # docs/computers.md
Master's chat page imports `./control.js`; the Telegram connector's page links the chat's files one by one and had no link to it, so on master it never started (Alex's review). It links it now, and a templates test (crates/templates/tests/page_imports.rs) holds every template's pages to it: each relative import, a script's or its page's, names a file its template serves (the platform's own `__*` aside), so a module the chat gains fails the tests, naming the template to link it in, not a page in production. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alex's review: not only its labels. Its chat's composer is ready and open to its owner, the still-loading line gone, and its setup drawn from its state, so a module it imports and does not serve fails here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks. Fixed at fa644f8, with master merged in (no force-push). 1. The relative-import closure.
The browser check proves the page starts (fa644f8). The templates lane opens a fresh Telegram connector as its owner. It now waits for:
With Master merge. Master rewrote
Validation on the exact heads:
Non-blocking: the A → unlink → B and first-binding-to-a-non-first-agent cases are now #875, so they aren't lost. |
alexlwn123
left a comment
There was a problem hiding this comment.
Pass at fa644f84d5fcf024559397cafdcb35584059868d.
The previous missing-asset blocker is fixed: Telegram now packages the shared control.js through the existing symlink/release-index path. The new import check passes, and the browser assertion now requires the chat's ready composer, enabled input, removed loading notice, and rendered setup. The earlier identity and routing fixes remain unchanged. I found no new blocking regression after the merge from master.
The correction is proportionate: one shared asset link and focused regression coverage, without new runtime machinery.
Validation: cargo test -p fragment-templates --test page_imports --quiet, cargo check -p fragment-e2e --quiet, and git diff --check against the master merge base passed. CI images check passes; main check and real-Hermes Docker are pending, and e2e build/shards were skipped. I did not run local workerd/browser e2e or use a real Telegram bot. The prescribed external engineering-style file is unavailable on this machine.
Non-blocking: the previously requested A → unlink → B and initial non-first-agent integration cases remain useful follow-up coverage, including suppression of the other agent's reply.
Stacked on #807 (page secrets). The Telegram connector:
templates/telegram, a blessed chat that is also its owner's direct messages with a bot of their own. It's an ordinary connector fragment built only from the generic pieces below it.Reworked after the security review and Alex's review. Only the owner's words count as the owner's.
telegramis a connector (blessed::CONNECTORS), so under #804's rule no one but its owner edits a Telegram chat. Its agent, acting for its owner or as an editor member, can't set its webhook secret (path a), push code to rewrite its pairing (path b), or write a page on its origin (path c), and a fork makes it no door for good.How it works
api.telegram.org(__secrets; never through its code or a model).connectchecks the token (getMe) and points the bot's webhook at the chat's inbox (setWebhook, with the secret as itssecret_token), both filled into the URL at the egress point.inbox), so no member can forge one./start <nonce>link, good for ten minutes. The account that answers it is proposed.confirm, an owner's operation an agent can't call, whomever it acts for)./startwith an account of its choosing. When two accounts answer one link, nobody is proposed and the owner pairs again.__door, A connector its owner alone edits, bound by them as their agent's door #804).intake(its inbox's trigger alone; a direct call is refused) publishes each DM from the paired account as their words to their agent.mirrorsends each reply the agent posts back to Telegram, in at most four messages.job.doorsays,tothe bound agent; no guessing among members (Alex's second point).mirrorsends only the bound agent's replies (record.principal === job.door.to); another agent's in the chat stay there (re-review, leftover 4).worktakes viewer posts, and a reply whose file a viewer can't upload goes with its words, saying so (the bridge used to drop the whole reply).Tests
crates/fakes/src/telegram.rs).turn.start.asker),toit, through thetelegramdoor, and the reply arriving at the fake;seenandpropose; no one made an editor of it (400), and the agent, as itself, refused the secret;__door), after which the agent's web reply isn't sent;On by default (Paul, 10-31):
telegramis in the deployment's default connectors besidesimplex. A deployment that wants it off namesconnectorswithout it, and org policy will narrow it per org. #709's check of a connector the deployment doesn't name usessignalnow.Not yet: a real bot. No real Telegram bot was created or messaged; that needs the coordinator's say-so, then a preview check.
Complexity: +1024 / −21 (without docs, e2e and the fake: +593 / −14). The template (manifest, app, setup page) is most of it; the platform adds only its name to the blessed list, and the shell two tags.
🤖 Generated with Claude Code