docs(security): add Telegram egress owner request draft [skip ci]

This commit is contained in:
Your Name
2026-06-18 19:27:57 +08:00
parent d886212398
commit f171ffc2b4
11 changed files with 2409 additions and 6 deletions

View File

@@ -6,6 +6,7 @@
- 新增 `scripts/security/telegram-notification-egress-inventory.py`repo-only 掃描 `.gitea/workflows``scripts/ops``scripts/ci``apps/api/src`;不讀 secret、不送 Telegram、不改 workflow / script。
- 新增 `docs/security/telegram-notification-egress-inventory.snapshot.json``docs/security/TELEGRAM-NOTIFICATION-EGRESS-INVENTORY.md`
- `security-mirror-progress-guard.py` 已鎖住 Telegram egress snapshot`direct_bot_api_call_count=18``direct_bot_api_file_count=11`、workflow `13`、ops script `4`、API direct `1``gateway_normalized_callsite_count=56``gateway_final_exit_formatter_present_count=1`
- 新增 `scripts/security/telegram-notification-egress-owner-request-draft.py``docs/security/telegram-notification-egress-owner-request-draft.snapshot.json``docs/security/TELEGRAM-NOTIFICATION-EGRESS-OWNER-REQUEST-DRAFT.md`,把 11 個 direct egress 檔案聚成 11 份人工送件前 owner request 草稿workflow `6`、ops script `4`、API direct `1`
- direct egress 目前分布:
- Gitea workflow`.gitea/workflows/cd.yaml``cd-dev.yaml``code-review.yaml``deploy-alerts.yaml``e2e-health.yaml``run-migration.yml`
- Ops script`scripts/ops/docker-health-monitor.sh``pg-backup.sh``dr-drill.sh``backup-from-110.sh`
@@ -13,15 +14,17 @@
**邊界**
- 這是 notification egress inventory / owner gate不是 workflow 修改、script 修改、`channel_hub.py` 重構、Telegram 實發、Bot API call、secret 讀取、chat route 變更、production deploy 或 runtime gate。
- owner response received / accepted、formatter convergence accepted、redaction contract accepted、delivery receipt accepted、direct Bot API migration authorized、Telegram send authorized、workflow / script modification authorized、secret collection、raw payload storage、production write、runtime gate、action button 全部維持 `0 / false`
- request sent、recipient confirmed、audit event emitted、owner response received / accepted、formatter convergence accepted、redaction contract accepted、delivery receipt accepted、direct Bot API migration authorized、Telegram send authorized、workflow / script modification authorized、secret collection、raw payload storage、production write、runtime gate、action button 全部維持 `0 / false`
- direct Bot API call count 大於 0 代表仍有已知旁路風險;不得把 API 內部 formatter 完成誤判成全域通知出口已完全治理。
**驗證**
- `python3 scripts/security/telegram-notification-egress-inventory.py --root . --generated-at 2026-06-18T22:30:00+08:00 --output docs/security/telegram-notification-egress-inventory.snapshot.json``TELEGRAM_NOTIFICATION_EGRESS_INVENTORY_OK direct_calls=18 files=11 workflow=13 ops=4 api=1 runtime_gate=0`
- `python3 scripts/security/telegram-notification-egress-owner-request-draft.py --root . --generated-at 2026-06-18T22:45:00+08:00 --output docs/security/telegram-notification-egress-owner-request-draft.snapshot.json``TELEGRAM_NOTIFICATION_EGRESS_OWNER_REQUEST_DRAFT_OK drafts=11 workflow=6 ops=4 api=1 sent=0 runtime_gate=0`
- `python3 -m json.tool docs/security/telegram-notification-egress-inventory.snapshot.json`:通過。
**完成度同步**
- Telegram notification egress inventory / guard`100%`
- Telegram notification egress owner request draft`100%`
- Direct Bot API formatter convergence`0%`,需 owner response / maintenance window 後分批處理。
- AI 自動化告警產品化通知出口治理從「API final-exit formatter」推進到「repo-wide egress inventory」下一步是 owner response package 與 migration plan。
- IwoooS headline 仍維持 `64%`active runtime gate 仍 `0`

View File

