feat(telegram): re-introduce vouched-only guest replies #80

Merged
weiwen merged 1 commit from fm/evie-guest-mode-reintroduce into main 2026-07-30 16:29:12 +08:00
Owner

What this does

Re-introduces Telegram Guest Mode / guest replies. Bot API guest mode delivers a guest_message update when a user mentions or replies to evie in a chat evie is not a member of; evie answers exactly once via answerGuestQuery and streams the reply into that single inline message via editMessageText(inline_message_id). ADR 0023 removed this wholesale (allowed_updates became ["message"] - the 074e2cd regression); this revives it under a new authorization model.

Authorization model (vouched-only + own-sandbox)

  • Only a Vouched User may summon. resolve_guest_message authorizes the guest_message's from id against the UserRegistry, identical to resolve_message for a normal message, minus the group-addressing gate (a guest summons is addressed by construction). A non-vouched summoner is dropped and logged with no reply, mirroring Route::DropUnauthorized. The removed per-chat three-list model (trusted_sender_ids/trusted_chat_ids/sandboxed_chat_ids) is not revived.
  • The reply runs in the summoner's OWN sandbox. The session key is the summoner's own (Sender, foreign-chat, Topic, sudo=false), so SessionManager provisions their home / capabilities / privilege-drop target exactly as a typed DM from them would - never as root/daemon, never in another user's sandbox, never sudo. No parallel spawn path is added.

Security rationale: a guest reply introduces no new trust surface - it is just the vouched user acting from a foreign chat, confined to the sandbox they already own. No anonymous access, no cross-user access.

