feat(security): 鎖住 Telegram 通知出口新增旁路
Some checks failed
Code Review / ai-code-review (push) Successful in 13s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-19 00:22:17 +08:00
parent 4d0150e178
commit 9ebab2db6e
13 changed files with 3809 additions and 1 deletions

View File

@@ -1,3 +1,41 @@
## 2026-06-19Telegram 通知出口 owner response 驗收帳本與 no-new-bypass guard
**背景**Telegram 通知出口清冊已固定 repo 內 `18` 個 direct Bot API `sendMessage` call site並完成 11 份 owner request 草稿與三個 no-runtime 遷移波次。但若只有清冊與遷移草稿,仍無法阻止新 direct Bot API 旁路繼續出現,也無法讓 reviewer 判斷 owner response 是否合格、是否夾帶 secret / raw payload、是否把 CD success / route 200 / UI 可見誤當 delivery receipt。
**完成內容**
- 新增 `scripts/security/telegram-notification-egress-owner-response-acceptance.py`
- 新增 `docs/security/telegram-notification-egress-owner-response-acceptance.snapshot.json`
- 新增 `docs/security/TELEGRAM-NOTIFICATION-EGRESS-OWNER-RESPONSE-ACCEPTANCE.md`
- 新增 `scripts/security/telegram-notification-egress-no-new-bypass-guard.py`
- 新增 `docs/security/telegram-notification-egress-no-new-bypass-guard.snapshot.json`
- 新增 `docs/security/TELEGRAM-NOTIFICATION-EGRESS-NO-NEW-BYPASS-GUARD.md`
- `security-mirror-progress-guard.py` 已鎖住兩個 snapshot 的 schema、summary、false flags、source paths、blocked actions 與 runtime gate。
- `iwooos-config-control-guard.py` 已把兩份新 Markdown 納入高價值配置控管文件清單。
- `docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md``docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md``docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md` 與 P0 工作表已同步更新。
- Owner response acceptance 固定 `candidates=11`、workflow `6`、ops script `4`、API direct `1``acceptance_fields=32``owner_fields=19``reviewer_checks=22``outcome_lanes=10``forbidden_payloads=14``blocked_actions=35`
- No-new-bypass guard 固定 baseline `18`、current direct call `18`、current files `11`、guarded methods `9``sendMessage=18``sendDocument/sendPhoto/sendMediaGroup/editMessageText=0``new_bypass=0`
**驗證**
- `python3 -m json.tool docs/security/telegram-notification-egress-owner-response-acceptance.snapshot.json` 通過。
- `python3 -m json.tool docs/security/telegram-notification-egress-no-new-bypass-guard.snapshot.json` 通過。
- `python3 -m py_compile scripts/security/telegram-notification-egress-owner-response-acceptance.py scripts/security/telegram-notification-egress-no-new-bypass-guard.py scripts/security/security-mirror-progress-guard.py` 通過。
- `python3 scripts/security/telegram-notification-egress-owner-response-acceptance.py --root .` 通過,輸出 `TELEGRAM_NOTIFICATION_EGRESS_OWNER_RESPONSE_ACCEPTANCE_OK candidates=11 workflow=6 ops=4 api=1 accepted=0 runtime_gate=0`
- `python3 scripts/security/telegram-notification-egress-no-new-bypass-guard.py --root .` 通過,輸出 `TELEGRAM_NOTIFICATION_EGRESS_NO_NEW_BYPASS_GUARD_OK current=18 baseline=18 new=0 sendDocument=0 runtime_gate=0`
- `python3 scripts/security/security-mirror-progress-guard.py --root .``SECURITY_MIRROR_PROGRESS_GUARD_OK`
- `python3 scripts/security/source-control-owner-response-guard.py --root .``SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK`
- `python3 scripts/security/iwooos-config-control-guard.py --root .``IWOOOS_CONFIG_CONTROL_GUARD_OK`
- `python3 scripts/ops/doc-secrets-sanity-check.py docs .gitea scripts/security``DOC_SECRET_SANITY_OK scanned_files=924`
- `git diff --check`:通過。
**完成度同步**
- Telegram notification egress owner response acceptance artifact`100%`
- Telegram notification egress no-new-bypass guard`100%`
- Direct Bot API convergence`0%`,尚未修改 workflow / script / API sender。
- owner response dispatch / received / accepted`0%`
- IwoooS headline 仍維持 `64%`active runtime gate 仍 `0`
**邊界**:本段是 repo-only metadata / guard / snapshot / 文件收斂,沒有送 Telegram、沒有呼叫 Bot API、沒有 workflow dispatch、沒有觸發 CD、沒有修改 workflow / ops script / API sender、沒有讀 secret store、沒有收 secret value / hash / partial token、沒有保存 raw payload、沒有 production write、沒有 runtime gate、沒有 action button。不能把 no-new-bypass `pass` 誤判成既有 18 個 direct send 已批准或已收斂。
## 2026-06-19P2-110E 報表資料源缺口接入 AwoooP Work Items owner review
**背景**P2-110D 已讓 Reports 頁顯示三個 `report-source-gap:*` 的 PlayBook 草案與 Verifier 計畫,但操作員仍需要離開 AwoooP 工作鏈路才能看到報表資料源缺口;這會讓 KM / PlayBook / 腳本 / 排程 / Verifier 的沉澱結果看起來像「只存在 API 或文件裡」,沒有形成可追蹤工作項。

View File

@@ -54,6 +54,10 @@
同日再新增 `docs/security/TELEGRAM-NOTIFICATION-EGRESS-MIGRATION-PLAN-DRAFT.md``docs/security/telegram-notification-egress-migration-plan-draft.snapshot.json`,將 11 份草稿排成三個 no-runtime 遷移波次。固定 `migration_candidate_count=11`、workflow `6`、ops script `4`、API direct `1``proposed_wave_count=3``reviewer_check_count=15``blocked_action_count=21`owner response、migration authorized、workflow / script modification、API sender refactor、Telegram send、Bot API call、secret collection、production write、runtime gate 與 action button 仍全部為 `0 / false`
2026-06-19 再新增 `docs/security/TELEGRAM-NOTIFICATION-EGRESS-NO-NEW-BYPASS-GUARD.md``docs/security/telegram-notification-egress-no-new-bypass-guard.snapshot.json`,把既有 18 個 direct send 固定為 no-new-bypass baseline。固定 `guarded_method_count=9``current_direct_bot_api_call_count=18``new_bypass_count=0``sendDocument_call_count=0``sendPhoto_call_count=0``sendMediaGroup_call_count=0``runtime_gate_count=0`。這是 repo source 防新增旁路 guard不代表既有 direct send 已收斂。
同日再新增 `docs/security/TELEGRAM-NOTIFICATION-EGRESS-OWNER-RESPONSE-ACCEPTANCE.md``docs/security/telegram-notification-egress-owner-response-acceptance.snapshot.json`,把 11 份 direct egress 檔案轉成 owner response acceptance 候選。固定 `acceptance_candidate_count=11`、workflow `6`、ops script `4`、API direct `1``acceptance_field_count=32``required_owner_field_count=19``reviewer_check_count=22``outcome_lane_count=10``blocked_action_count=35`owner response received / accepted、formatter convergence accepted、redaction contract accepted、delivery receipt accepted、migration authorized、workflow / script / API sender modification、Telegram send、Bot API call、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