@@ -86,6 +86,7 @@ Host / runner 資源告警的第一版落地:
通知出口旁路清冊:
- `docs/security/telegram-notification-egress-inventory.snapshot.json` 目前固定 repo 內 `18` 個 direct Bot API `sendMessage`,分布為 Gitea workflow `13`、ops script `4`、API direct path `1`
- `docs/security/telegram-notification-egress-owner-request-draft.snapshot.json` 已將上述 direct egress 檔案聚成 `11` 份人工送件前草稿request sent 仍為 `0`
- 這些 direct egress 可能繞過 TelegramGateway formatter、dedup、outbound mirror、KM / PlayBook / Verifier 與 no-false-green gate因此只能視為已知待治理 surface不能視為已完成收斂。
- 清冊階段只允許保存 path、line、redacted excerpt、owner 欄位與 reviewer gate不送 Telegram、不讀 Bot token、不保存 chat secret、不改 workflow / script、不開 runtime gate。
- 後續每個 direct egress 要收斂前,都必須補 owner route、message shape、redaction contract、formatter convergence plan、delivery receipt、fallback mode、rollback owner 與 post-check evidence。

View File

@@ -50,6 +50,8 @@
2026-06-18 再新增 `docs/security/TELEGRAM-NOTIFICATION-EGRESS-INVENTORY.md``docs/security/telegram-notification-egress-inventory.snapshot.json`,把 repo 內 direct Telegram Bot API `sendMessage` 旁路固定成 notification egress 清冊。固定 `direct_bot_api_call_count=18``direct_bot_api_file_count=11`、workflow `13`、ops script `4`、API direct `1``gateway_normalized_callsite_count=56``gateway_final_exit_formatter_present_count=1``required_owner_field_count=18``reviewer_check_count=14``outcome_lane_count=9``blocked_action_count=22`。此更新只表示旁路已可重跑盤點owner response、formatter convergence accepted、delivery receipt accepted、workflow / script modification、Telegram send、Bot API call、secret collection、production write、runtime gate 與 action button 仍全部為 `0 / false`
同日再新增 `docs/security/TELEGRAM-NOTIFICATION-EGRESS-OWNER-REQUEST-DRAFT.md``docs/security/telegram-notification-egress-owner-request-draft.snapshot.json`,將 11 個 direct egress 檔案轉成人工送件前草稿。固定 `request_draft_count=11`、workflow `6`、ops script `4`、API direct `1``required_owner_field_count=19``preflight_check_count=16``outcome_lane_count=9``forbidden_payload_count=14``blocked_action_count=26`request sent、recipient confirmed、owner response accepted、formatter convergence accepted、Telegram send、Bot API call、workflow / script modification、API sender refactor、secret collection、production write、runtime gate 與 action button 仍全部為 `0 / false`
## 1.2c 2026-06-18 Backup / Restore / Escrow 事故後回讀計畫
已新增 `docs/security/BACKUP-RESTORE-POST-INCIDENT-READBACK-PLAN.md``docs/security/backup-restore-post-incident-readback-plan.snapshot.json`,將 38 個 backup / restore / escrow / retention surface 轉成事故後回讀計畫。固定 `readback_candidate_count=38``write_capable_readback_candidate_count=27``live_evidence_required_readback_candidate_count=38``restore_drill_readback_required_candidate_count=38``offsite_or_escrow_readback_required_candidate_count=20``retention_or_remote_delete_readback_required_candidate_count=17``required_readback_field_count=34``reviewer_check_count=32``outcome_lane_count=11``blocked_action_count=51`,讓 `backup_restore_credential``64%` 推進到 `66%`,高價值配置平均維持 `71%`,需 live evidence 類別仍為 `9`

View File

@@ -89,6 +89,8 @@
此更新只表示 `.gitea/workflows``scripts/ops``apps/api/src/services/channel_hub.py` 的 direct egress 已可重跑盤點不代表已遷移。owner response received / accepted、formatter convergence accepted、redaction contract accepted、delivery receipt accepted、direct Bot API migration authorized、Telegram send、Bot API call、workflow / script modification、secret collection、raw payload storage、production write、runtime gate 仍全部為 `0 / false`
同日再新增 `telegram_notification_egress_owner_request_draft_v1`,將 11 個 direct egress 檔案轉成人工送件前 owner request 草稿。固定 `request_draft_count=11``workflow_request_draft_count=6``ops_script_request_draft_count=4``api_direct_request_draft_count=1``required_owner_field_count=19``preflight_check_count=16``outcome_lane_count=9``forbidden_payload_count=14``blocked_action_count=26`。request sent、recipient confirmed、audit event emitted、owner response accepted、formatter convergence accepted、break-glass fallback accepted、Telegram send、Bot API call、workflow / script modification、API sender refactor、secret collection、raw payload storage、production write、runtime gate 仍全部為 `0 / false`
### 0.3d 2026-06-15 Public / Admin / API runtime config 變更證據驗收
`public_runtime_config_change_evidence_acceptance_v1` 已把公開產品頁、AwoooP 後台、API / CORS、frontend env、Sentry tunnel、webhook / callback 與跨產品 runtime route 轉成 metadata-only 變更證據驗收只讀帳本。固定 `candidates=6``c0=5``c1=1``write_capable=6``source_refs=20``required_evidence_fields=21``reviewer_checks=21``outcome_lanes=8``blocked_actions=32`

