Skip to content

The Telegram connector: a blessed chat that is its owner's bot's DMs - #808

Merged
futurepaul merged 13 commits into
masterfrom
connect/telegram
Oct 31, 2026
Merged

futurepaul merged 13 commits into
masterfrom
connect/telegram

Conversation

@futurepaul

@futurepaul futurepaul commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

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. telegram is 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

  • Setup (the owner, on its page):
    • The bot's token is pasted, and a webhook secret is made by the browser. Each is set as this fragment's secret toward api.telegram.org (__secrets; never through its code or a model).
    • connect checks the token (getMe) and points the bot's webhook at the chat's inbox (setWebhook, with the secret as its secret_token), both filled into the URL at the egress point.
    • The inbox takes a delivery only with that secret in Telegram's header (inbox), so no member can forge one.
  • Pairing (browser confirmation):
    • A single-use /start <nonce> link, good for ten minutes. The account that answers it is proposed.
    • The owner confirms it on the page (confirm, an owner's operation an agent can't call, whomever it acts for).
    • Its numeric user and chat ids are bound; its name is only shown. Another bot's token forgets the account.
    • Members can read the inbox's records, so one could race the owner's own /start with an account of its choosing. When two accounts answer one link, nobody is proposed and the owner pairs again.
  • The door: the owner links it to their agent on the page (__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.
    • mirror sends each reply the agent posts back to Telegram, in at most four messages.
    • It publishes what job.door says, to the bound agent; no guessing among members (Alex's second point).
    • mirror sends only the bound agent's replies (record.principal === job.door.to); another agent's in the chat stay there (re-review, leftover 4).
    • The agent joins as a viewer. The template's work takes 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).
    • Strangers, groups, bots and repeated updates reach no one. Unlinked or turned off, nothing is relayed or sent; unlinked, Telegram is told, and the agent stays a viewer of the chat.
  • Labels (Paul, 10-31): both connectors are tagged "Experimental". SimpleX is tagged "Secure" (end to end, to the computer) in Settings, and Telegram "Less secure" on its page; that tag's tooltip says the literal reason, that Telegram's servers can read these messages. These are short tags under the heading, with no new paragraph; the page's old encryption sentence went, since the tag now says it. The Settings heading stays plain because the nav copies its text.
  • Plaintext: the template's description says Telegram messages aren't end-to-end encrypted. Direct messages and text only for now; a reply's files are named, not sent. Topics later: each connector chat is its own door.
  • The page is the chat's (its files linked, as the brain links the notes viewer), with a compact setup card above it, for the owner only.

Tests

  • A Bot API fake (crates/fakes/src/telegram.rs).
  • The computers lane runs it end to end on the stub agent:
    • connect, then pairing, including two accounts racing one link, a wrong nonce, a stranger's confirm, and a used link;
    • a forged delivery (403), and the owner's rotation of the webhook secret (the old one refused, the new one taken);
    • the binding, with the agent a viewer;
    • a DM reaching the agent asked as its owner (turn.start.asker), to it, through the telegram door, and the reply arriving at the fake;
    • interim words not sent, and a file named;
    • a stranger and a duplicate update;
    • the agent, for its owner, refused the secret, a page, a code token, seen and propose; no one made an editor of it (400), and the agent, as itself, refused the secret;
    • the agent, for its owner, refused visibility, an invite and the share link (rotation, and none shown in status), while on another fragment of its owner's it does all four (A connector its owner alone edits, bound by them as their agent's door #804's sharing rule);
    • Telegram off by policy: nothing published or sent;
    • an unlink from the page session (__door), after which the agent's web reply isn't sent;
    • a fork, then the template again: no door, and a DM is published nowhere.
  • The labels: the shell lane checks Settings' SimpleX tags (and that its nav link still reads "SimpleX"); the templates lane opens the Telegram page in Chrome as its owner and checks its tags and tooltip.
  • The templates crate holds it to the blessed rules, and the docs list it.

On by default (Paul, 10-31): telegram is in the deployment's default connectors beside simplex. A deployment that wants it off names connectors without it, and org policy will narrow it per org. #709's check of a connector the deployment doesn't name uses signal now.

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

@futurepaul
futurepaul added this pull request to stack #809 October 31, 2026 08:55
@futurepaul
futurepaul requested a review from alexlwn123 October 31, 2026 08:56

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. An editor can substitute the pairing candidate by suppressing the authentic update. templates/telegram/fragment.json exposes propose and seen as ordinary editor mutations; neither checks its caller (app.mjs:143–175). An editor (including an agent member) can read the incoming /start nonce and update id from inbox, call seen before the asynchronous intake reaches that step, then call propose with 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. Direct seen calls 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.

  2. 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_door retains A's membership and adds B, so door() still selects A. The platform correctly rejects the resulting relayed.agent mismatch; 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.

futurepaul added a commit that referenced this pull request Oct 31, 2026
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>
futurepaul added a commit that referenced this pull request Oct 31, 2026
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>
@futurepaul

Copy link
Copy Markdown
Collaborator Author

Both fixed:

  1. seen/propose substitution. Under A connector its owner alone edits, bound by them as their agent's door #804's rework, no one but its owner edits a connector, so the agent (for its owner, or as an editor member) can't call seen or propose. The lane checks both refusals. The two-account conflict stays for the remaining case, a member who reads the owner's own /start nonce.
  2. Recipient selection. door() is gone. The platform tells the job its binding (job.door: the relayed and the agent's identity), and intake publishes that, to the bound agent. Rebinding to another agent changes job.door.

Also from the security review:

  • mirror stops while unlinked;
  • seen clears on a bot change;
  • work lists outcome;
  • the lane asserts the DM's turn is asked as the owner, and covers the policy fence, interim words, __door from the page session, a fork, and rotation.

🤖 Generated with Claude Code

@futurepaul
futurepaul requested a review from alexlwn123 October 31, 2026 09:54

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

futurepaul added a commit that referenced this pull request Oct 31, 2026
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>
futurepaul added a commit that referenced this pull request Oct 31, 2026
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 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. The Telegram page cannot initialize because control.js is missing. templates/telegram/site/chat.js links 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, and blessed::site_file resolves 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 at templates/telegram/site/index.html:60–64 never executes either mount(document.body) or setup(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.

Base automatically changed from connect/page-secrets to master October 31, 2026 17:33
futurepaul and others added 10 commits October 31, 2026 12:33
`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>

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Telegram's page still cannot initialize. templates/telegram/site/chat.js:52 (the shared chat module) imports ./control.js, but templates/telegram/site/ contains no such file or link. The release builder indexes each template's files, and blessed::site_file resolves only within that template. A fresh Telegram page therefore cannot load its module graph; neither mount nor setup in templates/telegram/site/index.html:60–64 executes, 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.

futurepaul and others added 3 commits October 31, 2026 16:56
# 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>
@futurepaul

Copy link
Copy Markdown
Collaborator Author

Thanks. Fixed at fa644f8, with master merged in (no force-push).

1. control.js is missing. It's linked now (templates/telegram/site/control.js -> ../../chat/site/control.js), as Telegram's other shared chat modules are (2d12456).

The relative-import closure. crates/templates/tests/page_imports.rs (every_module_a_page_imports_is_its_own) reads every .js, .mjs and .html file under each template's site/, and requires each relative import to name a file that template serves. Platform modules (__fragment.js, __markdown.js) are skipped.

  • Every file is checked, including linked ones, so this covers the whole closure: control.js's own imports are checked too, now that it's one of Telegram's files.
  • Without the link it fails with telegram/site/chat.js imports ./control.js, which telegram does not serve: link it. With the link it passes, and every other template passes too.
  • It lives in tests/ because xtask's eval-sinks pass reads src/ as runtime code, and it flags the import( patterns the test searches for.

The browser check proves the page starts (fa644f8). The templates lane opens a fresh Telegram connector as its owner. It now waits for:

  • the chat to be live: #say data-ready="1" and the composer enabled;
  • the still-loading line to be gone;
  • the setup card to be drawn from state ("Connect Telegram", with its two tags).

With control.js missing, this is the check CI failed ([null]).

Master merge. Master rewrote docs/api.md, docs/bridge.md and docs/computers.md. I kept its text and re-applied this PR's three facts:

  • telegram is in the blessed list;
  • telegram is in the default connectors;
  • a refused upload sends the reply's words alone.

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.

@futurepaul
futurepaul requested a review from alexlwn123 October 31, 2026 22:11
@futurepaul
futurepaul removed this pull request from stack #809 October 31, 2026 22:12
@futurepaul
futurepaul enabled auto-merge October 31, 2026 22:12

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@futurepaul
futurepaul added this pull request to the merge queue Oct 31, 2026
Merged via the queue into master with commit 88da6fe Oct 31, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants