Skip to content

Telegram Remote Steering — Threat Model

Audience: operators evaluating whether to enable --telegram in production, and reviewers auditing the security posture of the M2–M4 Telegram surface. Operator-facing guidance lives in telegram-remote-steering.md; this document is the formal risk register.

This document enumerates the residual risks accepted when the Telegram remote-steering channel is enabled, the mitigations agents-fleet ships for each, and the actions that remain the operator's responsibility. It mirrors the structure used elsewhere in the codebase (see e.g. the H-1..H-7 risk catalogue in docs/proposals/brainstorm-feature/04-technical-design.md §11 / §13).

The Telegram channel is opt-in. No bot starts unless the operator both exports AGENTS_FLEET_TELEGRAM_TOKEN and launches with --telegram.


Threat model assumptions

  • Trust boundary 1 — the bot token. AGENTS_FLEET_TELEGRAM_TOKEN is effectively root credentials for the bot. agents-fleet never logs it, never echoes it, and never returns it from any command. Operator responsibility: environment hygiene (no .bash_history leaks, no log scraping, no committed dotfiles).
  • Trust boundary 2 — the paired Telegram account(s). Anyone with an unlocked device signed into a paired Telegram account can drive the fleet, subject to per-command confirmation flows. Operator responsibility: device locks, account 2FA, prompt revocation on loss.
  • Trust boundary 3 — Telegram infrastructure. Inbound updates and outbound messages traverse Telegram's servers. agents-fleet treats every inbound payload as untrusted (allowlist, nonce-bound callbacks, multi-tap for destructive ops) but cannot defend against compromise of Telegram itself. Operator responsibility: don't paste secrets you wouldn't paste into a Telegram chat.
  • Trust boundary 4 — the fleet host. Local code execution on the fleet host trivially defeats every mitigation here (anyone with shell can cat telegram.json, edit permission memory, etc.). The Telegram surface is not designed to harden against a local attacker.

Residual risks

Residual Risk 1 — Bot-token compromise grants full coordinator control

Threat. An attacker who obtains the bot token (env-var leak, screenshare, shell history, log scrape, stolen backup) can impersonate the bot from any host on the internet. They cannot directly impersonate a paired chat (the allowlist still applies), but they can read every message ever sent to the bot via the Bot API and inject responses on behalf of paired chats by calling sendMessage themselves — and once they have a paired chat's chatId, they can effectively author prompts that the fleet will treat as operator input.

Severity. Critical when realized — equivalent to coordinator shell access subject to confirmation flows.

Accepted mitigations.

  • Token is read only from AGENTS_FLEET_TELEGRAM_TOKEN env var; never written to disk or any log line. Verified by assertNoSecretLeak in src/bot/m2.integration.test.ts.
  • agents-fleet bot status allow-lists its output fields (token-configured boolean only; never the value, even masked).
  • agents-fleet bot rotate-token exists as the documented compromise recovery path: wipes pairings, bumps controlGeneration so any running bot detects the change and exits cleanly. The output explicitly tells the operator to update the env var.
  • BotFather supports /revoke so the token's authority can be killed upstream even if agents-fleet is offline.

Residual hazard. agents-fleet has no detection for token leakage — by design, because we never see the token outside the env var. Operator responsibility: treat the env var as a production secret, rotate on suspicion, and avoid sharing terminals while the var is set.


Residual Risk 2 — Lost or stolen paired device grants pocket access to the fleet

Threat. A holder of an unlocked phone signed into a paired Telegram account is a paired operator for all purposes. Multi-tap SEC raises the bar for destructive ops (/exit, /compete), but does not prevent a determined holder from completing three taps in 60 s.

Severity. High. Bounded by the confirmation flows: informational commands run silently, interactive commands require one tap, destructive commands require three taps in 60 s.

Accepted mitigations.

  • Per-chat allowlist enforced on every inbound update — only chats that completed pairing are accepted. src/bot/auth.ts.
  • Single-tap confirmation on every state-mutating command via the matrix in src/bot/commandMatrix.ts.
  • Multi-tap SEC confirmation (src/bot/multiTapConfirm.ts) on destructive operations: three taps of a nonce'd button within MULTI_TAP_DEFAULT_TTL_MS = 60_000 ms. The nonce prevents replay of a single captured callback_data.
  • bot revoke <chatId> revokes a single device.
  • bot revoke --user <fromId> bulk-revokes every pairing for a single Telegram account across all chats — the documented path for account-compromise recovery (src/bot/auth.ts:194removeAllPairingsForUser).
  • First-destructive-command warning (src/bot/firstDestructiveWarning.ts) surfaces the active --yolo ack posture the first time a destructive command runs in a paired session, so a thief who fat-fingers /exit at least sees the consequence inline.

