Skip to content

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

Merged
graycyrus merged 1 commit into
tinyhumansai:mainfrom
CodeGhost21:feat/1805-telegram-inline-approvals-spec
May 28, 2026
Merged

docs(spec): telegram remote-control phase 2 — inline approvals (#1805)#2502
graycyrus merged 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.

@CodeGhost21 CodeGhost21 added the docs Docs-only change; used by PR automation. label May 27, 2026
@CodeGhost21 CodeGhost21 marked this pull request as ready for review May 28, 2026 05:11
@oxoxDev oxoxDev assigned oxoxDev and unassigned oxoxDev May 28, 2026
Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

Solid spec. The architecture is well-reasoned — event-bus subscriber pattern keeps Telegram concerns out of the approval domain, the pre-insert race mitigation is the right move, security section is thorough, and the test matrix covers the meaningful edge cases. A few things worth flagging before the implementation PR picks this up:

**Issue**: [#1805](https://github.com/tinyhumansai/openhuman/issues/1805) (phase 2 of N)
**Date**: 2026-05-23
**Status**: Approved for implementation
**Phase 1**: [PR #2249](https://github.com/tinyhumansai/openhuman/pull/2249) (merged) — `/status`, `/sessions`, `/new`, `/help`
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.

[minor] Status: Approved for implementation is set before this PR has merged. It reads as accurate post-merge, but during review it's confusing — implementation PR authors who read this in-flight might take it as pre-approved direction when it's still pending. Consider Pending approval here and a follow-up commit to flip it once this lands, or note in the PR description that the status reflects intended final state.


---

## Files
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.

[minor] The OnceLock<Arc<TelegramApprovalSubscriber>> singleton pattern works fine in production, but the test matrix doesn't address how the integration test (telegram_integration.rs) handles multiple tests attempting set_global. OnceLock can only be initialized once per process — a second call will silently return the first value (or panic depending on the wrapper). If the integration suite runs multiple approval scenarios in the same process, only the first set_global takes effect. The implementation PR should either pass the subscriber instance directly into handle_callback_query (eliminating the global lookup in tests), or expose a reset_global_for_test helper behind #[cfg(test)].


| Concern | Mitigation |
|---|---|
| Non-allowlisted user taps a button | Re-check `from.username` + `from.id` against the allowlist on every callback. Non-allowlisted → toast "Not authorized" + log. Identical to the message-path check. |
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.

[minor] The pre-insert mitigation handles the case where ApprovalDecided fires before any sends — good. But there's a second case: ApprovalDecided fires while the broadcast loop is mid-flight. The remove() in the Decided handler drains the map. Subsequent successful sends in the Requested handler do get_mut(...).map(|v| v.push(...)) — that's a no-op because the entry was just removed. Those chats end up with live buttons that never get the "decided" edit. They self-correct on tap ("already decided or expired"), which is the degraded-but-acceptable path, but it's different from what the spec implies when it says "edit every recorded posted message."

The spec should explicitly call this out as an accepted tradeoff rather than leaving it implicit. If you want tighter behavior, the Requested handler could re-insert a tombstone entry after the remove, or the Decided handler could post the final edit directly to any in-flight sends it learns about. Up to the implementer, but the spec should commit to one.

@graycyrus graycyrus merged commit 3cc25e4 into tinyhumansai:main May 28, 2026
34 of 40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Docs-only change; used by PR automation. 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

3 participants