@@ -93,6 +93,10 @@
同日再新增 `telegram_notification_egress_migration_plan_draft_v1`,將 11 份 owner request 草稿排成 workflow notification wrapper、ops notification wrapper、API sender gateway 三個遷移波次。固定 `migration_candidate_count=11``workflow_migration_candidate_count=6``ops_script_migration_candidate_count=4``api_direct_migration_candidate_count=1``proposed_wave_count=3``owner_response_required_count=11``maintenance_window_required_count=11``rollback_owner_required_count=11`。owner response、migration authorized、workflow / script modification、API sender refactor、Telegram send、Bot API call、secret collection、raw payload storage、production write、runtime gate 仍全部為 `0 / false`
2026-06-19 再新增 `telegram_notification_egress_no_new_bypass_guard_v1`,將既有 18 個 direct send 固定成 baseline signature並掃描 `sendMessage``sendDocument``sendPhoto``sendMediaGroup``editMessageText``sendAnimation``sendVideo``sendAudio``sendVoice` 等 9 類 Bot API method。固定 `baseline_signature_count=18``current_direct_bot_api_call_count=18``new_bypass_count=0``sendDocument_call_count=0``runtime_gate_count=0`。此更新只代表 repo source 目前沒有新增未登記 Telegram 直送旁路;既有 18 個 direct send 仍未遷移owner response、migration authorized、workflow / script modification、API sender refactor、Telegram send、Bot API call、secret collection、raw payload storage、production write、runtime gate 仍全部為 `0 / false`
同日再新增 `telegram_notification_egress_owner_response_acceptance_v1`,把 11 份 owner request draft 與 11 份 migration candidate 轉成 owner response acceptance 帳本。固定 `acceptance_candidate_count=11`、workflow `6`、ops script `4`、API direct `1``acceptance_field_count=32``required_owner_field_count=19``reviewer_check_count=22``outcome_lane_count=10``forbidden_payload_count=14``blocked_action_count=35`。owner response received / accepted / rejected / quarantined、supplement requested、formatter convergence accepted、redaction contract accepted、delivery receipt accepted、break-glass fallback accepted、maintenance / rollback / postcheck accepted、migration authorized、workflow / script / API sender modification、Telegram send、Bot API call、workflow dispatch、production deploy、secret collection、raw payload storage、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

@@ -98,6 +98,14 @@
同日再新增 `telegram_notification_egress_migration_plan_draft_v1`,把 11 份草稿排成 workflow notification wrapper、ops notification wrapper、API sender gateway 三個 no-runtime 遷移波次。固定 `migration_candidate_count=11`、workflow `6`、ops script `4`、API direct `1``proposed_wave_count=3``owner_response_required_count=11``maintenance_window_required_count=11``rollback_owner_required_count=11``migration_authorized_count=0``runtime_gate_count=0`。這是遷移計畫草稿,不是 workflow / script / API sender 變更,也不是 Telegram send、Bot API call、secret collection 或 production write。
## 0.00aaaa2 2026-06-19 Telegram 通知出口防新增旁路與 owner response acceptance
本輪新增 `telegram_notification_egress_no_new_bypass_guard_v1`,把既有 18 個 direct send 固定成 no-new-bypass baseline並把 `sendDocument``sendPhoto``sendMediaGroup``editMessageText` 等附件 / 編輯型 Bot API method 一併納入 repo source guard。固定 `current_direct_bot_api_call_count=18``guarded_method_count=9``new_bypass_count=0``sendDocument_call_count=0``removed_baseline_call_count=0``runtime_gate_count=0`
同步新增 `telegram_notification_egress_owner_response_acceptance_v1`,把 11 個 direct egress 檔案轉成 reviewer 可驗收的 owner response acceptance 候選。固定 `acceptance_candidate_count=11`、workflow `6`、ops script `4`、API direct `1``acceptance_field_count=32``required_owner_field_count=19``reviewer_check_count=22``outcome_lane_count=10``forbidden_payload_count=14``blocked_action_count=35`
同步邊界IwoooS headline 維持 `64%`active runtime gate 維持 `0`;既有 direct Bot API 收斂仍為 `0%`owner response received / accepted、migration authorized、workflow / script / API sender modification、Telegram send、Bot API call、workflow dispatch、production deploy、secret value collection、raw payload storage、runtime gate 與 action buttons 全部仍為 `0 / false`。本段只更新文件、snapshot 與 guard不送 Telegram、不讀 Bot token、不改 workflow、不改 host、不 dispatch workflow、不觸發部署。
## 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

@@ -0,0 +1,79 @@
# Telegram 通知出口 No-New-Bypass Guard
| 項目 | 內容 |
|------|------|
| 日期 | 2026-06-19 |
| 狀態 | `pass_no_new_bypass` |
| 工具 | `scripts/security/telegram-notification-egress-no-new-bypass-guard.py` |
| Snapshot | `docs/security/telegram-notification-egress-no-new-bypass-guard.snapshot.json` |
| Source snapshot | `docs/security/telegram-notification-egress-inventory.snapshot.json` |
| 模式 | repo source scan不讀 secret、不送 Telegram、不修改 workflow / script / API sender |
| runtime gate | `0` |
## 1. 目的
`telegram_notification_egress_no_new_bypass_guard_v1` 用 committed inventory 當 baseline掃描 repo source 是否新增未登記的 Telegram Bot API direct endpoint。既有 `18``sendMessage` call site 仍是待 owner response 與 migration review 的基線;本 guard 的目的不是批准它們,而是防止新旁路在治理收斂期間繼續增加。
## 2. 掃描範圍
| 範圍 | 說明 |
|------|------|
| `.gitea/workflows` | Gitea Actions workflow 中的 direct Bot API |
| `scripts/ops` | 主機 / 備份 / DR 類 ops script |
| `scripts/ci` | CI helper script |
| `apps/api/src` | API sender 與 notification 相關 source |
Guard 目前保護的方法包含 `sendMessage``sendDocument``sendPhoto``sendMediaGroup``editMessageText``sendAnimation``sendVideo``sendAudio``sendVoice`
## 3. 固定數字
| 指標 | 數值 | 解讀 |
|------|------|------|
| `source_direct_bot_api_call_count` | `18` | committed inventory 的 direct call baseline |
| `source_direct_bot_api_file_count` | `11` | committed inventory 的 direct file baseline |
| `baseline_signature_count` | `18` | baseline signature 數 |
| `current_direct_bot_api_call_count` | `18` | 目前掃描到的 direct call 數 |
| `current_direct_bot_api_file_count` | `11` | 目前掃描到的 direct file 數 |
| `guarded_method_count` | `9` | 受保護 Bot API method 數 |
| `sendMessage_call_count` | `18` | 目前全部都是既有 `sendMessage` |
| `sendDocument_call_count` | `0` | 不允許新增附件型 direct send |
| `sendPhoto_call_count` | `0` | 不允許新增圖片型 direct send |
| `sendMediaGroup_call_count` | `0` | 不允許新增 media group direct send |
| `editMessageText_call_count` | `0` | 不允許新增 edit direct call |
| `new_bypass_count` | `0` | 新增未登記旁路必須維持 0 |
| `removed_baseline_call_count` | `0` | baseline 移除需另外走 migration evidence |
| `runtime_gate_count` | `0` | 不提供 runtime 執行授權 |
## 4. 判讀規則
- `new_bypass_count=0` 才代表沒有新增未登記 direct Bot API 旁路。
- 既有 `18` 個 direct `sendMessage` 只是治理基線,不代表已批准保留。
- 若新增 `sendDocument``sendPhoto``sendMediaGroup` 等附件型出口guard 會阻擋,必須先進 inventory、owner request、migration plan 與 acceptance ledger。
- 若移除既有 baseline也不能只靠 guard 結果宣稱完成;仍需 migration evidence、owner response、delivery receipt 與 postcheck。
## 5. 禁止事項
本 guard 不得被解讀成以下授權:
- 送 Telegram 或呼叫 Bot API。
- 修改 workflow、ops script、API sender、chat route 或 Bot token。
- 讀 secret store、收 secret value / hash / partial token。
- 保存 raw payload、raw log 或未脫敏截圖。
- 開 runtime gate 或新增 action button。
## 6. 驗證指令
```bash
python3 scripts/security/telegram-notification-egress-no-new-bypass-guard.py --root .
python3 -m json.tool docs/security/telegram-notification-egress-no-new-bypass-guard.snapshot.json >/dev/null
python3 -m py_compile scripts/security/telegram-notification-egress-no-new-bypass-guard.py
python3 scripts/security/security-mirror-progress-guard.py --root .
```
## 7. 完成度
| 項目 | 完成度 | 邊界 |
|------|--------|------|
| No-new-bypass source guard | `100%` | 目前 `new_bypass_count=0` |
| Existing direct Bot API convergence | `0%` | 18 個既有 direct send 尚未 migration |
| Runtime execution | `0%` | 不送 Telegram、不呼叫 Bot API、不改 live route |

