From b769eb68b6ca7ab7aecf4cbdf7979bfc3c56a9c2 Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Fri, 19 Jun 2026 19:27:12 +0200 Subject: [PATCH 1/5] Add Leo Telegram x402 bridge --- deploy/auto-deploy.sh | 2 + deploy/deploy.sh | 2 + .../telegram-leo-x402-bridge-proof.json | 21 +++ docs/telegram-leo-x402-bridge-pr-packet.md | 83 +++++++++++ docs/telegram-leo-x402-priority-spec.md | 133 ++++++++++++++++++ scripts/check_telegram_leo_x402_bridge.py | 88 ++++++++++++ telegram/agent_config.py | 21 ++- telegram/agents/leo.yaml | 57 ++++++++ telegram/bot.py | 124 +++++++++++++++- telegram/http_chat_proxy.py | 80 +++++++++++ tests/test_telegram_leo_x402_bridge.py | 87 ++++++++++++ 11 files changed, 692 insertions(+), 6 deletions(-) create mode 100644 docs/reports/telegram-leo-x402-bridge-proof.json create mode 100644 docs/telegram-leo-x402-bridge-pr-packet.md create mode 100644 docs/telegram-leo-x402-priority-spec.md create mode 100644 scripts/check_telegram_leo_x402_bridge.py create mode 100644 telegram/agents/leo.yaml create mode 100644 telegram/http_chat_proxy.py create mode 100644 tests/test_telegram_leo_x402_bridge.py diff --git a/deploy/auto-deploy.sh b/deploy/auto-deploy.sh index 46ebcf9..94b68fd 100755 --- a/deploy/auto-deploy.sh +++ b/deploy/auto-deploy.sh @@ -13,6 +13,7 @@ fi DEPLOY_CHECKOUT="/opt/teleo-eval/workspaces/deploy-infra" PIPELINE_DIR="/opt/teleo-eval/pipeline" +TELEGRAM_DIR="/opt/teleo-eval/telegram" DIAGNOSTICS_DIR="/opt/teleo-eval/diagnostics" AGENT_STATE_DIR="/opt/teleo-eval/ops/agent-state" STAMP_FILE="/opt/teleo-eval/.last-deploy-sha" @@ -74,6 +75,7 @@ for f in teleo-pipeline.py reweave.py fetch_coins.py pipeline-health-check.py; d done rsync "${RSYNC_OPTS[@]}" telegram/ "$PIPELINE_DIR/telegram/" +rsync "${RSYNC_OPTS[@]}" telegram/ "$TELEGRAM_DIR/" rsync "${RSYNC_OPTS[@]}" diagnostics/ "$DIAGNOSTICS_DIR/" rsync "${RSYNC_OPTS[@]}" agent-state/ "$AGENT_STATE_DIR/" rsync "${RSYNC_OPTS[@]}" tests/ "$PIPELINE_DIR/tests/" diff --git a/deploy/deploy.sh b/deploy/deploy.sh index f6abeed..d42135b 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -7,6 +7,7 @@ set -euo pipefail VPS_HOST="teleo@77.42.65.182" VPS_PIPELINE="/opt/teleo-eval/pipeline" +VPS_TELEGRAM="/opt/teleo-eval/telegram" VPS_DIAGNOSTICS="/opt/teleo-eval/diagnostics" VPS_AGENT_STATE="/opt/teleo-eval/ops/agent-state" REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" @@ -74,6 +75,7 @@ echo "" echo "=== Telegram bot ===" rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/telegram/" "$VPS_HOST:$VPS_PIPELINE/telegram/" +rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/telegram/" "$VPS_HOST:$VPS_TELEGRAM/" echo "" echo "=== Tests ===" diff --git a/docs/reports/telegram-leo-x402-bridge-proof.json b/docs/reports/telegram-leo-x402-bridge-proof.json new file mode 100644 index 0000000..bfb1148 --- /dev/null +++ b/docs/reports/telegram-leo-x402-bridge-proof.json @@ -0,0 +1,21 @@ +{ + "agent": "leo", + "currentTier": "T3_live_readonly", + "generatedAt": "2026-06-19T17:25:27.555494+00:00", + "httpStatus": 402, + "llmOk": true, + "notProven": [ + "teleo-agent@leo.service active", + "Telegram message delivery", + "Telegram reply delivery", + "new payment execution" + ], + "ok": true, + "reply": "This reached Leo HTTP via Telegram bridge confirmation.", + "requiredTier": "T3_live_readonly", + "routeSchema": "livingip.x402.leoChatResponse.v1", + "schema": "livingip.telegramLeoX402BridgeProof.v1", + "secretValuesIncluded": false, + "strongestClaimAllowed": "Telegram bridge helper can POST a no-secret payload to the public Leo HTTP chat route and extract a usable Leo reply. This proves the bridge parser/readback only; it does not prove the Telegram bot service is deployed or active.", + "url": "https://leo.livingip.xyz/api/agents/leo/chat" +} diff --git a/docs/telegram-leo-x402-bridge-pr-packet.md b/docs/telegram-leo-x402-bridge-pr-packet.md new file mode 100644 index 0000000..5d51947 --- /dev/null +++ b/docs/telegram-leo-x402-bridge-pr-packet.md @@ -0,0 +1,83 @@ +# Telegram Leo x402 Bridge PR Packet + +## Working Target + +Run Leo as a Telegram bot without duplicating Leo/x402 logic: Telegram receives +a user message, forwards it to `https://leo.livingip.xyz/api/agents/leo/chat`, +and replies with the hosted Leo answer. + +## Non-Destructive Boundary + +- This PR does not start, stop, restart, or mutate any live Telegram service. +- Deployment sync is updated to copy `telegram/` into both + `/opt/teleo-eval/pipeline/telegram/` and `/opt/teleo-eval/telegram/`, matching + the current `teleo-agent@.service` runtime path. +- Existing Rio and Theseus configs do not set `http_chat_proxy_url`, so their + current KB/retrieval path stays unchanged. +- Leo opts into the bridge with `telegram/agents/leo.yaml`. +- The live token's Telegram username readback is `@TeleoHumanBot`; `@teLEOhuman` + remains an alias for continuity with Leo's X identity. +- Secret contents are not stored or printed. The config references only the + expected token-file name: `leo-telegram-bot-token`. + +## Local Proof Commands + +```sh +.venv/bin/python -m pytest tests/test_telegram_leo_x402_bridge.py +.venv/bin/python -m py_compile telegram/agent_config.py telegram/http_chat_proxy.py telegram/bot.py telegram/agent_runner.py +.venv/bin/python telegram/agent_runner.py --agent leo --validate +.venv/bin/python scripts/check_telegram_leo_x402_bridge.py +bash -n deploy/deploy.sh deploy/auto-deploy.sh +git diff --check +``` + +Primary retained proof path: + +```text +docs/reports/telegram-leo-x402-bridge-proof.json +``` + +## Production Promotion Commands + +Run only after review and after confirming the token filename exists on the VPS: + +```sh +test -f /opt/teleo-eval/secrets/leo-telegram-bot-token +test -f /opt/teleo-eval/telegram/agents/leo.yaml +test -f /opt/teleo-eval/telegram/http_chat_proxy.py +/opt/teleo-eval/pipeline/.venv/bin/python3 /opt/teleo-eval/telegram/agent_runner.py --agent leo --validate +systemctl start teleo-agent@leo +journalctl -u teleo-agent@leo -n 100 --no-pager +``` + +Then send Leo a Telegram DM or tag the configured handle and retain: + +- Telegram message/reply screenshot or export. +- `journalctl -u teleo-agent@leo` lines showing the proxy path. +- Caddy access log line for `POST /api/agents/leo/chat` on `leo.livingip.xyz`. + +## Reviewer CTA + +Approve deploying this as the next non-destructive Telegram step if these facts +are acceptable: + +- `leo-telegram-bot-token` exists on the VPS. +- Telegram `getMe` for that token reports bot username `TeleoHumanBot`. +- `teleo-agent@leo.service` is currently inactive, so this is an additive new + agent process rather than a restart of Rio or Theseus. +- The public Leo HTTP route already returns a parseable Leo reply. +- Existing Rio/Theseus configs do not set `http_chat_proxy_url`. +- The deploy-path mismatch is fixed by syncing Telegram files to the runtime + path used by `teleo-agent@.service`. + +## Strongest Claim Before Promotion + +PR-ready local bridge only: config and parser tests prove Telegram can be wired +to the hosted Leo HTTP route without changing existing Rio/Theseus behavior. + +## Strongest Claim After Promotion + +If the production commands pass and a Telegram message returns a hosted Leo +answer, Telegram Leo is a live transport for Leo's public HTTP chat route. +Payment and external research claims still come from retained HTTP/x402 proof +artifacts, not from Telegram by itself. diff --git a/docs/telegram-leo-x402-priority-spec.md b/docs/telegram-leo-x402-priority-spec.md new file mode 100644 index 0000000..7a1c7a6 --- /dev/null +++ b/docs/telegram-leo-x402-priority-spec.md @@ -0,0 +1,133 @@ +# Telegram Leo x402 Priority And Spec + +## Definition Of Working + +Working target: a user can DM or tag `@TeleoHumanBot`; the Telegram Leo process +forwards the message to `https://leo.livingip.xyz/api/agents/leo/chat`; the user +receives a Leo answer; retained logs prove the request hit the public Leo HTTP +route. + +Operator path: + +```sh +/opt/teleo-eval/pipeline/.venv/bin/python3 /opt/teleo-eval/telegram/agent_runner.py --agent leo --validate +systemctl start teleo-agent@leo +journalctl -u teleo-agent@leo -n 100 --no-pager +``` + +Done means: + +- `teleo-agent@leo.service` is active on `77.42.65.182`. +- A real Telegram message to `@TeleoHumanBot` receives a Leo reply. +- Retained proof includes Telegram message/readback, `journalctl` proxy log, and + `leo.livingip.xyz` HTTP access/readback. +- Rio and Theseus remain unaffected. + +Not done: + +- HTTP-only proof without a live Telegram delivery. +- Candidate/local proof without the public bot service active. +- Payment evidence reused as Telegram delivery evidence. + +Required tier: `T3_live_readonly` for the Telegram transport; payment claims use +the separately retained x402/Faremeter/AgentCash evidence tiers. + +Current tier: `T3_live_readonly` for bridge-to-public-HTTP proof only. The bot +token exists on the VPS, `getMe` identifies `@TeleoHumanBot`, and temporary VPS +config validation passed. The live `teleo-agent@leo.service` deployment has not +been started by this PR-shaped patch. + +Promotion gate: current VPS readback showed `teleo-agent@leo.service` uses +`/opt/teleo-eval/telegram/agent_runner.py`, while deploy scripts historically +synced `telegram/` only into `/opt/teleo-eval/pipeline/telegram/`. This patch +updates both manual and auto deploy scripts to sync `telegram/` into the runtime +path too. Do not start `teleo-agent@leo` until `leo.yaml` and +`http_chat_proxy.py` read back from `/opt/teleo-eval/telegram/`. + +## Priority Matrix + +| Priority | Lane | Current State | Ship Decision | +| --- | --- | --- | --- | +| P0 | Telegram Leo bridge deploy/readback | PR-shaped patch exists; public HTTP proof is retained; VPS token and config validation are confirmed; deploy-path mismatch is patched locally. | Push/merge the bridge, confirm runtime files read back under `/opt/teleo-eval/telegram`, start `teleo-agent@leo`, and retain Telegram delivery logs. | +| P0 | Self-hosted Faremeter seller rail | Retained public and hosted mainnet canary receipts exist, and direct `77.42.65.182:3118` currently serves a valid 0.01 USDC mainnet challenge. Fresh `https://leo.livingip.xyz` readback currently returns a Devnet `payment_challenge_unavailable` response, so public host routing is not proving the mainnet Faremeter rail right now. | Keep Faremeter as the default seller rail, but repair/repoint public `leo.livingip.xyz` to the working mainnet route before claiming current public mainnet seller readiness. | +| P1 | Leo paid research outbound loop | AgentCash/StableEnrich paid answer and Leo analysis proof already exist. | Expose the result through Telegram after bridge deploy; add per-provider approval packets for new services. | +| P1 | Public Leo HTTP behavior | `https://leo.livingip.xyz/api/agents/leo/chat` returns a parseable Leo reply under the current schema. | Treat as the bridge backend; avoid duplicating Leo logic inside Telegram. | +| P2 | Corbits/Herd/payable external services | Corbits moved payment but failed upstream API-key validation; Herd still needs an authenticated/payable endpoint proof. | Keep as provider-specific follow-up; do not block Telegram/Faremeter shipping on it. | +| P2 | All inbound service coverage | Sponsor-research has the strongest retained x402 receipts; other catalog rows need per-service canaries. | Broaden after Telegram bridge is live. | + +## Spec Tickets + +### TLG-001: Merge And Deploy Telegram Leo Bridge + +Surface: `telegram/agent_config.py`, `telegram/bot.py`, +`telegram/http_chat_proxy.py`, `telegram/agents/leo.yaml`. + +Acceptance: + +- Deploy scripts sync `telegram/` into `/opt/teleo-eval/telegram/`, matching + `teleo-agent@.service`. +- Leo config validates in the production venv. +- `teleo-agent@leo.service` starts without restarting Rio or Theseus. +- A Telegram DM/tag reaches the HTTP proxy branch. +- Failure from the HTTP route returns a clear fail-closed Telegram response. + +Evidence: + +- `docs/reports/telegram-leo-x402-bridge-proof.json` +- `journalctl -u teleo-agent@leo -n 100 --no-pager` +- Telegram screenshot/export for the delivered reply. + +### TLG-002: Retain Live Telegram Proof + +Surface: `scripts/check_telegram_leo_x402_bridge.py` plus a live deployment +proof artifact after promotion. + +Acceptance: + +- Proof names the public Telegram bot handle and public Leo HTTP URL. +- Proof says whether the message was Telegram-delivered or HTTP-only. +- Proof includes no token values, secrets, chat-private content beyond the test + prompt and Leo reply. + +### X402-FARE-001: Make Faremeter The Default Seller Rail + +Surface: Living IP x402 route configuration and operator docs in the x402 +worktree. + +Acceptance: + +- Public sponsor-research route keeps using the self-hosted Faremeter path. +- Fresh public readback for `https://leo.livingip.xyz/api/initiatives/sponsor-research` + returns the intended mainnet 0.01 USDC challenge, not the stale Devnet + `payment_challenge_unavailable` response. +- A repeat public canary command is documented with the smallest safe spend cap. +- No PayAI/CDP dependency is required for the default seller rail. + +Existing evidence: + +- `ops/x402-faremeter-mainnet-public-payment-proof.json` +- `ops/x402-faremeter-hosted-candidate-payment-proof.json` +- `ops/x402-faremeter-direct-payment-proof.json` + +### LEO-OUT-001: Telegram Surface For Paid Research Results + +Surface: Telegram Leo bridge plus retained paid-source artifacts. + +Acceptance: + +- Telegram Leo can answer a question using the same public Leo HTTP behavior + that already consumed paid AgentCash research. +- The answer references retained paid-source evidence without claiming a fresh + payment unless a fresh payment receipt exists. + +Existing evidence: + +- `ops/x402-agentcash-paid-readback-proof.json` +- `ops/x402-leo-paid-research-analysis-proof.json` + +## Reviewer CTA + +Approve the PR-shaped Telegram bridge and then run the production promotion +commands from `docs/telegram-leo-x402-bridge-pr-packet.md`. Do not wait on +Corbits/Herd broadening to ship the Telegram transport and self-hosted Faremeter +seller rail. diff --git a/scripts/check_telegram_leo_x402_bridge.py b/scripts/check_telegram_leo_x402_bridge.py new file mode 100644 index 0000000..629631a --- /dev/null +++ b/scripts/check_telegram_leo_x402_bridge.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Prove the Telegram Leo bridge can consume the public Leo HTTP chat route.""" + +# ruff: noqa: E402, I001 + +from __future__ import annotations + +import argparse +import asyncio +import json +import sys +from datetime import datetime, timezone +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +TELEGRAM_DIR = REPO_ROOT / "telegram" +sys.path.insert(0, str(TELEGRAM_DIR)) + +from http_chat_proxy import build_chat_proxy_payload, post_chat_proxy + + +DEFAULT_URL = "https://leo.livingip.xyz/api/agents/leo/chat" +DEFAULT_OUTPUT = "docs/reports/telegram-leo-x402-bridge-proof.json" + + +async def run_check(url: str, message: str) -> dict: + payload = build_chat_proxy_payload( + message=message, + source="telegram-proof", + agent="leo", + chat_id=0, + message_id=0, + username="codex-proof", + ) + status, body, reply = await post_chat_proxy(url=url, payload=payload) + return { + "schema": "livingip.telegramLeoX402BridgeProof.v1", + "generatedAt": datetime.now(timezone.utc).isoformat(), + "ok": bool(reply), + "requiredTier": "T3_live_readonly", + "currentTier": "T3_live_readonly" if reply else "T2_runtime", + "url": url, + "httpStatus": status, + "routeSchema": body.get("schema") if isinstance(body, dict) else None, + "agent": body.get("agent") if isinstance(body, dict) else None, + "llmOk": ( + body.get("llmOk") + if isinstance(body, dict) and "llmOk" in body + else body.get("llm", {}).get("ok") + if isinstance(body.get("llm"), dict) + else None + ), + "reply": reply, + "secretValuesIncluded": False, + "strongestClaimAllowed": ( + "Telegram bridge helper can POST a no-secret payload to the public Leo HTTP chat route " + "and extract a usable Leo reply. This proves the bridge parser/readback only; it does " + "not prove the Telegram bot service is deployed or active." + ), + "notProven": [ + "teleo-agent@leo.service active", + "Telegram message delivery", + "Telegram reply delivery", + "new payment execution", + ], + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--url", default=DEFAULT_URL) + parser.add_argument( + "--message", + default="Telegram bridge proof: reply with one sentence confirming this reached Leo HTTP.", + ) + parser.add_argument("--output", default=DEFAULT_OUTPUT) + args = parser.parse_args() + + proof = asyncio.run(run_check(args.url, args.message)) + output_path = REPO_ROOT / args.output + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(proof, indent=2, sort_keys=True) + "\n") + print(json.dumps(proof, indent=2, sort_keys=True)) + return 0 if proof["ok"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/telegram/agent_config.py b/telegram/agent_config.py index a28c4a9..51cf510 100644 --- a/telegram/agent_config.py +++ b/telegram/agent_config.py @@ -11,8 +11,8 @@ import logging import os import re from dataclasses import dataclass, field -from pathlib import Path from typing import Optional +from urllib.parse import urlparse logger = logging.getLogger("tg.agent_config") @@ -43,6 +43,9 @@ class AgentConfig: triage_model: str = "anthropic/claude-haiku-4.5" max_tokens: int = 1024 max_response_per_user_per_hour: int = 30 + http_chat_proxy_url: Optional[str] = None + respond_to_private_chats: bool = False + mention_aliases: list[str] = field(default_factory=list) def to_dict(self) -> dict: """Convert to dict for passing to build_system_prompt.""" @@ -55,6 +58,9 @@ class AgentConfig: "voice_summary": self.voice_summary, "domain_expertise": self.domain_expertise, "pentagon_agent_id": self.pentagon_agent_id, + "http_chat_proxy_url": self.http_chat_proxy_url, + "respond_to_private_chats": self.respond_to_private_chats, + "mention_aliases": self.mention_aliases, } @property @@ -100,6 +106,16 @@ def load_agent_config(config_path: str) -> AgentConfig: if "learnings_file" not in raw: errors.append("Missing required field: learnings_file") + proxy_url = raw.get("http_chat_proxy_url") + if proxy_url: + parsed_proxy = urlparse(proxy_url) + if parsed_proxy.scheme not in {"http", "https"} or not parsed_proxy.netloc: + errors.append("http_chat_proxy_url must be an absolute http(s) URL") + + mention_aliases = raw.get("mention_aliases", []) + if mention_aliases and not isinstance(mention_aliases, list): + errors.append("mention_aliases must be a list") + if errors: raise ValueError( f"Agent config validation failed ({config_path}):\n" @@ -123,6 +139,9 @@ def load_agent_config(config_path: str) -> AgentConfig: triage_model=raw.get("triage_model", "anthropic/claude-haiku-4.5"), max_tokens=raw.get("max_tokens", 1024), max_response_per_user_per_hour=raw.get("max_response_per_user_per_hour", 30), + http_chat_proxy_url=proxy_url, + respond_to_private_chats=bool(raw.get("respond_to_private_chats", False)), + mention_aliases=mention_aliases, ) diff --git a/telegram/agents/leo.yaml b/telegram/agents/leo.yaml new file mode 100644 index 0000000..860bdf7 --- /dev/null +++ b/telegram/agents/leo.yaml @@ -0,0 +1,57 @@ +# Leo — Living IP x402 research agent +# This config makes Telegram a thin transport to Leo's hosted HTTP chat route. + +# ─── Identity ──────────────────────────────────────────────────────────── +name: Leo +handle: "@TeleoHumanBot" +x_handle: "@teLEOhuman" +mention_aliases: + - "@leo" + - "@teLEOhuman" + - "@TeleoHumanBot" + - "@teLEOhumanity" +bot_token_file: leo-telegram-bot-token +pentagon_agent_id: livingip-leo +domain: collective-intelligence +domain_expertise: > + collective intelligence, coordination systems, Living IP strategy, agent + markets, paid research, x402 service rails, and cross-domain synthesis + +# ─── Hosted Leo Runtime ────────────────────────────────────────────────── +http_chat_proxy_url: "https://leo.livingip.xyz/api/agents/leo/chat" +respond_to_private_chats: true + +# ─── KB Scope ──────────────────────────────────────────────────────────── +kb_scope: + primary: + - domains/collective-intelligence + - domains/ai-alignment + - domains/space-development + - foundations + - core + +# ─── Voice ─────────────────────────────────────────────────────────────── +voice_summary: "Cross-domain strategist. Direct, synthesis-heavy, proof-aware." + +voice_definition: | + ## Register + You are Leo, TeleoHumanity's cross-domain strategy and collective + intelligence agent. Be direct and synthesis-heavy. Prefer concrete + mechanisms, coordination failures, and next actions over broad abstractions. + + ## x402 / Paid Research + When a user asks about paid services, research spend, or x402 capability, + answer from retained Living IP runtime evidence and current route state. + Do not claim payment execution unless the HTTP route returns retained + payment/readback evidence. + +# ─── Learnings ─────────────────────────────────────────────────────────── +learnings_file: agents/leo/learnings.md + +# ─── Model ─────────────────────────────────────────────────────────────── +response_model: anthropic/claude-opus-4-6 +triage_model: anthropic/claude-haiku-4.5 +max_tokens: 500 + +# ─── Rate Limits ───────────────────────────────────────────────────────── +max_response_per_user_per_hour: 30 diff --git a/telegram/bot.py b/telegram/bot.py index 2a0c6b1..b7e9c86 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -50,6 +50,7 @@ from retrieval import orchestrate_retrieval from market_data import get_token_price, format_price_context from worktree_lock import main_worktree_lock from x_client import search_tweets, fetch_from_url, check_research_rate_limit, record_research_usage, get_research_remaining +from http_chat_proxy import build_chat_proxy_payload, post_chat_proxy # ─── Config ───────────────────────────────────────────────────────────── @@ -75,6 +76,15 @@ TRIAGE_MODEL = "anthropic/claude-haiku-4.5" # Haiku for batch triage # KB scope — None means all domains (Rio default). Set from YAML config for other agents. AGENT_KB_SCOPE: list[str] | None = None +AGENT_NAME = "Rio" +AGENT_HANDLE = "@FutAIrdBot" +AGENT_X_HANDLE = "@futaRdIO" +AGENT_DOMAIN_EXPERTISE = ( + "futarchy, prediction markets, token governance, and the MetaDAO ecosystem" +) +AGENT_HTTP_CHAT_PROXY_URL: str | None = None +AGENT_RESPOND_TO_PRIVATE_CHATS = False +AGENT_MENTION_ALIASES = ["@teleo", "@FutAIrdBot"] # Rate limits MAX_RESPONSE_PER_USER_PER_HOUR = 30 @@ -984,7 +994,7 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_tagged(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Handle messages that tag the bot — Rio responds with Opus.""" + """Handle messages that tag the bot.""" if not update.message or not update.message.text: return @@ -999,6 +1009,82 @@ async def handle_tagged(update: Update, context: ContextTypes.DEFAULT_TYPE): logger.info("Tagged by @%s: %s", user.username if user else "unknown", text[:100]) + if AGENT_HTTP_CHAT_PROXY_URL: + await msg.chat.send_action("typing") + payload = build_chat_proxy_payload( + message=text, + source="telegram", + agent=AGENT_NAME.lower(), + chat_id=msg.chat_id, + message_id=msg.message_id, + username=user.username if user else None, + ) + try: + status, proxy_body, proxy_reply = await post_chat_proxy( + url=AGENT_HTTP_CHAT_PROXY_URL, + payload=payload, + ) + except Exception as e: + logger.warning("%s HTTP chat proxy failed: %s", AGENT_NAME, e) + await msg.reply_text( + f"{AGENT_NAME}'s HTTP chat route is temporarily unavailable. " + "Try again after the service recovers.", + do_quote=True, + ) + return + + if not proxy_reply: + logger.warning("%s HTTP chat proxy returned no reply (status=%s)", AGENT_NAME, status) + await msg.reply_text( + f"{AGENT_NAME}'s HTTP chat route returned no usable reply. " + "The Telegram bridge is fail-closed.", + do_quote=True, + ) + return + + if len(proxy_reply) <= 4096: + await msg.reply_text(proxy_reply, do_quote=True) + else: + first = True + remaining = proxy_reply + while remaining: + chunk = remaining[:4096] + await msg.reply_text(chunk, quote=first) + first = False + remaining = remaining[4096:] + + if user: + username = user.username or "anonymous" + key = (msg.chat_id, user.id) + unanswered_count[key] = 0 + entry = {"user": text[:500], "bot": proxy_reply[:500], "username": username} + history = conversation_history.setdefault(key, []) + history.append(entry) + if len(history) > MAX_HISTORY_USER: + history.pop(0) + chat_key = (msg.chat_id, 0) + chat_history = conversation_history.setdefault(chat_key, []) + chat_history.append(entry) + if len(chat_history) > MAX_HISTORY_CHAT: + chat_history.pop(0) + user_response_times[user.id].append(time.time()) + + logger.info("%s proxied Telegram reply via HTTP chat route (status=%s)", AGENT_NAME, status) + _record_transcript( + msg, + proxy_reply, + is_bot=True, + rio_response=proxy_reply, + internal={ + "agent": AGENT_NAME.lower(), + "http_chat_proxy": True, + "http_status": status, + "schema": proxy_body.get("schema") if isinstance(proxy_body, dict) else None, + "llm_ok": proxy_body.get("llmOk") if isinstance(proxy_body, dict) else None, + }, + ) + return + # ─── Audit: init timing and tool call tracking ────────────────── response_start = time.monotonic() tool_calls = [] @@ -1913,9 +1999,9 @@ tags: [telegram, ownership-community] async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle /start command.""" await update.message.reply_text( - "I'm Rio, the internet finance agent for TeleoHumanity's collective knowledge base. " - "Tag me with @teleo to ask about futarchy, prediction markets, token governance, " - "or anything in our domain. I'll ground my response in our KB's evidence." + f"I'm {AGENT_NAME}, a TeleoHumanity agent. " + f"Tag me with {AGENT_HANDLE} to ask about {AGENT_DOMAIN_EXPERTISE}. " + "I'll ground my response in the configured agent runtime." ) @@ -1937,10 +2023,21 @@ def _load_agent_config(config_path: str): """Load agent YAML config and set module-level variables.""" global BOT_TOKEN_FILE, RESPONSE_MODEL, TRIAGE_MODEL, AGENT_KB_SCOPE global LEARNINGS_FILE, MAX_RESPONSE_PER_USER_PER_HOUR + global AGENT_NAME, AGENT_HANDLE, AGENT_X_HANDLE, AGENT_DOMAIN_EXPERTISE + global AGENT_HTTP_CHAT_PROXY_URL, AGENT_RESPOND_TO_PRIVATE_CHATS, AGENT_MENTION_ALIASES with open(config_path) as f: cfg = yaml.safe_load(f) + AGENT_NAME = cfg.get("name", AGENT_NAME) + AGENT_HANDLE = cfg.get("handle", AGENT_HANDLE) + AGENT_X_HANDLE = cfg.get("x_handle", AGENT_X_HANDLE) + AGENT_DOMAIN_EXPERTISE = cfg.get("domain_expertise", AGENT_DOMAIN_EXPERTISE) + AGENT_HTTP_CHAT_PROXY_URL = cfg.get("http_chat_proxy_url") + AGENT_RESPOND_TO_PRIVATE_CHATS = bool(cfg.get("respond_to_private_chats", False)) + aliases = [AGENT_HANDLE, *cfg.get("mention_aliases", [])] + AGENT_MENTION_ALIASES = sorted({alias for alias in aliases if alias}) + if cfg.get("bot_token_file"): BOT_TOKEN_FILE = f"/opt/teleo-eval/secrets/{cfg['bot_token_file']}" if cfg.get("response_model"): @@ -1959,6 +2056,17 @@ def _load_agent_config(config_path: str): return cfg +def _mention_filter_regex(agent_cfg: dict | None) -> str: + """Build the Telegram mention regex for the active agent.""" + aliases = ["@teleo", "@FutAIrdBot"] + if agent_cfg: + aliases = [agent_cfg.get("handle", ""), *agent_cfg.get("mention_aliases", [])] + cleaned = sorted({alias for alias in aliases if alias}) + if not cleaned: + cleaned = ["@teleo", "@FutAIrdBot"] + return "(?i)(" + "|".join(re.escape(alias) for alias in cleaned) + ")" + + def main(): """Start the bot.""" parser = argparse.ArgumentParser() @@ -2014,10 +2122,16 @@ def main(): # python-telegram-bot filters.Mention doesn't work for bot mentions in groups # Use a regex filter for the bot username app.add_handler(MessageHandler( - filters.TEXT & filters.Regex(r"(?i)(@teleo|@futairdbot)"), + filters.TEXT & filters.Regex(_mention_filter_regex(agent_cfg)), handle_tagged, )) + if agent_cfg and AGENT_RESPOND_TO_PRIVATE_CHATS: + app.add_handler(MessageHandler( + filters.ChatType.PRIVATE & filters.TEXT & ~filters.COMMAND, + handle_tagged, + )) + # Reply handler — replies to the bot's own messages continue the conversation reply_to_bot_filter = filters.TEXT & filters.REPLY & ~filters.COMMAND app.add_handler(MessageHandler( diff --git a/telegram/http_chat_proxy.py b/telegram/http_chat_proxy.py new file mode 100644 index 0000000..4bb17e2 --- /dev/null +++ b/telegram/http_chat_proxy.py @@ -0,0 +1,80 @@ +"""HTTP chat proxy helpers for opt-in Telegram agent routing.""" + +from __future__ import annotations + +from typing import Any + + +def build_chat_proxy_payload( + *, + message: str, + source: str, + agent: str, + chat_id: int | None = None, + message_id: int | None = None, + username: str | None = None, +) -> dict[str, Any]: + """Build the no-secret payload sent from Telegram to an agent HTTP chat route.""" + metadata = { + "source": source, + "agent": agent, + "chat_id": chat_id, + "message_id": message_id, + "username": username, + } + return { + "message": message, + "metadata": {k: v for k, v in metadata.items() if v is not None}, + } + + +def extract_chat_proxy_reply(payload: dict[str, Any]) -> str | None: + """Extract a reply from supported Living IP Leo chat response shapes.""" + if not isinstance(payload, dict): + return None + + direct_reply = payload.get("reply") + if isinstance(direct_reply, str) and direct_reply.strip(): + return direct_reply.strip() + + decision = payload.get("decision") + if isinstance(decision, dict): + decision_reply = decision.get("reply") + if isinstance(decision_reply, str) and decision_reply.strip(): + return decision_reply.strip() + + llm = payload.get("llm") + if isinstance(llm, dict): + llm_reply = llm.get("reply") + if isinstance(llm_reply, str) and llm_reply.strip(): + return llm_reply.strip() + llm_decision = llm.get("decision") + if isinstance(llm_decision, dict): + llm_decision_reply = llm_decision.get("reply") + if isinstance(llm_decision_reply, str) and llm_decision_reply.strip(): + return llm_decision_reply.strip() + + return None + + +async def post_chat_proxy( + *, + url: str, + payload: dict[str, Any], + timeout_seconds: int = 30, +) -> tuple[int, dict[str, Any], str | None]: + """POST to an agent HTTP chat route and return status, JSON body, and reply.""" + import aiohttp + + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout_seconds)) as session: + async with session.post( + url, + json=payload, + headers={ + "Accept": "application/json", + "Content-Type": "application/json", + "X-LivingIP-Source": "telegram-agent-proxy", + }, + ) as resp: + data = await resp.json(content_type=None) + return resp.status, data, extract_chat_proxy_reply(data) diff --git a/tests/test_telegram_leo_x402_bridge.py b/tests/test_telegram_leo_x402_bridge.py new file mode 100644 index 0000000..1e5baa4 --- /dev/null +++ b/tests/test_telegram_leo_x402_bridge.py @@ -0,0 +1,87 @@ +"""Regression coverage for the Leo Telegram -> Living IP HTTP chat bridge.""" + +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[1] +TELEGRAM_DIR = REPO_ROOT / "telegram" +sys.path.insert(0, str(TELEGRAM_DIR)) + +from agent_config import load_agent_config # noqa: E402 +from http_chat_proxy import build_chat_proxy_payload, extract_chat_proxy_reply # noqa: E402 + + +def test_leo_config_opts_into_http_chat_proxy_without_changing_default_agents(): + leo = load_agent_config(str(TELEGRAM_DIR / "agents" / "leo.yaml")) + rio = load_agent_config(str(TELEGRAM_DIR / "agents" / "rio.yaml")) + + assert leo.name == "Leo" + assert leo.http_chat_proxy_url == "https://leo.livingip.xyz/api/agents/leo/chat" + assert leo.respond_to_private_chats is True + assert "@teLEOhuman" in leo.mention_aliases + assert rio.http_chat_proxy_url is None + assert rio.respond_to_private_chats is False + + +def test_invalid_http_chat_proxy_url_fails_closed(tmp_path): + config = tmp_path / "bad.yaml" + config.write_text( + """ +name: Leo +handle: "@teLEOhuman" +bot_token_file: leo-telegram-bot-token +pentagon_agent_id: livingip-leo +domain: collective-intelligence +kb_scope: + primary: ["core"] +voice_summary: "test" +voice_definition: "test" +learnings_file: agents/leo/learnings.md +http_chat_proxy_url: "not-a-url" +""".strip() + ) + + with pytest.raises(ValueError, match="http_chat_proxy_url"): + load_agent_config(str(config)) + + +def test_proxy_payload_contains_no_secret_material(): + payload = build_chat_proxy_payload( + message="Can Leo use x402 paid research now?", + source="telegram", + agent="leo", + chat_id=123, + message_id=456, + username="tester", + ) + + assert payload == { + "message": "Can Leo use x402 paid research now?", + "metadata": { + "source": "telegram", + "agent": "leo", + "chat_id": 123, + "message_id": 456, + "username": "tester", + }, + } + assert "token" not in str(payload).lower() + assert "secret" not in str(payload).lower() + + +@pytest.mark.parametrize( + ("payload", "expected"), + [ + ({"reply": "public route reply"}, "public route reply"), + ({"decision": {"reply": "analysis route reply"}}, "analysis route reply"), + ({"llm": {"decision": {"reply": "nested decision reply"}}}, "nested decision reply"), + ], +) +def test_extract_chat_proxy_reply_accepts_retained_leo_shapes(payload, expected): + assert extract_chat_proxy_reply(payload) == expected + + +def test_extract_chat_proxy_reply_fails_closed_on_missing_reply(): + assert extract_chat_proxy_reply({"schema": "livingip.x402.leoChatResponse.v1"}) is None From 1e62a94d3ae9586b84dd40269b2159ea2abbae4a Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Fri, 19 Jun 2026 23:39:23 +0200 Subject: [PATCH 2/5] Restart Leo agent after Telegram deploy changes --- deploy/auto-deploy.sh | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/deploy/auto-deploy.sh b/deploy/auto-deploy.sh index 94b68fd..7fa799b 100755 --- a/deploy/auto-deploy.sh +++ b/deploy/auto-deploy.sh @@ -88,15 +88,24 @@ log "Files synced" # Restart services only if Python files changed RESTART="" +add_restart() { + case " $RESTART " in + *" $1 "*) ;; + *) RESTART="$RESTART $1" ;; + esac +} if [ "$OLD_SHA" != "none" ]; then if git diff --name-only "$OLD_SHA" "$NEW_SHA" -- lib/ teleo-pipeline.py reweave.py telegram/ 2>/dev/null | grep -q '\.py$'; then - RESTART="$RESTART teleo-pipeline" + add_restart teleo-pipeline + fi + if git diff --name-only "$OLD_SHA" "$NEW_SHA" -- telegram/ 2>/dev/null | grep -q '\.py$'; then + add_restart teleo-agent@leo fi if git diff --name-only "$OLD_SHA" "$NEW_SHA" -- diagnostics/ 2>/dev/null | grep -q '\.py$'; then - RESTART="$RESTART teleo-diagnostics" + add_restart teleo-diagnostics fi else - RESTART="teleo-pipeline teleo-diagnostics" + RESTART="teleo-pipeline teleo-diagnostics teleo-agent@leo" fi if [ -n "$RESTART" ]; then From 5c0222f0c4f95a28260a76cd87f420e93dc23e45 Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Sat, 20 Jun 2026 01:10:21 +0200 Subject: [PATCH 3/5] Add disposable Leo Telegram test agent --- docs/leo-disposable-test-agent.md | 60 ++++++++++++++++++++++++++ telegram/agents/leo-test.yaml | 59 +++++++++++++++++++++++++ tests/test_telegram_leo_x402_bridge.py | 7 +++ 3 files changed, 126 insertions(+) create mode 100644 docs/leo-disposable-test-agent.md create mode 100644 telegram/agents/leo-test.yaml diff --git a/docs/leo-disposable-test-agent.md b/docs/leo-disposable-test-agent.md new file mode 100644 index 0000000..e3ff503 --- /dev/null +++ b/docs/leo-disposable-test-agent.md @@ -0,0 +1,60 @@ +# Leo Disposable Test Agent + +## Working Target + +Run a second Leo Telegram transport against `https://leo.livingip.xyz/api/agents/leo/chat` without touching the production `@TeleoHumanBot` token or `teleo-agent@leo` service. + +## Why + +Production Leo is currently blocked by a Telegram `getUpdates` conflict from an unseen consumer. A disposable bot avoids that conflict by using a separate Telegram bot token and service instance. + +## Secret Boundary + +Do not commit the bot token. Store it only on the VPS as: + +```text +/opt/teleo-eval/secrets/leo-test-telegram-bot-token +``` + +The file should be readable by the `teleo` runtime user and should not be printed in logs. + +## Boot + +After syncing this branch or PR to the VPS: + +```sh +sudo -u teleo /opt/teleo-eval/pipeline/.venv/bin/python3 \ + /opt/teleo-eval/telegram/agent_runner.py --agent leo-test --validate + +sudo systemctl start teleo-agent@leo-test +sudo systemctl is-active teleo-agent@leo-test +``` + +Then DM the disposable Telegram bot from a user account. Do not post into public groups for this canary. + +## Evidence + +Collect sanitized logs only: + +```sh +journalctl -u teleo-agent@leo-test --since "10 minutes ago" --no-pager +``` + +Retained proof should say: + +- bot token value was not printed; +- production `teleo-agent@leo` was not stopped; +- disposable service name was `teleo-agent@leo-test`; +- public HTTP Leo route responded through the Telegram transport; +- no paid x402 spend was attempted unless separately authorized. + +## Tear Down + +```sh +sudo systemctl stop teleo-agent@leo-test +sudo systemctl is-active teleo-agent@leo-test || true +``` + +## Slack Note + +Slack is the preferred long-term internal transport, but this repository does not yet include a Slack bot transport. A Slack canary should be a separate PR with a Socket Mode or Events API adapter and separate `leo-slack-*` secret files. diff --git a/telegram/agents/leo-test.yaml b/telegram/agents/leo-test.yaml new file mode 100644 index 0000000..12a6714 --- /dev/null +++ b/telegram/agents/leo-test.yaml @@ -0,0 +1,59 @@ +# Leo Test — disposable Living IP x402 research agent +# Uses a separate Telegram bot token so test polling cannot collide with Leo prod. + +# ─── Identity ──────────────────────────────────────────────────────────── +name: Leo Test +handle: "@LivingIPLeoTestBot" +x_handle: "@teLEOhuman" +mention_aliases: + - "@leo-test" + - "@LivingIPLeoTestBot" +bot_token_file: leo-test-telegram-bot-token +pentagon_agent_id: livingip-leo-test +domain: collective-intelligence +domain_expertise: > + collective intelligence, Living IP strategy, agent markets, paid research, + x402 service rails, and transport canary validation + +# ─── Hosted Leo Runtime ────────────────────────────────────────────────── +http_chat_proxy_url: "https://leo.livingip.xyz/api/agents/leo/chat" +respond_to_private_chats: true + +# ─── KB Scope ──────────────────────────────────────────────────────────── +kb_scope: + primary: + - domains/collective-intelligence + - domains/ai-alignment + - domains/space-development + - foundations + - core + +# ─── Voice ─────────────────────────────────────────────────────────────── +voice_summary: "Disposable Leo transport canary. Direct, proof-aware, concise." + +voice_definition: | + ## Register + You are Leo Test, a disposable transport canary for Living IP's Leo agent. + Be direct, proof-aware, and concise. Prefer current route/readback evidence + over broad claims. + + ## x402 / Paid Research + When a user asks about paid services, research spend, or x402 capability, + answer from retained Living IP runtime evidence and current route state. + Do not claim payment execution unless the HTTP route returns retained + payment/readback evidence. + + ## Test Boundary + Make clear that this Telegram bot is a disposable test transport. Do not + claim it is the production Leo bot. + +# ─── Learnings ─────────────────────────────────────────────────────────── +learnings_file: agents/leo/learnings.md + +# ─── Model ─────────────────────────────────────────────────────────────── +response_model: anthropic/claude-opus-4-6 +triage_model: anthropic/claude-haiku-4.5 +max_tokens: 500 + +# ─── Rate Limits ───────────────────────────────────────────────────────── +max_response_per_user_per_hour: 10 diff --git a/tests/test_telegram_leo_x402_bridge.py b/tests/test_telegram_leo_x402_bridge.py index 1e5baa4..e4fbf3c 100644 --- a/tests/test_telegram_leo_x402_bridge.py +++ b/tests/test_telegram_leo_x402_bridge.py @@ -15,12 +15,19 @@ from http_chat_proxy import build_chat_proxy_payload, extract_chat_proxy_reply def test_leo_config_opts_into_http_chat_proxy_without_changing_default_agents(): leo = load_agent_config(str(TELEGRAM_DIR / "agents" / "leo.yaml")) + leo_test = load_agent_config(str(TELEGRAM_DIR / "agents" / "leo-test.yaml")) rio = load_agent_config(str(TELEGRAM_DIR / "agents" / "rio.yaml")) assert leo.name == "Leo" assert leo.http_chat_proxy_url == "https://leo.livingip.xyz/api/agents/leo/chat" assert leo.respond_to_private_chats is True assert "@teLEOhuman" in leo.mention_aliases + assert leo_test.name == "Leo Test" + assert leo_test.http_chat_proxy_url == leo.http_chat_proxy_url + assert leo_test.respond_to_private_chats is True + assert leo_test.bot_token_file == "leo-test-telegram-bot-token" + assert leo_test.bot_token_file != leo.bot_token_file + assert leo_test.handle != leo.handle assert rio.http_chat_proxy_url is None assert rio.respond_to_private_chats is False From fa2895c45b9276237a8eee537642d44a986a088c Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Sat, 20 Jun 2026 13:03:10 +0200 Subject: [PATCH 4/5] Add Leo test deploy readiness check --- deploy/check-leo-test-deploy-readiness.sh | 114 ++++++++++++++++++++++ docs/leo-test-deploy-readiness.md | 72 ++++++++++++++ 2 files changed, 186 insertions(+) create mode 100755 deploy/check-leo-test-deploy-readiness.sh create mode 100644 docs/leo-test-deploy-readiness.md diff --git a/deploy/check-leo-test-deploy-readiness.sh b/deploy/check-leo-test-deploy-readiness.sh new file mode 100755 index 0000000..8372677 --- /dev/null +++ b/deploy/check-leo-test-deploy-readiness.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# Read-only readiness check for the disposable Leo Telegram transport. +# Run on the VPS before resetting auto-deploy or booting teleo-agent@leo-test. +set -euo pipefail + +DEPLOY_CHECKOUT="${DEPLOY_CHECKOUT:-/opt/teleo-eval/workspaces/deploy}" +DEPLOY_INFRA_CHECKOUT="${DEPLOY_INFRA_CHECKOUT:-/opt/teleo-eval/workspaces/deploy-infra}" +RUNTIME_TELEGRAM_DIR="${RUNTIME_TELEGRAM_DIR:-/opt/teleo-eval/telegram}" +PIPELINE_TELEGRAM_DIR="${PIPELINE_TELEGRAM_DIR:-/opt/teleo-eval/pipeline/telegram}" +LEO_TEST_TOKEN_FILE="${LEO_TEST_TOKEN_FILE:-/opt/teleo-eval/secrets/leo-test-telegram-bot-token}" +EXPECTED_GITHUB_MAIN="${EXPECTED_GITHUB_MAIN:-4cc6a5d06e053d95b3bc64eb359c9d07e2611b0c}" +EXPECTED_AUTO_DEPLOY_EXEC="${EXPECTED_AUTO_DEPLOY_EXEC:-/opt/teleo-eval/workspaces/deploy-infra/deploy/auto-deploy.sh}" +FETCH_GITHUB_MAIN="${FETCH_GITHUB_MAIN:-0}" + +failures=0 + +emit() { + printf '%s=%s\n' "$1" "$2" +} + +pass() { + emit "$1" "ok" +} + +fail() { + emit "$1" "missing_or_mismatch" + emit "$1.reason" "$2" + failures=$((failures + 1)) +} + +check_file_present() { + local key="$1" + local path="$2" + if [ -f "$path" ]; then + pass "$key" + else + fail "$key" "$path not found" + fi +} + +emit schema livingip.teleo.leoTestDeployReadiness.v1 +emit secret_values_included false +emit live_service_reset_run false +emit telegram_or_slack_message_sent false +emit paid_spend_run false + +if [ ! -d "$DEPLOY_CHECKOUT/.git" ]; then + fail deploy_checkout_git "$DEPLOY_CHECKOUT/.git not found" +else + pass deploy_checkout_git + emit deploy_checkout_path "$DEPLOY_CHECKOUT" + emit deploy_checkout_branch "$(git -C "$DEPLOY_CHECKOUT" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" + emit deploy_checkout_head "$(git -C "$DEPLOY_CHECKOUT" rev-parse --short HEAD 2>/dev/null || true)" + emit deploy_checkout_status "$(git -C "$DEPLOY_CHECKOUT" status --short | tr '\n' ';' || true)" +fi + +if [ ! -d "$DEPLOY_INFRA_CHECKOUT/.git" ]; then + fail deploy_infra_git "$DEPLOY_INFRA_CHECKOUT/.git not found" +else + pass deploy_infra_git + emit deploy_infra_path "$DEPLOY_INFRA_CHECKOUT" + if [ "$FETCH_GITHUB_MAIN" = "1" ]; then + git -C "$DEPLOY_INFRA_CHECKOUT" fetch github main --quiet 2>/dev/null || true + emit deploy_infra_fetch_github_main attempted + else + emit deploy_infra_fetch_github_main skipped + fi + github_main="$(git -C "$DEPLOY_INFRA_CHECKOUT" rev-parse github/main 2>/dev/null || true)" + emit deploy_infra_github_main "$github_main" + if [ "$github_main" = "$EXPECTED_GITHUB_MAIN" ]; then + pass deploy_infra_github_main_expected + else + fail deploy_infra_github_main_expected "expected $EXPECTED_GITHUB_MAIN, got ${github_main:-none}" + fi + + if git -C "$DEPLOY_INFRA_CHECKOUT" cat-file -e github/main:telegram/agents/leo-test.yaml 2>/dev/null; then + pass deploy_infra_github_main_leo_test_config + else + fail deploy_infra_github_main_leo_test_config "github/main lacks telegram/agents/leo-test.yaml" + fi + + if git -C "$DEPLOY_INFRA_CHECKOUT" cat-file -e github/main:deploy/auto-deploy.sh 2>/dev/null; then + pass deploy_infra_github_main_auto_deploy + else + fail deploy_infra_github_main_auto_deploy "github/main lacks deploy/auto-deploy.sh" + fi +fi + +actual_exec="$(systemctl cat teleo-auto-deploy.service 2>/dev/null | awk -F= '/^ExecStart=/{print $2; exit}' || true)" +emit teleo_auto_deploy_exec_start "$actual_exec" +if [ "$actual_exec" = "$EXPECTED_AUTO_DEPLOY_EXEC" ]; then + pass teleo_auto_deploy_exec_start_expected +else + fail teleo_auto_deploy_exec_start_expected "expected $EXPECTED_AUTO_DEPLOY_EXEC, got ${actual_exec:-none}" +fi + +check_file_present runtime_leo_config "$RUNTIME_TELEGRAM_DIR/agents/leo.yaml" +check_file_present runtime_leo_test_config "$RUNTIME_TELEGRAM_DIR/agents/leo-test.yaml" +check_file_present pipeline_leo_test_config "$PIPELINE_TELEGRAM_DIR/agents/leo-test.yaml" + +if [ -f "$LEO_TEST_TOKEN_FILE" ]; then + pass leo_test_token_file +else + fail leo_test_token_file "$LEO_TEST_TOKEN_FILE not found" +fi + +if [ "$failures" -eq 0 ]; then + emit status ready_for_leo_test_validate_and_boot +else + emit status blocked + emit failure_count "$failures" +fi + +exit "$failures" diff --git a/docs/leo-test-deploy-readiness.md b/docs/leo-test-deploy-readiness.md new file mode 100644 index 0000000..25eaa75 --- /dev/null +++ b/docs/leo-test-deploy-readiness.md @@ -0,0 +1,72 @@ +# Leo Test Deploy Readiness + +## Working Target + +Confirm the VPS is ready to boot the disposable `teleo-agent@leo-test` service +without touching production Leo, sending Telegram/Slack messages, resetting +services, or reading token contents. + +## Why + +PR #5 added `telegram/agents/leo-test.yaml`, but the VPS deploy source can lag +behind GitHub. The readiness check keeps the next live step explicit: source +reconciliation first, token file second, service validation and boot third. + +## Command + +Run on the VPS: + +```sh +/opt/teleo-eval/workspaces/deploy-infra/deploy/check-leo-test-deploy-readiness.sh +``` + +The checker is read-only. It prints `key=value` rows and exits non-zero until +all required state is present. + +By default it does not fetch or update Git refs. To explicitly refresh +`github/main` before checking, run: + +```sh +FETCH_GITHUB_MAIN=1 /opt/teleo-eval/workspaces/deploy-infra/deploy/check-leo-test-deploy-readiness.sh +``` + +## Expected Blockers Before Reconciliation + +The current blocker set should include: + +- `teleo_auto_deploy_exec_start_expected`: systemd still points at the legacy + `/opt/teleo-eval/workspaces/deploy/ops/auto-deploy.sh` path. +- `runtime_leo_test_config`: runtime Telegram path does not yet have + `agents/leo-test.yaml`. +- `pipeline_leo_test_config`: pipeline Telegram mirror does not yet have + `agents/leo-test.yaml`. +- `leo_test_token_file`: `/opt/teleo-eval/secrets/leo-test-telegram-bot-token` + is absent. + +## Ready State + +The checker returns: + +```text +status=ready_for_leo_test_validate_and_boot +``` + +only after: + +- deploy-infra can fetch GitHub main at the expected PR #5 merge commit; +- GitHub main contains `telegram/agents/leo-test.yaml`; +- systemd points auto-deploy at the reviewed deploy-infra script; +- runtime and pipeline Telegram paths contain `agents/leo-test.yaml`; +- the separate disposable test bot token file exists. + +## Next Live Step After Ready + +After readiness passes, validate without sending a message: + +```sh +sudo -u teleo /opt/teleo-eval/pipeline/.venv/bin/python3 \ + /opt/teleo-eval/telegram/agent_runner.py --agent leo-test --validate +``` + +Only after that should `teleo-agent@leo-test` be started for a private, +explicitly authorized disposable bot DM test. From 3c20f08d8d180cef0de732f5d836138ef824e6f4 Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Sat, 20 Jun 2026 15:50:55 +0200 Subject: [PATCH 5/5] Stabilize leo-test readiness main check --- deploy/check-leo-test-deploy-readiness.sh | 5 +++-- docs/leo-test-deploy-readiness.md | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/deploy/check-leo-test-deploy-readiness.sh b/deploy/check-leo-test-deploy-readiness.sh index 8372677..6280998 100755 --- a/deploy/check-leo-test-deploy-readiness.sh +++ b/deploy/check-leo-test-deploy-readiness.sh @@ -67,10 +67,11 @@ else fi github_main="$(git -C "$DEPLOY_INFRA_CHECKOUT" rev-parse github/main 2>/dev/null || true)" emit deploy_infra_github_main "$github_main" - if [ "$github_main" = "$EXPECTED_GITHUB_MAIN" ]; then + emit deploy_infra_github_main_required_ancestor "$EXPECTED_GITHUB_MAIN" + if [ "$github_main" = "$EXPECTED_GITHUB_MAIN" ] || git -C "$DEPLOY_INFRA_CHECKOUT" merge-base --is-ancestor "$EXPECTED_GITHUB_MAIN" github/main 2>/dev/null; then pass deploy_infra_github_main_expected else - fail deploy_infra_github_main_expected "expected $EXPECTED_GITHUB_MAIN, got ${github_main:-none}" + fail deploy_infra_github_main_expected "expected github/main to equal or descend from $EXPECTED_GITHUB_MAIN, got ${github_main:-none}" fi if git -C "$DEPLOY_INFRA_CHECKOUT" cat-file -e github/main:telegram/agents/leo-test.yaml 2>/dev/null; then diff --git a/docs/leo-test-deploy-readiness.md b/docs/leo-test-deploy-readiness.md index 25eaa75..0a575e1 100644 --- a/docs/leo-test-deploy-readiness.md +++ b/docs/leo-test-deploy-readiness.md @@ -32,6 +32,10 @@ FETCH_GITHUB_MAIN=1 /opt/teleo-eval/workspaces/deploy-infra/deploy/check-leo-tes ## Expected Blockers Before Reconciliation +The checker treats PR #5's merge commit as a required ancestor of +`github/main`, not as the exact current tip. This keeps the readiness check +stable after follow-up merges such as this checker itself. + The current blocker set should include: - `teleo_auto_deploy_exec_start_expected`: systemd still points at the legacy @@ -53,7 +57,8 @@ status=ready_for_leo_test_validate_and_boot only after: -- deploy-infra can fetch GitHub main at the expected PR #5 merge commit; +- deploy-infra can read GitHub main at or after the expected PR #5 merge + commit; - GitHub main contains `telegram/agents/leo-test.yaml`; - systemd points auto-deploy at the reviewed deploy-infra script; - runtime and pipeline Telegram paths contain `agents/leo-test.yaml`;