View File

@@ -94,6 +94,8 @@
同步邊界IwoooS headline 維持 `64%`active runtime gate 維持 `0`direct Bot API migration authorized、Telegram send authorized、Bot API call authorized、workflow modification authorized、script modification authorized、secret value collection、raw payload storage、production write、runtime gate 與 action buttons 全部仍為 `0 / false`。本段只更新文件、snapshot、guard 與通知模型,不改 workflow、不重構 API、不呼叫 Telegram、不讀 Bot token、不 dispatch workflow、不觸發部署。
同日再新增 `telegram_notification_egress_owner_request_draft_v1`,把 11 個 direct egress 檔案轉成人工送件前草稿。固定 `request_draft_count=11`、workflow `6`、ops script `4`、API direct `1``required_owner_field_count=19``preflight_check_count=16``outcome_lane_count=9``forbidden_payload_count=14``blocked_action_count=26`。這是 handoff envelope不是 request sent、recipient confirmed、audit event emitted、owner response accepted、formatter convergence accepted、workflow / script modification、API sender refactor、Telegram send、Bot API call、secret collection、production write 或 runtime gate。
## 0.00aaa 2026-06-15 K8s / ArgoCD GitOps 變更證據驗收
本輪把 K8s / ArgoCD 從 owner response acceptance 推進到 GitOps 變更證據驗收只讀帳本:`k8s_argocd_change_evidence_acceptance_v1` 固定 `candidates=4``c0=3``write_capable=4``required_evidence_fields=18``reviewer_checks=18``outcome_lanes=8``blocked_actions=28`,並讓 `k8s_production_gitops` 只讀治理成熟度 `62% -> 64%`,高價值配置平均只讀成熟度仍維持 `68%`。這是 metadata-only 收件驗收,不是 change evidence received / accepted、runtime approval package、ArgoCD API read、ArgoCD sync、kubectl action、Helm upgrade、NetworkPolicy apply、NodePort change、RBAC change、live cluster read、production write 或 runtime gate。

View File

@@ -104,7 +104,8 @@ Reviewer 必須確認:
## 8. 下一步
1. 先把 Gitea workflow 13 個 direct send 分成 CD / code-review / migration / health 類,決定哪些改走 AWOOI Alertmanager / TelegramGateway哪些保留為 break-glass fallback
2. `scripts/ops/docker-health-monitor.sh``pg-backup.sh``dr-drill.sh``backup-from-110.sh` 需要 owner 確認 API 離線 fallback 的訊息形狀與 redaction contract
3. `apps/api/src/services/channel_hub.py` 的 interim Telegram path 需要評估是否改走 TelegramGateway 或至少套同一個 normalization / mirror / receipt 契約
4. 每個遷移都必須單獨做 owner approval、maintenance window、rollback owner、no-secret-value evidence 與 post-check不得用本清冊直接改 workflow 或送 Telegram。
1. `docs/security/TELEGRAM-NOTIFICATION-EGRESS-OWNER-REQUEST-DRAFT.md` 已把 11 個 direct egress 檔案聚成 11 份人工送件前草稿request sent 仍為 `0`
2. 先把 Gitea workflow 13 個 direct send 分成 CD / code-review / migration / health 類,決定哪些改走 AWOOI Alertmanager / TelegramGateway哪些保留為 break-glass fallback
3. `scripts/ops/docker-health-monitor.sh``pg-backup.sh``dr-drill.sh``backup-from-110.sh` 需要 owner 確認 API 離線 fallback 的訊息形狀與 redaction contract
4. `apps/api/src/services/channel_hub.py` 的 interim Telegram path 需要評估是否改走 TelegramGateway 或至少套同一個 normalization / mirror / receipt 契約。
5. 每個遷移都必須單獨做 owner approval、maintenance window、rollback owner、no-secret-value evidence 與 post-check不得用本清冊直接改 workflow 或送 Telegram。