View File

@@ -0,0 +1,95 @@
# Telegram 通知出口 Owner Response 驗收帳本
| 項目 | 內容 |
|------|------|
| 日期 | 2026-06-19 |
| 狀態 | `owner_response_acceptance_ledger_ready_no_runtime_action` |
| 工具 | `scripts/security/telegram-notification-egress-owner-response-acceptance.py` |
| Snapshot | `docs/security/telegram-notification-egress-owner-response-acceptance.snapshot.json` |
| Source snapshots | `docs/security/telegram-notification-egress-owner-request-draft.snapshot.json``docs/security/telegram-notification-egress-migration-plan-draft.snapshot.json` |
| 模式 | metadata-only不讀 secret、不送 Telegram、不呼叫 Bot API、不改 workflow / script / API sender |
| runtime gate | `0` |
## 1. 目的
`telegram_notification_egress_owner_response_acceptance_v1` 把 Telegram 通知出口 owner request 草稿與 migration plan 草稿轉成 reviewer 可驗收的帳本。它處理的是既有 `18` 個 direct Bot API `sendMessage` call site 的 owner response 收件規則避免把「CD success、route 200、UI 可見或 Telegram sent」誤判為 delivery receipt 或自動化閉環完成。
這份帳本不是 owner response received也不是 migration authorized。workflow、ops script 或 API sender 要真的改走 wrapper / gateway 時,仍需要獨立 change evidence、維護窗口、rollback owner 與 runtime approval。
## 2. 固定數字
| 指標 | 數值 | 解讀 |
|------|------|------|
| `source_request_draft_count` | `11` | 來源 owner request 草稿 |
| `source_migration_candidate_count` | `11` | 來源 migration candidate |
| `source_direct_bot_api_call_count` | `18` | 既有 direct Bot API call site |
| `acceptance_candidate_count` | `11` | 每個 direct egress 檔案一個驗收候選 |
| `workflow_acceptance_candidate_count` | `6` | Gitea workflow 類 |
| `ops_script_acceptance_candidate_count` | `4` | Ops script 類 |
| `api_direct_acceptance_candidate_count` | `1` | API direct sender 類 |
| `acceptance_field_count` | `32` | 每個 candidate 的驗收欄位 |
| `required_owner_field_count` | `19` | owner response 必填欄位 |
| `reviewer_check_count` | `22` | reviewer 必檢規則 |
| `outcome_lane_count` | `10` | 收件結果分流 |
| `forbidden_payload_count` | `14` | 禁止出現在回覆中的 payload 類型 |
| `blocked_action_count` | `35` | 帳本階段禁止動作 |
所有 request sent、recipient confirmed、audit event emitted、owner response received / accepted / rejected / quarantined、formatter convergence accepted、redaction contract accepted、delivery receipt accepted、break-glass fallback accepted、maintenance window accepted、rollback owner accepted、postcheck evidence accepted、dedup / fingerprint accepted、no-false-green accepted、migration authorized、workflow / script / API sender modification、Telegram send、Bot API call、workflow dispatch、production deploy、secret collection、raw payload storage、production write、runtime gate、action button 全部維持 `0 / false`
## 3. Reviewer checks
Reviewer 必須確認:
- source owner request draft 與 migration plan 都是目前版本。
- owner identity、decision、decision reason、affected scope 與 followup owner 完整。
- 所有 evidence refs 都是脫敏 metadata不包含 secret value、hash、partial token、raw message payload 或 raw workflow log。
- message shape contract、redaction contract、formatter convergence、gateway / Alertmanager target、break-glass fallback、delivery receipt、dedup / fingerprint、maintenance window、rollback owner 與 postcheck evidence 都明確存在。
- `no_secret_value_attestation``no_raw_payload_attestation``no_false_green_attestation` 都存在。
- migration authorization 與 runtime approval 必須分離;此帳本不能直接批准 workflow / script / API sender 修改。
- runtime gate 維持 `0`
## 4. Outcome lanes
| Lane | 說明 |
|------|------|
| `waiting_owner_response` | 等待合格 owner response |
| `quarantine_secret_or_raw_payload` | 收到 secret、raw payload 或 raw log 時隔離 |
| `reject_execution_request` | 回覆夾帶立即執行要求時拒收 |
| `request_owner_route_supplement` | owner、route 或 scope 不完整 |
| `request_formatter_convergence_supplement` | formatter convergence 或 target 不完整 |
| `request_redaction_or_receipt_supplement` | redaction contract、delivery receipt 或 dedup 不完整 |
| `request_maintenance_or_rollback_supplement` | maintenance window 或 rollback owner 不完整 |
| `ready_for_migration_review` | metadata 完整,可另開 migration review |
| `owner_review_only_update` | 只更新 owner review metadata |
| `waiting_runtime_gate` | owner review 完成後仍等待 runtime gate |
## 5. 禁止動作
此帳本階段禁止:
- 標記 owner response received / accepted。
- 送 Telegram、呼叫 Bot API、dispatch workflow、觸發 CD 或 production deploy。
- 修改 workflow、ops script、API sender、chat route、Bot token 或 secret。
- 讀 secret store、收 secret value / hash / partial token / chat id secret。
- 保存 raw message payload、raw workflow log、raw action log、內部協作逐字稿或未脫敏截圖。
- 把 CD success、route `200`、UI 可見或 Telegram sent 當作 delivery receipt。
- 跳過 formatter convergence、redaction contract、dedup / fingerprint、break-glass fallback 或 no-false-green review。
- 開 runtime gate 或新增 action button。
## 6. 驗證指令
```bash
python3 scripts/security/telegram-notification-egress-owner-response-acceptance.py --root .
python3 -m json.tool docs/security/telegram-notification-egress-owner-response-acceptance.snapshot.json >/dev/null
python3 -m py_compile scripts/security/telegram-notification-egress-owner-response-acceptance.py
python3 scripts/security/security-mirror-progress-guard.py --root .
```
## 7. 完成度
| 項目 | 完成度 | 邊界 |
|------|--------|------|
| Owner response acceptance artifact | `100%` | 11 個 candidate 已可 reviewer 驗收 |
| Owner response dispatch / received / accepted | `0%` | 尚未送件、尚未收件、尚未接受 |
| Direct Bot API convergence | `0%` | 尚未修改 workflow / script / API sender |
| Runtime execution | `0%` | 不送 Telegram、不呼叫 Bot API、不開 runtime gate |

View File