Mechanics recovered from git history / ADR 0010 & 0012

  • MessageView regains its Target::Chat | Target::Inline split and plan_plain; inline edits go through the hand-rolled edit_inline_message (working around rustigram-api 0.11 decoding an inline edit's bare-true response as Message).
  • Single-message answer with truncation of overflow pages (ADR 0010's accepted limit).
  • Streaming mode derives from the existing telegram.streaming_mode (Draft collapses to Edit, None waits for the final render); no new config knob.

Superseding ADR

Adds ADR 0024 (vouched-only guest replies in the summoner's own sandbox): partially supersedes the guest-removal decision in ADR 0023 and revives the answerGuestQuery mechanics from ADR 0010/0012 under the new authorization model. ADR 0023/0010/0012 banners updated to cross-reference it; CONTEXT.md gains a Guest Reply term and updated mod.rs/view.rs file-map entries.

Tests

  • resolve_guest_message_authorizes_vouched_summoner_in_their_own_sandbox: vouched summoner routes to a Session keyed to the summoner (asserts sender == summoner, != DAEMON, chat is the foreign chat, !is_sudo, capabilities carried); un-vouched -> DropUnauthorized.
  • webhook_subscribes_to_guest_messages: allowed_updates() contains guest_message (guards the 074e2cd regression).
  • guest_prompt_combines_reply_quote_and_summon: prompt assembly (summon+quote, summon-only, quote-only, neither -> None).
  • Restored view.rs inline-target coverage (plan_plain_*, message_view_new_inline_*, message_view_empty_stores_thread_id_on_target).

just check (fmt, clippy -D warnings, 480 tests) passes clean.

## What this does Re-introduces Telegram Guest Mode / guest replies. Bot API guest mode delivers a `guest_message` update when a user mentions or replies to evie in a chat evie is **not** a member of; evie answers exactly once via `answerGuestQuery` and streams the reply into that single inline message via `editMessageText(inline_message_id)`. ADR 0023 removed this wholesale (`allowed_updates` became `["message"]` - the `074e2cd` regression); this revives it under a new authorization model. ## Authorization model (vouched-only + own-sandbox) - **Only a Vouched User may summon.** `resolve_guest_message` authorizes the `guest_message`'s `from` id against the `UserRegistry`, identical to `resolve_message` for a normal message, minus the group-addressing gate (a guest summons is addressed by construction). A non-vouched summoner is dropped and logged with no reply, mirroring `Route::DropUnauthorized`. The removed per-chat three-list model (`trusted_sender_ids`/`trusted_chat_ids`/`sandboxed_chat_ids`) is **not** revived. - **The reply runs in the summoner's OWN sandbox.** The session key is the summoner's own `(Sender, foreign-chat, Topic, sudo=false)`, so `SessionManager` provisions their home / capabilities / privilege-drop target exactly as a typed DM from them would - never as root/daemon, never in another user's sandbox, never sudo. No parallel spawn path is added. Security rationale: a guest reply introduces no new trust surface - it is just the vouched user acting from a foreign chat, confined to the sandbox they already own. No anonymous access, no cross-user access. ## Mechanics recovered from git history / ADR 0010 & 0012 - `MessageView` regains its `Target::Chat | Target::Inline` split and `plan_plain`; inline edits go through the hand-rolled `edit_inline_message` (working around rustigram-api 0.11 decoding an inline edit's bare-`true` response as `Message`). - Single-message answer with truncation of overflow pages (ADR 0010's accepted limit). - Streaming mode derives from the existing `telegram.streaming_mode` (`Draft` collapses to `Edit`, `None` waits for the final render); **no new config knob**. ## Superseding ADR Adds **ADR 0024** (vouched-only guest replies in the summoner's own sandbox): partially supersedes the guest-removal decision in ADR 0023 and revives the `answerGuestQuery` mechanics from ADR 0010/0012 under the new authorization model. ADR 0023/0010/0012 banners updated to cross-reference it; `CONTEXT.md` gains a **Guest Reply** term and updated `mod.rs`/`view.rs` file-map entries. ## Tests - `resolve_guest_message_authorizes_vouched_summoner_in_their_own_sandbox`: vouched summoner routes to a Session keyed to the summoner (asserts `sender == summoner`, `!= DAEMON`, chat is the foreign chat, `!is_sudo`, capabilities carried); un-vouched -> `DropUnauthorized`. - `webhook_subscribes_to_guest_messages`: `allowed_updates()` contains `guest_message` (guards the `074e2cd` regression). - `guest_prompt_combines_reply_quote_and_summon`: prompt assembly (summon+quote, summon-only, quote-only, neither -> None). - Restored `view.rs` inline-target coverage (`plan_plain_*`, `message_view_new_inline_*`, `message_view_empty_stores_thread_id_on_target`). `just check` (fmt, clippy `-D warnings`, 480 tests) passes clean.
feat(telegram): re-introduce vouched-only guest replies
Some checks failed
CI / check (pull_request) Failing after 2m45s
PR Triage — label changes-requested reviews / triage-review (pull_request) Successful in 1s
7d97dd0576
Bot API guest mode lets a user summon evie from a chat evie is not a
member of via a guest_message update. ADR 0023 removed it wholesale;
this revives it with a new authorization model.

Only a Vouched User may summon: resolve_guest_message authorizes the
guest_message's 'from' id against the UserRegistry (same gate as a normal
message, no addressing check since a summons is addressed by
construction), dropping any non-vouched summoner with no reply. The turn
runs in the summoner's OWN sandbox - the SessionKey carries their Sender
axis in the foreign chat, so SessionManager provisions their
home/capabilities/drop-target exactly as a typed DM would, never as
root/daemon and never in another user's sandbox.

Evie answers once via answerGuestQuery then streams into the single
inline message via editMessageText, recovering the MessageView
Target::Inline split and the hand-rolled inline-edit workaround from the
pre-0023 design. guest_message goes back into allowed_updates (guarding
the 074e2cd regression). Streaming mode derives from the existing
telegram.streaming_mode; no new config knob.

Adds ADR 0024 (superseding the guest-removal decision in ADR 0023,
reviving the answerGuestQuery mechanics from ADR 0010/0012) and updates
CONTEXT.md with a Guest Reply term.

Tests: resolve_guest_message routing (vouched -> summoner-keyed session,
un-vouched -> DropUnauthorized), allowed_updates contains guest_message,
guest_prompt assembly, and the restored view.rs inline-target coverage.
test(session): skip sandboxed-spawn provisioning test on a root runner
All checks were successful
CI / check (pull_request) Successful in 2m24s
PR Triage — label changes-requested reviews / triage-review (pull_request) Successful in 2s
49d584b7af
sandboxed_spawn_provisions_home_and_seeds_agents_md drives the real
non-sudo spawn path, which drops privileges to a non-root account and
fail-closes when its only candidate target is root (a sandbox never runs
as root). The test's drop target is the runner's own account, so on a
root test runner (the Forgejo CI container) it resolves to uid 0 and
spawn_for_key returns before seeding AGENTS.md - the home and .sessions
dirs are created first, so the test panicked reading the un-seeded
AGENTS.md. It passed only because most dev machines run the suite as a
non-root user.

Skip the test when running as root: the non-root-drop provisioning it
asserts genuinely cannot be exercised as root with the runner's own
account, mirroring the repo's existing environment-gated tests. Non-root
runs are unchanged. Verified: full 'just check' passes both as a normal
user and under 'unshare -r' (uid 0).
weiwen force-pushed fm/evie-guest-mode-reintroduce from 49d584b7af
All checks were successful
CI / check (pull_request) Successful in 2m24s
PR Triage — label changes-requested reviews / triage-review (pull_request) Successful in 2s
to fae33f95ba
All checks were successful
CI / check (pull_request) Successful in 1m51s
PR Triage — label changes-requested reviews / triage-review (pull_request) Successful in 1s
2026-07-30 16:25:36 +08:00
Compare
weiwen merged commit 81240cd0b2 into main 2026-07-30 16:29:12 +08:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
weiwen/evie!80
No description provided.