View File

@@ -0,0 +1,101 @@
# Telegram 通知出口 Owner Request 草稿
| 項目 | 內容 |
|------|------|
| 日期 | 2026-06-18 |
| 狀態 | `owner_request_draft_ready_no_dispatch_no_runtime_action` |
| 工具 | `scripts/security/telegram-notification-egress-owner-request-draft.py` |
| Snapshot | `docs/security/telegram-notification-egress-owner-request-draft.snapshot.json` |
| Source snapshot | `docs/security/telegram-notification-egress-inventory.snapshot.json` |
| 模式 | metadata-only不送 request、不確認 recipient、不送 Telegram、不改 workflow / script |
| runtime gate | `0` |
## 1. 目的
`telegram_notification_egress_inventory_v1` 已確認 repo 內仍有 `18` 個 direct Bot API `sendMessage` call site分散在 `11` 個檔案。這份 Owner Request 草稿把 call site 依檔案聚合成 `11` 份人工送件前草稿,要求 owner 判定每個通知出口要如何收斂:
- 改走 `TelegramGateway` final-exit formatter。
- 改走 AWOOI Alertmanager / webhook 入口,再由 TelegramGateway 發送。
- 保留為明確的 break-glass fallback但必須有訊息形狀、redaction、delivery receipt 與 no-false-green gate。
本文件不是 request sent也不是批准修改 workflow、script、API sender 或 Telegram route。
## 2. 固定數字
| 指標 | 數值 | 解讀 |
|------|------|------|
| `source_direct_bot_api_call_count` | `18` | 來源清冊中的 direct send call site |
| `source_direct_bot_api_file_count` | `11` | 來源清冊中的 direct send 檔案 |
| `request_draft_count` | `11` | 每個檔案一份 owner request 草稿 |
| `workflow_request_draft_count` | `6` | Gitea workflow 草稿 |
| `ops_script_request_draft_count` | `4` | ops script 草稿 |
| `api_direct_request_draft_count` | `1` | API direct sender 草稿 |
| `request_field_count` | `27` | 草稿 envelope 欄位 |
| `required_owner_field_count` | `19` | owner 必填欄位 |
| `preflight_check_count` | `16` | 送件前預檢 |
| `outcome_lane_count` | `9` | 收件結果分流 |
| `forbidden_payload_count` | `14` | 禁止提交的 payload 類型 |
| `blocked_action_count` | `26` | 草稿階段禁止動作 |
所有 request sent、recipient confirmed、audit event emitted、owner response received / accepted、formatter convergence accepted、delivery receipt accepted、break-glass fallback accepted、direct Bot API migration authorized、Telegram send、Bot API call、workflow / script modification、API sender refactor、secret collection、raw payload storage、production write、runtime gate 與 action button 都維持 `0 / false`
## 3. Owner 必填欄位
每份草稿至少需要 owner 提供:
1. `owner_role_or_team`
2. `routing_purpose`
3. `current_sender`
4. `target_chat_route`
5. `message_shape_contract`
6. `redaction_contract`
7. `formatter_convergence_decision`
8. `gateway_or_alertmanager_target`
9. `break_glass_fallback_decision`
10. `delivery_receipt_ref`
11. `dedup_or_fingerprint_plan`
12. `fallback_or_degraded_mode`
13. `migration_or_exception_reason`
14. `maintenance_window`
15. `rollback_owner`
16. `postcheck_evidence_ref`
17. `no_secret_value_attestation`
18. `no_raw_payload_attestation`
19. `no_false_green_attestation`
Owner 回覆只能提供 metadata / ref不得包含 Bot token、chat secret、secret hash、partial token、masked token、authorization header、raw message payload、raw workflow log、raw action log、未脫敏截圖、內部工作視窗逐字稿、private namespace、未脫敏內部路徑或內網 IP。
## 4. Preflight checks
送件前必須確認:
- source inventory 是目前版本,且 draft count 與 direct file count 一致。
- owner role、route purpose、message shape、redaction contract、formatter convergence decision 都存在。
- break-glass fallback 必須顯式標示;不能把 fallback 當主路徑偷渡。
- delivery receipt、dedup / fingerprint、maintenance window、rollback owner 與 post-check evidence 都已指定。
- no-secret-value、no-raw-payload、no-false-green attestation 都存在。
- runtime gate 維持 0草稿不能送 Telegram、dispatch workflow、修改 script 或重構 API sender。
## 5. Outcome lanes
| Lane | 說明 |
|------|------|
| `draft_waiting_owner_dispatch` | 草稿待人工送件request sent 仍為 0 |
| `request_owner_route_supplement` | 缺 owner、route、purpose |
| `request_formatter_convergence_supplement` | 缺 formatter convergence decision 或 target |
| `request_break_glass_fallback_supplement` | 缺 fallback decision 或 exception reason |
| `request_redaction_or_receipt_supplement` | 缺 redaction contract、receipt 或 dedup plan |
| `quarantine_secret_or_raw_payload` | 收到 secret / raw payload / raw log 時隔離 |
| `reject_false_green_claim` | 把 CD success、route 200、UI 可見或 Telegram 送達當驗收時拒收 |
| `ready_for_manual_dispatch` | 草稿 metadata 完整,可由人工送件 |
| `waiting_runtime_gate` | 即使回覆 acceptedruntime send / 修改仍需獨立批准 |
## 6. 禁止動作
此草稿階段禁止送 owner request、確認 recipient、emit audit event、送 Telegram、呼叫 Bot API、改 workflow、改 script、重構 API sender、改 chat route、改 Bot token、讀 secret store、收 secret value / hash / partial token / chat id secret、保存 raw message payload / 未脫敏 log、workflow dispatch、production deploy、把 CD success / route 200 / UI 可見當 delivery receipt、跳過 formatter convergence、跳過 redaction contract、開 runtime gate 或新增 action button。
## 7. 下一步
1. 人工確認送件對象與 route owner 後,才可把這 11 份草稿送出;送出本身需另有 audit event不由本 snapshot 代替。
2. 收到 owner response 後,先做 intake preflight欄位完整、無 secret value、無 raw payload、無 false-green claim。
3. 通過 reviewer 後,才可為每個 surface 建 migration planworkflow / script / API sender 修改仍需獨立維護窗口、rollback owner 與 production post-check。