Residual hazard. Pocket access by a knowledgeable adversary remains fully effective until the operator revokes. Operator responsibility: revoke pairings on device loss; consider not pairing the destructive set on phones that leave the house.


Residual Risk 3 — Telegram infrastructure trust and message-content disclosure

Threat. Every command typed and every prompt rendered passes through Telegram's servers. A Telegram-side compromise, lawful intercept, or employee with API access can read the full transcript — including any secrets pasted into prompts, any file contents the fleet quoted back in diagnostic output, and the chatIds of every paired operator.

Severity. Medium-to-high depending on what the fleet is steering. For benign fleets (codebase exploration, public OSS), low. For fleets running against private/regulated data, this risk is non-negotiable and the operator must either accept it or not enable --telegram.

Accepted mitigations.

  • Egress chunking with Markdown safety (src/bot/egressPipeline.ts) bounds message size and escapes content; doesn't reduce trust but ensures predictable behaviour.
  • Outbound ledger (src/bot/outboundLedger.ts) dedupes on retry so a network blip can't accidentally re-leak the same payload N times.
  • Per-chat rate limit (src/bot/outboundQueue.ts, default 1 msg/sec) honours Telegram's retry_after on 429s and avoids triggering bot-wide throttling, which would otherwise smear leaked content across longer delivery windows.
  • /mute, /unmute, runtime control lets operators silence chatty channels if a long-running run would otherwise broadcast sensitive iterative state.
  • bot status redaction ensures local diagnostic commands never echo Telegram-side identifiers in cleartext beyond the necessary bot list masked form (1234***5678).
  • Pairing tokens never appear in outbound payloads; permission decisions carry only nonce'd callback ids, not the underlying decision shape.

Residual hazard. End-to-end content trust in Telegram is unavoidable when using the Bot API. agents-fleet does not implement client-side encryption of message contents. Operator responsibility: do not paste secrets into Telegram-routed prompts; do not enable --telegram for fleets steering regulated data without an acceptable-use review.


Residual Risk 4 — Implicit yolo inheritance can broaden a worker's authority beyond what the operator typed

Threat. To avoid forcing operators to retype --yolo --acknowledge-* flags for every Telegram-spawned worker, agents-fleet's src/bot/implicitYolo.ts inherits the parent session's acknowledged-SEC-flags posture to child workers (getYoloRequirements, hasYoloForOperation, commandSpawnsWorker). An operator who launched the fleet with --yolo --acknowledge-all-sec and then pairs Telegram has implicitly authorized every Telegram-triggered worker to run with that same full bypass posture — without re-typing any flag at the Telegram surface.

Severity. Medium. Bounded by what the operator already authorized on the host. Cannot escalate beyond the parent session's existing acks; cannot re-introduce SEC bypasses the operator did not acknowledge.

