teleo-infrastructure/scripts/install_telegram_smart_research_gates.py
twentyOne2x fd6132e6ce
Some checks are pending
CI / lint-and-test (push) Waiting to run
Allow Leo paid research gate by chat title (#32)
2026-07-02 01:19:17 +02:00

220 lines
8.6 KiB
Python

#!/usr/bin/env python3
"""Install Telegram smart-research paid gates without printing approval refs."""
from __future__ import annotations
import argparse
import getpass
import grp
import json
import os
import pwd
import re
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
APPROVAL_REF_RE = re.compile(r"^[A-Za-z0-9._:@/-]{8,256}$")
CHAT_ID_RE = re.compile(r"^-?\d+$")
CHAT_TITLE_RE = re.compile(r"^[^\r\n=]{1,128}$")
MAX_SMART_RESEARCH_USD = 0.06
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Install server-side gates for Leo Telegram smart-research paid execution.",
allow_abbrev=False,
)
parser.add_argument("--agent", default="leo-wallet-test", help="Agent instance name for teleo-agent@<agent>")
parser.add_argument("--secrets-dir", default="/opt/teleo-eval/secrets")
parser.add_argument("--allow-paid", action="store_true", help="Enable paid smart research for one allowed chat")
parser.add_argument("--allowed-chat-id", help="Telegram chat id allowed to trigger paid smart research")
parser.add_argument("--allowed-chat-title", help="Telegram chat title allowed to trigger paid smart research")
parser.add_argument("--max-usd", default="0.01", help="Maximum spend per Telegram smart-research call")
parser.add_argument(
"--approval-ref-from-stdin",
action="store_true",
help="Read approval ref from stdin instead of a hidden prompt",
)
parser.add_argument("--owner", default="teleo", help="Env/approval file owner after write")
parser.add_argument("--group", default="teleo", help="Env/approval file group after write")
parser.add_argument("--no-chown", action="store_true", help="Skip chown; useful for local tests")
parser.add_argument("--dry-run", action="store_true", help="Validate inputs without writing files")
parser.add_argument("--restart-service", action="store_true", help="Restart teleo-agent@<agent>.service")
parser.add_argument("--output", help="Optional sanitized JSON proof path")
namespace, unknown = parser.parse_known_args(argv)
if unknown:
print(
"ERROR: Unsupported arguments were provided. Secret-bearing CLI args are not accepted.",
file=sys.stderr,
)
raise SystemExit(2)
return namespace
def read_approval_ref(*, from_stdin: bool) -> str:
approval_ref = sys.stdin.read() if from_stdin else getpass.getpass("Leo smart-research approval ref: ")
return approval_ref.strip()
def validate_agent_name(agent: str) -> None:
if not re.match(r"^[A-Za-z0-9_.-]+$", agent):
raise ValueError("agent must contain only letters, numbers, dot, dash, or underscore")
def validate_max_usd(value: str) -> str:
try:
parsed = float(value)
except ValueError:
raise ValueError("max-usd must be numeric") from None
if parsed <= 0 or parsed > MAX_SMART_RESEARCH_USD:
raise ValueError(f"max-usd must be greater than 0 and no more than {MAX_SMART_RESEARCH_USD:.2f}")
return f"{parsed:.2f}"
def validate_paid_inputs(args: argparse.Namespace) -> str | None:
if not args.allow_paid:
return None
if args.allowed_chat_id and not CHAT_ID_RE.match(args.allowed_chat_id):
raise ValueError("--allowed-chat-id must be an integer")
if args.allowed_chat_title and not CHAT_TITLE_RE.match(args.allowed_chat_title.strip()):
raise ValueError("--allowed-chat-title shape is invalid")
if not args.allowed_chat_id and not args.allowed_chat_title:
raise ValueError("--allowed-chat-id or --allowed-chat-title is required for --allow-paid")
approval_ref = read_approval_ref(from_stdin=args.approval_ref_from_stdin)
if not APPROVAL_REF_RE.match(approval_ref):
raise ValueError("approval ref shape is invalid")
return approval_ref
def resolve_owner_group(owner: str, group: str, *, no_chown: bool) -> tuple[int | None, int | None]:
if no_chown:
return None, None
return pwd.getpwnam(owner).pw_uid, grp.getgrnam(group).gr_gid
def write_private_file(path: Path, content: str, *, uid: int | None, gid: int | None, dry_run: bool) -> None:
if dry_run:
return
path.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
tmp_path = path.with_name(f".{path.name}.tmp-{os.getpid()}")
fd = os.open(tmp_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
try:
with os.fdopen(fd, "w") as handle:
handle.write(content)
if not content.endswith("\n"):
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
os.chmod(tmp_path, 0o600)
if uid is not None or gid is not None:
os.chown(tmp_path, -1 if uid is None else uid, -1 if gid is None else gid)
os.replace(tmp_path, path)
os.chmod(path, 0o600)
finally:
if tmp_path.exists():
tmp_path.unlink()
def run_command(command: list[str], *, dry_run: bool) -> dict:
if dry_run:
return {"command": command, "skipped": "dry_run"}
proc = subprocess.run(command, check=False, text=True, capture_output=True)
return {
"command": command,
"returncode": proc.returncode,
"stdout": proc.stdout.strip(),
"stderr": proc.stderr.strip(),
}
def build_env_content(
*,
allow_paid: bool,
allowed_chat_id: str | None,
allowed_chat_title: str | None,
max_usd: str,
approval_ref_path: Path,
) -> str:
lines = [
f"LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_ALLOW_PAID={'1' if allow_paid else '0'}",
f"LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_MAX_USD={max_usd}",
f"LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_APPROVAL_REF_FILE={approval_ref_path}",
]
if allow_paid and allowed_chat_id:
lines.insert(1, f"LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_ALLOWED_CHAT_ID={allowed_chat_id}")
if allow_paid and allowed_chat_title:
lines.insert(2, f"LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_ALLOWED_CHAT_TITLE={allowed_chat_title.strip()}")
return "\n".join(lines) + "\n"
def main(argv: list[str] | None = None) -> int:
args = parse_args(sys.argv[1:] if argv is None else argv)
validate_agent_name(args.agent)
max_usd = validate_max_usd(args.max_usd)
approval_ref = validate_paid_inputs(args)
uid, gid = resolve_owner_group(args.owner, args.group, no_chown=args.no_chown)
secrets_dir = Path(args.secrets_dir)
env_path = secrets_dir / f"teleo-agent-{args.agent}.env"
approval_ref_path = secrets_dir / f"{args.agent}-smart-research-approval-ref"
env_content = build_env_content(
allow_paid=args.allow_paid,
allowed_chat_id=args.allowed_chat_id,
allowed_chat_title=args.allowed_chat_title,
max_usd=max_usd,
approval_ref_path=approval_ref_path,
)
write_private_file(env_path, env_content, uid=uid, gid=gid, dry_run=args.dry_run)
approval_ref_written = False
if args.allow_paid and approval_ref is not None:
write_private_file(approval_ref_path, approval_ref, uid=uid, gid=gid, dry_run=args.dry_run)
approval_ref_written = not args.dry_run
unit = f"teleo-agent@{args.agent}.service"
restart_result = None
if args.restart_service:
restart_result = run_command(["systemctl", "restart", unit], dry_run=args.dry_run)
proof = {
"schema": "livingip.telegramSmartResearchGateInstall.v1",
"generatedAt": datetime.now(timezone.utc).isoformat(),
"ok": True,
"agent": args.agent,
"envPath": str(env_path),
"envFileWritten": not args.dry_run,
"approvalRefPath": str(approval_ref_path),
"approvalRefWritten": approval_ref_written,
"approvalRefPresent": bool(args.allow_paid),
"allowedChatIdPresent": bool(args.allowed_chat_id),
"allowedChatTitlePresent": bool(args.allowed_chat_title),
"maxUsd": max_usd,
"paidEnabled": bool(args.allow_paid),
"dryRun": args.dry_run,
"fileMode": "0600",
"owner": None if args.no_chown else args.owner,
"group": None if args.no_chown else args.group,
"serviceUnit": unit,
"serviceRestart": restart_result,
"secretValuesIncluded": False,
}
output = json.dumps(proof, indent=2, sort_keys=True) + "\n"
if args.output:
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(output)
print(output, end="")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
raise SystemExit(1) from None