@@ -0,0 +1,197 @@
{
"current_direct_bot_api_calls": [
{
"line": 54,
"method": "sendMessage",
"path": ".gitea/workflows/cd-dev.yaml",
"sanitized_excerpt": "printf '%b' \"$MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": ".gitea/workflows/cd-dev.yaml::sendmessage::printf '%b' \"$MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 241,
"method": "sendMessage",
"path": ".gitea/workflows/cd-dev.yaml",
"sanitized_excerpt": "printf '%b' \"$MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": ".gitea/workflows/cd-dev.yaml::sendmessage::printf '%b' \"$MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 262,
"method": "sendMessage",
"path": ".gitea/workflows/cd-dev.yaml",
"sanitized_excerpt": "printf '%b' \"$MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": ".gitea/workflows/cd-dev.yaml::sendmessage::printf '%b' \"$MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 113,
"method": "sendMessage",
"path": ".gitea/workflows/cd.yaml",
"sanitized_excerpt": "curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": ".gitea/workflows/cd.yaml::sendmessage::curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 305,
"method": "sendMessage",
"path": ".gitea/workflows/cd.yaml",
"sanitized_excerpt": "curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": ".gitea/workflows/cd.yaml::sendmessage::curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 1203,
"method": "sendMessage",
"path": ".gitea/workflows/cd.yaml",
"sanitized_excerpt": "curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": ".gitea/workflows/cd.yaml::sendmessage::curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 1552,
"method": "sendMessage",
"path": ".gitea/workflows/cd.yaml",
"sanitized_excerpt": "printf '%b' \"$TG_MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": ".gitea/workflows/cd.yaml::sendmessage::printf '%b' \"$TG_MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 1575,
"method": "sendMessage",
"path": ".gitea/workflows/cd.yaml",
"sanitized_excerpt": "curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": ".gitea/workflows/cd.yaml::sendmessage::curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 137,
"method": "sendMessage",
"path": ".gitea/workflows/code-review.yaml",
"sanitized_excerpt": "curl -fsS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": ".gitea/workflows/code-review.yaml::sendmessage::curl -fsS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 216,
"method": "sendMessage",
"path": ".gitea/workflows/code-review.yaml",
"sanitized_excerpt": "curl -fsS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": ".gitea/workflows/code-review.yaml::sendmessage::curl -fsS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 69,
"method": "sendMessage",
"path": ".gitea/workflows/deploy-alerts.yaml",
"sanitized_excerpt": "curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": ".gitea/workflows/deploy-alerts.yaml::sendmessage::curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 98,
"method": "sendMessage",
"path": ".gitea/workflows/e2e-health.yaml",
"sanitized_excerpt": "curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": ".gitea/workflows/e2e-health.yaml::sendmessage::curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 210,
"method": "sendMessage",
"path": ".gitea/workflows/run-migration.yml",
"sanitized_excerpt": "curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": ".gitea/workflows/run-migration.yml::sendmessage::curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 1138,
"method": "sendMessage",
"path": "apps/api/src/services/channel_hub.py",
"sanitized_excerpt": "f\"https://api.telegram.org/bot<redacted>/sendMessage\",",
"signature": "apps/api/src/services/channel_hub.py::sendmessage::f\"https://api.telegram.org/bot<redacted>/sendMessage\","
},
{
"line": 64,
"method": "sendMessage",
"path": "scripts/ops/backup-from-110.sh",
"sanitized_excerpt": "curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": "scripts/ops/backup-from-110.sh::sendmessage::curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 90,
"method": "sendMessage",
"path": "scripts/ops/docker-health-monitor.sh",
"sanitized_excerpt": "curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": "scripts/ops/docker-health-monitor.sh::sendmessage::curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 63,
"method": "sendMessage",
"path": "scripts/ops/dr-drill.sh",
"sanitized_excerpt": "curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": "scripts/ops/dr-drill.sh::sendmessage::curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
},
{
"line": 52,
"method": "sendMessage",
"path": "scripts/ops/pg-backup.sh",
"sanitized_excerpt": "curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
"signature": "scripts/ops/pg-backup.sh::sendmessage::curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
}
],
"execution_boundaries": {
"action_buttons_allowed": false,
"api_sender_refactor_authorized": false,
"bot_api_call_authorized": false,
"bot_token_change_authorized": false,
"chat_route_change_authorized": false,
"not_authorization": true,
"partial_token_collection_allowed": false,
"production_write_authorized": false,
"raw_payload_storage_allowed": false,
"runtime_execution_authorized": false,
"script_modification_authorized": false,
"secret_hash_collection_allowed": false,
"secret_value_collection_allowed": false,
"telegram_send_authorized": false,
"workflow_modification_authorized": false
},
"generated_at": "2026-06-19T09:40:00+08:00",
"git_commit": "4d0150e1",
"guarded_bot_methods": [
"sendMessage",
"sendDocument",
"sendPhoto",
"sendMediaGroup",
"editMessageText",
"sendAnimation",
"sendVideo",
"sendAudio",
"sendVoice"
],
"guarded_roots": [
".gitea/workflows",
"scripts/ops",
"scripts/ci",
"apps/api/src"
],
"mode": "repo_source_scan_no_secret_value_no_telegram_send",
"new_bypass_findings": [],
"operator_interpretation": [
"new_bypass_count 維持 0 才代表沒有新增未登記 Telegram Bot API 直送旁路。",
"既有 18 個 sendMessage 旁路仍是待 owner response 的基線,不代表已批准或已收斂。",
"sendDocument / sendPhoto / sendMediaGroup 等附件型出口若出現在 repo source會被視為新增旁路並阻擋。",
"本 guard 只讀 repo source 與 committed snapshot不送 Telegram、不讀 Bot token、不修改 workflow / script / API sender。"
],
"removed_baseline_signatures": [],
"schema_version": "telegram_notification_egress_no_new_bypass_guard_v1",
"source_snapshot": "docs/security/telegram-notification-egress-inventory.snapshot.json",
"status": "pass_no_new_bypass",
"summary": {
"action_button_count": 0,
"baseline_signature_count": 18,
"current_direct_bot_api_call_count": 18,
"current_direct_bot_api_file_count": 11,
"editMessageText_call_count": 0,
"guarded_method_count": 9,
"new_bypass_count": 0,
"new_bypass_file_count": 0,
"other_guarded_method_call_count": 0,
"removed_baseline_call_count": 0,
"runtime_gate_count": 0,
"sendDocument_call_count": 0,
"sendMediaGroup_call_count": 0,
"sendMessage_call_count": 18,
"sendPhoto_call_count": 0,
"source_direct_bot_api_call_count": 18,
"source_direct_bot_api_file_count": 11
}
}

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%owner request draft 100%migration plan 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 草稿與 3 個遷移波次;後續需 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``telegram-notification-egress-migration-plan-draft.snapshot.json` |
| P0-6 | Telegram 監控告警 / 通知出口治理 | outbound 主鏈路 100%;靜音 / recurrence slice 88%;通知出口清冊 100%owner request draft 100%migration plan draft 100%防新增旁路 guard 100%owner response acceptance 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 草稿與 3 個遷移波次;本輪新增 no-new-bypass guard覆蓋 `sendMessage` / `sendDocument` / `sendPhoto` / `sendMediaGroup` / `editMessageText` 等 9 類 Bot API method並新增 11 份 owner response acceptance 候選;後續需 owner response 後分批收斂,不得把 API formatter 或防新增 guard 完成誤判成既有旁路已收斂 | 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``telegram-notification-egress-migration-plan-draft.snapshot.json``telegram-notification-egress-no-new-bypass-guard.snapshot.json``telegram-notification-egress-owner-response-acceptance.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

@@ -74,6 +74,8 @@ REQUIRED_CONTROL_DOCS = [
"docs/security/SECURITY-ASSET-CONTROL-LEDGER.md",
"docs/security/AI-PROVIDER-OWNER-RESPONSE-ACCEPTANCE.md",
"docs/security/AGENT-BOUNTY-OWNER-REQUEST-DRAFT.md",
"docs/security/TELEGRAM-NOTIFICATION-EGRESS-NO-NEW-BYPASS-GUARD.md",
"docs/security/TELEGRAM-NOTIFICATION-EGRESS-OWNER-RESPONSE-ACCEPTANCE.md",
"docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md",
]

View File

@@ -231,12 +231,18 @@ def validate(root: Path) -> None:
telegram_notification_egress_inventory = load_json(
security_dir / "telegram-notification-egress-inventory.snapshot.json"
)
telegram_notification_egress_no_new_bypass_guard = load_json(
security_dir / "telegram-notification-egress-no-new-bypass-guard.snapshot.json"
)
telegram_notification_egress_owner_request_draft = load_json(
security_dir / "telegram-notification-egress-owner-request-draft.snapshot.json"
)
telegram_notification_egress_migration_plan_draft = load_json(
security_dir / "telegram-notification-egress-migration-plan-draft.snapshot.json"
)
telegram_notification_egress_owner_response_acceptance = load_json(
security_dir / "telegram-notification-egress-owner-response-acceptance.snapshot.json"
)
public_runtime_config_change_evidence_acceptance = load_json(
security_dir / "public-runtime-config-change-evidence-acceptance.snapshot.json"
)
@@ -21894,6 +21900,238 @@ def validate(root: Path) -> None:
f"telegram_notification_egress_migration_plan_draft.{item['migration_candidate_id']}.{false_key}",
item[false_key],
)
assert_equal(
"telegram_notification_egress_no_new_bypass_guard.schema",
telegram_notification_egress_no_new_bypass_guard["schema_version"],
"telegram_notification_egress_no_new_bypass_guard_v1",
)
assert_equal(
"telegram_notification_egress_no_new_bypass_guard.status",
telegram_notification_egress_no_new_bypass_guard["status"],
"pass_no_new_bypass",
)
assert_equal(
"telegram_notification_egress_no_new_bypass_guard.mode",
telegram_notification_egress_no_new_bypass_guard["mode"],
"repo_source_scan_no_secret_value_no_telegram_send",
)
expected_telegram_egress_no_new_bypass_summary = {
"source_direct_bot_api_call_count": 18,
"source_direct_bot_api_file_count": 11,
"baseline_signature_count": 18,
"current_direct_bot_api_call_count": 18,
"current_direct_bot_api_file_count": 11,
"guarded_method_count": 9,
"sendMessage_call_count": 18,
"sendDocument_call_count": 0,
"sendPhoto_call_count": 0,
"sendMediaGroup_call_count": 0,
"editMessageText_call_count": 0,
"other_guarded_method_call_count": 0,
"new_bypass_count": 0,
"new_bypass_file_count": 0,
"removed_baseline_call_count": 0,
"runtime_gate_count": 0,
"action_button_count": 0,
}
for key, expected in expected_telegram_egress_no_new_bypass_summary.items():
assert_equal(
f"telegram_notification_egress_no_new_bypass_guard.summary.{key}",
telegram_notification_egress_no_new_bypass_guard["summary"][key],
expected,
)
assert_equal(
"telegram_notification_egress_no_new_bypass_guard.current_paths",
[item["path"] for item in telegram_notification_egress_no_new_bypass_guard["current_direct_bot_api_calls"]],
[
".gitea/workflows/cd-dev.yaml",
".gitea/workflows/cd-dev.yaml",
".gitea/workflows/cd-dev.yaml",
".gitea/workflows/cd.yaml",
".gitea/workflows/cd.yaml",
".gitea/workflows/cd.yaml",
".gitea/workflows/cd.yaml",
".gitea/workflows/cd.yaml",
".gitea/workflows/code-review.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_no_new_bypass_guard.new_bypass_findings",
telegram_notification_egress_no_new_bypass_guard["new_bypass_findings"],
[],
)
assert_equal(
"telegram_notification_egress_no_new_bypass_guard.removed_baseline_signatures",
telegram_notification_egress_no_new_bypass_guard["removed_baseline_signatures"],
[],
)
for key, value in telegram_notification_egress_no_new_bypass_guard["execution_boundaries"].items():
if key == "not_authorization":
assert_true(f"telegram_notification_egress_no_new_bypass_guard.execution_boundaries.{key}", value)
else:
assert_false(f"telegram_notification_egress_no_new_bypass_guard.execution_boundaries.{key}", value)
assert_equal(
"telegram_notification_egress_owner_response_acceptance.schema",
telegram_notification_egress_owner_response_acceptance["schema_version"],
"telegram_notification_egress_owner_response_acceptance_v1",
)
assert_equal(
"telegram_notification_egress_owner_response_acceptance.status",
telegram_notification_egress_owner_response_acceptance["status"],
"owner_response_acceptance_ledger_ready_no_runtime_action",
)
assert_equal(
"telegram_notification_egress_owner_response_acceptance.mode",
telegram_notification_egress_owner_response_acceptance["mode"],
"metadata_only_no_secret_value_no_telegram_send_no_workflow_script_api_change",
)
expected_telegram_egress_owner_response_acceptance_summary = {
"source_request_draft_count": 11,
"source_migration_candidate_count": 11,
"source_direct_bot_api_call_count": 18,
"acceptance_candidate_count": 11,
"workflow_acceptance_candidate_count": 6,
"ops_script_acceptance_candidate_count": 4,
"api_direct_acceptance_candidate_count": 1,
"acceptance_field_count": 32,
"required_owner_field_count": 19,
"reviewer_check_count": 22,
"outcome_lane_count": 10,
"forbidden_payload_count": 14,
"blocked_action_count": 35,
"request_sent_count": 0,
"recipient_confirmed_count": 0,
"audit_event_emitted_count": 0,
"owner_response_received_count": 0,
"owner_response_accepted_count": 0,
"owner_response_rejected_count": 0,
"owner_response_quarantined_count": 0,
"supplement_requested_count": 0,
"formatter_convergence_accepted_count": 0,
"redaction_contract_accepted_count": 0,
"delivery_receipt_accepted_count": 0,
"break_glass_fallback_accepted_count": 0,
"maintenance_window_accepted_count": 0,
"rollback_owner_accepted_count": 0,
"postcheck_evidence_accepted_count": 0,
"dedup_or_fingerprint_accepted_count": 0,
"no_false_green_accepted_count": 0,
"direct_bot_api_migration_authorized_count": 0,
"workflow_modification_authorized_count": 0,
"script_modification_authorized_count": 0,
"api_sender_refactor_authorized_count": 0,
"telegram_send_authorized_count": 0,
"bot_api_call_authorized_count": 0,
"workflow_dispatch_authorized_count": 0,
"production_deploy_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_response_acceptance_summary.items():
assert_equal(
f"telegram_notification_egress_owner_response_acceptance.summary.{key}",
telegram_notification_egress_owner_response_acceptance["summary"][key],
expected,
)
assert_equal(
"telegram_notification_egress_owner_response_acceptance.source_paths",
[item["source_path"] for item in telegram_notification_egress_owner_response_acceptance["acceptance_candidates"]],
expected_telegram_egress_request_paths,
)
for key, value in telegram_notification_egress_owner_response_acceptance["execution_boundaries"].items():
if key == "not_authorization":
assert_true(
f"telegram_notification_egress_owner_response_acceptance.execution_boundaries.{key}",
value,
)
else:
assert_false(
f"telegram_notification_egress_owner_response_acceptance.execution_boundaries.{key}",
value,
)
for item in telegram_notification_egress_owner_response_acceptance["acceptance_candidates"]:
assert_equal(
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_id']}.acceptance_fields",
len(item["acceptance_fields"]),
32,
)
assert_equal(
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_id']}.required_owner_fields",
len(item["required_owner_fields"]),
19,
)
assert_equal(
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_id']}.reviewer_checks",
len(item["reviewer_checks"]),
22,
)
assert_equal(
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_id']}.outcome_lanes",
len(item["outcome_lanes"]),
10,
)
assert_equal(
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_id']}.forbidden_payloads",
len(item["forbidden_payloads"]),
14,
)
assert_equal(
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_id']}.blocked_actions",
len(item["blocked_actions"]),
35,
)
assert_true(
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_id']}.not_authorization",
item["not_authorization"],
)
for false_key in [
"request_sent",
"recipient_confirmed",
"audit_event_emitted",
"owner_response_received",
"owner_response_accepted",
"owner_response_rejected",
"owner_response_quarantined",
"supplement_requested",
"formatter_convergence_accepted",
"redaction_contract_accepted",
"delivery_receipt_accepted",
"break_glass_fallback_accepted",
"maintenance_window_accepted",
"rollback_owner_accepted",
"postcheck_evidence_accepted",
"dedup_or_fingerprint_accepted",
"no_false_green_accepted",
"direct_bot_api_migration_authorized",
"workflow_modification_authorized",
"script_modification_authorized",
"api_sender_refactor_authorized",
"telegram_send_authorized",
"bot_api_call_authorized",
"workflow_dispatch_authorized",
"production_deploy_authorized",
"secret_value_collection_allowed",
"raw_payload_storage_allowed",
"production_write_authorized",
"runtime_gate",
"action_buttons_allowed",
]:
assert_false(
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_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,282 @@
#!/usr/bin/env python3
"""檢查 Telegram 通知出口不可新增未登記 direct Bot API 旁路。
本 guard 只掃描 repo 原始碼與 committed snapshot不讀 secret、不呼叫
Telegram、不修改 workflow / script / API sender。既有 direct send 仍是待
owner response 的基線;任何新增或變形的 direct Bot API endpoint 都必須先
進 inventory / owner request / migration plan而不是直接合併。
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from collections import Counter
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")
SCAN_ROOTS = (
Path(".gitea/workflows"),
Path("scripts/ops"),
Path("scripts/ci"),
Path("apps/api/src"),
)
SCAN_SUFFIXES = {".py", ".sh", ".js", ".yml", ".yaml"}
GUARDED_BOT_METHODS = (
"sendMessage",
"sendDocument",
"sendPhoto",
"sendMediaGroup",
"editMessageText",
"sendAnimation",
"sendVideo",
"sendAudio",
"sendVoice",
)
BOT_ENDPOINT_RE = re.compile(
r"api\.telegram\.org/bot.*?/(?P<method>"
+ "|".join(re.escape(method) for method in GUARDED_BOT_METHODS)
+ r")\b",
re.IGNORECASE,
)
SECRET_INTERPOLATION_RE = re.compile(r"\$\{\{\s*secrets\.[^}]+\}\}")
BOT_TOKEN_URL_RE = re.compile(
r"api\.telegram\.org/bot.*?/(?P<method>"
+ "|".join(re.escape(method) for method in GUARDED_BOT_METHODS)
+ r")\b",
re.IGNORECASE,
)
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 iter_scannable_files(root: Path) -> list[Path]:
files: list[Path] = []
for scan_root in SCAN_ROOTS:
absolute_root = root / scan_root
if not absolute_root.exists():
continue
for path in absolute_root.rglob("*"):
if path.is_file() and path.suffix in SCAN_SUFFIXES:
files.append(path)
return sorted(files)
def sanitize_excerpt(line: str) -> str:
excerpt = line.strip()
excerpt = SECRET_INTERPOLATION_RE.sub("${{ secrets.<redacted> }}", excerpt)
excerpt = BOT_TOKEN_URL_RE.sub(
lambda match: f"api.telegram.org/bot<redacted>/{match.group('method')}",
excerpt,
)
return excerpt[:180]
def signature(path: str, method: str, sanitized_excerpt: str) -> str:
return f"{path}::{method.lower()}::{sanitized_excerpt}"
def load_source_snapshot(root: Path) -> dict[str, Any]:
snapshot_path = root / SOURCE_SNAPSHOT
return json.loads(snapshot_path.read_text(encoding="utf-8"))
def build_baseline(source_snapshot: dict[str, Any]) -> Counter[str]:
baseline: Counter[str] = Counter()
for item in source_snapshot.get("direct_bot_api_calls", []):
excerpt = item.get("sanitized_excerpt", "")
match = BOT_ENDPOINT_RE.search(excerpt)
method = match.group("method") if match else "sendMessage"
baseline[signature(item["path"], method, excerpt)] += 1
return baseline
def scan_current_direct_endpoints(root: Path) -> list[dict[str, Any]]:
findings: list[dict[str, Any]] = []
for path in iter_scannable_files(root):
relative_path = path.relative_to(root).as_posix()
text = path.read_text(encoding="utf-8", errors="replace")
for line_number, line in enumerate(text.splitlines(), start=1):
for match in BOT_ENDPOINT_RE.finditer(line):
method = match.group("method")
sanitized = sanitize_excerpt(line)
findings.append(
{
"path": relative_path,
"line": line_number,
"method": method,
"sanitized_excerpt": sanitized,
"signature": signature(relative_path, method, sanitized),
}
)
return findings
def method_counts(findings: list[dict[str, Any]]) -> dict[str, int]:
counts = {method: 0 for method in GUARDED_BOT_METHODS}
for item in findings:
for method in GUARDED_BOT_METHODS:
if item["method"].lower() == method.lower():
counts[method] += 1
break
return counts
def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]:
generated = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
source_snapshot = load_source_snapshot(root)
baseline = build_baseline(source_snapshot)
current_findings = scan_current_direct_endpoints(root)
remaining_baseline = baseline.copy()
new_bypass_findings: list[dict[str, Any]] = []
for item in current_findings:
item_signature = item["signature"]
if remaining_baseline[item_signature] > 0:
remaining_baseline[item_signature] -= 1
continue
new_bypass_findings.append(item)
removed_baseline_signatures = [
{"signature": item_signature, "removed_count": count}
for item_signature, count in sorted(remaining_baseline.items())
if count > 0
]
current_files = sorted({item["path"] for item in current_findings})
new_bypass_files = sorted({item["path"] for item in new_bypass_findings})
counts_by_method = method_counts(current_findings)
source_summary = source_snapshot["summary"]
return {
"schema_version": "telegram_notification_egress_no_new_bypass_guard_v1",
"generated_at": generated,
"git_commit": git_short_sha(root),
"status": "pass_no_new_bypass" if not new_bypass_findings else "blocked_new_bypass_detected",
"mode": "repo_source_scan_no_secret_value_no_telegram_send",
"source_snapshot": SOURCE_SNAPSHOT.as_posix(),
"guarded_roots": [path.as_posix() for path in SCAN_ROOTS],
"guarded_bot_methods": list(GUARDED_BOT_METHODS),
"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"],
"baseline_signature_count": sum(baseline.values()),
"current_direct_bot_api_call_count": len(current_findings),
"current_direct_bot_api_file_count": len(current_files),
"guarded_method_count": len(GUARDED_BOT_METHODS),
"sendMessage_call_count": counts_by_method["sendMessage"],
"sendDocument_call_count": counts_by_method["sendDocument"],
"sendPhoto_call_count": counts_by_method["sendPhoto"],
"sendMediaGroup_call_count": counts_by_method["sendMediaGroup"],
"editMessageText_call_count": counts_by_method["editMessageText"],
"other_guarded_method_call_count": sum(
count
for method, count in counts_by_method.items()
if method
not in {
"sendMessage",
"sendDocument",
"sendPhoto",
"sendMediaGroup",
"editMessageText",
}
),
"new_bypass_count": len(new_bypass_findings),
"new_bypass_file_count": len(new_bypass_files),
"removed_baseline_call_count": sum(item["removed_count"] for item in removed_baseline_signatures),
"runtime_gate_count": 0,
"action_button_count": 0,
},
"execution_boundaries": {
"runtime_execution_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,
"secret_hash_collection_allowed": False,
"partial_token_collection_allowed": False,
"chat_route_change_authorized": False,
"bot_token_change_authorized": False,
"raw_payload_storage_allowed": False,
"production_write_authorized": False,
"action_buttons_allowed": False,
"not_authorization": True,
},
"current_direct_bot_api_calls": current_findings,
"new_bypass_findings": new_bypass_findings,
"removed_baseline_signatures": removed_baseline_signatures,
"operator_interpretation": [
"new_bypass_count 維持 0 才代表沒有新增未登記 Telegram Bot API 直送旁路。",
"既有 18 個 sendMessage 旁路仍是待 owner response 的基線,不代表已批准或已收斂。",
"sendDocument / sendPhoto / sendMediaGroup 等附件型出口若出現在 repo source會被視為新增旁路並阻擋。",
"本 guard 只讀 repo source 與 committed snapshot不送 Telegram、不讀 Bot token、不修改 workflow / script / API sender。",
],
}
def validate(root: Path) -> None:
report = build_report(root)
errors: list[str] = []
if report["summary"]["new_bypass_count"]:
for item in report["new_bypass_findings"]:
errors.append(
f"{item['path']}:{item['line']}: 新增未登記 Telegram Bot API 旁路 {item['method']}"
)
if errors:
raise SystemExit(
"BLOCKED telegram notification egress no-new-bypass guard:\n"
+ "\n".join(f"- {error}" for error in errors)
)
def main() -> None:
parser = argparse.ArgumentParser(description="檢查 Telegram 通知出口不可新增未登記 direct Bot API 旁路")
parser.add_argument("--root", default=".", help="repository root")
parser.add_argument("--output", help="寫出 JSON 報告")
parser.add_argument("--generated-at", help="固定 generated_at 時間,供 committed snapshot 使用")
args = parser.parse_args()
root = Path(args.root).resolve()
report = build_report(root, args.generated_at)
if args.output:
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
validate(root)
summary = report["summary"]
print(
"TELEGRAM_NOTIFICATION_EGRESS_NO_NEW_BYPASS_GUARD_OK "
f"current={summary['current_direct_bot_api_call_count']} "
f"baseline={summary['baseline_signature_count']} "
f"new={summary['new_bypass_count']} "
f"sendDocument={summary['sendDocument_call_count']} "
f"runtime_gate={summary['runtime_gate_count']}"
)
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,388 @@
#!/usr/bin/env python3
"""建立 Telegram 通知出口 owner response 驗收帳本。
此帳本把 Telegram 通知出口 owner request 草稿與 migration plan 草稿轉成
reviewer 可驗收的候選項。它不送 Telegram、不呼叫 Bot API、不讀 secret
也不修改 workflow、script、API sender、runtime config 或 production。
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
TAIPEI = timezone(timedelta(hours=8))
OWNER_REQUEST_SNAPSHOT = Path("docs/security/telegram-notification-egress-owner-request-draft.snapshot.json")
MIGRATION_PLAN_SNAPSHOT = Path("docs/security/telegram-notification-egress-migration-plan-draft.snapshot.json")
ACCEPTANCE_FIELDS = [
"acceptance_candidate_id",
"source_request_draft_id",
"source_migration_candidate_id",
"source_path",
"surface_kind",
"direct_call_count",
"proposed_wave",
"proposed_target",
"owner_response_ref",
"owner_role_or_team",
"decision",
"decision_reason",
"affected_scope",
"redacted_evidence_refs",
"message_shape_contract_ref",
"redaction_contract_ref",
"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",
"reviewer_outcome",
"followup_owner",
"not_authorization",
]
REVIEWER_CHECKS = [
"source_owner_request_current",
"source_migration_plan_current",
"owner_identity_present",
"decision_reason_present",
"affected_scope_matches_source",
"redacted_refs_only",
"no_secret_or_token_value",
"no_raw_message_payload",
"message_shape_contract_present",
"redaction_contract_present",
"formatter_convergence_explicit",
"gateway_or_alertmanager_target_valid",
"break_glass_fallback_explicit",
"delivery_receipt_metadata_only",
"dedup_or_fingerprint_present",
"maintenance_window_present",
"rollback_owner_present",
"postcheck_evidence_present",
"no_false_green_attested",
"migration_authorization_separate",
"counts_transition_safe",
"runtime_gate_stays_zero",
]
OUTCOME_LANES = [
"waiting_owner_response",
"quarantine_secret_or_raw_payload",
"reject_execution_request",
"request_owner_route_supplement",
"request_formatter_convergence_supplement",
"request_redaction_or_receipt_supplement",
"request_maintenance_or_rollback_supplement",
"ready_for_migration_review",
"owner_review_only_update",
"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 = [
"mark_owner_response_received_without_record",
"mark_owner_response_accepted_without_reviewer_record",
"send_telegram",
"call_bot_api",
"modify_workflow",
"modify_ops_script",
"refactor_api_sender",
"dispatch_workflow",
"trigger_cd",
"deploy_production",
"change_chat_route",
"change_bot_token",
"rotate_secret",
"read_secret_store",
"collect_secret_value",
"collect_secret_hash",
"collect_partial_token",
"collect_chat_id_secret",
"store_raw_message_payload",
"store_unredacted_log",
"store_internal_work_window_transcript",
"accept_cd_success_as_delivery_receipt",
"accept_route_200_as_notification_delivery",
"accept_ui_visible_as_notification_acceptance",
"accept_telegram_sent_without_delivery_receipt",
"skip_formatter_convergence",
"skip_redaction_contract",
"skip_dedup_or_fingerprint_review",
"skip_break_glass_fallback_review",
"authorize_migration",
"authorize_workflow_modification",
"authorize_script_modification",
"authorize_api_sender_refactor",
"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 build_candidate(request: dict[str, Any], migration: dict[str, Any]) -> dict[str, Any]:
return {
"acceptance_candidate_id": f"telegram_notification_egress_owner_response_acceptance:{request['source_path']}",
"status": "waiting_owner_response",
"source_request_draft_id": request["request_draft_id"],
"source_migration_candidate_id": migration["migration_candidate_id"],
"source_path": request["source_path"],
"surface_kind": request["surface_kind"],
"direct_call_count": request["direct_call_count"],
"line_refs": request["line_refs"],
"line_hash_refs": request["line_hash_refs"],
"proposed_wave": migration["proposed_wave"],
"proposed_target": migration["proposed_target"],
"proposed_change_summary": migration["proposed_change_summary"],
"owner_response_ref": None,
"owner_role_or_team": "pending_owner_response",
"decision": "pending_owner_response",
"decision_reason": "pending_owner_response",
"affected_scope": "pending_owner_response",
"redacted_evidence_refs": [],
"message_shape_contract_ref": None,
"redaction_contract_ref": None,
"formatter_convergence_decision": "pending_owner_response",
"gateway_or_alertmanager_target": "pending_owner_response",
"break_glass_fallback_decision": "pending_owner_response",
"delivery_receipt_ref": None,
"dedup_or_fingerprint_plan": "pending_owner_response",
"fallback_or_degraded_mode": "pending_owner_response",
"migration_or_exception_reason": "pending_owner_response",
"maintenance_window": "pending_owner_response",
"rollback_owner": "pending_owner_response",
"postcheck_evidence_ref": None,
"no_secret_value_attestation": "pending_owner_response",
"no_raw_payload_attestation": "pending_owner_response",
"no_false_green_attestation": "pending_owner_response",
"reviewer_outcome": "waiting_owner_response",
"followup_owner": "pending_owner_response",
"acceptance_fields": ACCEPTANCE_FIELDS,
"required_owner_fields": request["required_owner_fields"],
"reviewer_checks": REVIEWER_CHECKS,
"outcome_lanes": OUTCOME_LANES,
"forbidden_payloads": FORBIDDEN_PAYLOADS,
"blocked_actions": BLOCKED_ACTIONS,
"not_authorization": True,
"request_sent": False,
"recipient_confirmed": False,
"audit_event_emitted": False,
"owner_response_received": False,
"owner_response_accepted": False,
"owner_response_rejected": False,
"owner_response_quarantined": False,
"supplement_requested": False,
"formatter_convergence_accepted": False,
"redaction_contract_accepted": False,
"delivery_receipt_accepted": False,
"break_glass_fallback_accepted": False,
"maintenance_window_accepted": False,
"rollback_owner_accepted": False,
"postcheck_evidence_accepted": False,
"dedup_or_fingerprint_accepted": False,
"no_false_green_accepted": False,
"direct_bot_api_migration_authorized": False,
"workflow_modification_authorized": False,
"script_modification_authorized": False,
"api_sender_refactor_authorized": False,
"telegram_send_authorized": False,
"bot_api_call_authorized": False,
"workflow_dispatch_authorized": False,
"production_deploy_authorized": False,
"secret_value_collection_allowed": False,
"raw_payload_storage_allowed": False,
"production_write_authorized": False,
"runtime_gate": False,
"action_buttons_allowed": False,
}
def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]:
generated = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
owner_request = load_json(root / OWNER_REQUEST_SNAPSHOT)
migration_plan = load_json(root / MIGRATION_PLAN_SNAPSHOT)
migration_by_request_id = {
item["source_request_draft_id"]: item for item in migration_plan["migration_candidates"]
}
candidates = [
build_candidate(request, migration_by_request_id[request["request_draft_id"]])
for request in owner_request["request_drafts"]
]
workflow = [item for item in candidates if item["surface_kind"] == "gitea_workflow_direct_bot_api"]
ops = [item for item in candidates if item["surface_kind"] == "ops_script_direct_bot_api"]
api = [item for item in candidates if item["surface_kind"] == "api_direct_bot_api"]
return {
"schema_version": "telegram_notification_egress_owner_response_acceptance_v1",
"generated_at": generated,
"git_commit": git_short_sha(root),
"status": "owner_response_acceptance_ledger_ready_no_runtime_action",
"mode": "metadata_only_no_secret_value_no_telegram_send_no_workflow_script_api_change",
"source_owner_request_snapshot": OWNER_REQUEST_SNAPSHOT.as_posix(),
"source_owner_request_schema_version": owner_request["schema_version"],
"source_owner_request_status": owner_request["status"],
"source_migration_plan_snapshot": MIGRATION_PLAN_SNAPSHOT.as_posix(),
"source_migration_plan_schema_version": migration_plan["schema_version"],
"source_migration_plan_status": migration_plan["status"],
"summary": {
"source_request_draft_count": owner_request["summary"]["request_draft_count"],
"source_migration_candidate_count": migration_plan["summary"]["migration_candidate_count"],
"source_direct_bot_api_call_count": owner_request["summary"]["source_direct_bot_api_call_count"],
"acceptance_candidate_count": len(candidates),
"workflow_acceptance_candidate_count": len(workflow),
"ops_script_acceptance_candidate_count": len(ops),
"api_direct_acceptance_candidate_count": len(api),
"acceptance_field_count": len(ACCEPTANCE_FIELDS),
"required_owner_field_count": len(owner_request["request_drafts"][0]["required_owner_fields"]),
"reviewer_check_count": len(REVIEWER_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,
"owner_response_rejected_count": 0,
"owner_response_quarantined_count": 0,
"supplement_requested_count": 0,
"formatter_convergence_accepted_count": 0,
"redaction_contract_accepted_count": 0,
"delivery_receipt_accepted_count": 0,
"break_glass_fallback_accepted_count": 0,
"maintenance_window_accepted_count": 0,
"rollback_owner_accepted_count": 0,
"postcheck_evidence_accepted_count": 0,
"dedup_or_fingerprint_accepted_count": 0,
"no_false_green_accepted_count": 0,
"direct_bot_api_migration_authorized_count": 0,
"workflow_modification_authorized_count": 0,
"script_modification_authorized_count": 0,
"api_sender_refactor_authorized_count": 0,
"telegram_send_authorized_count": 0,
"bot_api_call_authorized_count": 0,
"workflow_dispatch_authorized_count": 0,
"production_deploy_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_response_mark_received_authorized": False,
"owner_response_mark_accepted_authorized": False,
"direct_bot_api_migration_authorized": False,
"workflow_modification_authorized": False,
"script_modification_authorized": False,
"api_sender_refactor_authorized": False,
"telegram_send_authorized": False,
"bot_api_call_authorized": False,
"workflow_dispatch_authorized": False,
"production_deploy_authorized": False,
"secret_value_collection_allowed": False,
"raw_payload_storage_allowed": False,
"production_write_authorized": False,
"action_buttons_allowed": False,
"not_authorization": True,
},
"acceptance_candidates": candidates,
"operator_interpretation": [
"此帳本只是 reviewer 驗收模板owner response received / accepted 仍維持 0。",
"CD success、route 200、UI 可見或 Telegram sent 狀態本身都不是 delivery receipt。",
"workflow、script 與 API sender 收斂仍需獨立 runtime approval 與 change evidence。",
],
}
def validate(root: Path) -> None:
report = build_report(root)
summary = report["summary"]
if summary["acceptance_candidate_count"] != summary["source_request_draft_count"]:
raise SystemExit("BLOCKED telegram egress owner response acceptance: candidate/request count mismatch")
if summary["acceptance_candidate_count"] != summary["source_migration_candidate_count"]:
raise SystemExit("BLOCKED telegram egress owner response acceptance: candidate/migration count mismatch")
if summary["runtime_gate_count"] != 0:
raise SystemExit("BLOCKED telegram egress owner response acceptance: runtime gate must stay 0")
def main() -> None:
parser = argparse.ArgumentParser(description="建立 Telegram 通知出口 owner response 驗收帳本")
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_RESPONSE_ACCEPTANCE_OK "
f"candidates={report['summary']['acceptance_candidate_count']} "
f"workflow={report['summary']['workflow_acceptance_candidate_count']} "
f"ops={report['summary']['ops_script_acceptance_candidate_count']} "
f"api={report['summary']['api_direct_acceptance_candidate_count']} "
f"accepted={report['summary']['owner_response_accepted_count']} "
f"runtime_gate={report['summary']['runtime_gate_count']}",
file=sys.stderr,
)
if __name__ == "__main__":
main()