Accepted mitigations.

  • No silent escalation. hasYoloForOperation() checks the parent's YoloAckState — if the parent did not acknowledge a SEC class, the child doesn't get it either. renderYoloShortage() formats a Telegram-friendly explanation of exactly which --acknowledge-* flag is missing so the operator sees the gap rather than a generic refusal.
  • First-destructive-command warning (Risk 2) surfaces the inherited posture inline the first time it matters, so the operator can /exit before authorizing further destructive ops.
  • /diagnose from Telegram lists the active SEC bypasses with source attribution (#88 — PR #154), so the operator can audit the inherited posture from the same surface that's about to use it.
  • No new ack flags introduced over Telegram. The Telegram surface only inherits — it cannot grant a new SEC ack that wasn't present at fleet launch. The acknowledgement flow remains anchored to the host process launch.

Residual hazard. An operator who launches their fleet with broad acks-on for convenience effectively widens the blast radius of any Telegram operator (including a holder of a stolen paired device — see Risk 2). Operator responsibility: match the launch-time --acknowledge-* posture to the actual operations Telegram will be allowed to drive. Prefer the narrowest set of acks for fleets steered remotely.


Residual Risk 5 — Inbound files land on disk with no scanning or type allowlist

Threat. Any paired operator can attach any file (photo, document, audio, video, voice note, video note — every type Telegram's Bot API supports). The bot downloads each attachment and persists it under ~/.fleet/attachments/<sessionId>/. There is no antivirus scan, no mime allowlist, no size cap on the agents-fleet side, and no executable-content sandboxing. A holder of a paired Telegram account can drop arbitrary bytes onto the fleet host as long as Telegram's own 50 MB-per-upload Bot API limit allows it.

Severity. Medium. Bounded by (a) the existing pairing allowlist — only paired chats can deliver files at all, so this is not an unauthenticated drop; (b) storage confined to ~/.fleet/attachments/ under per-session subdirectories with mode 0o700/0o600, so files do not land inside the source tree or in any directory a build system would scan; (c) agents-fleet never executes stored attachments — it hands the absolute path to the coordinator, which decides whether to view or pass it to a tool.

Accepted mitigations.

  • Filename sanitization (sanitizeAttachmentName in src/bot/attachmentStore.ts): directory separators stripped, leading dots removed (so attachments cannot become hidden files), characters outside [a-zA-Z0-9._-] replaced with _, embedded .. neutralized, trailing ./space trimmed. Empty / non-alphanumeric names fall back to attachment.bin. Traversal via fileName: '../../etc/passwd' is rejected: path.basename-style collapsing leaves only passwd, and the file lands inside the per-session attachment directory regardless.
  • Storage layout under the operator's home rather than the project working directory. Files cannot accidentally be staged for commit, picked up by tsc/webpack, or executed by a CI test runner that globs the source tree.
  • Restrictive modes: directories created at 0o700, files written at 0o600. Other local users cannot read or replace stored attachments.
  • Atomic writes: the file does not become visible at its final name until the stream has piped successfully (.partial → final rename); partial downloads can never be viewed as if complete.
  • Symlink-safe load: AttachmentStore.load() rejects paths outside the store root and refuses to follow symlinks, so a later operator smuggling a symlink into the directory cannot redirect a view outside ~/.fleet/attachments/.
  • PII redaction applies to outbound paths and filenames via the existing egress pipeline (PiiRedactor runs over every outbound message before MD2 escaping). A pasted token in a caption or a token-shaped substring inside an absolute path bounces through the same redaction as any other text — e.g. a stored path containing ghp_… would render as [REDACTED_GITHUB_TOKEN] in any coordinator message that quoted it back.

Residual hazard. The fleet host trusts paired operators not to upload malicious content. agents-fleet does not scan stored files for malware. If the coordinator hands an attachment to a worker that shells out (e.g. unzipping an archive, opening a PDF in a headless renderer), the worker inherits whatever risk the file carries — exactly as it would for any locally-supplied file. Likewise, a determined paired operator can fill the disk by uploading many large files in sequence; there is no size budget or quota enforcement on our side beyond Telegram's per-upload 50 MB cap. Operator responsibility: do not enable --telegram for fleets that auto-process attachments without a sandbox; periodically prune ~/.fleet/attachments/; treat attachment delivery as an authenticated-but-untrusted input channel.


Risk summary table

#RiskWorst-case impactPrimary mitigationOperator action required
1Bot-token compromiseCoordinator-level control via injected promptsenv-only token, no log echo, bot rotate-tokenTreat token as production secret; rotate on suspicion
2Lost / stolen paired devicePocket access; destructive ops need 3 taps in 60 sAllowlist, single-tap + multi-tap, bot revoke [--user]Revoke on device loss; restrict which devices are paired
3Telegram-side disclosureFull transcript readable by TelegramRedacted local commands; ledger dedupe; rate limitsDon't paste secrets; don't pair for regulated-data fleets
4Implicit yolo inheritanceTelegram-spawned workers inherit launch-time SEC acksNo silent escalation; first-destructive warning; /diagnoseLaunch with the narrowest --acknowledge-* set
5Unscanned inbound filesMalicious bytes on disk under ~/.fleet/attachments/; disk-fill via large uploadsSanitized names, 0o700/0o600 modes, no auto-exec, store outside source treeDon't auto-process attachments without a sandbox; periodically prune the attachment dir

See also