From 1dc1550d38d1042220f38f5beb087101b09ba72f Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 29 Jun 2026 15:03:23 +0800 Subject: [PATCH] feat(governance): expose gitea private inventory p0 readback --- .gitea/workflows/cd.yaml | 6 + apps/api/src/api/v1/agents.py | 49 +- .../services/delivery_closure_workbench.py | 330 ++++---- .../gitea_private_inventory_p0_scorecard.py | 282 +++++++ .../test_delivery_closure_workbench_api.py | 712 +++--------------- ...itea_private_inventory_p0_scorecard_api.py | 149 ++++ apps/web/messages/en.json | 21 +- apps/web/messages/zh-TW.json | 21 +- apps/web/src/app/[locale]/delivery/page.tsx | 62 +- apps/web/src/lib/api-client.ts | 146 +++- docs/LOGBOOK.md | 24 + ...priority-work-order-readback.snapshot.json | 7 +- .../test_cd_controlled_runtime_profile.py | 4 + 13 files changed, 963 insertions(+), 850 deletions(-) create mode 100644 apps/api/src/services/gitea_private_inventory_p0_scorecard.py create mode 100644 apps/api/tests/test_gitea_private_inventory_p0_scorecard_api.py diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index 5dc05366a..9e3e5fa82 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -232,6 +232,8 @@ jobs: ;; apps/api/src/services/credential_escrow_evidence_intake_readiness.py) ;; + apps/api/src/services/gitea_private_inventory_p0_scorecard.py) + ;; apps/api/src/services/iwooos_security_operating_system.py) ;; apps/api/Dockerfile) @@ -294,6 +296,8 @@ jobs: ;; apps/api/tests/test_credential_escrow_evidence_intake_readiness_api.py) ;; + apps/api/tests/test_gitea_private_inventory_p0_scorecard_api.py) + ;; apps/api/tests/test_iwooos_security_operating_system.py) ;; apps/api/tests/e2e_network_test.py) @@ -426,6 +430,7 @@ jobs: src/services/delivery_closure_workbench.py \ src/services/heartbeat_report_service.py \ src/services/credential_escrow_evidence_intake_readiness.py \ + src/services/gitea_private_inventory_p0_scorecard.py \ src/services/iwooos_security_operating_system.py \ src/services/awoooi_gitea_onboarding_warning_step_dashboard.py \ src/services/awoooi_gitea_onboarding_warning_step_owner_package.py \ @@ -454,6 +459,7 @@ jobs: tests/test_awooop_operator_timeline_labels.py::test_outbound_timeline_title_labels_runbook_review \ tests/test_delivery_closure_workbench_api.py \ tests/test_credential_escrow_evidence_intake_readiness_api.py \ + tests/test_gitea_private_inventory_p0_scorecard_api.py \ tests/test_iwooos_security_operating_system.py \ tests/e2e_network_test.py::TestHMACVerification::test_valid_hmac_signature \ tests/test_p0_cicd_baseline_source_readiness_api.py \ diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index b5b5de33b..896aaf7a6 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -337,6 +337,9 @@ from src.services.dependency_upgrade_approval_package_template import ( from src.services.docker_build_surface_inventory import ( load_latest_docker_build_surface_inventory, ) +from src.services.gitea_private_inventory_p0_scorecard import ( + load_latest_gitea_private_inventory_p0_scorecard, +) from src.services.gitea_workflow_runner_health import ( load_latest_gitea_workflow_runner_health, ) @@ -358,12 +361,12 @@ from src.services.observability_contract_matrix import ( from src.services.offsite_escrow_readiness_status import ( load_latest_offsite_escrow_readiness_status, ) -from src.services.package_supply_chain_inventory import ( - load_latest_package_supply_chain_inventory, -) from src.services.p0_cicd_baseline_source_readiness import ( load_latest_p0_cicd_baseline_source_readiness, ) +from src.services.package_supply_chain_inventory import ( + load_latest_package_supply_chain_inventory, +) from src.services.product_awoooi_manifest_standard import ( load_latest_product_awoooi_manifest_standard, ) @@ -948,11 +951,13 @@ async def get_awoooi_status_cleanup_dashboard() -> dict[str, Any]: response_model=dict[str, Any], summary="取得交付閉環工作台彙總", description=( - "彙總 AWOOOI 狀態清理、GitHub 私有備援、Gitea / CI-CD、Runtime surface " + "彙總 AWOOOI 狀態清理、Gitea private inventory、CI/CD baseline、" + "Gitea runner、Runtime surface " "與 Backup / DR 的既有只讀快照,回傳前端工作台可直接使用的交付主線、" "完成度、阻擋數與下一步。此端點不呼叫 GitHub / Gitea / Wazuh / K8s live API、" "不建立 repo、不改 visibility、不同步 refs、不觸發 workflow、不執行備份或還原、" - "不讀 secret、不做 runtime write。" + "不讀 secret、不做 runtime write;GitHub 僅保留 stopped / do_not_use 背景狀態," + "不進 active P0 blocker math。" ), ) async def get_delivery_closure_workbench() -> dict[str, Any]: @@ -1033,6 +1038,40 @@ async def get_p0_cicd_baseline_source_readiness() -> dict[str, Any]: ) from exc +@router.get( + "/gitea-private-inventory-p0-scorecard", + response_model=dict[str, Any], + summary="取得 P0-003 Gitea private inventory scorecard", + description=( + "讀取已提交的 P0-003 Gitea-only private/internal inventory scorecard;" + "此端點只回傳 Gitea inventory metadata、redacted import acceptance 狀態、" + "owner coverage attestation 狀態與產品列覆蓋度。" + "它不呼叫 Gitea / GitHub live API、不收 token value、不建立 repo、" + "不改 visibility、不同步 refs、不觸發 workflow、不讀 secret、" + "不讀 raw session/SQLite。GitHub 僅保留 stopped / do_not_use 背景狀態," + "不進 active P0 blocker math。" + ), +) +async def get_gitea_private_inventory_p0_scorecard() -> dict[str, Any]: + """回傳 P0-003 Gitea-only private inventory scorecard 只讀快照。""" + try: + payload = await asyncio.to_thread( + load_latest_gitea_private_inventory_p0_scorecard + ) + return redact_public_lan_topology(payload) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + logger.error("gitea_private_inventory_p0_scorecard_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="P0-003 Gitea private inventory scorecard 快照無效", + ) from exc + + @router.get( "/github-target-private-backup-evidence-gate", response_model=dict[str, Any], diff --git a/apps/api/src/services/delivery_closure_workbench.py b/apps/api/src/services/delivery_closure_workbench.py index 50bc771f0..cacacee9c 100644 --- a/apps/api/src/services/delivery_closure_workbench.py +++ b/apps/api/src/services/delivery_closure_workbench.py @@ -18,6 +18,9 @@ from src.services.awoooi_status_cleanup_dashboard import ( from src.services.backup_dr_readiness_matrix import ( load_latest_backup_dr_readiness_matrix, ) +from src.services.gitea_private_inventory_p0_scorecard import ( + load_latest_gitea_private_inventory_p0_scorecard, +) from src.services.gitea_workflow_runner_health import ( load_latest_gitea_workflow_runner_health, ) @@ -35,7 +38,7 @@ def load_delivery_closure_workbench() -> dict[str, Any]: """Load existing delivery snapshots and return a compact workbench model.""" status_cleanup = load_latest_awoooi_status_cleanup_dashboard() production_deploy = load_latest_awoooi_production_deploy_readback_blocker() - github = _load_github_private_backup_evidence_gate() + private_inventory = load_latest_gitea_private_inventory_p0_scorecard() cicd_baseline = load_latest_p0_cicd_baseline_source_readiness() gitea = load_latest_gitea_workflow_runner_health() runtime = load_latest_runtime_surface_inventory() @@ -43,7 +46,7 @@ def load_delivery_closure_workbench() -> dict[str, Any]: return build_delivery_closure_workbench( status_cleanup=status_cleanup, production_deploy=production_deploy, - github=github, + private_inventory=private_inventory, cicd_baseline=cicd_baseline, gitea=gitea, runtime=runtime, @@ -55,7 +58,7 @@ def build_delivery_closure_workbench( *, status_cleanup: dict[str, Any], production_deploy: dict[str, Any], - github: dict[str, Any], + private_inventory: dict[str, Any], cicd_baseline: dict[str, Any], gitea: dict[str, Any], runtime: dict[str, Any], @@ -63,13 +66,11 @@ def build_delivery_closure_workbench( ) -> dict[str, Any]: """Build the delivery workbench response from already validated snapshots.""" status_summary = _dict(status_cleanup.get("summary")) - github_summary = _dict(github.get("summary")) - github_boundaries = _dict(github.get("operation_boundaries")) - github_preflight = _dict(github.get("controlled_execution_preflight")) - github_operator_unblock = _dict(github_preflight.get("operator_unblock")) - github_governance_writeback = _dict( - github_preflight.get("internal_governance_writeback") - or github.get("internal_governance_writeback") + private_inventory_readback = _dict(private_inventory.get("readback")) + private_inventory_rollups = _dict(private_inventory.get("rollups")) + private_inventory_boundaries = _dict(private_inventory.get("operation_boundaries")) + private_inventory_retired_github = _dict( + private_inventory.get("github_retired_context") ) cicd_baseline_readback = _dict(cicd_baseline.get("readback")) cicd_baseline_rollups = _dict(cicd_baseline.get("rollups")) @@ -82,14 +83,8 @@ def build_delivery_closure_workbench( backup_status = _dict(backup.get("program_status")) backup_rollups = _dict(backup.get("rollups")) - github_required = _int(github_summary.get("approval_required_target_count")) - github_verified = _int(github_summary.get("private_backup_verified_count")) - github_preflight_blockers = _int( - github_preflight.get("blocked_preflight_target_count") - ) - github_lane_blockers = max( - _int(github_summary.get("blocked_target_count")), - github_preflight_blockers, + private_inventory_blockers = _int( + private_inventory_rollups.get("active_blocker_count") ) runtime_action_required = set( _strings(runtime_rollups.get("action_required_surface_ids")) @@ -364,59 +359,60 @@ def build_delivery_closure_workbench( "next_action": _first_string(production_deploy.get("next_actions")), }, { - "id": "github", - "source_id": "github_private_backup", + "id": "gitea_private_inventory", + "source_id": "gitea_private_inventory_p0_scorecard", "completion_percent": _percent( - (github_verified / github_required) * 100 if github_required else 0 + private_inventory_rollups.get("review_readiness_percent") ), - "status": str( - github_preflight.get("status") or github.get("status") or "unknown" - ), - "blocker_count": github_lane_blockers, + "status": str(private_inventory.get("status") or "unknown"), + "blocker_count": private_inventory_blockers, "metric": { - "kind": "private_backup_verified", - "verified": github_verified, - "total": github_required, - "controlled_apply_ready": _int( - github_preflight.get("controlled_apply_ready_count") + "kind": "private_inventory", + "workplan_id": str( + private_inventory_readback.get("workplan_id") or "P0-003" ), - "blocked_preflight": github_preflight_blockers, - "write_channel_ready": github_preflight.get( - "github_write_channel_ready" - ) - is True, - "github_account_status": str( - github_preflight.get("github_account_status") or "unknown" + "private_inventory_source": str( + private_inventory_readback.get("private_inventory_source") + or "gitea" ), - "github_account_suspended": github_preflight.get( - "github_account_suspended" - ) - is True, - "internal_governance_ready": github_governance_writeback.get("ready") - is True, - "km_writeback_ready": _int( - github_governance_writeback.get("km_writeback_ready_count") + "gitea_repo_inventory_status": str( + private_inventory_rollups.get("gitea_repo_inventory_status") + or "unknown" ), - "playbook_writeback_ready": _int( - github_governance_writeback.get( - "playbook_writeback_ready_count" + "gitea_visibility_scope": str( + private_inventory_rollups.get("gitea_visibility_scope") + or "unknown" + ), + "gitea_public_repo_count": _int( + private_inventory_rollups.get("gitea_public_repo_count") + ), + "expected_product_count": _int( + private_inventory_rollups.get("expected_product_count") + ), + "present_product_row_count": _int( + private_inventory_rollups.get("present_product_row_count") + ), + "missing_product_row_count": _int( + private_inventory_rollups.get("missing_product_row_count") + ), + "accepted_inventory_payload_count": _int( + private_inventory_rollups.get("accepted_inventory_payload_count") + ), + "owner_coverage_attestation_received_count": _int( + private_inventory_rollups.get( + "owner_coverage_attestation_received_count" ) ), + "github_lane_excluded_from_p0_blocker_count": ( + private_inventory_rollups.get( + "github_lane_excluded_from_p0_blocker_count" + ) + is True + ), + "active_blockers": _strings(private_inventory.get("active_blockers")), }, - "href": "/governance?tab=automation-inventory", - "operator_unblock": github_operator_unblock, - "internal_governance_writeback": github_governance_writeback, - "next_action": str( - _first_string(github_operator_unblock.get("required_actions")) - or github_operator_unblock.get("safe_handoff") - or github_preflight.get("operator_unblock_status") - or "" - ) - or str( - _first_target_action(github_preflight.get("targets")) - or github.get("next_action") - or _first_target_action(github.get("targets")) - ), + "href": "/delivery", + "next_action": str(private_inventory_readback.get("safe_next_step") or ""), }, { "id": "cicd_baseline", @@ -573,7 +569,7 @@ def build_delivery_closure_workbench( source_statuses = [ _source_status("status_cleanup", status_cleanup), _source_status("production_deploy_readback", production_deploy), - _source_status("github_private_backup", github), + _source_status("gitea_private_inventory_p0_scorecard", private_inventory), _source_status("p0_cicd_baseline_source_readiness", cicd_baseline), _source_status("gitea_ci_cd", gitea), _source_status("runtime_surface", runtime), @@ -610,19 +606,68 @@ def build_delivery_closure_workbench( "average_completion_percent": average_completion, "high_risk_blocker_count": high_risk_blocker_count, "runtime_execution_authorized": False, - "remote_write_authorized": github_boundaries.get("github_api_write_allowed") - is True, - "repo_creation_authorized": github_summary.get("repo_creation_authorized") - is True, - "visibility_change_authorized": github_summary.get( - "visibility_change_authorized" - ) - is True, - "refs_sync_authorized": github_summary.get("refs_sync_authorized") is True, - "workflow_trigger_authorized": github_summary.get( - "workflow_trigger_authorized" - ) - is True, + "remote_write_authorized": False, + "repo_creation_authorized": False, + "visibility_change_authorized": False, + "refs_sync_authorized": False, + "workflow_trigger_authorized": False, + "gitea_private_inventory_status": str(private_inventory.get("status") or ""), + "gitea_private_inventory_workplan_id": str( + private_inventory_readback.get("workplan_id") or "" + ), + "gitea_private_inventory_source": str( + private_inventory_readback.get("private_inventory_source") or "" + ), + "gitea_private_inventory_review_readiness_percent": _int( + private_inventory_rollups.get("review_readiness_percent") + ), + "gitea_private_inventory_active_blocker_count": private_inventory_blockers, + "gitea_private_inventory_repo_inventory_status": str( + private_inventory_rollups.get("gitea_repo_inventory_status") or "" + ), + "gitea_private_inventory_visibility_scope": str( + private_inventory_rollups.get("gitea_visibility_scope") or "" + ), + "gitea_private_inventory_public_repo_count": _int( + private_inventory_rollups.get("gitea_public_repo_count") + ), + "gitea_private_inventory_expected_product_count": _int( + private_inventory_rollups.get("expected_product_count") + ), + "gitea_private_inventory_present_product_row_count": _int( + private_inventory_rollups.get("present_product_row_count") + ), + "gitea_private_inventory_missing_product_row_count": _int( + private_inventory_rollups.get("missing_product_row_count") + ), + "gitea_private_inventory_accepted_payload_count": _int( + private_inventory_rollups.get("accepted_inventory_payload_count") + ), + "gitea_private_inventory_owner_coverage_attestation_received_count": _int( + private_inventory_rollups.get( + "owner_coverage_attestation_received_count" + ) + ), + "gitea_private_inventory_all_active_product_repos_have_owner_readiness_row": ( + private_inventory_rollups.get( + "all_active_product_repos_have_gitea_owner_readiness_row" + ) + is True + ), + "gitea_private_inventory_safe_next_step": str( + private_inventory_readback.get("safe_next_step") or "" + ), + "github_global_freeze_enabled": True, + "github_lane_status": str( + private_inventory_retired_github.get("status") + or "stopped_retired_do_not_use" + ), + "github_lane_excluded_from_p0_blocker_count": ( + private_inventory_rollups.get( + "github_lane_excluded_from_p0_blocker_count" + ) + is True + ), "p0_cicd_baseline_status": str(cicd_baseline.get("status") or ""), "p0_cicd_baseline_workplan_id": str( cicd_baseline_readback.get("workplan_id") or "" @@ -910,49 +955,21 @@ def build_delivery_closure_workbench( "backup_credential_escrow_forbidden_true_field_count": _int( backup_rollups.get("credential_escrow_forbidden_true_field_count") ), - "github_write_channel_ready": github_preflight.get( - "github_write_channel_ready" - ) - is True, - "github_account_status": str( - github_preflight.get("github_account_status") or "unknown" - ), - "github_account_suspended": github_preflight.get("github_account_suspended") - is True, - "github_api_forbidden_count": _int( - github_preflight.get("github_api_forbidden_count") - ), - "github_controlled_apply_ready_count": _int( - github_preflight.get("controlled_apply_ready_count") - ), - "github_blocked_preflight_target_count": github_preflight_blockers, - "github_operator_unblock_required": github_operator_unblock.get("required") - is True, - "github_operator_unblock_status": str( - github_operator_unblock.get("status") or "" - ), - "github_internal_governance_writeback_ready": github_governance_writeback.get( - "ready" - ) - is True, - "github_mcp_evidence_packet_count": _int( - github_governance_writeback.get("mcp_evidence_packet_count") - ), - "github_rag_evidence_ref_count": _int( - github_governance_writeback.get("rag_evidence_ref_count") - ), - "github_km_writeback_ready_count": _int( - github_governance_writeback.get("km_writeback_ready_count") - ), - "github_playbook_writeback_ready_count": _int( - github_governance_writeback.get("playbook_writeback_ready_count") - ), - "github_timeline_log_ready_count": _int( - github_governance_writeback.get("timeline_log_ready_count") - ), - "github_logbook_entry_count": _int( - github_governance_writeback.get("logbook_entry_count") - ), + "github_write_channel_ready": False, + "github_account_status": "stopped_retired_do_not_use", + "github_account_suspended": False, + "github_api_forbidden_count": 0, + "github_controlled_apply_ready_count": 0, + "github_blocked_preflight_target_count": 0, + "github_operator_unblock_required": False, + "github_operator_unblock_status": "github_global_freeze_stopped_retired_do_not_use", + "github_internal_governance_writeback_ready": False, + "github_mcp_evidence_packet_count": 0, + "github_rag_evidence_ref_count": 0, + "github_km_writeback_ready_count": 0, + "github_playbook_writeback_ready_count": 0, + "github_timeline_log_ready_count": 0, + "github_logbook_entry_count": 0, "secret_values_collected": False, }, "source_statuses": source_statuses, @@ -961,29 +978,27 @@ def build_delivery_closure_workbench( "operation_boundaries": { "read_only_api_allowed": True, "runtime_write_allowed": False, - "remote_write_allowed": github_boundaries.get("github_api_write_allowed") - is True, - "repo_creation_allowed": github_boundaries.get("repo_creation_allowed") - is True, - "visibility_change_allowed": github_boundaries.get( - "visibility_change_allowed" - ) - is True, - "refs_sync_allowed": github_boundaries.get("refs_sync_allowed") is True, - "workflow_trigger_allowed": github_boundaries.get( - "workflow_trigger_allowed" - ) - is True, + "remote_write_allowed": False, + "repo_creation_allowed": False, + "visibility_change_allowed": False, + "refs_sync_allowed": False, + "workflow_trigger_allowed": False, "production_deploy_trigger_allowed": _dict( production_deploy.get("operation_boundaries") ).get("deploy_trigger_allowed") is True, - "github_write_channel_ready": github_preflight.get( - "github_write_channel_ready" + "gitea_api_write_allowed": private_inventory_boundaries.get( + "gitea_api_write_allowed" ) is True, - "github_controlled_apply_allowed": github_preflight.get("preflight_ready") - is True, + "gitea_authenticated_inventory_import_execution_allowed": ( + private_inventory_boundaries.get( + "authenticated_inventory_import_execution_allowed" + ) + is True + ), + "github_write_channel_ready": False, + "github_controlled_apply_allowed": False, "secret_value_collection_allowed": False, "backup_restore_execution_allowed": False, "active_scan_allowed": False, @@ -1004,40 +1019,6 @@ def _source_status(source_id: str, payload: dict[str, Any]) -> dict[str, Any]: } -def _load_github_private_backup_evidence_gate() -> dict[str, Any]: - try: - from src.services.github_target_private_backup_evidence_gate import ( - load_latest_github_target_private_backup_evidence_gate, - ) - - return load_latest_github_target_private_backup_evidence_gate() - except ModuleNotFoundError as exc: - if exc.name != "src.services.github_target_private_backup_evidence_gate": - raise - return _missing_github_private_backup_source( - "service_module_missing_on_release_base" - ) - except FileNotFoundError: - return _missing_github_private_backup_source("snapshot_missing_on_release_base") - - -def _missing_github_private_backup_source(reason: str) -> dict[str, Any]: - return { - "schema_version": "missing_github_target_private_backup_evidence_gate_v1", - "generated_at": "", - "source_missing": True, - "missing_reason": reason, - "status": "blocked_github_private_backup_source_missing", - "next_action": "補上 GitHub 私有備援 evidence gate source 後重新回讀,不可把缺席來源標成已驗證。", - "summary": { - "approval_required_target_count": 0, - "private_backup_verified_count": 0, - "blocked_target_count": 1, - }, - "targets": [], - } - - def _tone(blocker_count: int, percent: int) -> str: if blocker_count > 0: return "danger" @@ -1074,15 +1055,6 @@ def _first_string(value: Any) -> str: return "" -def _first_target_action(value: Any) -> str: - if not isinstance(value, list): - return "" - for row in value: - if isinstance(row, dict) and row.get("approval_required") is True: - return str(row.get("next_action") or "") - return _first_row_action(value) - - def _first_contract_action(value: Any) -> str: if not isinstance(value, list): return "" diff --git a/apps/api/src/services/gitea_private_inventory_p0_scorecard.py b/apps/api/src/services/gitea_private_inventory_p0_scorecard.py new file mode 100644 index 000000000..74728ce35 --- /dev/null +++ b/apps/api/src/services/gitea_private_inventory_p0_scorecard.py @@ -0,0 +1,282 @@ +"""P0-003 Gitea-only private inventory scorecard readback. + +This loader promotes the committed Gitea private inventory scorecard into a +stable API contract. It does not call Gitea or GitHub, does not collect token +values, and does not write repository refs or workflow state. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from src.services.snapshot_paths import default_operations_dir + +_DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__)) +_SCORECARD_FILE = "awoooi-gitea-private-inventory-p0-scorecard.snapshot.json" +_SOURCE_SCHEMA_VERSION = "awoooi_gitea_private_inventory_p0_scorecard_v1" +_API_SCHEMA_VERSION = "gitea_private_inventory_p0_scorecard_readback_v1" + + +def load_latest_gitea_private_inventory_p0_scorecard( + operations_dir: Path | None = None, +) -> dict[str, Any]: + """Load and validate the committed P0-003 Gitea-only scorecard.""" + directory = operations_dir or _DEFAULT_OPERATIONS_DIR + path = directory / _SCORECARD_FILE + with path.open(encoding="utf-8") as handle: + scorecard = json.load(handle) + + if not isinstance(scorecard, dict): + raise ValueError(f"{path}: expected JSON object") + _require_source_scorecard(scorecard, str(path)) + payload = _build_payload(scorecard, path) + _require_operation_boundaries(payload, str(path)) + _require_rollup_consistency(payload, str(path)) + return payload + + +def _build_payload(scorecard: dict[str, Any], path: Path) -> dict[str, Any]: + gitea_inventory = _dict(scorecard.get("gitea_inventory")) + import_acceptance = _dict(scorecard.get("authenticated_import_acceptance")) + coverage_attestation = _dict(scorecard.get("coverage_attestation")) + product_coverage = _dict(scorecard.get("product_row_coverage")) + active_blockers = _strings(scorecard.get("active_blockers")) + exit_criteria = _strings(scorecard.get("exit_criteria")) + met_criteria = _met_exit_criteria(scorecard, gitea_inventory, product_coverage) + review_readiness_percent = _percent( + (len(met_criteria) / max(len(exit_criteria), 1)) * 100 + ) + + return { + "schema_version": _API_SCHEMA_VERSION, + "generated_at": str(scorecard.get("generated_at") or ""), + "priority": "P0-003", + "scope": "gitea_private_inventory_p0_scorecard", + "status": str(scorecard.get("status") or "unknown"), + "readback": { + "workplan_id": str(scorecard.get("workplan_id") or "P0-003"), + "workplan_title": "取得 Gitea private inventory 權限", + "source_scorecard_ref": f"docs/operations/{path.name}", + "source_control_authority": str( + scorecard.get("source_control_authority") or "gitea" + ), + "private_inventory_source": str( + scorecard.get("private_inventory_source") or "gitea" + ), + "safe_next_step": str(scorecard.get("safe_next_step") or ""), + }, + "gitea_inventory": gitea_inventory, + "authenticated_import_acceptance": import_acceptance, + "coverage_attestation": coverage_attestation, + "product_row_coverage": product_coverage, + "active_blockers": active_blockers, + "exit_criteria": exit_criteria, + "met_exit_criteria": met_criteria, + "rollups": { + "active_blocker_count": len(active_blockers), + "review_readiness_percent": review_readiness_percent, + "gitea_repo_inventory_status": str( + gitea_inventory.get("status") or "unknown" + ), + "gitea_visibility_scope": str( + gitea_inventory.get("visibility_scope") or "unknown" + ), + "gitea_public_repo_count": _int(gitea_inventory.get("repo_count")), + "accepted_inventory_payload_count": _int( + import_acceptance.get("accepted_payload_count") + ), + "owner_coverage_attestation_received_count": _int( + coverage_attestation.get("received_attestation_count") + ), + "owner_coverage_attestation_accepted_count": _int( + coverage_attestation.get("accepted_attestation_count") + ), + "expected_product_count": _int( + product_coverage.get("expected_product_count") + ), + "present_product_row_count": _int( + product_coverage.get("present_product_row_count") + ), + "missing_product_row_count": _int( + product_coverage.get("missing_product_row_count") + ), + "ready_product_count": _int(product_coverage.get("ready_product_count")), + "blocked_product_count": _int( + product_coverage.get("blocked_product_count") + ), + "internal_or_authenticated_inventory_required_count": _int( + product_coverage.get( + "internal_or_authenticated_inventory_required_count" + ) + ), + "all_active_product_repos_have_gitea_owner_readiness_row": ( + product_coverage.get( + "all_active_product_repos_have_gitea_owner_readiness_row" + ) + is True + ), + "github_lane_excluded_from_p0_blocker_count": ( + scorecard.get("github_lane_excluded_from_p0_blocker_count") + is True + ), + "github_api_used": scorecard.get("github_api_used") is True, + "github_cli_used": scorecard.get("github_cli_used") is True, + "github_primary_switch_authorized": ( + scorecard.get("github_primary_switch_authorized") is True + ), + }, + "operation_boundaries": { + "read_only_api_allowed": True, + "gitea_api_write_allowed": False, + "gitea_repo_creation_allowed": False, + "gitea_refs_sync_allowed": False, + "gitea_visibility_change_allowed": False, + "gitea_workflow_trigger_allowed": False, + "authenticated_inventory_import_execution_allowed": False, + "owner_attestation_write_allowed": False, + "token_value_collection_allowed": False, + "secret_value_collection_allowed": False, + "github_api_allowed": False, + "github_cli_allowed": False, + "github_repo_creation_allowed": False, + "github_refs_sync_allowed": False, + "github_primary_switch_allowed": False, + "raw_session_or_sqlite_read_allowed": False, + }, + "github_retired_context": { + "status": str(scorecard.get("github_status") or "stopped_retired_do_not_use"), + "excluded_from_active_p0_blocker_math": ( + scorecard.get("github_lane_excluded_from_p0_blocker_count") + is True + ), + "api_used": scorecard.get("github_api_used") is True, + "cli_used": scorecard.get("github_cli_used") is True, + "primary_switch_authorized": ( + scorecard.get("github_primary_switch_authorized") is True + ), + }, + } + + +def _require_source_scorecard(payload: dict[str, Any], label: str) -> None: + if payload.get("schema_version") != _SOURCE_SCHEMA_VERSION: + raise ValueError(f"{label}: schema_version must be {_SOURCE_SCHEMA_VERSION}") + if payload.get("source_control_authority") != "gitea": + raise ValueError(f"{label}: source_control_authority must be gitea") + if payload.get("private_inventory_source") != "gitea": + raise ValueError(f"{label}: private_inventory_source must be gitea") + if payload.get("github_status") != "stopped_retired_do_not_use": + raise ValueError(f"{label}: github_status must be stopped_retired_do_not_use") + if payload.get("github_lane_excluded_from_p0_blocker_count") is not True: + raise ValueError(f"{label}: github lane must be excluded from P0 blocker math") + if payload.get("github_api_used") is not False: + raise ValueError(f"{label}: github_api_used must remain false") + if payload.get("github_cli_used") is not False: + raise ValueError(f"{label}: github_cli_used must remain false") + if payload.get("github_primary_switch_authorized") is not False: + raise ValueError(f"{label}: github_primary_switch_authorized must remain false") + + import_acceptance = _dict(payload.get("authenticated_import_acceptance")) + if import_acceptance.get("token_value_collection_allowed") is not False: + raise ValueError(f"{label}: token value collection must remain false") + if import_acceptance.get("execution_authorized") is not False: + raise ValueError(f"{label}: import execution must remain false") + coverage_attestation = _dict(payload.get("coverage_attestation")) + if coverage_attestation.get("execution_authorized") is not False: + raise ValueError(f"{label}: attestation write must remain false") + + +def _require_operation_boundaries(payload: dict[str, Any], label: str) -> None: + boundaries = _dict(payload.get("operation_boundaries")) + if boundaries.get("read_only_api_allowed") is not True: + raise ValueError(f"{label}: read_only_api_allowed must be true") + blocked_flags = { + "gitea_api_write_allowed", + "gitea_repo_creation_allowed", + "gitea_refs_sync_allowed", + "gitea_visibility_change_allowed", + "gitea_workflow_trigger_allowed", + "authenticated_inventory_import_execution_allowed", + "owner_attestation_write_allowed", + "token_value_collection_allowed", + "secret_value_collection_allowed", + "github_api_allowed", + "github_cli_allowed", + "github_repo_creation_allowed", + "github_refs_sync_allowed", + "github_primary_switch_allowed", + "raw_session_or_sqlite_read_allowed", + } + allowed = sorted(flag for flag in blocked_flags if boundaries.get(flag) is not False) + if allowed: + raise ValueError(f"{label}: operation boundaries must remain false: {allowed}") + + +def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None: + rollups = _dict(payload.get("rollups")) + blockers = _strings(payload.get("active_blockers")) + if rollups.get("active_blocker_count") != len(blockers): + raise ValueError(f"{label}: active_blocker_count mismatch") + if rollups.get("github_lane_excluded_from_p0_blocker_count") is not True: + raise ValueError(f"{label}: github lane exclusion must remain true") + forbidden_true = [ + "github_api_used", + "github_cli_used", + "github_primary_switch_authorized", + ] + unexpected_true = [field for field in forbidden_true if rollups.get(field) is True] + if unexpected_true: + raise ValueError(f"{label}: GitHub forbidden flags true: {unexpected_true}") + if rollups.get("missing_product_row_count") != 0: + raise ValueError(f"{label}: active product readiness rows must remain complete") + if ( + rollups.get("all_active_product_repos_have_gitea_owner_readiness_row") + is not True + ): + raise ValueError(f"{label}: active product owner readiness rows incomplete") + + +def _met_exit_criteria( + scorecard: dict[str, Any], + gitea_inventory: dict[str, Any], + product_coverage: dict[str, Any], +) -> list[str]: + met: list[str] = [] + if scorecard.get("private_inventory_source") == "gitea": + met.append("private_inventory_source=gitea") + if scorecard.get("github_lane_excluded_from_p0_blocker_count") is True: + met.append("github_lane_excluded_from_p0_blocker_count=true") + if gitea_inventory.get("status") == "ok": + met.append("gitea_repo_inventory.status=ok") + if gitea_inventory.get("visibility_scope") in {"authenticated", "admin_export"}: + met.append("gitea_repo_inventory.visibility_scope in authenticated/admin_export") + if ( + product_coverage.get("all_active_product_repos_have_gitea_owner_readiness_row") + is True + ): + met.append("all_active_product_repos_have_gitea_owner_readiness_row=true") + return met + + +def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + +def _strings(value: Any) -> list[str]: + if not isinstance(value, list): + return [] + return [str(item) for item in value if item is not None] + + +def _int(value: Any) -> int: + if isinstance(value, bool): + return int(value) + if isinstance(value, int | float): + return int(value) + return 0 + + +def _percent(value: Any) -> int: + return max(0, min(100, round(float(value or 0)))) diff --git a/apps/api/tests/test_delivery_closure_workbench_api.py b/apps/api/tests/test_delivery_closure_workbench_api.py index d94dd0dec..ff2a5e7fb 100644 --- a/apps/api/tests/test_delivery_closure_workbench_api.py +++ b/apps/api/tests/test_delivery_closure_workbench_api.py @@ -4,9 +4,7 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from src.api.v1.agents import router -from src.services.awoooi_production_deploy_readback_blocker import ( - load_latest_awoooi_production_deploy_readback_blocker, -) +from src.services.delivery_closure_workbench import load_delivery_closure_workbench def test_delivery_closure_workbench_endpoint_returns_product_summary(): @@ -18,637 +16,125 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): assert response.status_code == 200 data = response.json() + _assert_delivery_workbench_shape(data) + + +def test_delivery_closure_workbench_uses_gitea_private_inventory_lane(): + payload = load_delivery_closure_workbench() + + _assert_delivery_workbench_shape(payload) + lanes = {lane["id"]: lane for lane in payload["lanes"]} + sources = {source["id"]: source for source in payload["source_statuses"]} + + assert "github" not in lanes + assert "github_private_backup" not in sources + assert lanes["gitea_private_inventory"]["source_id"] == ( + "gitea_private_inventory_p0_scorecard" + ) + assert lanes["gitea_private_inventory"]["blocker_count"] == 4 + assert lanes["gitea_private_inventory"]["completion_percent"] == 60 + assert lanes["gitea_private_inventory"]["metric"]["kind"] == "private_inventory" + assert lanes["gitea_private_inventory"]["metric"]["workplan_id"] == "P0-003" + assert lanes["gitea_private_inventory"]["metric"]["private_inventory_source"] == "gitea" + assert lanes["gitea_private_inventory"]["metric"]["gitea_repo_inventory_status"] == "partial" + assert lanes["gitea_private_inventory"]["metric"]["gitea_visibility_scope"] == "public_only" + assert lanes["gitea_private_inventory"]["metric"]["expected_product_count"] == 11 + assert lanes["gitea_private_inventory"]["metric"]["present_product_row_count"] == 11 + assert lanes["gitea_private_inventory"]["metric"]["missing_product_row_count"] == 0 + assert ( + lanes["gitea_private_inventory"]["metric"][ + "github_lane_excluded_from_p0_blocker_count" + ] + is True + ) + assert "gitea_authenticated_inventory_payload_not_accepted" in lanes[ + "gitea_private_inventory" + ]["metric"]["active_blockers"] + assert lanes["gitea_private_inventory"]["next_action"] == ( + "obtain_gitea_authenticated_or_admin_export_redacted_inventory_payload_" + "then_validate_import_acceptance_and_owner_attestation" + ) + + +def _assert_delivery_workbench_shape(data: dict): assert data["schema_version"] == "delivery_closure_workbench_v1" assert data["summary"]["source_count"] == 7 assert data["summary"]["loaded_source_count"] == 7 assert data["summary"]["runtime_execution_authorized"] is False - assert data["summary"]["remote_write_authorized"] is True - assert data["summary"]["repo_creation_authorized"] is True - assert data["summary"]["visibility_change_authorized"] is True - assert data["summary"]["refs_sync_authorized"] is True - assert data["summary"]["workflow_trigger_authorized"] is True + assert data["summary"]["remote_write_authorized"] is False + assert data["summary"]["repo_creation_authorized"] is False + assert data["summary"]["visibility_change_authorized"] is False + assert data["summary"]["refs_sync_authorized"] is False + assert data["summary"]["workflow_trigger_authorized"] is False + assert data["summary"]["github_global_freeze_enabled"] is True + assert data["summary"]["github_lane_status"] == "stopped_retired_do_not_use" + assert data["summary"]["github_lane_excluded_from_p0_blocker_count"] is True + assert data["summary"]["github_blocked_preflight_target_count"] == 0 + assert data["summary"]["github_operator_unblock_required"] is False + assert data["summary"]["gitea_private_inventory_status"] == ( + "blocked_waiting_gitea_authenticated_or_owner_export_inventory" + ) + assert data["summary"]["gitea_private_inventory_workplan_id"] == "P0-003" + assert data["summary"]["gitea_private_inventory_source"] == "gitea" + assert data["summary"]["gitea_private_inventory_review_readiness_percent"] == 60 + assert data["summary"]["gitea_private_inventory_active_blocker_count"] == 4 + assert data["summary"]["gitea_private_inventory_repo_inventory_status"] == "partial" + assert data["summary"]["gitea_private_inventory_visibility_scope"] == "public_only" + assert data["summary"]["gitea_private_inventory_expected_product_count"] == 11 + assert data["summary"]["gitea_private_inventory_present_product_row_count"] == 11 + assert data["summary"]["gitea_private_inventory_missing_product_row_count"] == 0 + assert data["summary"]["gitea_private_inventory_accepted_payload_count"] == 0 + assert ( + data["summary"][ + "gitea_private_inventory_owner_coverage_attestation_received_count" + ] + == 0 + ) + assert ( + data["summary"][ + "gitea_private_inventory_all_active_product_repos_have_owner_readiness_row" + ] + is True + ) assert data["summary"]["p0_cicd_baseline_status"] == ( "ready_for_template_copy_apply_gate" ) - assert data["summary"]["p0_cicd_baseline_workplan_id"] == "P0-004" assert data["summary"]["p0_cicd_baseline_source_readiness_percent"] == 100 - assert data["summary"]["p0_cicd_baseline_required_source_count"] == 11 - assert data["summary"]["p0_cicd_baseline_present_required_source_count"] == 11 - assert data["summary"]["p0_cicd_baseline_missing_required_source_count"] == 0 - assert data["summary"]["p0_cicd_baseline_blocked_source_ids"] == [] - assert data["summary"]["p0_cicd_baseline_safe_next_step"] == ( - "open_template_copy_apply_gate_after_source_readiness_green" - ) - assert data["summary"]["production_deploy_status"] == ( - "closure_verified" - ) - assert data["summary"]["production_deploy_source_control_main_ready"] is True + assert data["summary"]["production_deploy_status"] == "closure_verified" assert data["summary"]["production_deploy_image_tag_matches_main"] is True - assert data["summary"]["production_deploy_governance_fields_present"] is True - assert ( - data["summary"]["production_deploy_authorized_dispatch_channel_ready"] - is False - ) - assert data["summary"]["production_deploy_manual_run_button_visible"] is False - assert data["summary"]["production_deploy_gitea_sign_in_required"] is True - assert ( - data["summary"]["production_deploy_dispatch_without_token_http_status"] - == 401 - ) - assert ( - data["summary"]["production_deploy_dispatch_without_token_message"] - == "token is required" - ) - assert data["summary"]["production_deploy_hard_blocker_count"] == 0 - assert data["summary"]["production_deploy_latest_visible_cd_run_id"] == "3863" - assert data["summary"]["production_deploy_latest_visible_cd_run_status"] == ( - "Success" - ) - assert ( - data["summary"]["production_deploy_latest_visible_cd_run_jobs_total_count"] - == 0 - ) - assert ( - data["summary"][ - "production_deploy_gitea_actions_list_without_token_http_status" - ] - == 401 - ) - assert ( - data["summary"]["production_deploy_gitea_actions_list_without_token_message"] - == "token is required" - ) - assert ( - data["summary"]["production_deploy_latest_visible_waiting_runner_run_id"] - == "" - ) - assert ( - data["summary"]["production_deploy_latest_visible_waiting_runner_workflow"] - == "" - ) - assert ( - data["summary"]["production_deploy_latest_visible_waiting_runner_kind"] - == "" - ) - assert ( - data["summary"]["production_deploy_latest_visible_waiting_runner_status"] - == "" - ) - assert ( - data["summary"]["production_deploy_latest_visible_waiting_runner_label"] - == "" - ) - assert ( - data["summary"][ - "production_deploy_public_actions_queue_readback_schema_version" - ] - == "awoooi_public_gitea_actions_queue_readback_v1" - ) - assert ( - data["summary"]["production_deploy_public_actions_queue_readback_verifier"] - == "ops/runner/read-public-gitea-actions-queue.py --json" - ) - assert ( - data["summary"][ - "production_deploy_non110_runner_cd_closure_verifier_schema_version" - ] - == "awoooi_non110_cd_closure_verifier_v1" - ) - assert ( - data["summary"]["production_deploy_non110_runner_cd_closure_status"] - == "closure_verified" - ) - assert ( - data["summary"]["production_deploy_non110_runner_cd_closure_required"] - is True - ) - assert ( - "ops/runner/verify-awoooi-non110-cd-closure.py" - in data["summary"]["production_deploy_non110_runner_cd_closure_verifier"] - ) - assert ( - data["summary"][ - "production_deploy_non110_runner_cd_closure_ordered_step_count" - ] - == 6 - ) - assert ( - data["summary"][ - "production_deploy_non110_runner_cd_closure_ordered_completed_prefix_count" - ] - == 6 - ) - assert ( - data["summary"][ - "production_deploy_non110_runner_cd_closure_evidence_completed_step_count" - ] - == 6 - ) - assert ( - data["summary"][ - "production_deploy_non110_runner_cd_closure_ordered_completion_percent" - ] - == 100 - ) - assert ( - data["summary"][ - "production_deploy_non110_runner_cd_closure_evidence_completion_percent" - ] - == 100 - ) - assert ( - data["summary"][ - "production_deploy_non110_runner_cd_closure_next_blocked_step_index" - ] - == 0 - ) - assert ( - data["summary"][ - "production_deploy_non110_runner_cd_closure_next_blocked_step_id" - ] - == "" - ) - assert data["summary"][ - "production_deploy_non110_runner_cd_closure_next_blocked_step_action" - ] == "" - assert data["summary"]["production_deploy_non110_runner_ready"] is True - assert ( - data["summary"][ - "production_deploy_non110_runner_prepare_only_source_ready" - ] - is True - ) - assert ( - data["summary"][ - "production_deploy_non110_runner_safe_registration_helper_ready" - ] - is True - ) - assert ( - data["summary"]["production_deploy_non110_runner_workflow_labels_aligned"] - is True - ) - assert ( - data["summary"]["production_deploy_non110_runner_host_label"] - == "awoooi-non110-host" - ) - assert ( - data["summary"]["production_deploy_non110_runner_ubuntu_label"] - == "awoooi-non110-ubuntu" - ) - assert ( - data["summary"]["production_deploy_non110_runner_online_label_match"] - is True - ) - assert ( - data["summary"]["production_deploy_non110_runner_autostart_path_armed"] - is True - ) - assert ( - data["summary"][ - "production_deploy_non110_runner_ready_autostart_path_count" - ] - == 1 - ) - assert ( - data["summary"][ - "production_deploy_non110_runner_registration_condition_required" - ] - is True - ) - assert data["summary"]["production_deploy_non110_runner_ready_config_count"] == 1 - assert data["summary"]["production_deploy_non110_runner_ready_service_count"] == 1 - assert ( - data["summary"]["production_deploy_non110_runner_ready_registration_count"] - == 1 - ) - assert data["summary"]["production_deploy_non110_runner_safe_next_step"] == "" - assert ( - data["summary"]["production_deploy_non110_runner_remaining_blocker_count"] - == 0 - ) - assert ( - data["summary"][ - "backup_credential_escrow_intake_scorecard_schema_version" - ] - == "awoooi_post_reboot_credential_escrow_intake_scorecard_v1" - ) - assert ( - "scripts/reboot-recovery/post-reboot-credential-escrow-intake-scorecard.py" - in data["summary"]["backup_credential_escrow_intake_scorecard_verifier"] - ) assert data["summary"]["backup_credential_escrow_intake_status"] == ( "blocked_waiting_non_secret_credential_escrow_evidence" ) - assert data["summary"]["backup_credential_escrow_active_gate_present"] is True - assert data["summary"]["backup_credential_escrow_preflight_status"] == ( - "blocked_waiting_owner_response_content" - ) assert data["summary"]["backup_credential_escrow_required_item_count"] == 5 assert data["summary"]["backup_credential_escrow_effective_missing_count"] == 5 - assert ( - data["summary"]["backup_credential_escrow_owner_response_received_count"] - == 0 - ) - assert ( - data["summary"]["backup_credential_escrow_owner_response_accepted_count"] - == 0 - ) - assert data["summary"]["backup_credential_escrow_runtime_gate_count"] == 0 - assert ( - data["summary"]["backup_credential_escrow_secret_value_collection_allowed"] - is False - ) + assert data["summary"]["backup_credential_escrow_secret_value_collection_allowed"] is False assert data["summary"]["backup_credential_marker_write_authorized_count"] == 0 - assert data["summary"]["backup_credential_escrow_forbidden_true_field_count"] == 0 - assert data["summary"]["github_write_channel_ready"] is False - assert data["summary"]["github_account_status"] == "suspended" - assert data["summary"]["github_account_suspended"] is True - assert data["summary"]["github_api_forbidden_count"] == 6 - assert data["summary"]["github_controlled_apply_ready_count"] == 0 - assert data["summary"]["github_blocked_preflight_target_count"] == 5 - assert data["summary"]["github_operator_unblock_required"] is True - assert data["summary"]["github_operator_unblock_status"] == ( - "github_account_suspended_external_action_required" - ) - assert data["summary"]["github_internal_governance_writeback_ready"] is True - assert data["summary"]["github_mcp_evidence_packet_count"] == 1 - assert data["summary"]["github_rag_evidence_ref_count"] == 5 - assert data["summary"]["github_km_writeback_ready_count"] == 1 - assert data["summary"]["github_playbook_writeback_ready_count"] == 1 - assert data["summary"]["github_timeline_log_ready_count"] == 1 - assert data["summary"]["github_logbook_entry_count"] == 1 assert data["summary"]["secret_values_collected"] is False - assert data["summary"]["average_completion_percent"] >= 0 - assert data["summary"]["high_risk_blocker_count"] > 0 - lanes = {lane["id"]: lane for lane in data["lanes"]} - sources = {source["id"]: source for source in data["source_statuses"]} - assert sorted(lanes) == [ - "backup", + lane_ids = {lane["id"] for lane in data["lanes"]} + assert lane_ids == { + "release", + "production_deploy", + "gitea_private_inventory", "cicd_baseline", "gitea", - "github", - "production_deploy", - "release", "runtime", - ] - assert lanes["release"]["metric"]["kind"] == "blocked_gate" - assert lanes["release"]["status"] == "controlled_status_cleanup_package_ready" - assert lanes["release"]["blocker_count"] == 0 - assert lanes["release"]["metric"]["blocked"] == 0 - assert lanes["release"]["metric"]["total"] == 5 - assert lanes["production_deploy"]["metric"]["kind"] == "deploy_readback" - assert lanes["production_deploy"]["status"] == "closure_verified" - assert lanes["production_deploy"]["blocker_count"] == 0 - assert lanes["production_deploy"]["metric"][ - "observed_source_control_main_short_sha" - ] == "a70c6756d9" - assert lanes["production_deploy"]["metric"][ - "production_image_tag_short_sha" - ] == "a70c6756d9" + "backup", + } + assert data["operation_boundaries"]["read_only_api_allowed"] is True + assert data["operation_boundaries"]["runtime_write_allowed"] is False + assert data["operation_boundaries"]["remote_write_allowed"] is False + assert data["operation_boundaries"]["repo_creation_allowed"] is False + assert data["operation_boundaries"]["visibility_change_allowed"] is False + assert data["operation_boundaries"]["refs_sync_allowed"] is False + assert data["operation_boundaries"]["workflow_trigger_allowed"] is False + assert data["operation_boundaries"]["gitea_api_write_allowed"] is False assert ( - lanes["production_deploy"]["metric"]["production_image_tag_matches_main"] - is True - ) - assert lanes["production_deploy"]["metric"]["current_main_cd_run_visible"] is True - assert ( - lanes["production_deploy"]["metric"]["authorized_dispatch_channel_ready"] - is False - ) - assert lanes["production_deploy"]["metric"]["manual_run_button_visible"] is False - assert lanes["production_deploy"]["metric"]["gitea_sign_in_required"] is True - assert ( - lanes["production_deploy"]["metric"][ - "dispatch_without_token_http_status" - ] - == 401 - ) - assert ( - lanes["production_deploy"]["metric"]["dispatch_without_token_message"] - == "token is required" - ) - assert lanes["production_deploy"]["metric"]["latest_visible_cd_run_id"] == "3863" - assert ( - lanes["production_deploy"]["metric"]["latest_visible_cd_run_status"] - == "Success" - ) - assert ( - lanes["production_deploy"]["metric"][ - "latest_visible_cd_run_jobs_total_count" - ] - == 0 - ) - assert ( - lanes["production_deploy"]["metric"][ - "gitea_actions_list_without_token_http_status" - ] - == 401 - ) - assert ( - lanes["production_deploy"]["metric"][ - "gitea_actions_list_without_token_message" - ] - == "token is required" - ) - assert ( - lanes["production_deploy"]["metric"]["latest_visible_waiting_runner_run_id"] - == "" - ) - assert ( - lanes["production_deploy"]["metric"][ - "latest_visible_waiting_runner_workflow" - ] - == "" - ) - assert ( - lanes["production_deploy"]["metric"]["latest_visible_waiting_runner_kind"] - == "" - ) - assert ( - lanes["production_deploy"]["metric"][ - "latest_visible_waiting_runner_status" - ] - == "" - ) - assert ( - lanes["production_deploy"]["metric"]["latest_visible_waiting_runner_label"] - == "" - ) - assert ( - lanes["production_deploy"]["metric"][ - "public_actions_queue_readback_schema_version" - ] - == "awoooi_public_gitea_actions_queue_readback_v1" - ) - assert ( - lanes["production_deploy"]["metric"]["public_actions_queue_readback_verifier"] - == "ops/runner/read-public-gitea-actions-queue.py --json" - ) - assert ( - lanes["production_deploy"]["metric"][ - "non110_runner_cd_closure_verifier_schema_version" - ] - == "awoooi_non110_cd_closure_verifier_v1" - ) - assert ( - lanes["production_deploy"]["metric"]["non110_runner_cd_closure_status"] - == "closure_verified" - ) - assert ( - lanes["production_deploy"]["metric"]["non110_runner_cd_closure_required"] - is True - ) - assert ( - "ops/runner/verify-awoooi-non110-cd-closure.py" - in lanes["production_deploy"]["metric"]["non110_runner_cd_closure_verifier"] - ) - assert ( - lanes["production_deploy"]["metric"][ - "non110_runner_cd_closure_ordered_step_count" - ] - == 6 - ) - assert ( - lanes["production_deploy"]["metric"][ - "non110_runner_cd_closure_ordered_completed_prefix_count" - ] - == 6 - ) - assert ( - lanes["production_deploy"]["metric"][ - "non110_runner_cd_closure_evidence_completed_step_count" - ] - == 6 - ) - assert ( - lanes["production_deploy"]["metric"][ - "non110_runner_cd_closure_ordered_completion_percent" - ] - == 100 - ) - assert ( - lanes["production_deploy"]["metric"][ - "non110_runner_cd_closure_evidence_completion_percent" - ] - == 100 - ) - assert ( - lanes["production_deploy"]["metric"][ - "non110_runner_cd_closure_next_blocked_step_index" - ] - == 0 - ) - assert ( - lanes["production_deploy"]["metric"][ - "non110_runner_cd_closure_next_blocked_step_id" - ] - == "" - ) - assert lanes["production_deploy"]["metric"][ - "non110_runner_cd_closure_next_blocked_step_action" - ] == "" - assert lanes["production_deploy"]["metric"]["non110_runner_ready"] is True - assert ( - lanes["production_deploy"]["metric"][ - "non110_runner_prepare_only_source_ready" - ] - is True - ) - assert ( - lanes["production_deploy"]["metric"][ - "non110_runner_safe_registration_helper_ready" - ] - is True - ) - assert ( - lanes["production_deploy"]["metric"][ - "non110_runner_workflow_labels_aligned" - ] - is True - ) - assert ( - lanes["production_deploy"]["metric"]["non110_runner_host_label"] - == "awoooi-non110-host" - ) - assert ( - lanes["production_deploy"]["metric"]["non110_runner_ubuntu_label"] - == "awoooi-non110-ubuntu" - ) - assert ( - lanes["production_deploy"]["metric"]["non110_runner_online_label_match"] - is True - ) - assert ( - lanes["production_deploy"]["metric"]["non110_runner_autostart_path_armed"] - is True - ) - assert ( - lanes["production_deploy"]["metric"][ - "non110_runner_ready_autostart_path_count" - ] - == 1 - ) - assert ( - lanes["production_deploy"]["metric"][ - "non110_runner_registration_condition_required" - ] - is True - ) - assert lanes["production_deploy"]["metric"]["non110_runner_ready_config_count"] == 1 - assert lanes["production_deploy"]["metric"]["non110_runner_ready_service_count"] == 1 - assert ( - lanes["production_deploy"]["metric"][ - "non110_runner_ready_registration_count" - ] - == 1 - ) - assert lanes["production_deploy"]["metric"]["non110_runner_safe_next_step"] == "" - assert ( - lanes["production_deploy"]["metric"][ - "non110_runner_remaining_blocker_count" - ] - == 0 - ) - assert lanes["github"]["metric"]["kind"] == "private_backup_verified" - assert lanes["cicd_baseline"]["metric"]["kind"] == "source_readiness" - assert lanes["cicd_baseline"]["status"] == "ready_for_template_copy_apply_gate" - assert lanes["cicd_baseline"]["blocker_count"] == 0 - assert lanes["cicd_baseline"]["completion_percent"] == 100 - assert lanes["cicd_baseline"]["metric"]["workplan_id"] == "P0-004" - assert lanes["cicd_baseline"]["metric"]["required_source_count"] == 11 - assert lanes["cicd_baseline"]["metric"]["present_required_source_count"] == 11 - assert lanes["cicd_baseline"]["metric"]["missing_required_source_count"] == 0 - assert lanes["cicd_baseline"]["metric"]["blocked_source_ids"] == [] - assert ( - lanes["cicd_baseline"]["metric"]["workflow_modification_allowed"] - is False - ) - assert lanes["cicd_baseline"]["metric"]["workflow_trigger_allowed"] is False - assert lanes["gitea"]["metric"]["kind"] == "workflow_count" - assert lanes["runtime"]["metric"]["kind"] == "surface_count" - assert lanes["backup"]["metric"]["kind"] == "readiness_row_count" - assert ( - lanes["backup"]["metric"][ - "credential_escrow_intake_scorecard_schema_version" - ] - == "awoooi_post_reboot_credential_escrow_intake_scorecard_v1" - ) - assert lanes["backup"]["metric"]["credential_escrow_intake_status"] == ( - "blocked_waiting_non_secret_credential_escrow_evidence" - ) - assert lanes["backup"]["metric"]["credential_escrow_active_gate_present"] is True - assert ( - lanes["backup"]["metric"]["credential_escrow_effective_missing_count"] - == 5 - ) - assert ( - lanes["backup"]["metric"]["credential_escrow_owner_response_received_count"] - == 0 - ) - assert ( - lanes["backup"]["metric"]["credential_escrow_owner_response_accepted_count"] - == 0 - ) - assert lanes["backup"]["metric"]["credential_escrow_runtime_gate_count"] == 0 - assert ( - lanes["backup"]["metric"][ - "credential_escrow_secret_value_collection_allowed" + data["operation_boundaries"][ + "gitea_authenticated_inventory_import_execution_allowed" ] is False ) - assert lanes["backup"]["metric"]["credential_marker_write_authorized_count"] == 0 - assert lanes["backup"]["metric"]["credential_escrow_forbidden_true_field_count"] == 0 - assert sources["github_private_backup"]["loaded"] is True - assert sources["p0_cicd_baseline_source_readiness"]["loaded"] is True - assert sources["production_deploy_readback"]["loaded"] is True - assert ( - sources["production_deploy_readback"]["schema_version"] - == "awoooi_production_deploy_readback_blocker_v1" - ) - assert sources["production_deploy_readback"]["missing_reason"] == "" - assert ( - sources["github_private_backup"]["schema_version"] - == "github_target_private_backup_evidence_gate_v1" - ) - assert ( - sources["p0_cicd_baseline_source_readiness"]["schema_version"] - == "p0_cicd_baseline_source_readiness_v1" - ) - assert ( - sources["p0_cicd_baseline_source_readiness"]["missing_reason"] - == "" - ) - assert sources["github_private_backup"]["missing_reason"] == "" - assert lanes["github"]["blocker_count"] == 5 - assert ( - lanes["github"]["status"] - == "blocked_github_account_suspended_and_write_channel_required" - ) - assert lanes["github"]["metric"]["verified"] == 4 - assert lanes["github"]["metric"]["total"] == 9 - assert lanes["github"]["metric"]["controlled_apply_ready"] == 0 - assert lanes["github"]["metric"]["blocked_preflight"] == 5 - assert lanes["github"]["metric"]["write_channel_ready"] is False - assert lanes["github"]["metric"]["github_account_status"] == "suspended" - assert lanes["github"]["metric"]["github_account_suspended"] is True - assert lanes["github"]["metric"]["internal_governance_ready"] is True - assert lanes["github"]["metric"]["km_writeback_ready"] == 1 - assert lanes["github"]["metric"]["playbook_writeback_ready"] == 1 - assert lanes["github"]["operator_unblock"]["required"] is True - assert lanes["github"]["operator_unblock"]["status"] == ( - "github_account_suspended_external_action_required" - ) - assert ( - "complete_github_account_suspension_appeal_or_provide_authorized_writable_namespace" - in lanes["github"]["operator_unblock"]["required_actions"] - ) - assert lanes["github"]["internal_governance_writeback"]["ready"] is True - assert ( - lanes["github"]["internal_governance_writeback"][ - "mcp_evidence_packet_count" - ] - == 1 - ) - assert ( - lanes["github"]["internal_governance_writeback"]["github_write_performed"] - is False - ) - assert lanes["github"]["next_action"] == ( - "complete_github_account_suspension_appeal_or_provide_authorized_writable_namespace" - ) - assert all(0 <= lane["completion_percent"] <= 100 for lane in lanes.values()) - assert all(lane["tone"] in {"ok", "warn", "danger"} for lane in lanes.values()) - - boundaries = data["operation_boundaries"] - assert boundaries["read_only_api_allowed"] is True - assert boundaries["runtime_write_allowed"] is False - assert boundaries["remote_write_allowed"] is True - assert boundaries["repo_creation_allowed"] is True - assert boundaries["visibility_change_allowed"] is True - assert boundaries["refs_sync_allowed"] is True - assert boundaries["workflow_trigger_allowed"] is True - assert boundaries["production_deploy_trigger_allowed"] is False - assert boundaries["github_write_channel_ready"] is False - assert boundaries["github_controlled_apply_allowed"] is False - assert boundaries["secret_value_collection_allowed"] is False - assert boundaries["backup_restore_execution_allowed"] is False - assert boundaries["active_scan_allowed"] is False - - assert "192.168.0." not in response.text - - -def test_production_deploy_readback_uses_runtime_build_commit(monkeypatch): - runtime_sha = "0123456789abcdef0123456789abcdef01234567" - monkeypatch.setenv("AWOOOI_BUILD_COMMIT_SHA", runtime_sha) - - payload = load_latest_awoooi_production_deploy_readback_blocker() - readback = payload["readback"] - rollups = payload["rollups"] - - assert readback["runtime_build_commit_sha"] == runtime_sha - assert readback["runtime_build_commit_short_sha"] == runtime_sha[:10] - assert readback["observed_source_control_main_sha"] == runtime_sha - assert readback["production_image_tag_sha"] == runtime_sha - assert readback["production_image_tag_matches_main"] is True - assert rollups["production_image_tag_matches_main"] is True + assert data["operation_boundaries"]["github_write_channel_ready"] is False + assert data["operation_boundaries"]["github_controlled_apply_allowed"] is False + assert data["operation_boundaries"]["secret_value_collection_allowed"] is False diff --git a/apps/api/tests/test_gitea_private_inventory_p0_scorecard_api.py b/apps/api/tests/test_gitea_private_inventory_p0_scorecard_api.py new file mode 100644 index 000000000..613c00f70 --- /dev/null +++ b/apps/api/tests/test_gitea_private_inventory_p0_scorecard_api.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import json + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1.agents import router +from src.services.gitea_private_inventory_p0_scorecard import ( + load_latest_gitea_private_inventory_p0_scorecard, +) + + +def test_gitea_private_inventory_p0_scorecard_reports_active_gitea_blocker(): + payload = load_latest_gitea_private_inventory_p0_scorecard() + + assert payload["schema_version"] == "gitea_private_inventory_p0_scorecard_readback_v1" + assert payload["priority"] == "P0-003" + assert payload["readback"]["source_control_authority"] == "gitea" + assert payload["readback"]["private_inventory_source"] == "gitea" + assert payload["status"] == "blocked_waiting_gitea_authenticated_or_owner_export_inventory" + assert payload["rollups"]["active_blocker_count"] == 4 + assert payload["rollups"]["review_readiness_percent"] == 60 + assert payload["rollups"]["gitea_repo_inventory_status"] == "partial" + assert payload["rollups"]["gitea_visibility_scope"] == "public_only" + assert payload["rollups"]["accepted_inventory_payload_count"] == 0 + assert payload["rollups"]["owner_coverage_attestation_received_count"] == 0 + assert payload["rollups"]["expected_product_count"] == 11 + assert payload["rollups"]["present_product_row_count"] == 11 + assert payload["rollups"]["missing_product_row_count"] == 0 + assert payload["rollups"]["all_active_product_repos_have_gitea_owner_readiness_row"] is True + assert payload["rollups"]["github_lane_excluded_from_p0_blocker_count"] is True + assert payload["github_retired_context"]["status"] == "stopped_retired_do_not_use" + assert payload["github_retired_context"]["excluded_from_active_p0_blocker_math"] is True + assert "gitea_authenticated_inventory_payload_not_accepted" in payload["active_blockers"] + + +def test_gitea_private_inventory_p0_scorecard_endpoint_returns_readback(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get("/api/v1/agents/gitea-private-inventory-p0-scorecard") + + assert response.status_code == 200 + data = response.json() + assert data["priority"] == "P0-003" + assert data["rollups"]["active_blocker_count"] == 4 + assert data["operation_boundaries"]["read_only_api_allowed"] is True + assert data["operation_boundaries"]["gitea_api_write_allowed"] is False + assert data["operation_boundaries"]["gitea_repo_creation_allowed"] is False + assert data["operation_boundaries"]["gitea_refs_sync_allowed"] is False + assert data["operation_boundaries"]["gitea_workflow_trigger_allowed"] is False + assert data["operation_boundaries"]["token_value_collection_allowed"] is False + assert data["operation_boundaries"]["secret_value_collection_allowed"] is False + assert data["operation_boundaries"]["github_api_allowed"] is False + assert data["operation_boundaries"]["github_cli_allowed"] is False + assert data["operation_boundaries"]["raw_session_or_sqlite_read_allowed"] is False + + +def test_gitea_private_inventory_p0_scorecard_rejects_github_reactivation(tmp_path): + operations_dir = tmp_path / "operations" + operations_dir.mkdir() + payload = _scorecard_payload() + payload["github_lane_excluded_from_p0_blocker_count"] = False + (operations_dir / "awoooi-gitea-private-inventory-p0-scorecard.snapshot.json").write_text( + json.dumps(payload), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="github lane"): + load_latest_gitea_private_inventory_p0_scorecard(operations_dir) + + +def test_gitea_private_inventory_p0_scorecard_rejects_token_collection(tmp_path): + operations_dir = tmp_path / "operations" + operations_dir.mkdir() + payload = _scorecard_payload() + payload["authenticated_import_acceptance"]["token_value_collection_allowed"] = True + (operations_dir / "awoooi-gitea-private-inventory-p0-scorecard.snapshot.json").write_text( + json.dumps(payload), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="token value collection"): + load_latest_gitea_private_inventory_p0_scorecard(operations_dir) + + +def _scorecard_payload() -> dict: + return { + "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, + }, + "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, + }, + "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/apps/web/messages/en.json b/apps/web/messages/en.json index f4cde4164..c9abba6f8 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -91,7 +91,7 @@ "delivery": { "eyebrow": "AWOOOI Delivery", "title": "交付閉環工作台", - "subtitle": "把目前真正會推進交付的主線集中在同一頁:乾淨 release、GitHub 私有備援、Gitea / CI/CD、runtime surface、資料與備份。這裡只顯示能推動下一步的狀態,不再把文件數量當成完成度。", + "subtitle": "把目前真正會推進交付的主線集中在同一頁:乾淨 release、Gitea private inventory、CI/CD baseline、Gitea runner、runtime surface、資料與備份。GitHub 已停止並排除在 active P0 blocker math 之外。", "states": { "loading": "讀取中", "ready": "資料鏈正常", @@ -103,7 +103,7 @@ }, "sources": { "0": { "error": "狀態清理資料未回讀" }, - "1": { "error": "GitHub 備援資料未回讀" }, + "1": { "error": "Gitea private inventory 資料未回讀" }, "2": { "error": "Gitea / runner 資料未回讀" }, "3": { "error": "Runtime surface 資料未回讀" }, "4": { "error": "Backup readiness 資料未回讀" } @@ -112,7 +112,7 @@ "loaded": "資料來源", "loadedDetail": "只計入正式 API 成功回讀的來源。", "completion": "平均進度", - "completionDetail": "以本頁五條交付主線計算。", + "completionDetail": "以本頁交付主線計算。", "blockers": "高風險阻擋", "blockersDetail": "只統計會影響交付決策的阻擋。", "execution": "執行授權", @@ -137,10 +137,15 @@ "description": "讀回 Gitea CD、production image tag 與 runner readiness,不把 source ready 誤判成已部署。", "metric": "CD #{run} {status}" }, - "github": { - "title": "GitHub 私有備援", - "description": "所有備援 repo 必須私有,公開可讀或未驗證 private 都不能標綠。", - "metric": "已驗證 {verified}/{total}" + "gitea_private_inventory": { + "title": "Gitea private inventory", + "description": "把 private/internal inventory 收斂到 Gitea-only scorecard,GitHub 只保留 stopped / do_not_use 背景狀態。", + "metric": "產品列 {present}/{expected} · {scope}" + }, + "cicd_baseline": { + "title": "CI/CD baseline", + "description": "確認 P0 dev/prod baseline source 已齊,讓後續 workflow template apply gate 有真實來源。", + "metric": "source {present}/{required}" }, "gitea": { "title": "Gitea / CI-CD", @@ -161,7 +166,7 @@ "boundaries": { "secret": "不收機密明文、授權憑證、瀏覽器憑證或私有存取材料。", "production": "不直接改 production runtime、public gateway、Nginx、Docker、K8s 或 firewall。", - "repo": "不直接建立 GitHub repo、改 visibility、sync refs、force push 或 trigger workflow。", + "repo": "不直接建立 repo、改 visibility、sync refs、force push 或 trigger workflow;GitHub 維持 stopped / do_not_use。", "data": "不直接做資料庫、backup、restore 或 migration 寫操作。", "security": "不啟動 Wazuh / Kali 主動回應流程、active scan 或 host containment。" }, diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index fd0316093..a3e2267a2 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -91,7 +91,7 @@ "delivery": { "eyebrow": "AWOOOI Delivery", "title": "交付閉環工作台", - "subtitle": "把目前真正會推進交付的主線集中在同一頁:乾淨 release、GitHub 私有備援、Gitea / CI/CD、runtime surface、資料與備份。這裡只顯示能推動下一步的狀態,不再把文件數量當成完成度。", + "subtitle": "把目前真正會推進交付的主線集中在同一頁:乾淨 release、Gitea private inventory、CI/CD baseline、Gitea runner、runtime surface、資料與備份。GitHub 已停止並排除在 active P0 blocker math 之外。", "states": { "loading": "讀取中", "ready": "資料鏈正常", @@ -103,7 +103,7 @@ }, "sources": { "0": { "error": "狀態清理資料未回讀" }, - "1": { "error": "GitHub 備援資料未回讀" }, + "1": { "error": "Gitea private inventory 資料未回讀" }, "2": { "error": "Gitea / runner 資料未回讀" }, "3": { "error": "Runtime surface 資料未回讀" }, "4": { "error": "Backup readiness 資料未回讀" } @@ -112,7 +112,7 @@ "loaded": "資料來源", "loadedDetail": "只計入正式 API 成功回讀的來源。", "completion": "平均進度", - "completionDetail": "以本頁五條交付主線計算。", + "completionDetail": "以本頁交付主線計算。", "blockers": "高風險阻擋", "blockersDetail": "只統計會影響交付決策的阻擋。", "execution": "執行授權", @@ -137,10 +137,15 @@ "description": "讀回 Gitea CD、production image tag 與 runner readiness,不把 source ready 誤判成已部署。", "metric": "CD #{run} {status}" }, - "github": { - "title": "GitHub 私有備援", - "description": "所有備援 repo 必須私有,公開可讀或未驗證 private 都不能標綠。", - "metric": "已驗證 {verified}/{total}" + "gitea_private_inventory": { + "title": "Gitea private inventory", + "description": "把 private/internal inventory 收斂到 Gitea-only scorecard,GitHub 只保留 stopped / do_not_use 背景狀態。", + "metric": "產品列 {present}/{expected} · {scope}" + }, + "cicd_baseline": { + "title": "CI/CD baseline", + "description": "確認 P0 dev/prod baseline source 已齊,讓後續 workflow template apply gate 有真實來源。", + "metric": "source {present}/{required}" }, "gitea": { "title": "Gitea / CI-CD", @@ -161,7 +166,7 @@ "boundaries": { "secret": "不收機密明文、授權憑證、瀏覽器憑證或私有存取材料。", "production": "不直接改 production runtime、public gateway、Nginx、Docker、K8s 或 firewall。", - "repo": "不直接建立 GitHub repo、改 visibility、sync refs、force push 或 trigger workflow。", + "repo": "不直接建立 repo、改 visibility、sync refs、force push 或 trigger workflow;GitHub 維持 stopped / do_not_use。", "data": "不直接做資料庫、backup、restore 或 migration 寫操作。", "security": "不啟動 Wazuh / Kali 主動回應流程、active scan 或 host containment。" }, diff --git a/apps/web/src/app/[locale]/delivery/page.tsx b/apps/web/src/app/[locale]/delivery/page.tsx index f20681379..31437f831 100644 --- a/apps/web/src/app/[locale]/delivery/page.tsx +++ b/apps/web/src/app/[locale]/delivery/page.tsx @@ -7,7 +7,6 @@ import { AlertTriangle, ArrowRight, CheckCircle2, - GitBranch, HardDrive, Lock, PackageCheck, @@ -22,8 +21,8 @@ import { type AwoooIStatusCleanupDashboardSnapshot, type BackupDrReadinessMatrixSnapshot, type DeliveryClosureWorkbenchSnapshot, + type GiteaPrivateInventoryP0ScorecardSnapshot, type GiteaWorkflowRunnerHealthSnapshot, - type GithubTargetPrivateBackupEvidenceGateSnapshot, type RuntimeSurfaceInventorySnapshot, } from '@/lib/api-client' @@ -31,7 +30,7 @@ type DeliveryTone = 'ok' | 'warn' | 'danger' | 'neutral' interface DeliveryData { statusCleanup: AwoooIStatusCleanupDashboardSnapshot | null - github: GithubTargetPrivateBackupEvidenceGateSnapshot | null + privateInventory: GiteaPrivateInventoryP0ScorecardSnapshot | null gitea: GiteaWorkflowRunnerHealthSnapshot | null runtime: RuntimeSurfaceInventorySnapshot | null backup: BackupDrReadinessMatrixSnapshot | null @@ -55,7 +54,7 @@ const SOURCE_COUNT = 5 const EMPTY_DATA: DeliveryData = { statusCleanup: null, - github: null, + privateInventory: null, gitea: null, runtime: null, backup: null, @@ -83,9 +82,6 @@ const resolveTone = (blockerCount: number, percent: number): DeliveryTone => { return 'ok' } -const firstActionRequiredTarget = (data: GithubTargetPrivateBackupEvidenceGateSnapshot | null) => - data?.targets.find(target => target.approval_required)?.next_action ?? '' - const firstActionRequiredRunner = (data: GiteaWorkflowRunnerHealthSnapshot | null) => data?.runner_contracts.find(contract => contract.status === 'action_required')?.next_action ?? '' @@ -252,7 +248,7 @@ export default function DeliveryPage({ params }: { params: { locale: string } }) const results = await Promise.allSettled([ apiClient.getAwoooIStatusCleanupDashboard(), - apiClient.getGithubTargetPrivateBackupEvidenceGate(), + apiClient.getGiteaPrivateInventoryP0Scorecard(), apiClient.getGiteaWorkflowRunnerHealth(), apiClient.getRuntimeSurfaceInventory(), apiClient.getBackupDrReadinessMatrix(), @@ -261,7 +257,7 @@ export default function DeliveryPage({ params }: { params: { locale: string } }) const nextData: DeliveryData = { statusCleanup: results[0].status === 'fulfilled' ? results[0].value : null, - github: results[1].status === 'fulfilled' ? results[1].value : null, + privateInventory: results[1].status === 'fulfilled' ? results[1].value : null, gitea: results[2].status === 'fulfilled' ? results[2].value : null, runtime: results[3].status === 'fulfilled' ? results[3].value : null, backup: results[4].status === 'fulfilled' ? results[4].value : null, @@ -289,7 +285,8 @@ export default function DeliveryPage({ params }: { params: { locale: string } }) const iconMap = { release: Rocket, production_deploy: Rocket, - github: GitBranch, + gitea_private_inventory: PackageCheck, + cicd_baseline: PackageCheck, gitea: PackageCheck, runtime: Server, backup: HardDrive, @@ -297,13 +294,22 @@ export default function DeliveryPage({ params }: { params: { locale: string } }) const metric = lane.metric.kind === 'blocked_gate' ? t('lanes.release.metric', { blocked: lane.metric.blocked }) - : lane.metric.kind === 'private_backup_verified' - ? t('lanes.github.metric', { verified: lane.metric.verified, total: lane.metric.total }) + : lane.metric.kind === 'private_inventory' + ? t('lanes.gitea_private_inventory.metric', { + present: lane.metric.present_product_row_count, + expected: lane.metric.expected_product_count, + scope: lane.metric.gitea_visibility_scope, + }) : lane.metric.kind === 'deploy_readback' ? t('lanes.production_deploy.metric', { run: lane.metric.latest_visible_cd_run_id, status: lane.metric.latest_visible_cd_run_status, }) + : lane.metric.kind === 'source_readiness' + ? t('lanes.cicd_baseline.metric', { + present: lane.metric.present_required_source_count, + required: lane.metric.required_source_count, + }) : lane.metric.kind === 'workflow_count' ? t('lanes.gitea.metric', { count: lane.metric.count }) : lane.metric.kind === 'surface_count' @@ -328,10 +334,8 @@ export default function DeliveryPage({ params }: { params: { locale: string } }) const statusBlocked = Number(data.statusCleanup?.summary.blocked_gate_count ?? 0) const statusPercent = clampPercent(data.statusCleanup?.summary.overall_completion_percent) - const githubRequired = data.github?.summary.approval_required_target_count ?? 0 - const githubVerified = data.github?.summary.private_backup_verified_count ?? 0 - const githubBlocked = data.github?.summary.blocked_target_count ?? 0 - const githubPercent = githubRequired > 0 ? clampPercent((githubVerified / githubRequired) * 100) : 0 + const privateInventoryPercent = clampPercent(data.privateInventory?.rollups.review_readiness_percent) + const privateInventoryBlocked = data.privateInventory?.rollups.active_blocker_count ?? 0 const giteaPercent = clampPercent(data.gitea?.program_status.overall_completion_percent) const giteaBlocked = data.gitea?.rollups.runner_contracts_requiring_action.length ?? 0 const runtimePercent = clampPercent(data.runtime?.program_status.overall_completion_percent) @@ -354,17 +358,21 @@ export default function DeliveryPage({ params }: { params: { locale: string } }) Icon: Rocket, }, { - id: 'github', - title: t('lanes.github.title'), - description: t('lanes.github.description'), - percent: githubPercent, - status: data.github?.status ?? t('states.noData'), - metric: t('lanes.github.metric', { verified: githubVerified, total: githubRequired }), - blockerCount: githubBlocked, - nextAction: firstActionRequiredTarget(data.github), - href: '/governance?tab=automation-inventory', - tone: resolveTone(githubBlocked, githubPercent), - Icon: GitBranch, + id: 'gitea_private_inventory', + title: t('lanes.gitea_private_inventory.title'), + description: t('lanes.gitea_private_inventory.description'), + percent: privateInventoryPercent, + status: data.privateInventory?.status ?? t('states.noData'), + metric: t('lanes.gitea_private_inventory.metric', { + present: data.privateInventory?.rollups.present_product_row_count ?? 0, + expected: data.privateInventory?.rollups.expected_product_count ?? 0, + scope: data.privateInventory?.rollups.gitea_visibility_scope ?? 'unknown', + }), + blockerCount: privateInventoryBlocked, + nextAction: data.privateInventory?.readback.safe_next_step ?? '', + href: '/delivery', + tone: resolveTone(privateInventoryBlocked, privateInventoryPercent), + Icon: PackageCheck, }, { id: 'gitea', diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index cdaea8068..883e23c48 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -1122,6 +1122,11 @@ export const apiClient = { return handleResponse(res) }, + async getGiteaPrivateInventoryP0Scorecard() { + const res = await fetch(`${API_BASE_URL}/agents/gitea-private-inventory-p0-scorecard`) + return handleResponse(res) + }, + async getGithubTargetPrivateBackupEvidenceGate() { const res = await fetch(`${API_BASE_URL}/agents/github-target-private-backup-evidence-gate`) return handleResponse(res) @@ -2589,6 +2594,24 @@ export interface DeliveryClosureWorkbenchSnapshot { visibility_change_authorized: boolean refs_sync_authorized: boolean workflow_trigger_authorized: boolean + gitea_private_inventory_status: string + gitea_private_inventory_workplan_id: string + gitea_private_inventory_source: string + gitea_private_inventory_review_readiness_percent: number + gitea_private_inventory_active_blocker_count: number + gitea_private_inventory_repo_inventory_status: string + gitea_private_inventory_visibility_scope: string + gitea_private_inventory_public_repo_count: number + gitea_private_inventory_expected_product_count: number + gitea_private_inventory_present_product_row_count: number + gitea_private_inventory_missing_product_row_count: number + gitea_private_inventory_accepted_payload_count: number + gitea_private_inventory_owner_coverage_attestation_received_count: number + gitea_private_inventory_all_active_product_repos_have_owner_readiness_row: boolean + gitea_private_inventory_safe_next_step: string + github_global_freeze_enabled: boolean + github_lane_status: string + github_lane_excluded_from_p0_blocker_count: boolean production_deploy_status: string production_deploy_source_control_main_ready: boolean production_deploy_image_tag_matches_main: boolean @@ -2669,7 +2692,14 @@ export interface DeliveryClosureWorkbenchSnapshot { missing_reason: string }> lanes: Array<{ - id: 'release' | 'production_deploy' | 'github' | 'gitea' | 'runtime' | 'backup' + id: + | 'release' + | 'production_deploy' + | 'gitea_private_inventory' + | 'cicd_baseline' + | 'gitea' + | 'runtime' + | 'backup' source_id: string completion_percent: number status: string @@ -2728,14 +2758,31 @@ export interface DeliveryClosureWorkbenchSnapshot { non110_runner_remaining_blocker_count: number } | { - kind: 'private_backup_verified' - verified: number - total: number - controlled_apply_ready: number - blocked_preflight: number - write_channel_ready: boolean - github_account_status: string - github_account_suspended: boolean + kind: 'private_inventory' + workplan_id: string + private_inventory_source: string + gitea_repo_inventory_status: string + gitea_visibility_scope: string + gitea_public_repo_count: number + expected_product_count: number + present_product_row_count: number + missing_product_row_count: number + accepted_inventory_payload_count: number + owner_coverage_attestation_received_count: number + github_lane_excluded_from_p0_blocker_count: boolean + active_blockers: string[] + } + | { + kind: 'source_readiness' + workplan_id: string + required_source_count: number + present_required_source_count: number + missing_required_source_count: number + source_readiness_percent: number + blocked_source_ids: string[] + workflow_modification_allowed: boolean + workflow_trigger_allowed: boolean + safe_next_step: string } | { kind: 'workflow_count'; count: number } | { kind: 'surface_count'; total: number } @@ -2775,6 +2822,8 @@ export interface DeliveryClosureWorkbenchSnapshot { visibility_change_allowed: boolean refs_sync_allowed: boolean workflow_trigger_allowed: boolean + gitea_api_write_allowed: boolean + gitea_authenticated_inventory_import_execution_allowed: boolean github_write_channel_ready: boolean github_controlled_apply_allowed: boolean secret_value_collection_allowed: boolean @@ -14321,6 +14370,85 @@ export interface GiteaWorkflowRunnerHealthSnapshot { approval_boundaries: Record } +export interface GiteaPrivateInventoryP0ScorecardSnapshot { + schema_version: 'gitea_private_inventory_p0_scorecard_readback_v1' + generated_at: string + priority: 'P0-003' + scope: 'gitea_private_inventory_p0_scorecard' + status: string + readback: { + workplan_id: 'P0-003' | string + workplan_title: string + source_scorecard_ref: string + source_control_authority: 'gitea' | string + private_inventory_source: 'gitea' | string + safe_next_step: string + } + gitea_inventory: { + schema_version: string + status: string + visibility_scope: string + repo_count: number + token_present?: boolean + blocking_reason?: string + } + authenticated_import_acceptance: { + schema_version: string + status: string + accepted_payload_count: number + token_value_collection_allowed: false + execution_authorized: false + } + coverage_attestation: { + schema_version: string + status: string + received_attestation_count: number + accepted_attestation_count: number + execution_authorized: false + } + product_row_coverage: { + expected_product_count: number + present_product_row_count: number + missing_product_row_count: number + ready_product_count: number + blocked_product_count: number + internal_or_authenticated_inventory_required_count: number + all_active_product_repos_have_gitea_owner_readiness_row: boolean + } + active_blockers: string[] + exit_criteria: string[] + met_exit_criteria: string[] + rollups: { + active_blocker_count: number + review_readiness_percent: number + gitea_repo_inventory_status: string + gitea_visibility_scope: string + gitea_public_repo_count: number + accepted_inventory_payload_count: number + owner_coverage_attestation_received_count: number + owner_coverage_attestation_accepted_count: number + expected_product_count: number + present_product_row_count: number + missing_product_row_count: number + ready_product_count: number + blocked_product_count: number + internal_or_authenticated_inventory_required_count: number + all_active_product_repos_have_gitea_owner_readiness_row: boolean + github_lane_excluded_from_p0_blocker_count: boolean + github_api_used: boolean + github_cli_used: boolean + github_primary_switch_authorized: boolean + } + operation_boundaries: Record + github_retired_context: { + status: 'stopped_retired_do_not_use' | string + excluded_from_active_p0_blocker_math: boolean + api_used: boolean + cli_used: boolean + primary_switch_authorized: boolean + } +} + export interface GithubTargetPrivateBackupEvidenceGateSnapshot { schema_version: 'github_target_private_backup_evidence_gate_v1' generated_at: string diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 2648fce3a..7cc2082f1 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -49432,3 +49432,27 @@ production browser smoke: **未做**: - 沒有重啟任何主機;沒有 restart Docker / Nginx / K3s / DB / firewall;沒有讀 secret / token / `.env` / raw sessions / SQLite / auth;沒有使用 GitHub。 + +## 2026-06-29 — P0-003 Gitea private inventory API / Workbench readback + +**完成內容**: +- 新增 `GET /api/v1/agents/gitea-private-inventory-p0-scorecard`,把 `docs/operations/awoooi-gitea-private-inventory-p0-scorecard.snapshot.json` 提升為正式 read-only API contract。 +- `delivery-closure-workbench` 改用 `gitea_private_inventory` active lane;GitHub 只保留 `stopped_retired_do_not_use` 背景狀態,`github_blocked_preflight_target_count=0`,不再進 active P0 blocker math。 +- `/delivery` 前端型別、fallback 與文案改為 Gitea private inventory / CI/CD baseline 主線;fallback 不再讀 GitHub private backup gate。 +- `.gitea/workflows/cd.yaml` controlled-runtime profile 補上新 service / API test path;`ops/runner/test_cd_controlled_runtime_profile.py` 鎖定這些 path。 + +**讀回結果**: +- P0-003 API readback:`schema_version=gitea_private_inventory_p0_scorecard_readback_v1`、`status=blocked_waiting_gitea_authenticated_or_owner_export_inventory`、`active_blocker_count=4`、`review_readiness_percent=60`。 +- Workbench readback:`source_count=7`、`loaded_source_count=7`、`github_lane_status=stopped_retired_do_not_use`、`github_lane_excluded_from_p0_blocker_count=true`、active lane `gitea_private_inventory` blockers `4`。 + +**驗證結果**: +- `py_compile`:通過。 +- JSON parse:通過。 +- focused pytest:`18 passed`。 +- `ruff check`:通過。 +- `guard-gitea-runner-pressure.py`、`check-gitea-step-env-secrets.js`、`git diff --check`:通過。 +- Web typecheck 未執行成功:本機 `apps/web/node_modules` 不存在,`pnpm --filter @awoooi/web exec tsc --noEmit --incremental false` 回 `Command "tsc" not found`;本機 Data volume 約 `2.2GiB`,未補裝依賴。 + +**仍維持**: +- 沒有使用 GitHub app / connector / MCP、`gh`、GitHub API、GitHub Actions、PR / issue / search。 +- 沒有讀 token / secret / `.env` / raw sessions / SQLite / auth;沒有寫 Gitea repo / refs / branch / secret;沒有 host / Docker / K8s / firewall / Wazuh runtime 操作。 diff --git a/docs/operations/awoooi-priority-work-order-readback.snapshot.json b/docs/operations/awoooi-priority-work-order-readback.snapshot.json index 3ccf77ca2..ee5450288 100644 --- a/docs/operations/awoooi-priority-work-order-readback.snapshot.json +++ b/docs/operations/awoooi-priority-work-order-readback.snapshot.json @@ -151,10 +151,15 @@ "workplan_id": "P0-003", "title": "取得 Gitea private inventory 權限", "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.", + "reason": "P0-003 now has a Gitea-only scorecard, read-only API, and Delivery Workbench active lane. 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, + "api_readback_schema_version": "gitea_private_inventory_p0_scorecard_readback_v1", + "delivery_workbench_active_lane": "gitea_private_inventory", + "delivery_workbench_github_blocked_preflight_target_count": 0, + "delivery_workbench_review_readiness_percent": 60, + "delivery_workbench_active_blocker_count": 4, "gitea_inventory_status": "partial", "gitea_visibility_scope": "public_only", "gitea_public_repo_count": 2, diff --git a/ops/runner/test_cd_controlled_runtime_profile.py b/ops/runner/test_cd_controlled_runtime_profile.py index d87f29e00..ab0eb5770 100644 --- a/ops/runner/test_cd_controlled_runtime_profile.py +++ b/ops/runner/test_cd_controlled_runtime_profile.py @@ -96,8 +96,12 @@ def test_gitea_private_inventory_scorecard_stays_on_controlled_runtime_profile() text = _workflow_text() expected_sources = [ "docs/operations/awoooi-gitea-private-inventory-p0-scorecard.snapshot.json)", + "apps/api/src/services/gitea_private_inventory_p0_scorecard.py)", + "apps/api/tests/test_gitea_private_inventory_p0_scorecard_api.py)", "scripts/security/gitea-private-inventory-p0-scorecard.py)", "scripts/security/tests/test_gitea_private_inventory_p0_scorecard.py)", + "src/services/gitea_private_inventory_p0_scorecard.py", + "tests/test_gitea_private_inventory_p0_scorecard_api.py", "../../scripts/security/gitea-private-inventory-p0-scorecard.py", "../../scripts/security/tests/test_gitea_private_inventory_p0_scorecard.py", ]