File diff suppressed because it is too large Load Diff

View File

@@ -82,7 +82,7 @@
| P0-3 | AwoooP 同步封包 | 100% | 已送至 AwoooP 平行工作 thread `019e9154-7d5e-7b72-85be-c9d97e43ecc9`;後續仍需每次推版前重新 fetch / fast-forward | 本文件、thread send readback、mirror checklist readback |
| P0-4 | production live sanity 節點 | 100% | desktop / mobile / 展開區塊 / overflow / action href 檢查已完成 | Playwright production sanity 通過 |
| P0-5 | LOGBOOK 與完成度更新 | 100% | D2 comments-only、D2 AIOps sample、D2 Code Review 候選分類與 D2 AwoooP Runs fallback 皆已回填;可見 / bundle 變更皆已補 local / production desktop + mobile smoke | `docs/LOGBOOK.md` readback |
| P0-6 | Telegram 監控告警 / 通知出口治理 | outbound 主鏈路 100%;靜音 / recurrence slice 88%;通知出口清冊 100%direct Bot API convergence 0% | Alertmanager 缺 project context、既有 approval 收斂告警靜音、AI 分析中重複告警靜音皆已修復並正式 smoke`TelegramGateway` final-exit formatter 已完成 host / multi-signal 卡片化;本輪新增 direct Bot API egress inventory固定 workflow 13、ops script 4、API direct 1後續需 owner response 後分批收斂,不得把 API formatter 完成誤判成全域完成 | API health、Telegram health、API pod Alertmanager smoke、production logs `converged_alert_recurrence_sent``telegram-notification-egress-inventory.snapshot.json` |
| P0-6 | Telegram 監控告警 / 通知出口治理 | outbound 主鏈路 100%;靜音 / recurrence slice 88%;通知出口清冊 100%owner request draft 100%direct Bot API convergence 0% | Alertmanager 缺 project context、既有 approval 收斂告警靜音、AI 分析中重複告警靜音皆已修復並正式 smoke`TelegramGateway` final-exit formatter 已完成 host / multi-signal 卡片化;本輪新增 direct Bot API egress inventory固定 workflow 13、ops script 4、API direct 1並聚成 11 份 owner request 草稿;後續需 owner response 後分批收斂,不得把 API formatter 完成誤判成全域完成 | API health、Telegram health、API pod Alertmanager smoke、production logs `converged_alert_recurrence_sent``telegram-notification-egress-inventory.snapshot.json``telegram-notification-egress-owner-request-draft.snapshot.json` |
| P0-7 | Telegram 批准後執行真相鏈止血 | 100% | no-action approval 不再顯示批准 / 執行中;可執行修復 approval 會寫入 `auto_repair_executions`、KM 與 verifier下一步補 MCP evidence / PlayBook trust 產生真正修復候選 | 目標 pytest `125 passed`、py_compile、guard、production health、API / worker rollout、production pod classifier readback |
| P0-8 | Telegram no-action 人工處置包與操作入口 | 100% | no-action 卡片已新增人工處置包、證據補齊清單、AwoooP 修復候選建立步驟、verifier / KM / PlayBook 回寫提醒,並改成 `處置包``重診``歷史``靜默``真相鏈``Runs` 鍵盤;舊訊息不 retroactive 改寫 | 目標 pytest `64 passed + 44 passed`、py_compile、guard、production health、API / worker rollout、production pod render / keyboard smoke |
| P0-9 | MCP evidence -> PlayBook 修復候選產生 | D5 `88%`Approvals ledger `100%`Runs ledger desktop `100%`Alerts ledger desktop / mobile `100%` | 已補 webhook fallback 先建立 incident再收 MCP evidence、查 approved PlayBook、檢查 trust / command safety、產生 medium approval candidate 與 verifier planD1 追加通用兜底 PlayBook / 診斷型命令不可誤當修復、阻擋理由繁中化D2 在缺候選時產生 `repair_candidate_draft_package_v1``playbook_draft_required`、下一步與必填欄位D3 新增 `awooop_repair_candidate_draft_work_item_v1` read-only projection 與 Telegram `工作項目` deeplinkD4 讓 AwoooP Work Items 詳細呈現 PlayBook 草案處置板、必填欄位、阻擋原因、下一步、Runs / 審批連結D5 新增 `repair_candidate_coverage_gap_v1`,讓 blocked result 帶出 coverage key、target kind、blocking stage、必收 MCP evidence refs、PlayBook template fields 與 runtime 0 / false 邊界Approvals / Runs / Alerts 已新增 `資產沉澱` 欄或焦點矩陣,可直接看到 KM / PlayBook / 腳本 / 排程 / Verifier 的完成與卡點;下一步要補 Runs mobile smoke並把同一總帳接到正式 Telegram 告警卡、Observability 與 Tenants用真實告警驗證 approval -> execution -> verifier -> KM / PlayBook 回寫 | Approvals code `dafe5342` 已隨 deploy marker `42c08ece` 正式站 desktop / mobile smokeRuns code `11c2b5d4` 已隨 deploy marker `8b6ab87c` 正式站 desktop DOM smokeAlerts code `10cd6167` 已隨 deploy marker `d36d764a` 正式站 desktop / mobile DOM smokeP2-407 API production readback `overall_completion_percent=100`status-chain 後續仍必須看到 tool call、PlayBook id、risk gate、repair candidate、verifier plan |

