From 892e68dccac338a34b5a222963878fc5d3856dad Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 13 May 2026 00:33:31 +0800 Subject: [PATCH] docs(security): add source control reconcile plan [skip ci] --- docs/LOGBOOK.md | 24 ++ ...urce_control_reconcile_plan_v1.schema.json | 130 ++++++++++ ...WOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md | 3 + ...ECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md | 4 +- .../GITEA-GITHUB-MIGRATION-INVENTORY.md | 14 +- ...SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md | 3 +- .../SECURITY-SUPPLY-CHAIN-PROGRESS.md | 9 +- .../SOURCE-CONTROL-MIGRATION-MATRIX.md | 3 +- .../security/SOURCE-CONTROL-RECONCILE-PLAN.md | 137 ++++++++++ ...pply-chain-contract-manifest.snapshot.json | 22 +- ...ource-control-reconcile-plan.snapshot.json | 176 +++++++++++++ .../security/source-control-reconcile-plan.py | 234 ++++++++++++++++++ 12 files changed, 746 insertions(+), 13 deletions(-) create mode 100644 docs/schemas/source_control_reconcile_plan_v1.schema.json create mode 100644 docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md create mode 100644 docs/security/source-control-reconcile-plan.snapshot.json create mode 100644 scripts/security/source-control-reconcile-plan.py diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index ea91d16a..7ec2af58 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,27 @@ +## 2026-05-12 | Source Control Draft Reconcile Plan 草案 + +**背景**:統帥批准繼續推進後,本輪先同步最新 `gitea/main`,納入另一個 AwoooP Session 的 `legacy mcp audit -> gateway timeline` 進度,避免雙 Session 分歧。同步後繼續沿用低摩擦原則,只針對 refs-blocked repo 產生草案,不執行同步。 + +**本次交付**: +- 新增 `scripts/security/source-control-reconcile-plan.py`,只讀既有 redacted snapshot,不呼叫遠端 Git,不 fetch、不 push、不改 remote。 +- 新增 `docs/schemas/source_control_reconcile_plan_v1.schema.json`。 +- 產出 `docs/security/source-control-reconcile-plan.snapshot.json` 與 `docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md`。 +- Draft plan 涵蓋 3 個 refs-blocked mapped repos:`wooo/awoooi`、`wooo/clawbot-v5`、`wooo/wooo-aiops`。 +- 更新 `SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST`,contract count 從 13 增至 14,新增 `source_control_reconcile_plan_v1`。 +- 更新 `SECURITY-SUPPLY-CHAIN-PROGRESS` 與 `AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST`,讓 AwoooP 可 mirror draft plan 但不得執行 refs sync。 + +**邊界**: +- Plan 狀態為 `draft_blocked`;authenticated / admin_export server-side inventory 尚未完成前,不可執行。 +- 未 push refs、未 force push、未刪 refs、未建立 GitHub repo、未改 visibility、未切 GitHub primary、未部署。 +- 人工批准未來也必須單一 repo 生效,不得批次套用到所有 repo。 + +**驗證**: +- `source-control-reconcile-plan.py` 產生 3 plans。 +- JSON / schema / snapshot parse 通過。 +- `scripts/security/*.py` 可編譯。 +- `git diff --check` 通過。 +- PR diff added lines 未命中本輪敏感 token / credential pattern。 + ## 2026-05-12 | Source Control Approval Board 低摩擦決策隊列 **背景**:統帥批准繼續推進後,下一步原本是 Gitea authenticated read-only inventory;但目前 `GITEA_READONLY_TOKEN` 未提供。本輪因此不使用可 push 的既有 Gitea remote credential 代替 read-only token,避免把 inventory 與寫入權限憑證混在一起。 diff --git a/docs/schemas/source_control_reconcile_plan_v1.schema.json b/docs/schemas/source_control_reconcile_plan_v1.schema.json new file mode 100644 index 00000000..0b582cbf --- /dev/null +++ b/docs/schemas/source_control_reconcile_plan_v1.schema.json @@ -0,0 +1,130 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:source-control-reconcile-plan-v1", + "title": "AWOOOI Source Control Reconcile Plan (v1)", + "description": "針對 Gitea -> GitHub refs-blocked repo 產生只讀、不可執行的 reconcile plan 草案。", + "type": "object", + "required": [ + "schema_version", + "status", + "date", + "default_mode", + "inventory_gate", + "plan_count", + "plans" + ], + "properties": { + "schema_version": { + "const": "source_control_reconcile_plan_v1" + }, + "status": { + "type": "string", + "enum": ["draft_blocked"] + }, + "date": { + "type": "string" + }, + "default_mode": { + "type": "string", + "enum": ["plan_only"] + }, + "inventory_gate": { + "type": "object", + "required": ["status", "reason", "required_before_execution"], + "properties": { + "status": { + "type": "string", + "enum": ["blocked", "ready"] + }, + "reason": { + "type": "string" + }, + "required_before_execution": { + "type": "array", + "items": {"type": "string"} + } + }, + "additionalProperties": false + }, + "plan_count": { + "type": "integer", + "minimum": 0 + }, + "plans": { + "type": "array", + "items": { + "type": "object", + "required": [ + "gitea_repo", + "github_repo", + "risk", + "source_status", + "divergence_summary", + "proposed_plan_steps", + "execution_gates", + "allowed_now", + "still_forbidden", + "evidence_refs", + "awooop_consumption" + ], + "properties": { + "gitea_repo": {"type": "string"}, + "github_repo": {"type": "string"}, + "risk": { + "type": "string", + "enum": ["LOW", "MEDIUM", "HIGH"] + }, + "source_status": {"type": "string"}, + "divergence_summary": { + "type": "object", + "required": [ + "gitea_branch_count", + "github_branch_count", + "gitea_tag_count", + "github_tag_count", + "gitea_main_sha", + "github_main_sha", + "blocking_reason" + ], + "properties": { + "gitea_branch_count": {"type": "integer"}, + "github_branch_count": {"type": "integer"}, + "gitea_tag_count": {"type": "integer"}, + "github_tag_count": {"type": "integer"}, + "gitea_main_sha": {"type": "string"}, + "github_main_sha": {"type": "string"}, + "blocking_reason": {"type": "string"} + }, + "additionalProperties": false + }, + "proposed_plan_steps": { + "type": "array", + "items": {"type": "string"} + }, + "execution_gates": { + "type": "array", + "items": {"type": "string"} + }, + "allowed_now": { + "type": "array", + "items": {"type": "string"} + }, + "still_forbidden": { + "type": "array", + "items": {"type": "string"} + }, + "evidence_refs": { + "type": "array", + "items": {"type": "string"} + }, + "awooop_consumption": { + "type": "string", + "enum": ["mirror_only", "approval_candidate"] + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md b/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md index 556301f7..3dd8d76f 100644 --- a/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md +++ b/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md @@ -34,6 +34,7 @@ AwoooP 初期不得直接啟動掃描、不得呼叫 Codex patch runner、不得 | `github_target_decision_v1` | GitHub target 建立與可見性決策草案 | Approval candidate、Migration target evidence | mirror-only | approval 前不得建立 repo、修改 visibility、同步 refs | | `github_target_repo_approval_package_v1` | GitHub target 逐 repo approval package | Approval queue、Migration target evidence | mirror-only | 低摩擦,只 gate 高風險執行 | | `source_control_approval_board_v1` | 逐 repo owner / visibility / canonical / refs 決策 board | Approval queue、PR reviewer handoff | approval-only | 只顯示決策隊列,不執行 board item | +| `source_control_reconcile_plan_v1` | refs-blocked repo draft reconcile plan | Approval candidate、migration reviewer handoff | approval-only | 只顯示草案,不 push refs、不切 primary | | `local_repo_canonical_probe_v1` | 本機 working tree lineage 比對 | Canonical decision evidence | mirror-only | 不自動合併、不自動建 repo、不刪除 | | `git_remote_refs_probe_v1` | 指定 repo remote refs read-only probe | Source readiness evidence | mirror-only | 不 fetch、不 push、不自動 mirror | | `approval_required_event_v1` | 上述事件的高風險 gate | Approval queue、Audit | approval-only | `blocked_until_approved=true` | @@ -83,6 +84,7 @@ AwoooP 初期不得直接啟動掃描、不得呼叫 Codex patch runner、不得 | `github_target_decision_v1.approval_required_count>0` | `approve_required` | 產生 approval candidate,但不執行 repo 建立或 visibility 修改 | | `github_target_repo_approval_package_v1.status=draft` | `observe` | 建立 approval queue draft,不阻擋 read-only evidence | | `source_control_approval_board_v1.pending_approval_count>0` | `approve_required` | 顯示逐 repo 決策隊列,不執行 repo 建立、visibility 修改、refs sync | +| `source_control_reconcile_plan_v1.status=draft_blocked` | `approve_required` | 只顯示 refs reconcile 草案與 gate,不執行 sync | | `local_repo_canonical_probe_v1.status=unrelated` | `approve_required` | 禁止自動合併,需人工 canonical 判定 | | `git_remote_refs_probe_v1.status=ok` | `observe` | 可作 source evidence,但仍需 GitHub target 與 approval | | `security_rollout_policy_v1.enforcement_level=mirror_only` | `observe` | 只顯示 policy,不阻擋既有流程 | @@ -124,6 +126,7 @@ AwoooP 初期不得直接啟動掃描、不得呼叫 Codex patch runner、不得 | GitHub target 決策 snapshot | `docs/security/github-target-decision.snapshot.json` / `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` | | GitHub target repo approval package | `docs/security/github-target-repo-approval-package.snapshot.json` / `docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md` | | Source Control approval board | `docs/security/source-control-approval-board.snapshot.json` / `docs/security/SOURCE-CONTROL-APPROVAL-BOARD.md` | +| Source Control draft reconcile plan | `docs/security/source-control-reconcile-plan.snapshot.json` / `docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md` | | 本機 repo canonical lineage snapshot | `docs/security/local-repo-canonical-ewoooc-momo.snapshot.json` / `docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md` | | Internal 110 refs snapshot | `docs/security/git-remote-refs-bitan-tsenyang.snapshot.json` / `docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md` | | wooo-infra-config refs snapshot | `docs/security/git-remote-refs-wooo-infra-config.snapshot.json` / `docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md` | diff --git a/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md b/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md index e64b772b..d6d3a5b6 100644 --- a/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md +++ b/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md @@ -521,7 +521,7 @@ Console 初期不提供高風險 action button。 第一版 observe-only host mapping 已建立於 `docs/security/DEV-HOSTS-112-111-168-OBSERVE-ONLY-MAPPING.md`。第一版 Codex patch-only handoff prompt 已建立於 `docs/security/CODEX-PATCH-ONLY-HANDOFF-PROMPT.md`。 -2026-05-12 更新:已新增 `scripts/security/source-control-migration-inventory.py`,並產出 `docs/security/gitea-github-awoooi-inventory.snapshot.json`。目前 `source_control_migration_event_v1.status=blocked`,因 Gitea heads 115、GitHub heads 2、Gitea tags 2、GitHub tags 0,且 `main` SHA 不一致。AwoooP Session 應將此視為 supply-chain evidence,不得視為可切 GitHub primary。 +2026-05-12 更新:已新增 `scripts/security/source-control-migration-inventory.py`,並產出 `docs/security/gitea-github-awoooi-inventory.snapshot.json`。目前 `source_control_migration_event_v1.status=blocked`,因 Gitea heads 117、GitHub heads 2、Gitea tags 2、GitHub tags 0,且 `main` SHA 不一致。AwoooP Session 應將此視為 supply-chain evidence,不得視為可切 GitHub primary。 2026-05-12 追加:已新增 `scripts/security/gitea-repo-inventory.py`、`docs/schemas/gitea_repo_inventory_v1.schema.json`,並產出 `docs/security/gitea-repo-inventory.snapshot.json`。目前 `gitea_repo_inventory_v1.status=partial`,因未提供 token 時只能取得 public-only `wooo/awoooi` 與 `wooo/ewoooc`;完整全量仍需只讀 token 或管理匯出。 @@ -537,6 +537,8 @@ Console 初期不提供高風險 action button。 2026-05-12 refs diff 追加:已新增 `docs/security/source-control-clawbot-v5.snapshot.json`、`docs/security/source-control-wooo-aiops.snapshot.json`。`wooo/clawbot-v5` 與 `wooo/wooo-aiops` 目前都為 `source_control_migration_event_v1.status=blocked`,不得視為 GitHub primary ready。 +2026-05-12 draft reconcile plan 追加:已新增 `scripts/security/source-control-reconcile-plan.py`、`docs/schemas/source_control_reconcile_plan_v1.schema.json`,並產出 `docs/security/source-control-reconcile-plan.snapshot.json` 與 `docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md`。此 plan 只涵蓋 `awoooi`、`clawbot-v5`、`wooo-aiops` 三個 refs-blocked mapped repos,狀態為 `draft_blocked`;AwoooP 可 mirror 成 approval candidate,但不得 push refs、force push、刪 refs、切 GitHub primary。 + 2026-05-12 public search / canonical 追加:Gitea public search 在未提供 token 時可見 `wooo/awoooi`、`wooo/ewoooc`。已新增 `docs/security/SOURCE-CONTROL-CANONICAL-DECISION-TABLE.md`,其中 `wooo/ewoooc`、`root/momo-pro-system`、`momo-pro-system`、`momo_pro_system` 仍需人工判定 canonical 關係,不得自動合併。 2026-05-12 GitHub target probe 追加:已新增 `scripts/security/github-target-probe.py`、`docs/schemas/github_target_probe_v1.schema.json` 與 `docs/security/github-target-probe.snapshot.json`。8 個候選中 5 個可讀,`owenhytsai/ewoooc`、`owenhytsai/bitan-pharmacy`、`owenhytsai/tsenyang-website` 為 `not_found_or_private`。 diff --git a/docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md b/docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md index f736ffd1..4e2e5d92 100644 --- a/docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md +++ b/docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md @@ -23,6 +23,7 @@ | wooo-infra-config refs probe | `docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md` / `docs/security/git-remote-refs-wooo-infra-config.snapshot.json` | | GitHub target 決策表 | `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` / `docs/security/github-target-decision.snapshot.json` | | GitHub target repo-by-repo approval package | `docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md` / `docs/security/github-target-repo-approval-package.snapshot.json` | +| Source Control draft reconcile plan | `docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md` / `docs/security/source-control-reconcile-plan.snapshot.json` | | Source Control 遷移矩陣 | `docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md` | | Canonical repo 判定表 | `docs/security/SOURCE-CONTROL-CANONICAL-DECISION-TABLE.md` | @@ -33,7 +34,7 @@ 第二輪只讀盤點顯示,至少目前工作中的 `awoooi` repo 存在以下差異: - GitHub `origin` 與 Gitea `gitea` 的 `main` SHA 不一致。 -- Gitea 有大量 `drift/adopt-*` 分支,GitHub 沒有;截至 2026-05-12,Gitea heads 為 115 條,GitHub heads 為 2 條。 +- Gitea 有大量 `drift/adopt-*` 分支,GitHub 沒有;截至 2026-05-12 最新 snapshot,Gitea heads 為 117 條,GitHub heads 為 2 條。 - Gitea 有 release tags,GitHub 目前查不到 tags。 - 本機 `gitea` remote URL 內嵌憑證,這是 credential hygiene 風險;不得寫入文件、不得複製到 GitHub,後續需移除並輪替。 - Gitea `wooo` user endpoint 在未提供 token 時可見 `wooo/awoooi` 與 `wooo/ewoooc`,目前 `gitea_repo_inventory_v1.status=partial`。 @@ -55,7 +56,7 @@ |------|------| | `git remote -v` | 已確認 `origin` 指向 GitHub,`gitea` 指向本地 Gitea;未在文件中保存憑證 | | GitHub heads | 2 條 | -| Gitea heads | 115 條 | +| Gitea heads | 117 條 | | GitHub tags | 0 條 | | Gitea tag refs | 4 條 raw refs,實際 tag 為 `v7.2.0`、`v7.3.0` | | Gitea org API | 未認證查詢 `http://192.168.0.110:3001/api/v1/orgs/wooo/repos` 回 404,保留為 endpoint 判定 evidence | @@ -72,6 +73,7 @@ | wooo-infra-config refs 工具 | `python3 scripts/security/git-remote-refs-probe.py --group-name wooo-infra-config-remotes --repo wooo-infra-config-gitea=/Users/ogt/wooo-infra-config=gitea --repo wooo-infra-config-github=/Users/ogt/wooo-infra-config=origin --output-json docs/security/git-remote-refs-wooo-infra-config.snapshot.json --output-md docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md` | | GitHub target 決策 snapshot | `docs/security/github-target-decision.snapshot.json`,依前述 read-only evidence 人工彙整,非執行工具,不授權 repo 建立或 visibility 修改 | | GitHub target repo-by-repo approval snapshot | `docs/security/github-target-repo-approval-package.snapshot.json`,逐 repo 拆分 approval path,不授權執行 | +| Source Control draft reconcile plan | `docs/security/source-control-reconcile-plan.snapshot.json`,只產生 `draft_blocked` 草案,不授權 refs sync | ## 1.1 Gitea repo list snapshot @@ -142,7 +144,7 @@ GitHub target repo-by-repo approval package 已建立於 `docs/security/GITHUB-T | 欄位 | Gitea | GitHub | 狀態 | |------|-------|--------|------| | Repo | `wooo/awoooi` | `owenhytsai/awoooi` | 已有對應 | -| `main` | `a18e2f9c3f403050d0fb7476bf6fdb860225731a` | `202071f7a8724d5e8c29de441c3f380575a0ea94` | 不一致,阻塞主控切換 | +| `main` | `0bc187877884f6fa6fe87a03dab99e9c6622fd42` | `202071f7a8724d5e8c29de441c3f380575a0ea94` | 不一致,阻塞主控切換 | | `release/v1.0` | `d15fb7d9f4bac86873d5c16b9c17c527b8f38bef` | `d15fb7d9f4bac86873d5c16b9c17c527b8f38bef` | 一致 | | `dev` | `25889d4b8edcb83b6ec707c5eef3c21ae5d432b0` | 無 | GitHub 缺分支 | | `drift/adopt-*` | 多條 | 無 | GitHub 缺分支 | @@ -193,11 +195,11 @@ GitHub target repo-by-repo approval package 已建立於 `docs/security/GITHUB-T "schema_version": "source_control_migration_event_v1", "gitea_repo": "wooo/awoooi", "github_repo": "owenhytsai/awoooi", - "branch_count_gitea": 115, + "branch_count_gitea": 117, "branch_count_github": 2, "tag_count_gitea": 2, "tag_count_github": 0, - "latest_sha_gitea": "a18e2f9c3f403050d0fb7476bf6fdb860225731a", + "latest_sha_gitea": "0bc187877884f6fa6fe87a03dab99e9c6622fd42", "latest_sha_github": "202071f7a8724d5e8c29de441c3f380575a0ea94", "workflows_mapped": false, "webhooks_mapped": false, @@ -213,7 +215,7 @@ GitHub target repo-by-repo approval package 已建立於 `docs/security/GITHUB-T 1. 依 `docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md` 取得 Gitea 只讀 repo inventory 批准,不使用寫入 token。 2. 依 `github_target_decision_v1` 對需要人工批准的 target 做 owner / visibility / canonical 決策。 -3. 產生每個 repo 的 heads/tags SHA diff。 +3. 依 `docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md` 產生每個 repo 的 heads/tags SHA diff;仍不 push refs。 4. 標記「可 mirror」、「需人工判斷」、「需封存」、「不可搬」。 5. 產出 GitHub primary ADR,定義切換 gate 與 rollback。 6. 將 `source_control_migration_event_v1`、`gitea_repo_inventory_v1`、`local_git_remote_inventory_v1` mirror 到 AwoooP,初期只作為 evidence。 diff --git a/docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md b/docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md index f1847bb7..a48d0c26 100644 --- a/docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md +++ b/docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md @@ -11,7 +11,7 @@ ## 0. 核心結論 -目前 Security Supply Chain 已有 13 個主要契約可交給 AwoooP 消費。Manifest 的用途是把分散的 schema、snapshot、人讀文件、允許動作與禁止動作收成一份入口,避免不同 Session 各自解讀。 +目前 Security Supply Chain 已有 14 個主要契約可交給 AwoooP 消費。Manifest 的用途是把分散的 schema、snapshot、人讀文件、允許動作與禁止動作收成一份入口,避免不同 Session 各自解讀。 初期預設仍是 `mirror_only`。Manifest 不授權 runtime enforcement、不授權 GitHub/Gitea 主控切換、不授權 repo 建立或 refs sync。 @@ -29,6 +29,7 @@ | `github_target_decision_v1` | mirror-only | GitHub target 決策 | `github-target-decision.snapshot.json` | | `github_target_repo_approval_package_v1` | approval-only | 逐 repo approval queue draft | `github-target-repo-approval-package.snapshot.json` | | `source_control_approval_board_v1` | approval-only | 逐 repo owner / visibility / canonical / refs 決策 board | `source-control-approval-board.snapshot.json` | +| `source_control_reconcile_plan_v1` | approval-only | refs-blocked repo 的 draft reconcile plan | `source-control-reconcile-plan.snapshot.json` | | `local_repo_canonical_probe_v1` | mirror-only | momo/ewoooc lineage evidence | `local-repo-canonical-ewoooc-momo.snapshot.json` | | `git_remote_refs_probe_v1` | mirror-only | 110 / GitHub remote refs readiness | `bitan-tsenyang`、`wooo-infra-config` | | `approval_required_event_v1` | approval-only | 高風險 / 敏感邊界 approval | `gitea-readonly-inventory-approval.snapshot.json` | diff --git a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md index 1257f064..dbb3ae07 100644 --- a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md +++ b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md @@ -4,7 +4,7 @@ |------|------| | 日期 | 2026-05-12 | | 狀態 | S0/S1 read-only evidence 建置中 | -| 本階段完成 | Security Supply Chain contract manifest + Source Control Approval Board | +| 本階段完成 | Security Supply Chain contract manifest + Source Control Approval Board + Draft Reconcile Plan | | 原則 | 低摩擦分階段;文件、schema、read-only evidence 優先;不做 runtime enforcement、不切 primary | ## 0. 本階段完成後整體進度 @@ -16,8 +16,9 @@ | S1.0 Gitea 全量 inventory approval | 完成草案 | 已建立 read-only token / admin export approval package | 統帥或 repo owner 批准 | | S1.1 GitHub target 決策 | 完成草案 | 8 個 target 候選,7 個需人工批准;3 個 `not_found_or_private` 不得自動建立 | owner / visibility / canonical approval | | S1.2 GitHub target 逐 repo approval | 完成草案 | 7 個 approval-required targets 已拆成逐 repo pending package,並彙整成 8-item approval board | 低摩擦逐項批准 | +| S1.2a refs reconcile plan | 完成草案 | `awoooi`、`clawbot-v5`、`wooo-aiops` 已產生 draft plan;狀態仍為 `draft_blocked` | authenticated inventory + branch/tag diff + single-repo approval | | S1.3 低摩擦 rollout policy | 完成草案 | observe-first / mirror-only matrix 已建立 | AwoooP read-only policy 消費 | -| S1.4 Contract manifest | 完成草案 | 13 個主要 contract 已集中成 manifest | AwoooP mirror-only contract registry | +| S1.4 Contract manifest | 完成草案 | 14 個主要 contract 已集中成 manifest | AwoooP mirror-only contract registry | | S2 AwoooP mirror-only | 可交接 | `AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md` 已列出可消費事件與禁止動作 | AwoooP 主線建立只讀入口 | | S3 approval gate | 未開始 | 已定義哪些動作要進 approval | 不得繞過人工批准 | | S4 migration execution | 未開始 | GitHub primary 長期方向已確認,但 refs / tags / workflow / secret 名稱尚未全量驗證 | SHA/tag/workflow parity 與 rollback ADR | @@ -42,6 +43,8 @@ | GitHub target repo approval JSON | `docs/security/github-target-repo-approval-package.snapshot.json` | | Source Control approval board | `docs/security/SOURCE-CONTROL-APPROVAL-BOARD.md` | | Source Control approval board JSON | `docs/security/source-control-approval-board.snapshot.json` | +| Source Control draft reconcile plan | `docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md` | +| Source Control draft reconcile plan JSON | `docs/security/source-control-reconcile-plan.snapshot.json` | | 低摩擦 rollout policy | `docs/security/SECURITY-LOW-FRICTION-ROLLOUT-POLICY.md` | | 低摩擦 rollout policy JSON | `docs/security/security-rollout-policy.snapshot.json` | | Security Supply Chain contract manifest | `docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md` | @@ -68,7 +71,7 @@ 1. 等待 Gitea read-only inventory approval 被批准後,再用只讀 token 或管理匯出補 private/internal server-side 全量 repo list。 2. 依 `SOURCE-CONTROL-APPROVAL-BOARD.md` 對 7 個 `approval_required=true` 的 GitHub target 做 owner / visibility / canonical 決策。 -3. 對 `awoooi`、`clawbot-v5`、`wooo-aiops` 做 refs / tags reconcile 計畫。 +3. 依 `SOURCE-CONTROL-RECONCILE-PLAN.md` 對 `awoooi`、`clawbot-v5`、`wooo-aiops` 補 branch-by-branch / tag-by-tag diff;仍不得 push refs。 4. 對 `ewoooc` / `momo-pro-system` 完成 server-side canonical 判定。 5. AwoooP 主線只建立 mirror-only / read-only policy 入口,不新增執行按鈕。 6. AwoooP 主線消費 `security_rollout_policy_v1` 時,只做 read-only policy,不做 runtime blocking。 diff --git a/docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md b/docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md index 27082468..0014ca0a 100644 --- a/docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md +++ b/docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md @@ -10,6 +10,7 @@ | GitHub target probe | `docs/security/GITHUB-TARGET-PROBE-SNAPSHOT.md` | | GitHub target 決策 | `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` | | GitHub target repo approval | `docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md` | +| Source Control draft reconcile plan | `docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md` | | 原則 | 只做盤點與分類,不做同步、不刪除、不切 primary | ## 0. 核心結論 @@ -134,7 +135,7 @@ Repo-by-repo approval package 已建立,7 個 approval-required targets 皆為 | Repo | Status | Gitea branches | GitHub branches | Gitea tags | GitHub tags | Gitea main | GitHub main | Evidence | |------|--------|----------------|-----------------|------------|-------------|------------|-------------|----------| -| `wooo/awoooi` -> `owenhytsai/awoooi` | `blocked` | `115` | `2` | `2` | `0` | `a18e2f9c3f403050d0fb7476bf6fdb860225731a` | `202071f7a8724d5e8c29de441c3f380575a0ea94` | `docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md` | +| `wooo/awoooi` -> `owenhytsai/awoooi` | `blocked` | `117` | `2` | `2` | `0` | `0bc187877884f6fa6fe87a03dab99e9c6622fd42` | `202071f7a8724d5e8c29de441c3f380575a0ea94` | `docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md` | | `wooo/clawbot-v5` -> `owenhytsai/clawbot-v5` | `blocked` | `1` | `1` | `1` | `0` | `22074fbe4d6ec6c11c86f76139eea55756d1d160` | `7a769de46450087f9d6a8ef0d2ac23ed15565d2c` | `docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md` | | `wooo/wooo-aiops` -> `owenhytsai/wooo-aiops` | `blocked` | `2` | `3` | `0` | `19` | `507384a2e1943f4183942bf17d7b52e223067853` | `7c7aa109d93da6d75d687d6ee5131151afee37e8` | `docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md` | diff --git a/docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md b/docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md new file mode 100644 index 00000000..5d4c645e --- /dev/null +++ b/docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md @@ -0,0 +1,137 @@ +# Source Control Draft Reconcile Plan + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-12 | +| 狀態 | `draft_blocked` | +| 預設模式 | `plan_only` | +| inventory gate | `blocked` | +| gate 原因 | Gitea authenticated / admin_export server-side inventory 尚未完成;本 plan 只能作草案,不可執行 refs sync。 | +| plan count | 3 | + +## 0. 核心結論 + +這份文件只是 refs reconcile 草案,不是同步腳本,也不授權任何 GitHub primary 切換。AwoooP 可以 mirror 成 approval candidate,但不得執行 board item 或呼叫任何 push / sync 工具。 + +## 1. Repo 差異摘要 + +| Repo | Risk | Gitea branches | GitHub branches | Gitea tags | GitHub tags | Gitea main | GitHub main | +|------|------|----------------|-----------------|------------|-------------|------------|-------------| +| `wooo/awoooi -> owenhytsai/awoooi` | `HIGH` | `117` | `2` | `2` | `0` | `0bc18787` | `202071f7` | +| `wooo/clawbot-v5 -> owenhytsai/clawbot-v5` | `MEDIUM` | `1` | `1` | `1` | `0` | `22074fbe` | `7a769de4` | +| `wooo/wooo-aiops -> owenhytsai/wooo-aiops` | `MEDIUM` | `2` | `3` | `0` | `19` | `507384a2` | `7c7aa109` | + +## 2. Draft Plan + +### wooo/awoooi -> owenhytsai/awoooi + +- 狀態:`blocked` +- 阻塞原因:branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致 +- 允許現在做: + - 更新 read-only evidence + - 更新 approval board + - 產生 draft reconcile plan + - 讓 AwoooP mirror plan 狀態 +- 草案步驟: + - 先確認目前 production deploy 真相來源與 deploy marker 流程,避免主控切換影響發版。 + - 針對 `wooo/awoooi` 與 `owenhytsai/awoooi` 產生 branch-by-branch diff 表。 + - 針對 `wooo/awoooi` 與 `owenhytsai/awoooi` 產生 tag-by-tag diff 表。 + - 標記每個 diff 的真相來源候選:Gitea、GitHub、人工指定或 deprecated。 + - 列出 workflow / webhook / runner / secret 名稱差異,只記名稱不記 value。 + - 產生 dry-run PR / ADR 草案,仍不 push refs。 +- 執行前 gate: + - Gitea authenticated 或 admin_export server-side repo inventory status=ok + - branch-by-branch SHA diff 已完成 + - tag-by-tag SHA diff 已完成 + - workflow / webhook / runner / secret 名稱 inventory 已完成 + - repo owner / visibility / branch protection / CODEOWNERS 已確認 + - rollback plan 與 GitHub primary ADR 已完成 + - 人工批准只針對單一 repo 生效,不得批次套用到所有 repo +- 仍然禁止: + - push refs + - force push + - delete refs + - create GitHub repo + - change repo visibility + - switch GitHub primary + - disable Gitea + - move secret values +- Evidence refs: + - `docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md` + +### wooo/clawbot-v5 -> owenhytsai/clawbot-v5 + +- 狀態:`blocked` +- 阻塞原因:branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致 +- 允許現在做: + - 更新 read-only evidence + - 更新 approval board + - 產生 draft reconcile plan + - 讓 AwoooP mirror plan 狀態 +- 草案步驟: + - 針對 `wooo/clawbot-v5` 與 `owenhytsai/clawbot-v5` 產生 branch-by-branch diff 表。 + - 針對 `wooo/clawbot-v5` 與 `owenhytsai/clawbot-v5` 產生 tag-by-tag diff 表。 + - 標記每個 diff 的真相來源候選:Gitea、GitHub、人工指定或 deprecated。 + - 列出 workflow / webhook / runner / secret 名稱差異,只記名稱不記 value。 + - 產生 dry-run PR / ADR 草案,仍不 push refs。 +- 執行前 gate: + - Gitea authenticated 或 admin_export server-side repo inventory status=ok + - branch-by-branch SHA diff 已完成 + - tag-by-tag SHA diff 已完成 + - workflow / webhook / runner / secret 名稱 inventory 已完成 + - repo owner / visibility / branch protection / CODEOWNERS 已確認 + - rollback plan 與 GitHub primary ADR 已完成 + - 人工批准只針對單一 repo 生效,不得批次套用到所有 repo +- 仍然禁止: + - push refs + - force push + - delete refs + - create GitHub repo + - change repo visibility + - switch GitHub primary + - disable Gitea + - move secret values +- Evidence refs: + - `docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md` + +### wooo/wooo-aiops -> owenhytsai/wooo-aiops + +- 狀態:`blocked` +- 阻塞原因:branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致 +- 允許現在做: + - 更新 read-only evidence + - 更新 approval board + - 產生 draft reconcile plan + - 讓 AwoooP mirror plan 狀態 +- 草案步驟: + - 針對 `wooo/wooo-aiops` 與 `owenhytsai/wooo-aiops` 產生 branch-by-branch diff 表。 + - 針對 `wooo/wooo-aiops` 與 `owenhytsai/wooo-aiops` 產生 tag-by-tag diff 表。 + - 標記每個 diff 的真相來源候選:Gitea、GitHub、人工指定或 deprecated。 + - 列出 workflow / webhook / runner / secret 名稱差異,只記名稱不記 value。 + - 產生 dry-run PR / ADR 草案,仍不 push refs。 +- 執行前 gate: + - Gitea authenticated 或 admin_export server-side repo inventory status=ok + - branch-by-branch SHA diff 已完成 + - tag-by-tag SHA diff 已完成 + - workflow / webhook / runner / secret 名稱 inventory 已完成 + - repo owner / visibility / branch protection / CODEOWNERS 已確認 + - rollback plan 與 GitHub primary ADR 已完成 + - 人工批准只針對單一 repo 生效,不得批次套用到所有 repo +- 仍然禁止: + - push refs + - force push + - delete refs + - create GitHub repo + - change repo visibility + - switch GitHub primary + - disable Gitea + - move secret values +- Evidence refs: + - `docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md` + +## 3. AwoooP 消費方式 + +1. 只 mirror `source_control_reconcile_plan_v1`。 +2. 只顯示 `draft_blocked` 與 blocking reason。 +3. 可產生 approval candidate,但不得自動批准。 +4. 不得新增 execution action button。 diff --git a/docs/security/security-supply-chain-contract-manifest.snapshot.json b/docs/security/security-supply-chain-contract-manifest.snapshot.json index 1ad3d515..214f1eb2 100644 --- a/docs/security/security-supply-chain-contract-manifest.snapshot.json +++ b/docs/security/security-supply-chain-contract-manifest.snapshot.json @@ -2,7 +2,7 @@ "schema_version": "security_supply_chain_contract_manifest_v1", "status": "draft", "default_enforcement_level": "mirror_only", - "contract_count": 13, + "contract_count": 14, "contracts": [ { "contract": "security_rollout_policy_v1", @@ -137,6 +137,26 @@ ], "notes": "彙整 8 個 target,其中 7 個 pending approval;authenticated inventory gate 仍 blocked。" }, + { + "contract": "source_control_reconcile_plan_v1", + "schema_path": "docs/schemas/source_control_reconcile_plan_v1.schema.json", + "snapshot_paths": ["docs/security/source-control-reconcile-plan.snapshot.json"], + "human_docs": ["docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md"], + "consumer": "AwoooP approval candidate / migration reviewer", + "consumption_mode": "approval_only", + "allowed_actions": [ + "mirror_draft_reconcile_plan", + "display_refs_blocking_reason", + "request_single_repo_approval" + ], + "forbidden_actions": [ + "execute_reconcile_plan", + "push_refs", + "force_push", + "switch_github_primary" + ], + "notes": "只針對 3 個 refs-blocked mapped repos 產生 draft plan;inventory gate 仍 blocked,不可執行。" + }, { "contract": "local_repo_canonical_probe_v1", "schema_path": "docs/schemas/local_repo_canonical_probe_v1.schema.json", diff --git a/docs/security/source-control-reconcile-plan.snapshot.json b/docs/security/source-control-reconcile-plan.snapshot.json new file mode 100644 index 00000000..ebb5b136 --- /dev/null +++ b/docs/security/source-control-reconcile-plan.snapshot.json @@ -0,0 +1,176 @@ +{ + "schema_version": "source_control_reconcile_plan_v1", + "status": "draft_blocked", + "date": "2026-05-12", + "default_mode": "plan_only", + "inventory_gate": { + "status": "blocked", + "reason": "Gitea authenticated / admin_export server-side inventory 尚未完成;本 plan 只能作草案,不可執行 refs sync。", + "required_before_execution": [ + "Gitea authenticated 或 admin_export server-side repo inventory status=ok", + "branch-by-branch SHA diff 已完成", + "tag-by-tag SHA diff 已完成", + "workflow / webhook / runner / secret 名稱 inventory 已完成", + "repo owner / visibility / branch protection / CODEOWNERS 已確認", + "rollback plan 與 GitHub primary ADR 已完成", + "人工批准只針對單一 repo 生效,不得批次套用到所有 repo" + ] + }, + "plan_count": 3, + "plans": [ + { + "gitea_repo": "wooo/awoooi", + "github_repo": "owenhytsai/awoooi", + "risk": "HIGH", + "source_status": "blocked", + "divergence_summary": { + "gitea_branch_count": 117, + "github_branch_count": 2, + "gitea_tag_count": 2, + "github_tag_count": 0, + "gitea_main_sha": "0bc187877884f6fa6fe87a03dab99e9c6622fd42", + "github_main_sha": "202071f7a8724d5e8c29de441c3f380575a0ea94", + "blocking_reason": "branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致" + }, + "proposed_plan_steps": [ + "先確認目前 production deploy 真相來源與 deploy marker 流程,避免主控切換影響發版。", + "針對 `wooo/awoooi` 與 `owenhytsai/awoooi` 產生 branch-by-branch diff 表。", + "針對 `wooo/awoooi` 與 `owenhytsai/awoooi` 產生 tag-by-tag diff 表。", + "標記每個 diff 的真相來源候選:Gitea、GitHub、人工指定或 deprecated。", + "列出 workflow / webhook / runner / secret 名稱差異,只記名稱不記 value。", + "產生 dry-run PR / ADR 草案,仍不 push refs。" + ], + "execution_gates": [ + "Gitea authenticated 或 admin_export server-side repo inventory status=ok", + "branch-by-branch SHA diff 已完成", + "tag-by-tag SHA diff 已完成", + "workflow / webhook / runner / secret 名稱 inventory 已完成", + "repo owner / visibility / branch protection / CODEOWNERS 已確認", + "rollback plan 與 GitHub primary ADR 已完成", + "人工批准只針對單一 repo 生效,不得批次套用到所有 repo" + ], + "allowed_now": [ + "更新 read-only evidence", + "更新 approval board", + "產生 draft reconcile plan", + "讓 AwoooP mirror plan 狀態" + ], + "still_forbidden": [ + "push refs", + "force push", + "delete refs", + "create GitHub repo", + "change repo visibility", + "switch GitHub primary", + "disable Gitea", + "move secret values" + ], + "evidence_refs": [ + "docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md" + ], + "awooop_consumption": "approval_candidate" + }, + { + "gitea_repo": "wooo/clawbot-v5", + "github_repo": "owenhytsai/clawbot-v5", + "risk": "MEDIUM", + "source_status": "blocked", + "divergence_summary": { + "gitea_branch_count": 1, + "github_branch_count": 1, + "gitea_tag_count": 1, + "github_tag_count": 0, + "gitea_main_sha": "22074fbe4d6ec6c11c86f76139eea55756d1d160", + "github_main_sha": "7a769de46450087f9d6a8ef0d2ac23ed15565d2c", + "blocking_reason": "branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致" + }, + "proposed_plan_steps": [ + "針對 `wooo/clawbot-v5` 與 `owenhytsai/clawbot-v5` 產生 branch-by-branch diff 表。", + "針對 `wooo/clawbot-v5` 與 `owenhytsai/clawbot-v5` 產生 tag-by-tag diff 表。", + "標記每個 diff 的真相來源候選:Gitea、GitHub、人工指定或 deprecated。", + "列出 workflow / webhook / runner / secret 名稱差異,只記名稱不記 value。", + "產生 dry-run PR / ADR 草案,仍不 push refs。" + ], + "execution_gates": [ + "Gitea authenticated 或 admin_export server-side repo inventory status=ok", + "branch-by-branch SHA diff 已完成", + "tag-by-tag SHA diff 已完成", + "workflow / webhook / runner / secret 名稱 inventory 已完成", + "repo owner / visibility / branch protection / CODEOWNERS 已確認", + "rollback plan 與 GitHub primary ADR 已完成", + "人工批准只針對單一 repo 生效,不得批次套用到所有 repo" + ], + "allowed_now": [ + "更新 read-only evidence", + "更新 approval board", + "產生 draft reconcile plan", + "讓 AwoooP mirror plan 狀態" + ], + "still_forbidden": [ + "push refs", + "force push", + "delete refs", + "create GitHub repo", + "change repo visibility", + "switch GitHub primary", + "disable Gitea", + "move secret values" + ], + "evidence_refs": [ + "docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md" + ], + "awooop_consumption": "approval_candidate" + }, + { + "gitea_repo": "wooo/wooo-aiops", + "github_repo": "owenhytsai/wooo-aiops", + "risk": "MEDIUM", + "source_status": "blocked", + "divergence_summary": { + "gitea_branch_count": 2, + "github_branch_count": 3, + "gitea_tag_count": 0, + "github_tag_count": 19, + "gitea_main_sha": "507384a2e1943f4183942bf17d7b52e223067853", + "github_main_sha": "7c7aa109d93da6d75d687d6ee5131151afee37e8", + "blocking_reason": "branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致" + }, + "proposed_plan_steps": [ + "針對 `wooo/wooo-aiops` 與 `owenhytsai/wooo-aiops` 產生 branch-by-branch diff 表。", + "針對 `wooo/wooo-aiops` 與 `owenhytsai/wooo-aiops` 產生 tag-by-tag diff 表。", + "標記每個 diff 的真相來源候選:Gitea、GitHub、人工指定或 deprecated。", + "列出 workflow / webhook / runner / secret 名稱差異,只記名稱不記 value。", + "產生 dry-run PR / ADR 草案,仍不 push refs。" + ], + "execution_gates": [ + "Gitea authenticated 或 admin_export server-side repo inventory status=ok", + "branch-by-branch SHA diff 已完成", + "tag-by-tag SHA diff 已完成", + "workflow / webhook / runner / secret 名稱 inventory 已完成", + "repo owner / visibility / branch protection / CODEOWNERS 已確認", + "rollback plan 與 GitHub primary ADR 已完成", + "人工批准只針對單一 repo 生效,不得批次套用到所有 repo" + ], + "allowed_now": [ + "更新 read-only evidence", + "更新 approval board", + "產生 draft reconcile plan", + "讓 AwoooP mirror plan 狀態" + ], + "still_forbidden": [ + "push refs", + "force push", + "delete refs", + "create GitHub repo", + "change repo visibility", + "switch GitHub primary", + "disable Gitea", + "move secret values" + ], + "evidence_refs": [ + "docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md" + ], + "awooop_consumption": "approval_candidate" + } + ] +} diff --git a/scripts/security/source-control-reconcile-plan.py b/scripts/security/source-control-reconcile-plan.py new file mode 100644 index 00000000..816119e7 --- /dev/null +++ b/scripts/security/source-control-reconcile-plan.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +"""產生 source-control refs reconcile plan 草案。 + +此工具只讀取既有 redacted snapshot,不呼叫遠端 Git,不 push、不 fetch、不改 remote。 +輸出用途是 review / AwoooP mirror / approval candidate,不是 execution plan runner。 +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + + +DEFAULT_STILL_FORBIDDEN = [ + "push refs", + "force push", + "delete refs", + "create GitHub repo", + "change repo visibility", + "switch GitHub primary", + "disable Gitea", + "move secret values", +] + +DEFAULT_EXECUTION_GATES = [ + "Gitea authenticated 或 admin_export server-side repo inventory status=ok", + "branch-by-branch SHA diff 已完成", + "tag-by-tag SHA diff 已完成", + "workflow / webhook / runner / secret 名稱 inventory 已完成", + "repo owner / visibility / branch protection / CODEOWNERS 已確認", + "rollback plan 與 GitHub primary ADR 已完成", + "人工批准只針對單一 repo 生效,不得批次套用到所有 repo", +] + + +def load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def risk_for_repo(gitea_repo: str) -> str: + if gitea_repo == "wooo/awoooi": + return "HIGH" + return "MEDIUM" + + +def proposed_steps(event: dict[str, Any]) -> list[str]: + gitea_repo = str(event.get("gitea_repo", "")) + github_repo = str(event.get("github_repo", "")) + steps = [ + f"針對 `{gitea_repo}` 與 `{github_repo}` 產生 branch-by-branch diff 表。", + f"針對 `{gitea_repo}` 與 `{github_repo}` 產生 tag-by-tag diff 表。", + "標記每個 diff 的真相來源候選:Gitea、GitHub、人工指定或 deprecated。", + "列出 workflow / webhook / runner / secret 名稱差異,只記名稱不記 value。", + "產生 dry-run PR / ADR 草案,仍不 push refs。", + ] + if gitea_repo == "wooo/awoooi": + steps.insert( + 0, + "先確認目前 production deploy 真相來源與 deploy marker 流程,避免主控切換影響發版。", + ) + return steps + + +def build_plan(args: argparse.Namespace) -> dict[str, Any]: + events = [load_json(Path(path)) for path in args.source_events] + gitea_inventory = load_json(Path(args.gitea_inventory)) + + inventory_ready = gitea_inventory.get("status") == "ok" + inventory_gate_status = "ready" if inventory_ready else "blocked" + inventory_reason = ( + "Gitea server-side inventory 已完成,可在人工批准後進入更細 diff。" + if inventory_ready + else "Gitea authenticated / admin_export server-side inventory 尚未完成;本 plan 只能作草案,不可執行 refs sync。" + ) + + plans: list[dict[str, Any]] = [] + for event in events: + gitea_repo = str(event.get("gitea_repo", "")) + github_repo = str(event.get("github_repo", "")) + plans.append( + { + "gitea_repo": gitea_repo, + "github_repo": github_repo, + "risk": risk_for_repo(gitea_repo), + "source_status": str(event.get("status", "")), + "divergence_summary": { + "gitea_branch_count": int(event.get("branch_count_gitea", 0)), + "github_branch_count": int(event.get("branch_count_github", 0)), + "gitea_tag_count": int(event.get("tag_count_gitea", 0)), + "github_tag_count": int(event.get("tag_count_github", 0)), + "gitea_main_sha": str(event.get("latest_sha_gitea", "")), + "github_main_sha": str(event.get("latest_sha_github", "")), + "blocking_reason": str(event.get("blocking_reason", "")), + }, + "proposed_plan_steps": proposed_steps(event), + "execution_gates": DEFAULT_EXECUTION_GATES, + "allowed_now": [ + "更新 read-only evidence", + "更新 approval board", + "產生 draft reconcile plan", + "讓 AwoooP mirror plan 狀態", + ], + "still_forbidden": DEFAULT_STILL_FORBIDDEN, + "evidence_refs": [str(value) for value in event.get("evidence_refs", [])], + "awooop_consumption": "approval_candidate", + } + ) + + return { + "schema_version": "source_control_reconcile_plan_v1", + "status": "draft_blocked", + "date": args.date, + "default_mode": "plan_only", + "inventory_gate": { + "status": inventory_gate_status, + "reason": inventory_reason, + "required_before_execution": DEFAULT_EXECUTION_GATES, + }, + "plan_count": len(plans), + "plans": plans, + } + + +def short_sha(value: str) -> str: + return value[:8] if value else "" + + +def write_markdown(plan: dict[str, Any], path: Path) -> None: + gate = plan["inventory_gate"] + lines = [ + "# Source Control Draft Reconcile Plan", + "", + "| 項目 | 內容 |", + "|------|------|", + f"| 日期 | {plan['date']} |", + f"| 狀態 | `{plan['status']}` |", + f"| 預設模式 | `{plan['default_mode']}` |", + f"| inventory gate | `{gate['status']}` |", + f"| gate 原因 | {gate['reason']} |", + f"| plan count | {plan['plan_count']} |", + "", + "## 0. 核心結論", + "", + "這份文件只是 refs reconcile 草案,不是同步腳本,也不授權任何 GitHub primary 切換。AwoooP 可以 mirror 成 approval candidate,但不得執行 board item 或呼叫任何 push / sync 工具。", + "", + "## 1. Repo 差異摘要", + "", + "| Repo | Risk | Gitea branches | GitHub branches | Gitea tags | GitHub tags | Gitea main | GitHub main |", + "|------|------|----------------|-----------------|------------|-------------|------------|-------------|", + ] + for item in plan["plans"]: + diff = item["divergence_summary"] + lines.append( + "| " + + " | ".join( + [ + f"`{item['gitea_repo']} -> {item['github_repo']}`", + f"`{item['risk']}`", + f"`{diff['gitea_branch_count']}`", + f"`{diff['github_branch_count']}`", + f"`{diff['gitea_tag_count']}`", + f"`{diff['github_tag_count']}`", + f"`{short_sha(diff['gitea_main_sha'])}`", + f"`{short_sha(diff['github_main_sha'])}`", + ] + ) + + " |" + ) + + lines.extend(["", "## 2. Draft Plan", ""]) + for item in plan["plans"]: + diff = item["divergence_summary"] + lines.extend( + [ + f"### {item['gitea_repo']} -> {item['github_repo']}", + "", + f"- 狀態:`{item['source_status']}`", + f"- 阻塞原因:{diff['blocking_reason']}", + "- 允許現在做:", + ] + ) + for value in item["allowed_now"]: + lines.append(f" - {value}") + lines.append("- 草案步驟:") + for value in item["proposed_plan_steps"]: + lines.append(f" - {value}") + lines.append("- 執行前 gate:") + for value in item["execution_gates"]: + lines.append(f" - {value}") + lines.append("- 仍然禁止:") + for value in item["still_forbidden"]: + lines.append(f" - {value}") + lines.append("- Evidence refs:") + for value in item["evidence_refs"]: + lines.append(f" - `{value}`") + lines.append("") + + lines.extend( + [ + "## 3. AwoooP 消費方式", + "", + "1. 只 mirror `source_control_reconcile_plan_v1`。", + "2. 只顯示 `draft_blocked` 與 blocking reason。", + "3. 可產生 approval candidate,但不得自動批准。", + "4. 不得新增 execution action button。", + "", + ] + ) + path.write_text("\n".join(lines), encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--date", required=True) + parser.add_argument("--gitea-inventory", default="docs/security/gitea-repo-inventory.snapshot.json") + parser.add_argument("--source-event", action="append", dest="source_events", required=True) + parser.add_argument("--output-json", required=True) + parser.add_argument("--output-md", required=True) + args = parser.parse_args() + + plan = build_plan(args) + Path(args.output_json).write_text( + json.dumps(plan, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + write_markdown(plan, Path(args.output_md)) + print(f"OK source-control reconcile plan count={plan['plan_count']}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())