#!/usr/bin/env python3 """Build owner request drafts for Telegram notification egress surfaces. The draft groups direct Bot API sendMessage call sites by file path. It creates metadata-only handoff envelopes and never sends requests, calls Telegram, reads secrets, or modifies workflows/scripts. """ from __future__ import annotations import argparse import json import subprocess import sys from collections import defaultdict from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any TAIPEI = timezone(timedelta(hours=8)) SOURCE_SNAPSHOT = Path("docs/security/telegram-notification-egress-inventory.snapshot.json") REQUEST_FIELDS = [ "request_draft_id", "source_inventory_schema_version", "source_path", "surface_kind", "direct_call_count", "line_refs", "line_hash_refs", "owner_role_or_team", "routing_purpose", "current_sender", "target_chat_route", "message_shape_contract", "redaction_contract", "formatter_convergence_decision", "gateway_or_alertmanager_target", "break_glass_fallback_decision", "delivery_receipt_ref", "dedup_or_fingerprint_plan", "fallback_or_degraded_mode", "migration_or_exception_reason", "maintenance_window", "rollback_owner", "postcheck_evidence_ref", "no_secret_value_attestation", "no_raw_payload_attestation", "no_false_green_attestation", "not_authorization", ] REQUIRED_OWNER_FIELDS = [ "owner_role_or_team", "routing_purpose", "current_sender", "target_chat_route", "message_shape_contract", "redaction_contract", "formatter_convergence_decision", "gateway_or_alertmanager_target", "break_glass_fallback_decision", "delivery_receipt_ref", "dedup_or_fingerprint_plan", "fallback_or_degraded_mode", "migration_or_exception_reason", "maintenance_window", "rollback_owner", "postcheck_evidence_ref", "no_secret_value_attestation", "no_raw_payload_attestation", "no_false_green_attestation", ] PREFLIGHT_CHECKS = [ "source_inventory_current", "owner_role_present", "route_purpose_present", "message_shape_contract_present", "redaction_contract_present", "formatter_convergence_decision_present", "break_glass_fallback_explicit", "delivery_receipt_metadata_present", "dedup_or_fingerprint_present", "maintenance_window_present_for_change", "rollback_owner_present", "postcheck_evidence_present", "no_secret_value_attested", "no_raw_payload_attested", "no_false_green_attested", "runtime_gate_stays_zero", ] OUTCOME_LANES = [ "draft_waiting_owner_dispatch", "request_owner_route_supplement", "request_formatter_convergence_supplement", "request_break_glass_fallback_supplement", "request_redaction_or_receipt_supplement", "quarantine_secret_or_raw_payload", "reject_false_green_claim", "ready_for_manual_dispatch", "waiting_runtime_gate", ] FORBIDDEN_PAYLOADS = [ "bot_token_value", "chat_secret_value", "secret_hash", "partial_token", "masked_token", "authorization_header", "raw_message_payload", "raw_workflow_log", "raw_action_log", "raw_screenshot_with_secret", "internal_work_window_transcript", "private_namespace", "unredacted_internal_path", "unredacted_private_ip", ] BLOCKED_ACTIONS = [ "send_owner_request", "confirm_recipient", "emit_audit_event", "telegram_send", "bot_api_call", "workflow_modification", "script_modification", "api_sender_refactor", "change_chat_route", "change_bot_token", "read_secret_store", "collect_secret_value", "collect_secret_hash", "collect_partial_token", "collect_chat_id_secret", "store_raw_message_payload", "store_unredacted_log", "workflow_dispatch", "production_deploy", "accept_cd_success_as_delivery_receipt", "accept_route_200_as_notification_delivery", "accept_ui_visible_as_notification_acceptance", "skip_formatter_convergence", "skip_redaction_contract", "open_runtime_gate", "add_action_button", ] def git_short_sha(root: Path) -> str: try: result = subprocess.run( ["git", "rev-parse", "--short", "HEAD"], cwd=root, check=True, capture_output=True, text=True, ) return result.stdout.strip() except Exception: return "unknown" def load_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) def request_id_for(path: str) -> str: safe = path.replace("/", "_").replace(".", "_").replace("-", "_") return f"telegram_notification_egress_owner_request:{safe}" def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]: generated = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds") source_path = root / SOURCE_SNAPSHOT source = load_json(source_path) grouped: dict[str, list[dict[str, Any]]] = defaultdict(list) for item in source["direct_bot_api_calls"]: grouped[item["path"]].append(item) request_drafts: list[dict[str, Any]] = [] for path in sorted(grouped): calls = sorted(grouped[path], key=lambda item: item["line"]) surface_kind = calls[0]["surface_kind"] request_drafts.append( { "request_draft_id": request_id_for(path), "source_inventory_schema_version": source["schema_version"], "source_path": path, "surface_kind": surface_kind, "direct_call_count": len(calls), "line_refs": [item["line"] for item in calls], "line_hash_refs": [item["line_hash"] for item in calls], "request_fields": REQUEST_FIELDS, "required_owner_fields": REQUIRED_OWNER_FIELDS, "preflight_checks": PREFLIGHT_CHECKS, "outcome_lanes": OUTCOME_LANES, "forbidden_payloads": FORBIDDEN_PAYLOADS, "blocked_actions": BLOCKED_ACTIONS, "request_sent": False, "recipient_confirmed": False, "audit_event_emitted": False, "owner_response_received": False, "owner_response_accepted": False, "formatter_convergence_accepted": False, "redaction_contract_accepted": False, "delivery_receipt_accepted": False, "break_glass_fallback_accepted": False, "direct_bot_api_migration_authorized": False, "telegram_send_authorized": False, "bot_api_call_authorized": False, "workflow_modification_authorized": False, "script_modification_authorized": False, "api_sender_refactor_authorized": False, "secret_value_collection_allowed": False, "raw_payload_storage_allowed": False, "production_write_authorized": False, "runtime_gate": False, "action_buttons_allowed": False, "not_authorization": True, } ) workflow_drafts = [item for item in request_drafts if item["surface_kind"] == "gitea_workflow_direct_bot_api"] ops_drafts = [item for item in request_drafts if item["surface_kind"] == "ops_script_direct_bot_api"] ci_drafts = [item for item in request_drafts if item["surface_kind"] == "ci_script_direct_bot_api"] api_drafts = [item for item in request_drafts if item["surface_kind"] == "api_direct_bot_api"] return { "schema_version": "telegram_notification_egress_owner_request_draft_v1", "generated_at": generated, "git_commit": git_short_sha(root), "status": "owner_request_draft_ready_no_dispatch_no_runtime_action", "mode": "metadata_only_no_secret_value_no_telegram_send_no_workflow_change", "source_snapshot": SOURCE_SNAPSHOT.as_posix(), "source_schema_version": source["schema_version"], "source_status": source["status"], "summary": { "source_direct_bot_api_call_count": source["summary"]["direct_bot_api_call_count"], "source_direct_bot_api_file_count": source["summary"]["direct_bot_api_file_count"], "request_draft_count": len(request_drafts), "workflow_request_draft_count": len(workflow_drafts), "ops_script_request_draft_count": len(ops_drafts), "ci_script_request_draft_count": len(ci_drafts), "api_direct_request_draft_count": len(api_drafts), "request_field_count": len(REQUEST_FIELDS), "required_owner_field_count": len(REQUIRED_OWNER_FIELDS), "preflight_check_count": len(PREFLIGHT_CHECKS), "outcome_lane_count": len(OUTCOME_LANES), "forbidden_payload_count": len(FORBIDDEN_PAYLOADS), "blocked_action_count": len(BLOCKED_ACTIONS), "request_sent_count": 0, "recipient_confirmed_count": 0, "audit_event_emitted_count": 0, "owner_response_received_count": 0, "owner_response_accepted_count": 0, "formatter_convergence_accepted_count": 0, "redaction_contract_accepted_count": 0, "delivery_receipt_accepted_count": 0, "break_glass_fallback_accepted_count": 0, "direct_bot_api_migration_authorized_count": 0, "telegram_send_authorized_count": 0, "bot_api_call_authorized_count": 0, "workflow_modification_authorized_count": 0, "script_modification_authorized_count": 0, "api_sender_refactor_authorized_count": 0, "secret_value_collection_allowed_count": 0, "raw_payload_storage_allowed_count": 0, "production_write_authorized_count": 0, "runtime_gate_count": 0, "action_button_count": 0, }, "execution_boundaries": { "runtime_execution_authorized": False, "owner_request_send_authorized": False, "recipient_confirmation_authorized": False, "audit_event_emit_authorized": False, "telegram_send_authorized": False, "bot_api_call_authorized": False, "workflow_modification_authorized": False, "script_modification_authorized": False, "api_sender_refactor_authorized": False, "secret_value_collection_allowed": False, "raw_payload_storage_allowed": False, "production_write_authorized": False, "action_buttons_allowed": False, "not_authorization": True, }, "request_drafts": request_drafts, "operator_interpretation": [ "This is a draft packet for manual owner dispatch; request_sent_count remains 0.", "Each file-level draft must decide whether the path converges to TelegramGateway, AWOOI Alertmanager, or a documented break-glass fallback.", "No workflow, script, API, secret, Telegram, or production action is authorized by this snapshot.", ], } def validate(root: Path) -> None: report = build_report(root) if report["summary"]["request_draft_count"] != report["summary"]["source_direct_bot_api_file_count"]: raise SystemExit("BLOCKED telegram egress owner request draft: file/draft count mismatch") if report["summary"]["runtime_gate_count"] != 0: raise SystemExit("BLOCKED telegram egress owner request draft: runtime gate must stay 0") def main() -> None: parser = argparse.ArgumentParser(description="Build Telegram notification egress owner request draft") parser.add_argument("--root", default=".", help="repository root") parser.add_argument("--output", help="write JSON snapshot") parser.add_argument("--generated-at", help="fixed generated_at timestamp") args = parser.parse_args() root = Path(args.root).resolve() report = build_report(root, args.generated_at) payload = json.dumps(report, ensure_ascii=False, indent=2) + "\n" if args.output: Path(args.output).write_text(payload, encoding="utf-8") else: sys.stdout.write(payload) print( "TELEGRAM_NOTIFICATION_EGRESS_OWNER_REQUEST_DRAFT_OK " f"drafts={report['summary']['request_draft_count']} " f"workflow={report['summary']['workflow_request_draft_count']} " f"ops={report['summary']['ops_script_request_draft_count']} " f"api={report['summary']['api_direct_request_draft_count']} " f"sent={report['summary']['request_sent_count']} " f"runtime_gate={report['summary']['runtime_gate_count']}", file=sys.stderr, ) if __name__ == "__main__": main()