diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index ad8498e25..1fa88b01a 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -49379,3 +49379,19 @@ production browser smoke: **仍維持**: - 未寫 credential marker、未讀 secret / token / `.env` / raw sessions / SQLite / auth,未做 host / Docker / Nginx / firewall / K3s / DB 操作,未使用 GitHub。 + +## 2026-06-29 — 14:09 P0-003 Gitea-only private inventory scorecard + +**完成內容**: +- 新增 `scripts/security/gitea-private-inventory-p0-scorecard.py`,把 P0-003 收斂為 Gitea-only scorecard;GitHub lane 固定 `stopped_retired_do_not_use`,且 `github_lane_excluded_from_p0_blocker_count=true`。 +- 產生 `docs/operations/awoooi-gitea-private-inventory-p0-scorecard.snapshot.json`:目前 `gitea_repo_inventory.status=partial`、`visibility_scope=public_only`、public repo count `2`,authenticated/admin export payload accepted `0`,owner coverage attestation received `0`。 +- 讀回 active product rows:expected `11`、present `11`、missing `0`、ready `3`、blocked `8`,`all_active_product_repos_have_gitea_owner_readiness_row=true`。 +- 更新 priority readback 的 P0-003 row:active blockers 只剩 Gitea authenticated/admin export redacted payload 與 owner coverage attestation,不再把 GitHub recovery/primary/target 當 P0 blocker。 + +**本地驗證結果**: +- `python3.11 -m py_compile scripts/security/gitea-private-inventory-p0-scorecard.py`:通過。 +- `python3.11 -m pytest scripts/security/tests/test_gitea_private_inventory_p0_scorecard.py -q`:`3 passed`。 +- `python3 scripts/security/gitea-private-inventory-p0-scorecard.py --generated-at 2026-06-29T14:09:49+08:00 --output docs/operations/awoooi-gitea-private-inventory-p0-scorecard.snapshot.json` + `jq empty`:通過。 + +**未做**: +- 沒有使用 GitHub / gh / GitHub API;沒有讀 token / secret / `.env` / raw sessions / SQLite / auth;沒有寫 Gitea repo / refs / branch / secret;沒有 host 或 runtime 操作。 diff --git a/docs/operations/awoooi-gitea-private-inventory-p0-scorecard.snapshot.json b/docs/operations/awoooi-gitea-private-inventory-p0-scorecard.snapshot.json new file mode 100644 index 000000000..69c09c8e6 --- /dev/null +++ b/docs/operations/awoooi-gitea-private-inventory-p0-scorecard.snapshot.json @@ -0,0 +1,210 @@ +{ + "schema_version": "awoooi_gitea_private_inventory_p0_scorecard_v1", + "generated_at": "2026-06-29T14:09:49+08:00", + "workplan_id": "P0-003", + "status": "blocked_waiting_gitea_authenticated_or_owner_export_inventory", + "source_control_authority": "gitea", + "private_inventory_source": "gitea", + "github_status": "stopped_retired_do_not_use", + "github_lane_excluded_from_p0_blocker_count": true, + "github_api_used": false, + "github_cli_used": false, + "github_primary_switch_authorized": false, + "gitea_inventory": { + "schema_version": "gitea_repo_inventory_v1", + "status": "partial", + "visibility_scope": "public_only", + "repo_count": 2, + "token_present": false, + "blocking_reason": "未提供 token,結果只代表公開可見 repo;private/internal repos 仍需只讀 token 或管理匯出" + }, + "authenticated_import_acceptance": { + "schema_version": "gitea_authenticated_inventory_import_acceptance_v1", + "status": "draft_waiting_redacted_inventory_payload", + "accepted_payload_count": 0, + "token_value_collection_allowed": false, + "execution_authorized": false + }, + "coverage_attestation": { + "schema_version": "gitea_inventory_coverage_attestation_v1", + "status": "draft_waiting_owner_attestation", + "received_attestation_count": 0, + "accepted_attestation_count": 0, + "execution_authorized": false + }, + "product_row_coverage": { + "expected_product_count": 11, + "present_product_row_count": 11, + "missing_product_row_count": 0, + "ready_product_count": 3, + "blocked_product_count": 8, + "internal_or_authenticated_inventory_required_count": 3, + "all_active_product_repos_have_gitea_owner_readiness_row": true + }, + "product_rows": [ + { + "product_id": "awoooi", + "state": "ready", + "status": "ready", + "gitea_repo": "wooo/awoooi", + "remote_main_present": true, + "remote_dev_present": true, + "next_gate": "use codex-workspaces/awoooi-dev and branch from dev for tasks", + "blockers": [], + "owner_readiness_row_present": true + }, + { + "product_id": "momo-pro", + "state": "ready", + "status": "ready", + "gitea_repo": "wooo/ewoooc", + "remote_main_present": true, + "remote_dev_present": true, + "next_gate": "use this workspace for Codex tasks by branching from dev into codex/*; do not edit dev directly", + "blockers": [], + "owner_readiness_row_present": true + }, + { + "product_id": "awooogo", + "state": "ready", + "status": "ready", + "gitea_repo": "wooo/AwoooGo", + "remote_main_present": true, + "remote_dev_present": true, + "next_gate": "use this workspace for Codex tasks by branching from dev into codex/*; do not edit dev directly", + "blockers": [], + "owner_readiness_row_present": true + }, + { + "product_id": "2026fifa", + "state": "blocked", + "status": "blocked_local_drift_review_required", + "gitea_repo": "", + "remote_main_present": false, + "remote_dev_present": false, + "next_gate": "build narrow drift review and owner pick list before any dev branch creation", + "blockers": [ + "local_head_differs_from_gitea_main", + "diff_readback_timeout", + "production_branch_checked_out" + ], + "owner_readiness_row_present": true + }, + { + "product_id": "agent-bounty-protocol", + "state": "blocked", + "status": "blocked_local_drift_review_required", + "gitea_repo": "", + "remote_main_present": false, + "remote_dev_present": false, + "next_gate": "owner pick list for A2A/traffic/propose changes before dev branch creation", + "blockers": [ + "local_head_differs_from_gitea_main", + "tracked_dirty_worktree", + "production_branch_checked_out" + ], + "owner_readiness_row_present": true + }, + { + "product_id": "stockplatform-v2", + "state": "blocked", + "status": "blocked_dirty_worktree_review_required", + "gitea_repo": "", + "remote_main_present": false, + "remote_dev_present": false, + "next_gate": "review dirty files and owner-selected include/exclude list before dev branch creation", + "blockers": [ + "tracked_dirty_worktree", + "production_branch_checked_out" + ], + "owner_readiness_row_present": true + }, + { + "product_id": "vibework", + "state": "blocked", + "status": "blocked_local_drift_review_required", + "gitea_repo": "", + "remote_main_present": false, + "remote_dev_present": false, + "next_gate": "owner pick list for growth/admin/runtime changes before dev branch creation", + "blockers": [ + "local_head_differs_from_gitea_main", + "tracked_dirty_worktree", + "production_branch_checked_out" + ], + "owner_readiness_row_present": true + }, + { + "product_id": "clawbot-openclaw", + "state": "blocked", + "status": "blocked_local_drift_review_required", + "gitea_repo": "", + "remote_main_present": false, + "remote_dev_present": false, + "next_gate": "review docker-compose and main.py drift before dev branch creation", + "blockers": [ + "local_head_differs_from_gitea_main", + "tracked_dirty_worktree", + "production_branch_checked_out" + ], + "owner_readiness_row_present": true + }, + { + "product_id": "bitan-pharmacy", + "state": "blocked", + "status": "blocked_internal_inventory_and_dirty_review_required", + "gitea_repo": "", + "remote_main_present": false, + "remote_dev_present": false, + "next_gate": "owner export or authenticated inventory plus dirty review", + "blockers": [ + "internal_repo_requires_owner_export_or_authenticated_inventory", + "dirty_worktree" + ], + "owner_readiness_row_present": true + }, + { + "product_id": "tsenyang-website", + "state": "blocked", + "status": "blocked_internal_inventory_and_dirty_review_required", + "gitea_repo": "", + "remote_main_present": false, + "remote_dev_present": false, + "next_gate": "owner export or authenticated inventory plus dirty review", + "blockers": [ + "internal_repo_requires_owner_export_or_authenticated_inventory", + "dirty_worktree" + ], + "owner_readiness_row_present": true + }, + { + "product_id": "vtuber", + "state": "blocked", + "status": "blocked_repository_inventory_repair_required", + "gitea_repo": "", + "remote_main_present": false, + "remote_dev_present": false, + "next_gate": "repair local repository inventory before any Gitea action", + "blockers": [ + "remote_repository_not_found_or_unauthenticated", + "local_head_readback_abnormal", + "dirty_worktree" + ], + "owner_readiness_row_present": true + } + ], + "active_blockers": [ + "gitea_repo_inventory_status_not_ok", + "gitea_visibility_scope_public_only_or_unknown", + "gitea_authenticated_inventory_payload_not_accepted", + "gitea_owner_coverage_attestation_not_received" + ], + "exit_criteria": [ + "private_inventory_source=gitea", + "github_lane_excluded_from_p0_blocker_count=true", + "gitea_repo_inventory.status=ok", + "gitea_repo_inventory.visibility_scope in authenticated/admin_export", + "all_active_product_repos_have_gitea_owner_readiness_row=true" + ], + "safe_next_step": "obtain_gitea_authenticated_or_admin_export_redacted_inventory_payload_then_validate_import_acceptance_and_owner_attestation" +} diff --git a/docs/operations/awoooi-priority-work-order-readback.snapshot.json b/docs/operations/awoooi-priority-work-order-readback.snapshot.json index f91c4c911..992e8ea55 100644 --- a/docs/operations/awoooi-priority-work-order-readback.snapshot.json +++ b/docs/operations/awoooi-priority-work-order-readback.snapshot.json @@ -1,7 +1,7 @@ { "schema_version": "awoooi_priority_work_order_readback_v1", - "generated_at": "2026-06-29T14:05:53+08:00", - "status": "p0_005_dr_escrow_checklist_ready_waiting_redacted_refs", + "generated_at": "2026-06-29T14:11:34+08:00", + "status": "p0_005_waiting_refs_p0_003_gitea_only_scorecard_ready", "source_refs": { "global_scorecard": "~/.codex/product-runtime-governance-completion-scorecard.snapshot.json", "workstation_dashboard": "~/.codex/codex-workstation-sync-dashboard.snapshot.json", @@ -10,10 +10,11 @@ "delivery_closure_workbench": "https://awoooi.wooo.work/api/v1/agents/delivery-closure-workbench", "public_gitea_queue_readback": "ops/runner/read-public-gitea-actions-queue.py --json", "credential_escrow_scorecard": "/tmp/awoooi-credential-escrow-intake-scorecard-20260629-1200-priority.json", - "dr_escrow_evidence_checklist_generator": "scripts/reboot-recovery/dr-escrow-evidence-checklist.py" + "dr_escrow_evidence_checklist_generator": "scripts/reboot-recovery/dr-escrow-evidence-checklist.py", + "gitea_private_inventory_p0_scorecard": "docs/operations/awoooi-gitea-private-inventory-p0-scorecard.snapshot.json" }, "current_head": { - "gitea_main_sha": "c98e6d5e5309dad9f0be4399a3d2e1fe2ec669ca", + "gitea_main_sha": "86e674cf9e31118c6c52067a27b9c4e8e69cf8a0", "latest_successful_deploy_marker": "9362588ce chore(cd): deploy a423301 [skip ci]", "latest_successful_deployed_source_sha": "a4233017ad5fd03977233f3db6a4bb45d71507ed", "latest_source_readiness_commit_sha": "0c8d4e88c39157b92322fa41a92e6b15c317ac49", @@ -148,17 +149,33 @@ { "workplan_id": "P0-003", "title": "取得 Gitea private inventory 權限", - "status": "blocked_external_or_authenticated_inventory_required", - "reason": "Global scorecard still reports private/internal inventory incomplete; GitHub remains stopped and must not be used as a fallback.", + "status": "blocked_waiting_gitea_authenticated_or_owner_export_inventory", + "reason": "P0-003 now has a Gitea-only scorecard. GitHub is excluded from active P0 blocker math; remaining blockers are Gitea authenticated/admin export payload and owner coverage attestation.", + "evidence": { + "private_inventory_source": "gitea", + "github_lane_excluded_from_p0_blocker_count": true, + "gitea_inventory_status": "partial", + "gitea_visibility_scope": "public_only", + "gitea_public_repo_count": 2, + "accepted_inventory_payload_count": 0, + "owner_coverage_attestation_received_count": 0, + "expected_product_count": 11, + "present_product_row_count": 11, + "missing_product_row_count": 0, + "all_active_product_repos_have_gitea_owner_readiness_row": true + }, "professional_fix": { "owner": "Gitea inventory lane", - "action": "Use Gitea-only inventory readback and retire GitHub from P0 completion math; keep GitHub as do_not_use historical context only.", + "action": "Use the Gitea-only P0 scorecard, accept only redacted authenticated/admin export inventory payloads, and keep GitHub as do_not_use historical context only.", "exit_criteria": [ "private_inventory_source=gitea", "github_lane_excluded_from_p0_blocker_count=true", - "all active product repos have Gitea owner/readiness row" + "gitea_repo_inventory.status=ok", + "gitea_repo_inventory.visibility_scope in authenticated/admin_export", + "all_active_product_repos_have_gitea_owner_readiness_row=true" ] - } + }, + "safe_next_step": "validate_gitea_authenticated_or_admin_export_redacted_inventory_payload" }, { "workplan_id": "P0-006", diff --git a/scripts/security/gitea-private-inventory-p0-scorecard.py b/scripts/security/gitea-private-inventory-p0-scorecard.py new file mode 100644 index 000000000..defd8b2d5 --- /dev/null +++ b/scripts/security/gitea-private-inventory-p0-scorecard.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +"""Build the P0-003 Gitea-only private inventory scorecard.""" + +from __future__ import annotations + +import argparse +import json +from datetime import datetime +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[2] +SCHEMA_VERSION = "awoooi_gitea_private_inventory_p0_scorecard_v1" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Summarize P0-003 using Gitea-only inventory evidence.", + ) + parser.add_argument( + "--gitea-inventory", + type=Path, + default=ROOT / "docs/security/gitea-repo-inventory.snapshot.json", + ) + parser.add_argument( + "--import-acceptance", + type=Path, + default=ROOT / "docs/security/gitea-authenticated-inventory-import-acceptance.snapshot.json", + ) + parser.add_argument( + "--coverage-attestation", + type=Path, + default=ROOT / "docs/security/gitea-inventory-coverage-attestation.snapshot.json", + ) + parser.add_argument( + "--remaining-products", + type=Path, + default=ROOT / "docs/operations/codex-gitea-remaining-products-readback.snapshot.json", + ) + parser.add_argument("--generated-at", help="Override generated_at for stable snapshots.") + parser.add_argument("--output", type=Path, help="Write JSON to this path.") + return parser.parse_args() + + +def load_json(path: Path) -> dict[str, Any]: + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise SystemExit(f"json_not_object={path}") + return payload + + +def as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + +def as_int(value: Any, default: int = 0) -> int: + try: + return int(value) + except (TypeError, ValueError): + return default + + +def product_row(row: dict[str, Any], state: str) -> dict[str, Any]: + return { + "product_id": str(row.get("product_id", "")), + "state": state, + "status": str(row.get("status") or row.get("candidate_status") or "ready"), + "gitea_repo": str(row.get("gitea_repo", "")), + "remote_main_present": bool(row.get("remote_main_present", False)), + "remote_dev_present": bool(row.get("remote_dev_present", False)), + "next_gate": str(row.get("next_gate", "")), + "blockers": [str(item) for item in as_list(row.get("blockers"))], + "owner_readiness_row_present": bool(row.get("product_id")), + } + + +def build_product_rows(remaining: dict[str, Any]) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + for row in as_list(remaining.get("ready_products")): + if isinstance(row, dict): + rows.append(product_row(row, "ready")) + for row in as_list(remaining.get("blocked_products")): + if isinstance(row, dict): + rows.append(product_row(row, "blocked")) + return rows + + +def build_scorecard(args: argparse.Namespace) -> dict[str, Any]: + gitea_inventory = load_json(args.gitea_inventory) + import_acceptance = load_json(args.import_acceptance) + coverage_attestation = load_json(args.coverage_attestation) + remaining_products = load_json(args.remaining_products) + + rows = build_product_rows(remaining_products) + summary = remaining_products.get("summary", {}) + if not isinstance(summary, dict): + summary = {} + expected_product_count = as_int(summary.get("product_count"), len(rows)) + missing_row_count = max(expected_product_count - len(rows), 0) + + gitea_status = str(gitea_inventory.get("status", "unknown")) + visibility_scope = str(gitea_inventory.get("visibility_scope", "unknown")) + accepted_payload_count = as_int(import_acceptance.get("accepted_payload_count")) + received_attestation_count = as_int(coverage_attestation.get("received_attestation_count")) + + blockers: list[str] = [] + if gitea_status != "ok": + blockers.append("gitea_repo_inventory_status_not_ok") + if visibility_scope not in {"authenticated", "admin_export"}: + blockers.append("gitea_visibility_scope_public_only_or_unknown") + if accepted_payload_count < 1: + blockers.append("gitea_authenticated_inventory_payload_not_accepted") + if received_attestation_count < 1: + blockers.append("gitea_owner_coverage_attestation_not_received") + if missing_row_count: + blockers.append("active_product_readiness_rows_missing") + + return { + "schema_version": SCHEMA_VERSION, + "generated_at": args.generated_at + or datetime.now().astimezone().isoformat(timespec="seconds"), + "workplan_id": "P0-003", + "status": "ready_for_private_inventory_review" if not blockers else "blocked_waiting_gitea_authenticated_or_owner_export_inventory", + "source_control_authority": "gitea", + "private_inventory_source": "gitea", + "github_status": "stopped_retired_do_not_use", + "github_lane_excluded_from_p0_blocker_count": True, + "github_api_used": False, + "github_cli_used": False, + "github_primary_switch_authorized": False, + "gitea_inventory": { + "schema_version": gitea_inventory.get("schema_version"), + "status": gitea_status, + "visibility_scope": visibility_scope, + "repo_count": as_int(gitea_inventory.get("repo_count")), + "token_present": bool(gitea_inventory.get("token_present", False)), + "blocking_reason": str(gitea_inventory.get("blocking_reason", "")), + }, + "authenticated_import_acceptance": { + "schema_version": import_acceptance.get("schema_version"), + "status": str(import_acceptance.get("status", "unknown")), + "accepted_payload_count": accepted_payload_count, + "token_value_collection_allowed": bool( + import_acceptance.get("token_value_collection_allowed", False) + ), + "execution_authorized": bool(import_acceptance.get("execution_authorized", False)), + }, + "coverage_attestation": { + "schema_version": coverage_attestation.get("schema_version"), + "status": str(coverage_attestation.get("status", "unknown")), + "received_attestation_count": received_attestation_count, + "accepted_attestation_count": as_int( + coverage_attestation.get("accepted_attestation_count") + ), + "execution_authorized": bool(coverage_attestation.get("execution_authorized", False)), + }, + "product_row_coverage": { + "expected_product_count": expected_product_count, + "present_product_row_count": len(rows), + "missing_product_row_count": missing_row_count, + "ready_product_count": len([row for row in rows if row["state"] == "ready"]), + "blocked_product_count": len([row for row in rows if row["state"] == "blocked"]), + "internal_or_authenticated_inventory_required_count": as_int( + summary.get("internal_or_authenticated_inventory_required_count") + ), + "all_active_product_repos_have_gitea_owner_readiness_row": missing_row_count == 0 + and all(row["owner_readiness_row_present"] for row in rows), + }, + "product_rows": rows, + "active_blockers": blockers, + "exit_criteria": [ + "private_inventory_source=gitea", + "github_lane_excluded_from_p0_blocker_count=true", + "gitea_repo_inventory.status=ok", + "gitea_repo_inventory.visibility_scope in authenticated/admin_export", + "all_active_product_repos_have_gitea_owner_readiness_row=true", + ], + "safe_next_step": ( + "obtain_gitea_authenticated_or_admin_export_redacted_inventory_payload_" + "then_validate_import_acceptance_and_owner_attestation" + ), + } + + +def main() -> int: + args = parse_args() + scorecard = build_scorecard(args) + text = json.dumps(scorecard, ensure_ascii=False, indent=2) + "\n" + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(text, encoding="utf-8") + else: + print(text, end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/security/tests/test_gitea_private_inventory_p0_scorecard.py b/scripts/security/tests/test_gitea_private_inventory_p0_scorecard.py new file mode 100644 index 000000000..b872bbba7 --- /dev/null +++ b/scripts/security/tests/test_gitea_private_inventory_p0_scorecard.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[3] +SCRIPT = ROOT / "scripts" / "security" / "gitea-private-inventory-p0-scorecard.py" + + +def load_scorecard() -> dict: + result = subprocess.run( + [sys.executable, str(SCRIPT), "--generated-at", "2026-06-29T14:12:00+08:00"], + text=True, + capture_output=True, + check=True, + ) + return json.loads(result.stdout) + + +def test_scorecard_excludes_github_from_p0_blocker_math() -> None: + scorecard = load_scorecard() + + assert scorecard["schema_version"] == "awoooi_gitea_private_inventory_p0_scorecard_v1" + assert scorecard["workplan_id"] == "P0-003" + assert scorecard["source_control_authority"] == "gitea" + assert scorecard["private_inventory_source"] == "gitea" + assert scorecard["github_status"] == "stopped_retired_do_not_use" + assert scorecard["github_lane_excluded_from_p0_blocker_count"] is True + assert scorecard["github_api_used"] is False + assert scorecard["github_cli_used"] is False + assert scorecard["github_primary_switch_authorized"] is False + assert not any("github" in blocker.lower() for blocker in scorecard["active_blockers"]) + + +def test_scorecard_preserves_current_gitea_inventory_blocker() -> None: + scorecard = load_scorecard() + + assert scorecard["status"] == "blocked_waiting_gitea_authenticated_or_owner_export_inventory" + assert scorecard["gitea_inventory"]["status"] == "partial" + assert scorecard["gitea_inventory"]["visibility_scope"] == "public_only" + assert scorecard["gitea_inventory"]["repo_count"] == 2 + assert scorecard["authenticated_import_acceptance"]["accepted_payload_count"] == 0 + assert scorecard["coverage_attestation"]["received_attestation_count"] == 0 + assert "gitea_repo_inventory_status_not_ok" in scorecard["active_blockers"] + assert "gitea_authenticated_inventory_payload_not_accepted" in scorecard["active_blockers"] + + +def test_scorecard_keeps_all_active_product_rows_visible() -> None: + scorecard = load_scorecard() + coverage = scorecard["product_row_coverage"] + + assert coverage["expected_product_count"] == 11 + assert coverage["present_product_row_count"] == 11 + assert coverage["missing_product_row_count"] == 0 + assert coverage["ready_product_count"] == 3 + assert coverage["blocked_product_count"] == 8 + assert coverage["all_active_product_repos_have_gitea_owner_readiness_row"] is True + + product_ids = {row["product_id"] for row in scorecard["product_rows"]} + assert {"awoooi", "momo-pro", "awooogo", "bitan-pharmacy", "tsenyang-website"} <= product_ids