From cd72808560dbd5acf28715c8bbc576036053e6a9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 12 May 2026 23:43:48 +0800 Subject: [PATCH] docs(security): add source control approval board [skip ci] --- docs/LOGBOOK.md | 24 ++ ...urce_control_approval_board_v1.schema.json | 134 +++++++++ ...WOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md | 3 + ...SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md | 3 +- .../SECURITY-SUPPLY-CHAIN-PROGRESS.md | 10 +- .../security/SOURCE-CONTROL-APPROVAL-BOARD.md | 186 ++++++++++++ ...pply-chain-contract-manifest.snapshot.json | 22 +- ...ource-control-approval-board.snapshot.json | 271 ++++++++++++++++++ .../security/source-control-approval-board.py | 258 +++++++++++++++++ 9 files changed, 905 insertions(+), 6 deletions(-) create mode 100644 docs/schemas/source_control_approval_board_v1.schema.json create mode 100644 docs/security/SOURCE-CONTROL-APPROVAL-BOARD.md create mode 100644 docs/security/source-control-approval-board.snapshot.json create mode 100644 scripts/security/source-control-approval-board.py diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index d1025cf4..c90c116d 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,27 @@ +## 2026-05-12 | Source Control Approval Board 低摩擦決策隊列 + +**背景**:統帥批准繼續推進後,下一步原本是 Gitea authenticated read-only inventory;但目前 `GITEA_READONLY_TOKEN` 未提供。本輪因此不使用可 push 的既有 Gitea remote credential 代替 read-only token,避免把 inventory 與寫入權限憑證混在一起。 + +**本次交付**: +- 新增 `scripts/security/source-control-approval-board.py`,只讀既有 redacted snapshot,不呼叫 Gitea/GitHub API,不需要 token。 +- 新增 `docs/schemas/source_control_approval_board_v1.schema.json`。 +- 產出 `docs/security/source-control-approval-board.snapshot.json` 與 `docs/security/SOURCE-CONTROL-APPROVAL-BOARD.md`。 +- Board 彙整 8 個 target,其中 7 個為 pending approval:`awoooi`、`clawbot-v5`、`wooo-aiops`、`wooo-infra-config`、`ewoooc`、`bitan-pharmacy`、`tsenyang-website`;`nexu-io/open-design` 維持 scope review。 +- 更新 `SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST`,contract count 從 12 增至 13,新增 `source_control_approval_board_v1`。 +- 更新 `SECURITY-SUPPLY-CHAIN-PROGRESS` 與 `AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST`,讓 AwoooP 可 mirror board 但不得執行 board item。 + +**邊界**: +- 未使用 Gitea write-capable remote credential 做 authenticated inventory。 +- 未建立 GitHub repo、未改 visibility、未同步 refs、未切 GitHub primary、未部署。 +- authenticated inventory gate 仍為 `blocked`,等待 read-only token 或 redacted admin export。 + +**驗證**: +- `source-control-approval-board.py` 產生 8 items,pending approval 7。 +- JSON / schema / snapshot parse 通過。 +- `scripts/security/*.py` 可編譯。 +- `git diff --check` 通過。 +- PR diff added lines 未命中本輪敏感 token / credential pattern。 + ## 2026-05-12 | Security Supply Chain PR #117 與 AwoooP 主線同步 **背景**:Security Supply Chain docs-only 分支完成首次推版後,另一個 AwoooP Session 已將 `feat(awooop): harden outbound truth chain mirror` 與 deploy marker 推入 `gitea/main`。為避免雙 Session 推進互相衝突,本輪先把最新 `gitea/main` 合入資安分支,再建立 review-only PR。 diff --git a/docs/schemas/source_control_approval_board_v1.schema.json b/docs/schemas/source_control_approval_board_v1.schema.json new file mode 100644 index 00000000..aa873776 --- /dev/null +++ b/docs/schemas/source_control_approval_board_v1.schema.json @@ -0,0 +1,134 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:source-control-approval-board-v1", + "title": "AWOOOI Source Control Approval Board (v1)", + "description": "把 Gitea -> GitHub 低摩擦遷移中的逐 repo owner / visibility / canonical / refs reconcile 決策整理成可供 AwoooP mirror 的只讀 board。", + "type": "object", + "required": [ + "schema_version", + "status", + "date", + "default_mode", + "authenticated_inventory_gate", + "item_count", + "pending_approval_count", + "board_items" + ], + "properties": { + "schema_version": { + "const": "source_control_approval_board_v1" + }, + "status": { + "type": "string", + "enum": ["draft"] + }, + "date": { + "type": "string" + }, + "default_mode": { + "type": "string", + "enum": ["mirror_only"] + }, + "authenticated_inventory_gate": { + "type": "object", + "required": [ + "status", + "reason", + "allowed_next_step", + "still_forbidden" + ], + "properties": { + "status": { + "type": "string", + "enum": ["blocked", "ready"] + }, + "reason": { + "type": "string" + }, + "allowed_next_step": { + "type": "array", + "items": {"type": "string"} + }, + "still_forbidden": { + "type": "array", + "items": {"type": "string"} + } + }, + "additionalProperties": false + }, + "item_count": { + "type": "integer", + "minimum": 0 + }, + "pending_approval_count": { + "type": "integer", + "minimum": 0 + }, + "board_items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "github_repo", + "source_key", + "lane", + "risk", + "probe_status", + "target_state", + "approval_status", + "required_decision", + "low_friction_next_step", + "blocked_until", + "allowed_after_approval", + "still_forbidden", + "evidence_refs", + "awooop_consumption" + ], + "properties": { + "github_repo": {"type": "string"}, + "source_key": {"type": "string"}, + "lane": { + "type": "string", + "enum": [ + "refs_reconcile", + "target_creation_or_access", + "internal_remote_purpose", + "scope_review" + ] + }, + "risk": { + "type": "string", + "enum": ["LOW", "MEDIUM", "HIGH"] + }, + "probe_status": {"type": "string"}, + "target_state": {"type": "string"}, + "approval_status": {"type": "string"}, + "required_decision": {"type": "string"}, + "low_friction_next_step": {"type": "string"}, + "blocked_until": { + "type": "array", + "items": {"type": "string"} + }, + "allowed_after_approval": { + "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", "scope_review_only"] + } + }, + "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 13d97b03..556301f7 100644 --- a/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md +++ b/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md @@ -33,6 +33,7 @@ AwoooP 初期不得直接啟動掃描、不得呼叫 Codex patch runner、不得 | `github_target_probe_v1` | 候選 GitHub repo read-only probe | Migration target evidence | mirror-only | `not_found_or_private` 不等同確認不存在 | | `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 | | `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` | @@ -81,6 +82,7 @@ AwoooP 初期不得直接啟動掃描、不得呼叫 Codex patch runner、不得 | `github_target_probe_v1.status=ok` 且有 `not_found_or_private` | `observe` | 補 GitHub target 決策,不自動建立 repo | | `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 | | `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,不阻擋既有流程 | @@ -121,6 +123,7 @@ AwoooP 初期不得直接啟動掃描、不得呼叫 Codex patch runner、不得 | GitHub target probe snapshot | `docs/security/github-target-probe.snapshot.json` / `docs/security/GITHUB-TARGET-PROBE-SNAPSHOT.md` | | 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` | | 本機 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/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md b/docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md index 9706ede8..f1847bb7 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 已有 12 個主要契約可交給 AwoooP 消費。Manifest 的用途是把分散的 schema、snapshot、人讀文件、允許動作與禁止動作收成一份入口,避免不同 Session 各自解讀。 +目前 Security Supply Chain 已有 13 個主要契約可交給 AwoooP 消費。Manifest 的用途是把分散的 schema、snapshot、人讀文件、允許動作與禁止動作收成一份入口,避免不同 Session 各自解讀。 初期預設仍是 `mirror_only`。Manifest 不授權 runtime enforcement、不授權 GitHub/Gitea 主控切換、不授權 repo 建立或 refs sync。 @@ -28,6 +28,7 @@ | `github_target_probe_v1` | mirror-only | GitHub target visibility | `github-target-probe.snapshot.json` | | `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` | | `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 4f48156f..1257f064 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 | +| 本階段完成 | Security Supply Chain contract manifest + Source Control Approval Board | | 原則 | 低摩擦分階段;文件、schema、read-only evidence 優先;不做 runtime enforcement、不切 primary | ## 0. 本階段完成後整體進度 @@ -15,9 +15,9 @@ | S1 source-control read-only inventory | 進行中 | 已有 Gitea/GitHub refs、Gitea public-only user repo list、本機 remote、GitHub target probe、canonical lineage、110 refs evidence | Gitea private/internal 全量 repo list | | 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 | 低摩擦逐項批准 | +| S1.2 GitHub target 逐 repo approval | 完成草案 | 7 個 approval-required targets 已拆成逐 repo pending package,並彙整成 8-item approval board | 低摩擦逐項批准 | | S1.3 低摩擦 rollout policy | 完成草案 | observe-first / mirror-only matrix 已建立 | AwoooP read-only policy 消費 | -| S1.4 Contract manifest | 完成草案 | 12 個主要 contract 已集中成 manifest | AwoooP mirror-only contract registry | +| S1.4 Contract manifest | 完成草案 | 13 個主要 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 | @@ -40,6 +40,8 @@ | GitHub target 決策 JSON | `docs/security/github-target-decision.snapshot.json` | | GitHub target repo approval package | `docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md` | | 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` | | 低摩擦 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` | @@ -65,7 +67,7 @@ ## 3. 下一階段建議 1. 等待 Gitea read-only inventory approval 被批准後,再用只讀 token 或管理匯出補 private/internal server-side 全量 repo list。 -2. 對 7 個 `approval_required=true` 的 GitHub target 做 owner / visibility / canonical 決策。 +2. 依 `SOURCE-CONTROL-APPROVAL-BOARD.md` 對 7 個 `approval_required=true` 的 GitHub target 做 owner / visibility / canonical 決策。 3. 對 `awoooi`、`clawbot-v5`、`wooo-aiops` 做 refs / tags reconcile 計畫。 4. 對 `ewoooc` / `momo-pro-system` 完成 server-side canonical 判定。 5. AwoooP 主線只建立 mirror-only / read-only policy 入口,不新增執行按鈕。 diff --git a/docs/security/SOURCE-CONTROL-APPROVAL-BOARD.md b/docs/security/SOURCE-CONTROL-APPROVAL-BOARD.md new file mode 100644 index 00000000..2cd875e4 --- /dev/null +++ b/docs/security/SOURCE-CONTROL-APPROVAL-BOARD.md @@ -0,0 +1,186 @@ +# Source Control Approval Board + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-12 | +| 狀態 | `draft` | +| 預設模式 | `mirror_only` | +| authenticated inventory gate | `blocked` | +| gate 原因 | GITEA_READONLY_TOKEN 未提供,且不使用可 push 的既有 remote credential 當 read-only token;server-side private/internal repo list 仍未完成。 | +| repo items | 8 | +| pending approval | 7 | + +## 0. 核心原則 + +本 board 只整理決策,不授權執行。AwoooP 可以 mirror 成 approval candidate,但不得建立 repo、修改 visibility、同步 refs、切 GitHub primary 或保存 credential value。 + +## 1. 逐 repo 決策隊列 + +| GitHub repo | Lane | Risk | Probe | Approval | 下一步 | +|-------------|------|------|-------|----------|--------| +| `owenhytsai/awoooi` | `refs_reconcile` | `HIGH` | `exists` | `pending` | 先產生 draft reconcile plan,不 push refs、不切 primary。 | +| `owenhytsai/clawbot-v5` | `refs_reconcile` | `MEDIUM` | `exists` | `pending` | 先產生 draft reconcile plan,不 push refs、不切 primary。 | +| `owenhytsai/wooo-aiops` | `refs_reconcile` | `MEDIUM` | `exists` | `pending` | 先產生 draft reconcile plan,不 push refs、不切 primary。 | +| `owenhytsai/wooo-infra-config` | `internal_remote_purpose` | `MEDIUM` | `exists` | `pending` | 先文件化用途與風險,不刪除 remote、不同步 refs。 | +| `owenhytsai/ewoooc` | `target_creation_or_access` | `HIGH` | `not_found_or_private` | `pending` | 先取得 owner / visibility 決策,不自動建立 repo。 | +| `owenhytsai/bitan-pharmacy` | `target_creation_or_access` | `MEDIUM` | `not_found_or_private` | `pending` | 先取得 owner / visibility 決策,不自動建立 repo。 | +| `owenhytsai/tsenyang-website` | `target_creation_or_access` | `MEDIUM` | `not_found_or_private` | `pending` | 先取得 owner / visibility 決策,不自動建立 repo。 | +| `nexu-io/open-design` | `scope_review` | `LOW` | `exists` | `not_required` | 只標記 scope review,不納入主控切換。 | + +## 2. 詳細阻塞點 + +### owenhytsai/awoooi + +- Source key:`wooo/awoooi` +- Required decision:決定 Gitea / GitHub refs 真相來源,並批准只產生 reconcile plan。 +- AwoooP consumption:`approval_candidate` +- Blocked until: + - Gitea server-side 全量 repo inventory status=ok + - branches/tags/workflows/webhooks/secrets 名稱 inventory 完成 + - 部署真相來源已決定 + - GitHub primary ADR 與 rollback plan 完成 +- Still forbidden: + - 直接 push refs + - 直接切 GitHub primary + - 直接停用 Gitea + - 搬 secret value +- Evidence refs: + - `docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md` + - `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` + - `docs/security/github-target-probe.snapshot.json` + +### owenhytsai/clawbot-v5 + +- Source key:`wooo/clawbot-v5` +- Required decision:決定 Gitea / GitHub refs 真相來源,並批准只產生 reconcile plan。 +- AwoooP consumption:`approval_candidate` +- Blocked until: + - Gitea/GitHub main SHA 對齊或人工指定真相來源 + - GitHub 缺 Gitea tag 的處理方式已決定 +- Still forbidden: + - 直接 push refs + - 直接切 primary + - 刪除任一端 repo +- Evidence refs: + - `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` + - `docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md` + - `docs/security/github-target-probe.snapshot.json` + +### owenhytsai/wooo-aiops + +- Source key:`wooo/wooo-aiops` +- Required decision:決定 Gitea / GitHub refs 真相來源,並批准只產生 reconcile plan。 +- AwoooP consumption:`approval_candidate` +- Blocked until: + - Gitea/GitHub main SHA 對齊或人工指定真相來源 + - GitHub-only branch 與 tags 的來源已釐清 +- Still forbidden: + - 直接 push refs + - 直接切 primary + - 刪除 GitHub-only refs +- Evidence refs: + - `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` + - `docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md` + - `docs/security/github-target-probe.snapshot.json` + +### owenhytsai/wooo-infra-config + +- Source key:`wooo/wooo-infra-config` +- Required decision:決定 110 internal remote 是 active source、legacy mirror 或應降級。 +- AwoooP consumption:`approval_candidate` +- Blocked until: + - 110 internal remote 用途已確認 + - 若 110 remote 為舊主控,已降級或移除 + - infra secrets 名稱 inventory 完成 +- Still forbidden: + - 直接刪除 remote + - 直接同步 refs + - 搬 infra secret value +- Evidence refs: + - `docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md` + - `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` + - `docs/security/github-target-probe.snapshot.json` + +### owenhytsai/ewoooc + +- Source key:`wooo/ewoooc / root/momo-pro-system / momo working trees` +- Required decision:決定 GitHub repo owner / visibility / 是否建立或授權既有 repo。 +- AwoooP consumption:`approval_candidate` +- Blocked until: + - ewoooc/momo-pro-system canonical 關係人工確認 + - server-side refs diff 完成 + - GitHub repo owner 與 visibility 決策完成 +- Still forbidden: + - 自動建立 mirror + - 自動合併 unrelated histories + - 刪除任一 momo/ewoooc working tree + - 切 GitHub primary +- Evidence refs: + - `docs/security/GITEA-PUBLIC-REPO-SEARCH-SNAPSHOT.md` + - `docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md` + - `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` + - `docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md` + - `docs/security/github-target-probe.snapshot.json` + +### owenhytsai/bitan-pharmacy + +- Source key:`bitan-pharmacy` +- Required decision:決定 GitHub repo owner / visibility / 是否建立或授權既有 repo。 +- AwoooP consumption:`approval_candidate` +- Blocked until: + - 確認 repo 是否仍 active + - GitHub repo owner 與 visibility 決策完成 +- Still forbidden: + - 自動建立 repo + - 自動 push refs + - 刪除 110 remote +- Evidence refs: + - `docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md` + - `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` + - `docs/security/github-target-probe.snapshot.json` + +### owenhytsai/tsenyang-website + +- Source key:`tsenyang-website` +- Required decision:決定 GitHub repo owner / visibility / 是否建立或授權既有 repo。 +- AwoooP consumption:`approval_candidate` +- Blocked until: + - 確認 repo 是否仍 active + - GitHub repo owner 與 visibility 決策完成 +- Still forbidden: + - 自動建立 repo + - 自動 push refs + - 刪除 110 remote +- Evidence refs: + - `docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md` + - `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` + - `docs/security/github-target-probe.snapshot.json` + +### nexu-io/open-design + +- Source key:`open-design` +- Required decision:決定此 repo 是否屬於 AWOOOI 資安供應鏈範圍。 +- AwoooP consumption:`scope_review_only` +- Blocked until: + - 確認是否屬於 AWOOOI 資安網範圍 +- Still forbidden: + - auto_execute + - sync_refs + - switch_primary +- Evidence refs: + - `docs/security/github-target-probe.snapshot.json` + +## 3. Gate 前允許做的事 + +1. 更新 read-only evidence。 +2. 更新 approval board / decision table。 +3. 寫 draft reconcile plan。 +4. 把 pending approval mirror 到 AwoooP。 + +## 4. Gate 前仍禁止 + +- 使用 write-capable credential 當作 read-only token +- 建立 GitHub repo +- 修改 repo visibility +- sync refs +- switch GitHub primary diff --git a/docs/security/security-supply-chain-contract-manifest.snapshot.json b/docs/security/security-supply-chain-contract-manifest.snapshot.json index 294de058..1ad3d515 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": 12, + "contract_count": 13, "contracts": [ { "contract": "security_rollout_policy_v1", @@ -117,6 +117,26 @@ "forbidden_actions": ["execute_approval_item", "push_refs", "change_visibility"], "notes": "7 個 pending packages,逐 repo 低摩擦批准。" }, + { + "contract": "source_control_approval_board_v1", + "schema_path": "docs/schemas/source_control_approval_board_v1.schema.json", + "snapshot_paths": ["docs/security/source-control-approval-board.snapshot.json"], + "human_docs": ["docs/security/SOURCE-CONTROL-APPROVAL-BOARD.md"], + "consumer": "AwoooP approval board / PR reviewer", + "consumption_mode": "approval_only", + "allowed_actions": [ + "mirror_repo_decision_board", + "display_pending_owner_visibility_canonical_decisions", + "request_human_approval" + ], + "forbidden_actions": [ + "execute_board_item", + "sync_refs", + "create_repo", + "switch_github_primary" + ], + "notes": "彙整 8 個 target,其中 7 個 pending approval;authenticated 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-approval-board.snapshot.json b/docs/security/source-control-approval-board.snapshot.json new file mode 100644 index 00000000..7d8186f7 --- /dev/null +++ b/docs/security/source-control-approval-board.snapshot.json @@ -0,0 +1,271 @@ +{ + "schema_version": "source_control_approval_board_v1", + "status": "draft", + "date": "2026-05-12", + "default_mode": "mirror_only", + "authenticated_inventory_gate": { + "status": "blocked", + "reason": "GITEA_READONLY_TOKEN 未提供,且不使用可 push 的既有 remote credential 當 read-only token;server-side private/internal repo list 仍未完成。", + "allowed_next_step": [ + "提供 read-only token 後重跑 gitea-repo-inventory", + "或提供 redacted admin export JSON", + "在 gate 前仍可維護 approval board 與 decision table" + ], + "still_forbidden": [ + "使用 write-capable credential 當作 read-only token", + "建立 GitHub repo", + "修改 repo visibility", + "sync refs", + "switch GitHub primary" + ] + }, + "item_count": 8, + "pending_approval_count": 7, + "board_items": [ + { + "github_repo": "owenhytsai/awoooi", + "source_key": "wooo/awoooi", + "lane": "refs_reconcile", + "risk": "HIGH", + "probe_status": "exists", + "target_state": "exists_refs_blocked", + "approval_status": "pending", + "required_decision": "決定 Gitea / GitHub refs 真相來源,並批准只產生 reconcile plan。", + "low_friction_next_step": "先產生 draft reconcile plan,不 push refs、不切 primary。", + "blocked_until": [ + "Gitea server-side 全量 repo inventory status=ok", + "branches/tags/workflows/webhooks/secrets 名稱 inventory 完成", + "部署真相來源已決定", + "GitHub primary ADR 與 rollback plan 完成" + ], + "allowed_after_approval": [ + "產生 refs reconcile plan", + "產生 draft migration PR 或 ADR", + "更新 migration matrix 與 evidence" + ], + "still_forbidden": [ + "直接 push refs", + "直接切 GitHub primary", + "直接停用 Gitea", + "搬 secret value" + ], + "evidence_refs": [ + "docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md", + "docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md", + "docs/security/github-target-probe.snapshot.json" + ], + "awooop_consumption": "approval_candidate" + }, + { + "github_repo": "owenhytsai/clawbot-v5", + "source_key": "wooo/clawbot-v5", + "lane": "refs_reconcile", + "risk": "MEDIUM", + "probe_status": "exists", + "target_state": "exists_refs_blocked", + "approval_status": "pending", + "required_decision": "決定 Gitea / GitHub refs 真相來源,並批准只產生 reconcile plan。", + "low_friction_next_step": "先產生 draft reconcile plan,不 push refs、不切 primary。", + "blocked_until": [ + "Gitea/GitHub main SHA 對齊或人工指定真相來源", + "GitHub 缺 Gitea tag 的處理方式已決定" + ], + "allowed_after_approval": [ + "產生 refs reconcile plan", + "更新 migration matrix" + ], + "still_forbidden": [ + "直接 push refs", + "直接切 primary", + "刪除任一端 repo" + ], + "evidence_refs": [ + "docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md", + "docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json" + ], + "awooop_consumption": "approval_candidate" + }, + { + "github_repo": "owenhytsai/wooo-aiops", + "source_key": "wooo/wooo-aiops", + "lane": "refs_reconcile", + "risk": "MEDIUM", + "probe_status": "exists", + "target_state": "exists_refs_blocked", + "approval_status": "pending", + "required_decision": "決定 Gitea / GitHub refs 真相來源,並批准只產生 reconcile plan。", + "low_friction_next_step": "先產生 draft reconcile plan,不 push refs、不切 primary。", + "blocked_until": [ + "Gitea/GitHub main SHA 對齊或人工指定真相來源", + "GitHub-only branch 與 tags 的來源已釐清" + ], + "allowed_after_approval": [ + "產生 refs reconcile plan", + "更新 migration matrix" + ], + "still_forbidden": [ + "直接 push refs", + "直接切 primary", + "刪除 GitHub-only refs" + ], + "evidence_refs": [ + "docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md", + "docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json" + ], + "awooop_consumption": "approval_candidate" + }, + { + "github_repo": "owenhytsai/wooo-infra-config", + "source_key": "wooo/wooo-infra-config", + "lane": "internal_remote_purpose", + "risk": "MEDIUM", + "probe_status": "exists", + "target_state": "exists_aligned", + "approval_status": "pending", + "required_decision": "決定 110 internal remote 是 active source、legacy mirror 或應降級。", + "low_friction_next_step": "先文件化用途與風險,不刪除 remote、不同步 refs。", + "blocked_until": [ + "110 internal remote 用途已確認", + "若 110 remote 為舊主控,已降級或移除", + "infra secrets 名稱 inventory 完成" + ], + "allowed_after_approval": [ + "標記 110 remote 為 mirror、legacy 或 active source", + "更新 canonical decision table" + ], + "still_forbidden": [ + "直接刪除 remote", + "直接同步 refs", + "搬 infra secret value" + ], + "evidence_refs": [ + "docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md", + "docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md", + "docs/security/github-target-probe.snapshot.json" + ], + "awooop_consumption": "approval_candidate" + }, + { + "github_repo": "owenhytsai/ewoooc", + "source_key": "wooo/ewoooc / root/momo-pro-system / momo working trees", + "lane": "target_creation_or_access", + "risk": "HIGH", + "probe_status": "not_found_or_private", + "target_state": "not_found_or_private", + "approval_status": "pending", + "required_decision": "決定 GitHub repo owner / visibility / 是否建立或授權既有 repo。", + "low_friction_next_step": "先取得 owner / visibility 決策,不自動建立 repo。", + "blocked_until": [ + "ewoooc/momo-pro-system canonical 關係人工確認", + "server-side refs diff 完成", + "GitHub repo owner 與 visibility 決策完成" + ], + "allowed_after_approval": [ + "決定建立 GitHub repo 或授權既有 private repo", + "產生 migration plan" + ], + "still_forbidden": [ + "自動建立 mirror", + "自動合併 unrelated histories", + "刪除任一 momo/ewoooc working tree", + "切 GitHub primary" + ], + "evidence_refs": [ + "docs/security/GITEA-PUBLIC-REPO-SEARCH-SNAPSHOT.md", + "docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md", + "docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md", + "docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json" + ], + "awooop_consumption": "approval_candidate" + }, + { + "github_repo": "owenhytsai/bitan-pharmacy", + "source_key": "bitan-pharmacy", + "lane": "target_creation_or_access", + "risk": "MEDIUM", + "probe_status": "not_found_or_private", + "target_state": "not_found_or_private", + "approval_status": "pending", + "required_decision": "決定 GitHub repo owner / visibility / 是否建立或授權既有 repo。", + "low_friction_next_step": "先取得 owner / visibility 決策,不自動建立 repo。", + "blocked_until": [ + "確認 repo 是否仍 active", + "GitHub repo owner 與 visibility 決策完成" + ], + "allowed_after_approval": [ + "決定建立 GitHub repo 或授權既有 private repo", + "產生 migration plan" + ], + "still_forbidden": [ + "自動建立 repo", + "自動 push refs", + "刪除 110 remote" + ], + "evidence_refs": [ + "docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md", + "docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md", + "docs/security/github-target-probe.snapshot.json" + ], + "awooop_consumption": "approval_candidate" + }, + { + "github_repo": "owenhytsai/tsenyang-website", + "source_key": "tsenyang-website", + "lane": "target_creation_or_access", + "risk": "MEDIUM", + "probe_status": "not_found_or_private", + "target_state": "not_found_or_private", + "approval_status": "pending", + "required_decision": "決定 GitHub repo owner / visibility / 是否建立或授權既有 repo。", + "low_friction_next_step": "先取得 owner / visibility 決策,不自動建立 repo。", + "blocked_until": [ + "確認 repo 是否仍 active", + "GitHub repo owner 與 visibility 決策完成" + ], + "allowed_after_approval": [ + "決定建立 GitHub repo 或授權既有 private repo", + "產生 migration plan" + ], + "still_forbidden": [ + "自動建立 repo", + "自動 push refs", + "刪除 110 remote" + ], + "evidence_refs": [ + "docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md", + "docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md", + "docs/security/github-target-probe.snapshot.json" + ], + "awooop_consumption": "approval_candidate" + }, + { + "github_repo": "nexu-io/open-design", + "source_key": "open-design", + "lane": "scope_review", + "risk": "LOW", + "probe_status": "exists", + "target_state": "external_scope", + "approval_status": "not_required", + "required_decision": "決定此 repo 是否屬於 AWOOOI 資安供應鏈範圍。", + "low_friction_next_step": "只標記 scope review,不納入主控切換。", + "blocked_until": [ + "確認是否屬於 AWOOOI 資安網範圍" + ], + "allowed_after_approval": [ + "mirror_decision_only" + ], + "still_forbidden": [ + "auto_execute", + "sync_refs", + "switch_primary" + ], + "evidence_refs": [ + "docs/security/github-target-probe.snapshot.json" + ], + "awooop_consumption": "scope_review_only" + } + ] +} diff --git a/scripts/security/source-control-approval-board.py b/scripts/security/source-control-approval-board.py new file mode 100644 index 00000000..a5e47f0a --- /dev/null +++ b/scripts/security/source-control-approval-board.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +"""產生 Gitea -> GitHub 逐 repo approval board。 + +此工具只讀取既有 redacted snapshot,不呼叫 Gitea/GitHub API,不需要 token。 +用途是讓 AwoooP / PR reviewer 可以看見每個 repo 的下一個低摩擦決策點。 +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + + +def load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def infer_lane(recommended_action: str) -> str: + if recommended_action == "hold_refs_reconcile": + return "refs_reconcile" + if recommended_action == "confirm_internal_remote_purpose": + return "internal_remote_purpose" + if recommended_action == "scope_review_only": + return "scope_review" + return "target_creation_or_access" + + +def required_decision(lane: str) -> str: + mapping = { + "refs_reconcile": "決定 Gitea / GitHub refs 真相來源,並批准只產生 reconcile plan。", + "target_creation_or_access": "決定 GitHub repo owner / visibility / 是否建立或授權既有 repo。", + "internal_remote_purpose": "決定 110 internal remote 是 active source、legacy mirror 或應降級。", + "scope_review": "決定此 repo 是否屬於 AWOOOI 資安供應鏈範圍。", + } + return mapping[lane] + + +def low_friction_next_step(lane: str) -> str: + mapping = { + "refs_reconcile": "先產生 draft reconcile plan,不 push refs、不切 primary。", + "target_creation_or_access": "先取得 owner / visibility 決策,不自動建立 repo。", + "internal_remote_purpose": "先文件化用途與風險,不刪除 remote、不同步 refs。", + "scope_review": "只標記 scope review,不納入主控切換。", + } + return mapping[lane] + + +def awooop_consumption(lane: str, approval_required: bool) -> str: + if lane == "scope_review": + return "scope_review_only" + if approval_required: + return "approval_candidate" + return "mirror_only" + + +def build_board(args: argparse.Namespace) -> dict[str, Any]: + decisions = load_json(Path(args.github_target_decision)) + packages = load_json(Path(args.repo_approval_package)) + gitea_inventory = load_json(Path(args.gitea_inventory)) + + package_by_repo = { + str(item.get("github_repo", "")): item + for item in packages.get("approval_items", []) + if isinstance(item, dict) + } + + board_items: list[dict[str, Any]] = [] + pending_count = 0 + for decision in decisions.get("decisions", []): + if not isinstance(decision, dict): + continue + github_repo = str(decision.get("github_repo", "")) + package = package_by_repo.get(github_repo, {}) + approval_required = bool(decision.get("approval_required", False)) + if approval_required: + pending_count += 1 + + lane = infer_lane(str(decision.get("recommended_action", ""))) + approval_status = str(package.get("approval_status") or ("pending" if approval_required else "not_required")) + blocked_until = package.get("blocked_until") or decision.get("blocked_until") or [] + evidence_refs = sorted( + { + *[str(value) for value in decision.get("evidence_refs", [])], + *[str(value) for value in package.get("evidence_refs", [])], + } + ) + + board_items.append( + { + "github_repo": github_repo, + "source_key": str(decision.get("source_key", "")), + "lane": lane, + "risk": str(decision.get("risk", "LOW")), + "probe_status": str(decision.get("probe_status", "")), + "target_state": str(decision.get("target_state", "")), + "approval_status": approval_status, + "required_decision": required_decision(lane), + "low_friction_next_step": low_friction_next_step(lane), + "blocked_until": [str(value) for value in blocked_until], + "allowed_after_approval": [ + str(value) + for value in (package.get("allowed_after_approval") or ["mirror_decision_only"]) + ], + "still_forbidden": [ + str(value) + for value in ( + package.get("still_forbidden") + or ["auto_execute", "sync_refs", "switch_primary"] + ) + ], + "evidence_refs": evidence_refs, + "awooop_consumption": awooop_consumption(lane, approval_required), + } + ) + + inventory_status = str(gitea_inventory.get("status", "blocked")) + gate_status = "ready" if inventory_status == "ok" else "blocked" + gate_reason = ( + "Gitea authenticated 或 admin_export inventory 已完成。" + if gate_status == "ready" + else "GITEA_READONLY_TOKEN 未提供,且不使用可 push 的既有 remote credential 當 read-only token;server-side private/internal repo list 仍未完成。" + ) + + return { + "schema_version": "source_control_approval_board_v1", + "status": "draft", + "date": args.date, + "default_mode": "mirror_only", + "authenticated_inventory_gate": { + "status": gate_status, + "reason": gate_reason, + "allowed_next_step": [ + "提供 read-only token 後重跑 gitea-repo-inventory", + "或提供 redacted admin export JSON", + "在 gate 前仍可維護 approval board 與 decision table", + ], + "still_forbidden": [ + "使用 write-capable credential 當作 read-only token", + "建立 GitHub repo", + "修改 repo visibility", + "sync refs", + "switch GitHub primary", + ], + }, + "item_count": len(board_items), + "pending_approval_count": pending_count, + "board_items": board_items, + } + + +def write_markdown(board: dict[str, Any], path: Path) -> None: + gate = board["authenticated_inventory_gate"] + lines = [ + "# Source Control Approval Board", + "", + "| 項目 | 內容 |", + "|------|------|", + f"| 日期 | {board['date']} |", + f"| 狀態 | `{board['status']}` |", + f"| 預設模式 | `{board['default_mode']}` |", + f"| authenticated inventory gate | `{gate['status']}` |", + f"| gate 原因 | {gate['reason']} |", + f"| repo items | {board['item_count']} |", + f"| pending approval | {board['pending_approval_count']} |", + "", + "## 0. 核心原則", + "", + "本 board 只整理決策,不授權執行。AwoooP 可以 mirror 成 approval candidate,但不得建立 repo、修改 visibility、同步 refs、切 GitHub primary 或保存 credential value。", + "", + "## 1. 逐 repo 決策隊列", + "", + "| GitHub repo | Lane | Risk | Probe | Approval | 下一步 |", + "|-------------|------|------|-------|----------|--------|", + ] + for item in board["board_items"]: + lines.append( + "| " + + " | ".join( + [ + f"`{item['github_repo']}`", + f"`{item['lane']}`", + f"`{item['risk']}`", + f"`{item['probe_status']}`", + f"`{item['approval_status']}`", + item["low_friction_next_step"], + ] + ) + + " |" + ) + + lines.extend(["", "## 2. 詳細阻塞點", ""]) + for item in board["board_items"]: + lines.extend( + [ + f"### {item['github_repo']}", + "", + f"- Source key:`{item['source_key']}`", + f"- Required decision:{item['required_decision']}", + f"- AwoooP consumption:`{item['awooop_consumption']}`", + "- Blocked until:", + ] + ) + for value in item["blocked_until"]: + lines.append(f" - {value}") + lines.append("- Still forbidden:") + 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. Gate 前允許做的事", + "", + "1. 更新 read-only evidence。", + "2. 更新 approval board / decision table。", + "3. 寫 draft reconcile plan。", + "4. 把 pending approval mirror 到 AwoooP。", + "", + "## 4. Gate 前仍禁止", + "", + ] + ) + for value in gate["still_forbidden"]: + lines.append(f"- {value}") + lines.append("") + path.write_text("\n".join(lines), encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--date", required=True) + parser.add_argument("--github-target-decision", default="docs/security/github-target-decision.snapshot.json") + parser.add_argument( + "--repo-approval-package", + default="docs/security/github-target-repo-approval-package.snapshot.json", + ) + parser.add_argument("--gitea-inventory", default="docs/security/gitea-repo-inventory.snapshot.json") + parser.add_argument("--output-json", required=True) + parser.add_argument("--output-md", required=True) + args = parser.parse_args() + + board = build_board(args) + Path(args.output_json).write_text( + json.dumps(board, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + write_markdown(board, Path(args.output_md)) + print(f"OK source-control approval board items={board['item_count']}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())