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¶
- No new platform plumbing. The Hermes gateway already delivers cron output to Telegram
and renders
clarifyquestions as numbered prompts / native buttons. The approval round-trip is aclarifycall sent by the round job, answered by the owner in the Telegram chat. The message never carries secrets; the decision carries authority. - 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. - Fail-closed. Approval prompt undelivered / gateway down / expired → the proposal is
expired-unanswered, journaled with candidate status. No default-approve path exists. - 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:
- 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.
- The card is a
clarifyquestion — Telegram renders native buttons; the reply is structured; reject/defer must attach a one-line reason (free text, captured verbatim). - 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"}
decision=approvedin M1 flips the paper book status toTRADING(paper entry recorded in the position ledger withprice_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 anorder_planref (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_byincludes 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.pyandexecute_approved.pywere 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¶
- Position ledger — decisions land adjacent to fills
- Automation architecture — stage 7 definition (the one-way valve)
- Hermes pilot reconciliation — owner decision provenance