Skip to content

type: Concept title: "Telegram Approval Flow — the bidirectional human-in-the-loop channel" description: Design for moving the stage-7 human gate from localhost-web to Telegram, using Hermes' native clarify-in-Telegram rendering; two implementations (M1 paper-era, M2 execution-era), threat model, and fail-closed rules. tags: [approval, telegram, human-in-the-loop, gate, stage-7, hermes, execution] generated: { by: agent/hermes, at: '2026-09-19T00:00:00Z' } status: stable verified: { by: human:yasu, at: 2026-09-20T21:50:08Z } sources: - id: reconciliation resource: /80-system-design/hermes-pilot-reconciliation.md title: Hermes pilot reconciliation (owner decision: Telegram = the approval channel) - id: automation resource: /80-system-design/automation-architecture.md title: Automation architecture (stage 7 human approval, stage 8 boundary) - id: ledger resource: /80-system-design/position-ledger.md title: Position ledger (fill receipts as the approval's downstream artifact)


Summary

Owner decision (2026-09-15, recorded in the reconciliation): approvals move from the localhost web UI to Telegram as a bidirectional human-in-the-loop channel. This concept designs that flow. It is deliberately two-phase: M1 proves the loop on the paper pilot (no order can exist), M2 extends the same protocol to the execution adapter under the stage-8 governance boundary. Nothing here authorizes an order.

Design principles

  1. No new platform plumbing. The Hermes gateway already delivers cron output to Telegram and renders clarify questions as numbered prompts / native buttons. The approval round-trip is a clarify call sent by the round job, answered by the owner in the Telegram chat. The message never carries secrets; the decision carries authority.
  2. One approval = one append-only record. Every decision (approve AND reject, with reason) lands in hermes-pilot/ledger/approvals.jsonl — the decision log pattern from the journal. Silence is not a decision; non-response expires.
  3. Fail-closed. Approval prompt undelivered / gateway down / expired → the proposal is expired-unanswered, journaled with candidate status. No default-approve path exists.
  4. The chat is not the arithmetician. The proposal text may be LLM prose, but every number in it must carry an attested ladder receipt ref (existing contract, unchanged). The owner approves the proposition, backed by receipts; the tooling re-verifies gates at execution time (M2).

M1 — Paper-era approval loop (proves the channel; zero order risk)

Trigger: round analyst finishes a round with ≥1 candidate whose gates all PASS and whose verdict is TRADE/CONDITIONAL (today: conditional-watch, proposal-draft, etc.).

Flow:

  1. Analyst emits, per proposal, a Telegram message card:
PROPOSAL <id> — <structure> <sym> <legs-short> @ <ref debit/credit>
Regime: <label> | EV_RN <x> / EV_PHYS <y> (<z% of risk)> | qK <lots-cap>
Gates: fit ✓ ev ✓ risk ✓ | Receipt: ev-ladders-<date>.attestation.json
Management: entry gate / profit rule / loss stop / time stop (required, else auto-fail)
Reply 1 = approve, 2 = reject, 3 = defer.
  1. The card is a clarify question — Telegram renders native buttons; the reply is structured; reject/defer must attach a one-line reason (free text, captured verbatim).
  2. Analyst appends to approvals.jsonl:
{"v":0, "proposal_id":"2026-09-22-spy-debit", "decision":"approved|rejected|deferred",
 "decided_by":"human:yasu (telegram chat a1b2)", "decided_at":"ISO8601",
 "channel":"telegram", "reason":"...", "expiry":"2026-09-22 15:00 PT",
 "proposal_ref":"hermes-pilot/trades-2026-09-22.json"}
  1. decision=approved in M1 flips the paper book status to TRADING (paper entry recorded in the position ledger with price_kind=paper-fill, fill produced by the pilot paper-fill logic, no broker call).

M1 acceptance test (weekend proves this): fake proposal → Telegram card → owner taps reject with a reason → approvals.jsonl gains the record → proposal does not enter the paper book. Then approve a dummy → paper book gains a price_kind=paper-fill open record. Both directions exercised end-to-end without touching cron prompts (a separate dry-run job or a manually invoked script fires the cards).

M2 — Execution-era approval (separately governed; blocked until stage-8 contract is approved)

M2 uses the same protocol with additive fields; the differences:

  • Approval is required again after any content change — no re-approval across rounds.
  • Approval record gains ladder_receipt_sha256, proposal_version, and an order_plan ref (the exact OCC legs/quantities the adapter would submit).
  • The stage-8 adapter re-runs deterministic gates at submission time; a gate failure VOIDs a stale approval (fail-closed, voided-reason-recorded).
  • Fill receipts from the broker land in the position ledger as price_kind=live-fill, closing the loop from human intent to recorded execution.
  • The adapter runs submit-only, no cancels/overrides in M2-v0: a rejected/voided state means the order was never sent; there is no "amend in flight" mode until it passes review.

Boundary (unchanged): the TOMIC 2.0 framework ends at stage 7 (approval). M2 belongs to the stage-8 control plane and needs its own charter artifact + human approval before any code touches TASTYTRADE_WRITE. M1 is safe to build now precisely because it cannot do that.

Threat model / rules

  • Chat is the channel, not the vault. No credentials, no 2FA codes, no account numbers travel through approval messages. Approval authority = the Telegram chat identity binding (decided_by includes the chat id and Hermes allow-list), never the message content.
  • Single-approver. Only chat-bound human:yasu can move a prompt from pending. Hermes' allow-list (already configured 2026-09-18) is the identity check; the decider hash in the approvals record makes it auditable.
  • Expiry windows are part of the proposal text, proposed by the analyst per structure type (event-driven proposals get short windows), confirmed at approval time. Prompt unanswered past expiry → expired-unanswered. A stale approval cannot be resurrected; the round must re-issue.
  • Rejections are inputs to ideation. Rejected/deferred proposals with reasons feed the decision log precedent_refs (existing convention) — the loop closes on rejection too.

Implementation notes (hermes-host reality check)

  • approval_server.py and execute_approved.py were not migrated from ai-rig — they are absent on this host; the reconciliation doc's tooling inventory is stale for these two tools. Approvals JSON now lives with the pilot ledger instead. (Correction to be filed against the reconciliation concept when the owner ratifies this doc.)
  • The M1 approval writer is a pure function over the clarify result: same append-only JSONL, same fail-closed semantics as the ledger. No server process needed.

Links