View File

@@ -231,6 +231,9 @@ def validate(root: Path) -> None:
telegram_notification_egress_inventory = load_json(
security_dir / "telegram-notification-egress-inventory.snapshot.json"
)
telegram_notification_egress_owner_request_draft = load_json(
security_dir / "telegram-notification-egress-owner-request-draft.snapshot.json"
)
public_runtime_config_change_evidence_acceptance = load_json(
security_dir / "public-runtime-config-change-evidence-acceptance.snapshot.json"
)
@@ -21630,6 +21633,146 @@ def validate(root: Path) -> None:
f"telegram_notification_egress_inventory.{item['egress_surface_id']}.{false_key}",
item[false_key],
)
assert_equal(
"telegram_notification_egress_owner_request_draft.schema",
telegram_notification_egress_owner_request_draft["schema_version"],
"telegram_notification_egress_owner_request_draft_v1",
)
assert_equal(
"telegram_notification_egress_owner_request_draft.status",
telegram_notification_egress_owner_request_draft["status"],
"owner_request_draft_ready_no_dispatch_no_runtime_action",
)
assert_equal(
"telegram_notification_egress_owner_request_draft.mode",
telegram_notification_egress_owner_request_draft["mode"],
"metadata_only_no_secret_value_no_telegram_send_no_workflow_change",
)
expected_telegram_egress_owner_request_summary = {
"source_direct_bot_api_call_count": 18,
"source_direct_bot_api_file_count": 11,
"request_draft_count": 11,
"workflow_request_draft_count": 6,
"ops_script_request_draft_count": 4,
"ci_script_request_draft_count": 0,
"api_direct_request_draft_count": 1,
"request_field_count": 27,
"required_owner_field_count": 19,
"preflight_check_count": 16,
"outcome_lane_count": 9,
"forbidden_payload_count": 14,
"blocked_action_count": 26,
"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,
}
for key, expected in expected_telegram_egress_owner_request_summary.items():
assert_equal(
f"telegram_notification_egress_owner_request_draft.summary.{key}",
telegram_notification_egress_owner_request_draft["summary"][key],
expected,
)
expected_telegram_egress_request_paths = [
".gitea/workflows/cd-dev.yaml",
".gitea/workflows/cd.yaml",
".gitea/workflows/code-review.yaml",
".gitea/workflows/deploy-alerts.yaml",
".gitea/workflows/e2e-health.yaml",
".gitea/workflows/run-migration.yml",
"apps/api/src/services/channel_hub.py",
"scripts/ops/backup-from-110.sh",
"scripts/ops/docker-health-monitor.sh",
"scripts/ops/dr-drill.sh",
"scripts/ops/pg-backup.sh",
]
assert_equal(
"telegram_notification_egress_owner_request_draft.source_paths",
[item["source_path"] for item in telegram_notification_egress_owner_request_draft["request_drafts"]],
expected_telegram_egress_request_paths,
)
for key, value in telegram_notification_egress_owner_request_draft["execution_boundaries"].items():
if key == "not_authorization":
assert_true(f"telegram_notification_egress_owner_request_draft.execution_boundaries.{key}", value)
else:
assert_false(f"telegram_notification_egress_owner_request_draft.execution_boundaries.{key}", value)
for item in telegram_notification_egress_owner_request_draft["request_drafts"]:
assert_equal(
f"telegram_notification_egress_owner_request_draft.{item['request_draft_id']}.request_fields",
len(item["request_fields"]),
27,
)
assert_equal(
f"telegram_notification_egress_owner_request_draft.{item['request_draft_id']}.required_owner_fields",
len(item["required_owner_fields"]),
19,
)
assert_equal(
f"telegram_notification_egress_owner_request_draft.{item['request_draft_id']}.preflight_checks",
len(item["preflight_checks"]),
16,
)
assert_equal(
f"telegram_notification_egress_owner_request_draft.{item['request_draft_id']}.outcome_lanes",
len(item["outcome_lanes"]),
9,
)
assert_equal(
f"telegram_notification_egress_owner_request_draft.{item['request_draft_id']}.forbidden_payloads",
len(item["forbidden_payloads"]),
14,
)
assert_equal(
f"telegram_notification_egress_owner_request_draft.{item['request_draft_id']}.blocked_actions",
len(item["blocked_actions"]),
26,
)
assert_true(
f"telegram_notification_egress_owner_request_draft.{item['request_draft_id']}.not_authorization",
item["not_authorization"],
)
for false_key in [
"request_sent",
"recipient_confirmed",
"audit_event_emitted",
"owner_response_received",
"owner_response_accepted",
"formatter_convergence_accepted",
"redaction_contract_accepted",
"delivery_receipt_accepted",
"break_glass_fallback_accepted",
"direct_bot_api_migration_authorized",
"telegram_send_authorized",
"bot_api_call_authorized",
"workflow_modification_authorized",
"script_modification_authorized",
"api_sender_refactor_authorized",
"secret_value_collection_allowed",
"raw_payload_storage_allowed",
"production_write_authorized",
"runtime_gate",
"action_buttons_allowed",
]:
assert_false(
f"telegram_notification_egress_owner_request_draft.{item['request_draft_id']}.{false_key}",
item[false_key],
)
assert_equal(
"public_runtime_config_change_evidence_acceptance.schema",
public_runtime_config_change_evidence_acceptance["schema_version"],

View File

@@ -0,0 +1,339 @@
#!/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()