Skip to content

docs(spec): telegram remote-control phase 2 — inline approvals (#1805)#2502

Draft
CodeGhost21 wants to merge 1 commit into
tinyhumansai:mainfrom
CodeGhost21:feat/1805-telegram-inline-approvals-spec
Draft

docs(spec): telegram remote-control phase 2 — inline approvals (#1805)#2502
CodeGhost21 wants to merge 1 commit into
tinyhumansai:mainfrom
CodeGhost21:feat/1805-telegram-inline-approvals-spec

Conversation

@CodeGhost21
Copy link
Copy Markdown
Contributor

@CodeGhost21 CodeGhost21 commented May 22, 2026

Summary

  • Design doc for phase 2 of Bring OpenHuman’s Telegram channel to OpenCode-level remote-control parity #1805: route ApprovalGate prompts to all bound Telegram chats with inline approve / approve-always / deny buttons.
  • Cross-chat sync: edit messages with "decided by @who via " attribution when an approval is resolved (from any source).
  • Adds a /pending slash command for on-demand recovery when the auto-broadcast was missed (e.g. across a core restart).
  • Extends ApprovalDecided event + approval_decide RPC + approval_audit row with optional decided_by_actor / decided_by_surface.
  • Spec only — no code in this PR. Implementation will follow in a separate PR after spec approval.

Problem

Phase 1 (#2249) shipped /status, /sessions, /new, /help — useful for visibility, but operators on Telegram still can't act on permission requests. Today the ApprovalGate parks tool calls and surfaces them only to the desktop UI. The fully-stated goal of #1805 covers many more slices (live ticks, /abort, /task, /files, /models, /worktree); each is its own spec/plan/PR cycle. This spec narrowly addresses the inline approvals slice — the most-requested operator-visible gap.

Solution

TelegramApprovalSubscriber listens on the approval domain of the event bus:

  • On ApprovalRequested → broadcasts a prompt + 3-button inline keyboard to every chat that has a session binding; records (chat_id, message_id) in an in-memory pending_map.
  • On ApprovalDecided (from any surface) → edits every recorded message to a final "✓ decided by @who via X" state and removes the keyboard.

A new callback_query branch in channel_recv.rs re-checks the allowlist on every tap, calls approval_decide with actor=@username, surface=\"telegram\", and toasts the result via answerCallbackQuery. Stale taps (already-decided / expired) self-correct to a toast + best-effort message edit.

Key design choices (full rationale in the spec):

  • Routing: broadcast to all bound chats; first-decision-wins.
  • Pending map: in-memory in the subscriber — stale taps after core restart degrade gracefully to "already decided".
  • Telegram API surface: three new pub(crate) methods on TelegramChannel (send_with_inline_keyboard, edit_message_text, answer_callback_query); generic Channel trait untouched.
  • Decider identity: ApprovalDecided event + RPC + audit row extended with optional decided_by_actor / decided_by_surface. Server defaults surface=\"desktop\" when absent for back-compat with stale frontend builds.
  • /pending is chat-local (not a broadcast) and dedupes per chat against pending_map.

Explicit non-goals for this slice: agent Q&A elicitation, active expiry sweeping, real Telegram E2E automation, and every other #1805 slice. Each gets its own spec.

Submission Checklist

Spec-only PR — no production code or tests change in this PR.

  • Tests added or updated — N/A: documentation-only change.
  • Diff coverage ≥ 80% — N/A: documentation-only change (no executable lines).
  • Coverage matrix updated — N/A: behaviour-only design proposal; matrix update will accompany the implementation PR.
  • All affected feature IDs from the matrix are listed in the PR description under ## RelatedN/A: no code surfaces touched yet.
  • No new external network dependencies introduced — N/A: no code change.
  • Manual smoke checklist updated — N/A: no release-cut surfaces touched.
  • Linked issue listed via #NNN in the ## Related section (this PR documents phase 2 of Bring OpenHuman’s Telegram channel to OpenCode-level remote-control parity #1805; the implementation PR will close it).

Impact

  • Runtime: none — spec only.
  • Compatibility: the spec's RPC changes are additive (optional params + nullable columns + default surface=\"desktop\"). Existing desktop frontend continues to work unchanged until the wrapper is updated alongside the implementation PR.
  • Security: spec mandates allowlist re-check on every callback_query, reuses existing approval/redact.rs scrubbing, never logs message bodies / args / bot token. [telegram-approval] log prefix throughout.
  • Migration: planned SQLite migration adds two nullable TEXT columns to approval_audit — additive, no data loss.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

Commit & Branch

  • Branch: feat/1805-telegram-inline-approvals-spec
  • Commit SHA: 4d18cddd70a09da09a33d41306edb0fac2abef7b

Validation Run

  • pnpm --filter openhuman-app format:check — N/A (no app code changed)
  • pnpm typecheck — N/A (no TS changed)
  • Focused tests: N/A (spec only)
  • Rust fmt/check (if changed): N/A (no Rust changed)
  • Tauri fmt/check (if changed): N/A (no Tauri changed)

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: none in this PR (design doc only).
  • User-visible effect: none in this PR. Once the follow-up implementation PR ships, operators will receive inline-button approval prompts in every bound Telegram chat with cross-chat sync.

Parity Contract

  • Legacy behavior preserved: yes — the spec's RPC + event changes are additive (optional params, nullable columns, server defaults preserve current approval_decide callers).
  • Guard/fallback/dispatch parity checks: spec covers fallback to desktop-only approvals when downcast fails / no Telegram chats are bound / core restart loses the in-memory pending map.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none known.
  • Canonical PR: this one.
  • Resolution: N/A.

Summary by CodeRabbit

  • Documentation
    • Added design specification for Telegram inline approvals, including architecture for broadcasting approval prompts across chats and handling approval decisions with operator recovery mechanisms.

Review Change Stack

…tinyhumansai#1805)

Design doc for phase 2 of tinyhumansai#1805. Scope: route ApprovalGate prompts to
all bound Telegram chats with inline approve / approve-always / deny
buttons, edit messages cross-chat on decision with actor + surface
attribution, and add /pending for on-demand recovery.

Approvals only — Q&A and other remote-control slices (live ticks,
/abort, /task, /files, /models, /worktree) are explicit non-goals,
each its own future spec.
@CodeGhost21 CodeGhost21 requested a review from a team May 22, 2026 20:28
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive design specification for Telegram inline approvals (phase 2 of the remote-control initiative). The spec details multi-chat approval broadcasting, callback-driven decision handling with identity attribution, a /pending recovery command, file changes across core and Telegram domains, testing strategy, acceptance criteria tied to issue #1805, and identified implementation risks.

Changes

Telegram Inline Approvals Specification

Layer / File(s) Summary
Specification overview and architecture
docs/superpowers/specs/2026-05-23-telegram-inline-approvals-design.md
Scope/non-goals, multi-chat broadcasting, in-memory pending message tracking, callback bridging, /pending recovery command, identity propagation rules (decided_by_actor, decided_by_surface), and expiry rendering without background sweeping.
Core data flow and implementation blueprint
docs/superpowers/specs/2026-05-23-telegram-inline-approvals-design.md
Planned file changes (Telegram provider, approval domain, runtime wiring, catalog); subscriber registration flow; ApprovalRequested broadcasting with race-condition mitigation; callback-data format; error handling (already decided, expired, unauthorized, disabled); /pending per-chat de-duplication; security validation (allowlist re-check, callback spoofing, logging constraints).
Testing strategy and acceptance criteria
docs/superpowers/specs/2026-05-23-telegram-inline-approvals-design.md
Rust unit/integration tests for subscriber, callback-query, /pending, approval round-tripping, JSON-RPC E2E, frontend, and deterministic Telegram integration scenario; acceptance criteria including approval inline handling, decision attribution, coverage ≥80%, and capability catalog updates.
Implementation roadmap and risk mitigation
docs/superpowers/specs/2026-05-23-telegram-inline-approvals-design.md
Step-by-step sequencing: core approval changes, Telegram helpers, subscriber + callback, /pending, startup wiring, integration/E2E, catalog/frontend; documented risks (restart state loss, concurrent taps, callback validation, test injection patterns) and open questions.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~20 minutes

Suggested labels

feature

Poem

🐰 A blueprint in markdown form,
For approvals sent through every swarm,
In Telegram chats, decisions bloom,
No more guessing in the room.
Phase two hops toward the goal,
Making Telegram a whole! 📱✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely describes the main change: a design specification document for Telegram remote-control phase 2 focusing on inline approvals, directly addressing issue #1805.
Linked Issues check ✅ Passed The design spec comprehensively maps to issue #1805 acceptance criteria by documenting inline approval/deny functionality, cross-chat message sync, event publishing, security allowlist checks, and audit trail extensions for attribution.
Out of Scope Changes check ✅ Passed The specification stays within scope by explicitly documenting non-goals (Q&A, active expiry, full automation, other #1805 slices) and focusing solely on phase 2 inline approvals design without implementing unrelated features.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@CodeGhost21 CodeGhost21 marked this pull request as draft May 22, 2026 20:29
@coderabbitai coderabbitai Bot added the feature Net-new user-facing capability or product behavior. label May 22, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/specs/2026-05-23-telegram-inline-approvals-design.md`:
- Line 478: The table row contains unescaped pipe characters in the snippet
`appr:<o|a|d>:` which breaks the Markdown table parse; update the table cell to
escape the pipes (e.g. `appr:<o\|a\|d>:`) or wrap the entire fragment in a code
span that preserves pipes, and ensure the same escaped form or explanatory note
is used where `approval_keyboard` is described so the table stays valid and the
construction-time validation remark still matches the token format.
- Around line 25-77: The fenced architecture diagram block (the block showing
ApprovalGate, DomainEvent::ApprovalRequested, TelegramApprovalSubscriber,
channel_recv.rs callback_query handler, etc.) is missing a language tag which
triggers MD040; update the opening fence from ``` to ```text so the diagram is
fenced as a text block and no other changes are needed. Ensure the closing fence
remains ``` and leave the inner diagram (ApprovalGate, Telegram Bot API,
TelegramApprovalSubscriber.handle, pending_map.remove) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9f49d7b8-1355-492c-8588-413c25de5f09

📥 Commits

Reviewing files that changed from the base of the PR and between 0f439fe and 4d18cdd.

📒 Files selected for processing (1)
  • docs/superpowers/specs/2026-05-23-telegram-inline-approvals-design.md

Comment on lines +25 to +77
```
┌──────────────────┐
│ ApprovalGate │ intercepts external-effect tool calls
│ (existing) │ parks future on oneshot
└────────┬─────────┘
│ publishes
┌───────────────────────────────┐
│ DomainEvent::ApprovalRequested│
└─────────────┬─────────────────┘
│ event bus
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌──────────────────────┐ ┌────────────────┐
│ Desktop UI │ │ TelegramApproval- │ │ (future │
│ (existing) │ │ Subscriber (new) │ │ providers) │
└───────┬────────┘ └──────────┬───────────┘ └────────────────┘
│ │
│ │ for every bound chat:
│ │ sendMessage + inline_keyboard
│ │ record (chat_id, msg_id) in pending_map
│ ▼
│ ┌─────────────────┐
│ │ Telegram Bot │
│ │ API │
│ └────────┬────────┘
│ │ user taps button
│ ▼
│ ┌─────────────────────────┐
│ │ channel_recv.rs: │
│ │ callback_query handler │ ← allowlist re-check
│ │ (new branch) │
│ └────────────┬────────────┘
│ │ approval_decide(
│ │ request_id, decision,
│ │ actor=@user, surface="telegram")
▼ ▼
approval_decide RPC ───────┐
│ publishes
┌──────────────────────────────────┐
│ DomainEvent::ApprovalDecided │
│ (extended w/ decided_by_actor + │
│ decided_by_surface) │
└────────────────┬─────────────────┘
TelegramApprovalSubscriber.handle()
for each (chat_id, msg_id) in pending_map:
editMessageText("✓ decided by @who via X")
[remove inline_keyboard]
pending_map.remove(request_id)
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to the architecture fenced block.

Line 25 opens a fenced code block without a language, which triggers MD040 and reduces renderer/tooling consistency.

Proposed fix
-```
+```text
                      ┌──────────────────┐
                      │  ApprovalGate    │  intercepts external-effect tool calls
...
                    pending_map.remove(request_id)
-```
+```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
┌──────────────────┐
│ ApprovalGate │ intercepts external-effect tool calls
│ (existing) │ parks future on oneshot
└────────┬─────────┘
│ publishes
┌───────────────────────────────┐
│ DomainEvent::ApprovalRequested│
└─────────────┬─────────────────┘
│ event bus
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌──────────────────────┐ ┌────────────────┐
│ Desktop UI │ │ TelegramApproval- │ │ (future │
│ (existing) │ │ Subscriber (new) │ │ providers) │
└───────┬────────┘ └──────────┬───────────┘ └────────────────┘
│ │
│ │ for every bound chat:
│ │ sendMessage + inline_keyboard
│ │ record (chat_id, msg_id) in pending_map
│ ▼
│ ┌─────────────────┐
│ │ Telegram Bot │
│ │ API │
│ └────────┬────────┘
│ │ user taps button
│ ▼
│ ┌─────────────────────────┐
│ │ channel_recv.rs: │
│ │ callback_query handler │ ← allowlist re-check
│ │ (new branch) │
│ └────────────┬────────────┘
│ │ approval_decide(
│ │ request_id, decision,
│ │ actor=@user, surface="telegram")
▼ ▼
approval_decide RPC ───────┐
│ publishes
┌──────────────────────────────────┐
│ DomainEvent::ApprovalDecided │
│ (extended w/ decided_by_actor + │
│ decided_by_surface) │
└────────────────┬─────────────────┘
TelegramApprovalSubscriber.handle()
for each (chat_id, msg_id) in pending_map:
editMessageText("✓ decided by @who via X")
[remove inline_keyboard]
pending_map.remove(request_id)
```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 25-25: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-05-23-telegram-inline-approvals-design.md` around
lines 25 - 77, The fenced architecture diagram block (the block showing
ApprovalGate, DomainEvent::ApprovalRequested, TelegramApprovalSubscriber,
channel_recv.rs callback_query handler, etc.) is missing a language tag which
triggers MD040; update the opening fence from ``` to ```text so the diagram is
fenced as a text block and no other changes are needed. Ensure the closing fence
remains ``` and leave the inner diagram (ApprovalGate, Telegram Bot API,
TelegramApprovalSubscriber.handle, pending_map.remove) unchanged.

| Existing desktop frontend calls `approval_decide` without the new params | Server defaults `surface = Some("desktop")` when both are absent. Frontend update is additive. |
| `Arc<dyn Channel>` → `Arc<TelegramChannel>` downcast may need a small trait change | Plan covers a `try_downcast` helper option first; falls back to `as_any` only if needed. |
| Real Telegram users tap stale buttons after core restart | Acceptable — "already decided or expired" toast + best-effort edit covers it. |
| `callback_data` length cap (64 bytes) is exceeded by long request_ids | Request ids are uuids (36 bytes); `appr:<o|a|d>:` adds 7. Total 43. Comfortable headroom. Validate at construction time in `approval_keyboard` to fail loud if this ever changes. |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape pipe characters inside the table cell to preserve column structure.

Line 478 includes unescaped | inside a table row (appr:<o|a|d>:), which breaks the 2-column table parse (MD056).

Proposed fix
-| `callback_data` length cap (64 bytes) is exceeded by long request_ids | Request ids are uuids (36 bytes); `appr:<o|a|d>:` adds 7. Total 43. Comfortable headroom. Validate at construction time in `approval_keyboard` to fail loud if this ever changes. |
+| `callback_data` length cap (64 bytes) is exceeded by long request_ids | Request ids are uuids (36 bytes); `appr:<o\|a\|d>:` adds 7. Total 43. Comfortable headroom. Validate at construction time in `approval_keyboard` to fail loud if this ever changes. |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `callback_data` length cap (64 bytes) is exceeded by long request_ids | Request ids are uuids (36 bytes); `appr:<o|a|d>:` adds 7. Total 43. Comfortable headroom. Validate at construction time in `approval_keyboard` to fail loud if this ever changes. |
| `callback_data` length cap (64 bytes) is exceeded by long request_ids | Request ids are uuids (36 bytes); `appr:<o\|a\|d>:` adds 7. Total 43. Comfortable headroom. Validate at construction time in `approval_keyboard` to fail loud if this ever changes. |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 478-478: Table column count
Expected: 2; Actual: 4; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-05-23-telegram-inline-approvals-design.md` at
line 478, The table row contains unescaped pipe characters in the snippet
`appr:<o|a|d>:` which breaks the Markdown table parse; update the table cell to
escape the pipes (e.g. `appr:<o\|a\|d>:`) or wrap the entire fragment in a code
span that preserves pipes, and ensure the same escaped form or explanatory note
is used where `approval_keyboard` is described so the table stays valid and the
construction-time validation remark still matches the token format.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Net-new user-facing capability or product behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bring OpenHuman’s Telegram channel to OpenCode-level remote-control parity

1 participant