From 7079b1e0ecdfe100ca1b981c261b429dc8369cd8 Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 10 Jul 2026 20:04:56 +0800 Subject: [PATCH] fix(agent): preserve runtime receipts and publish code audit --- .../awoooi_priority_work_order_readback.py | 174 ++++++++++++++++-- .../awooop_ansible_check_mode_service.py | 35 +++- ...awoooi_priority_work_order_readback_api.py | 29 ++- .../tests/test_awooop_truth_chain_service.py | 4 + 4 files changed, 211 insertions(+), 31 deletions(-) diff --git a/apps/api/src/services/awoooi_priority_work_order_readback.py b/apps/api/src/services/awoooi_priority_work_order_readback.py index 66543ff65..f48cf7989 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -128,18 +128,111 @@ _AI_AUTOMATION_REQUIRED_OUTCOME_METRICS = list( _AI_AUTOMATION_PROFESSIONAL_REVIEW_SOURCE = ( "docs/reviews/2026-07-10-security-governance-professional-review.snapshot.json" ) +_AI_AUTOMATION_CODEBASE_REVIEW = { + "schema_version": "ai_automation_codebase_review_v1", + "reviewed_at": "2026-07-10T20:00:00+08:00", + "review_basis": "full baseline review through 542fbda1 plus targeted runtime delta review through 43ceb3b2", + "scope": { + "reviewed_file_count": 977, + "reviewed_line_count": 402994, + "api_service_file_count": 227, + "api_route_file_count": 66, + "worker_job_file_count": 15, + "ops_script_file_count": 199, + "web_file_count": 69, + "k8s_file_count": 12, + "ansible_file_count": 23, + "test_file_count": 366, + }, + "risk_metrics": { + "exception_handler_count": 1359, + "broad_exception_handler_count": 715, + "silent_exception_pass_count": 44, + "db_context_without_explicit_project_count": 125, + "fire_and_forget_task_count": 28, + "not_implemented_count": 7, + }, + "findings": [ + { + "id": "ACR-P0-001", + "severity": "critical", + "title": "Public API and worker share cluster-wide executor identity and SSH mounts", + "source_refs": [ + "k8s/awoooi-prod/06-deployment-api.yaml", + "k8s/awoooi-prod/07-rbac.yaml", + "k8s/awoooi-prod/08-deployment-worker.yaml", + ], + "mapped_work_item_ids": ["AIA-P0-011"], + }, + { + "id": "ACR-P0-002", + "severity": "critical", + "title": "Decision manager and Ansible candidate path can create competing execution routes", + "source_refs": [ + "apps/api/src/services/decision_manager.py", + "apps/api/src/services/repair_candidate_service.py", + "apps/api/src/services/awooop_ansible_check_mode_service.py", + ], + "mapped_work_item_ids": ["AIA-P0-002"], + }, + { + "id": "ACR-P0-003", + "severity": "high", + "title": "Post verifier can map executor return code to success without independent outcome evidence", + "source_refs": [ + "apps/api/src/services/awooop_ansible_check_mode_service.py" + ], + "mapped_work_item_ids": ["AIA-P0-003", "AIA-P0-005"], + }, + { + "id": "ACR-P0-004", + "severity": "high", + "title": "KM, RAG, and PlayBook trust write acknowledgement is not transactional or identity-safe", + "source_refs": [ + "apps/api/src/services/awooop_ansible_check_mode_service.py", + "apps/api/src/services/learning_service.py", + "apps/api/src/services/knowledge_service.py", + ], + "mapped_work_item_ids": ["AIA-P0-004"], + }, + { + "id": "ACR-P1-001", + "severity": "high", + "title": "Tenant context gaps and fire-and-forget writes can silently lose automation evidence", + "source_refs": [ + "apps/api/src/services/decision_manager.py", + "apps/api/src/services/knowledge_service.py", + "apps/api/src/services/telegram_gateway.py", + ], + "mapped_work_item_ids": ["AIA-P0-001", "AIA-P1-008"], + }, + { + "id": "ACR-P1-002", + "severity": "medium", + "title": "Control-plane monoliths make invariants, ownership, and failure handling hard to verify", + "source_refs": [ + "apps/api/src/services/decision_manager.py", + "apps/api/src/services/ai_agent_autonomous_runtime_control.py", + "apps/web/src/app/[locale]/awooop/work-items/page.tsx", + "apps/web/src/app/[locale]/governance/components/automation-inventory-tab.tsx", + ], + "mapped_work_item_ids": ["AIA-P1-008"], + }, + ], +} _AI_AUTOMATION_PROGRAM_DEPENDENCIES = { "AIA-P0-000": [], "AIA-P0-001": [], - "AIA-P0-006": ["AIA-P0-001"], - "AIA-P0-008": ["AIA-P0-001", "AIA-P0-006"], - "AIA-P0-009": ["AIA-P0-001"], - "AIA-P0-010": ["AIA-P0-001", "AIA-P0-006"], - "AIA-P0-002": ["AIA-P0-001", "AIA-P0-006"], + "AIA-P0-011": ["AIA-P0-001"], + "AIA-P0-002": ["AIA-P0-001", "AIA-P0-011"], "AIA-P0-003": ["AIA-P0-002"], - "AIA-P0-004": ["AIA-P0-002", "AIA-P0-003"], + "AIA-P0-004": ["AIA-P0-002"], "AIA-P0-005": ["AIA-P0-002", "AIA-P0-003", "AIA-P0-004"], - "AIA-P0-007": ["AIA-P0-002", "AIA-P0-003", "AIA-P0-004", "AIA-P0-005"], + "AIA-P0-006": ["AIA-P0-001"], + "AIA-P0-008": ["AIA-P0-002", "AIA-P0-003", "AIA-P0-004", "AIA-P0-005", "AIA-P0-006"], + "AIA-P0-009": ["AIA-P0-011"], + "AIA-P0-010": ["AIA-P0-006", "AIA-P0-011"], + "AIA-P0-007": ["AIA-P0-002", "AIA-P0-003", "AIA-P0-004", "AIA-P0-005", "AIA-P0-006", "AIA-P0-008"], } _AI_AUTOMATION_PROGRAM_SCOPE_CLASSES: list[dict[str, Any]] = [ { @@ -413,12 +506,29 @@ _AI_AUTOMATION_PROGRAM_WORK_ITEMS: list[dict[str, Any]] = [ "metadata-only、source、test、UI、CD 與歷史 aggregate 不得關閉當前 run", "移除 hardcoded 100 completion 並公開缺少的 stage receipts", ], - "next_action": "為 asset identity、truth diff、risk decision、retry/rollback、RAG、PlayBook trust 與 timeline 補齊同 run durable receipts。", + "next_action": "先部署 service evidence、retry terminal 與 timeline same-run receipts,再完成 MCP、RAG 與 PlayBook trust 真實寫回。", + }, + { + "id": "AIA-P0-011", + "priority": "P0", + "order": 2, + "status": "pending", + "title": "收斂 API、Worker 與 executor 的最小權限信任邊界", + "problem": "public API 與 worker 共用 cluster-wide executor ServiceAccount、workload patch/delete 權限及 SSH key mounts,任一 API compromise 都可擴大為叢集與主機 side effect。", + "professional_practice": "zero trust workload identity、least privilege RBAC、namespace-scoped broker、capability allowlist、short-lived credentials", + "asset_scope_ids": ["hosts", "services", "products", "tools", "observability_and_security"], + "acceptance": [ + "API 使用 read-only ServiceAccount,無 pod delete、workload patch/scale 與 executor SSH key mount", + "worker 只經 namespace-scoped broker 與 action allowlist 執行,不能持有 cluster-wide raw capability", + "每次 capability issuance、apply、expiry 與 revoke 共用 run_id 並具 durable receipt", + "RBAC 與 mount verifier 在 production 證明 API 無 executor 權限、worker 無越界權限", + ], + "next_action": "先拆分 API/Worker ServiceAccount 與 mounts,再把 worker raw SSH capability 遷到 allowlisted execution broker。", }, { "id": "AIA-P0-002", "priority": "P0", - "order": 6, + "order": 3, "status": "pending", "title": "統一 AI decision router 與唯一受控 executor", "problem": "主決策路徑與 Ansible candidate backfill 分離,可能直接 apply 後再補證據。", @@ -434,7 +544,7 @@ _AI_AUTOMATION_PROGRAM_WORK_ITEMS: list[dict[str, Any]] = [ { "id": "AIA-P0-003", "priority": "P0", - "order": 7, + "order": 4, "status": "pending", "title": "實作獨立且資產專屬的 post-verifier", "problem": "目前部分 verifier 僅把 executor return code 映射成成功,沒有驗服務、路由、指標或資料。", @@ -450,7 +560,7 @@ _AI_AUTOMATION_PROGRAM_WORK_ITEMS: list[dict[str, Any]] = [ { "id": "AIA-P0-004", "priority": "P0", - "order": 8, + "order": 5, "status": "pending", "title": "修正 PlayBook identity、KM 與 trust 真實回寫", "problem": "catalog ID 與 PB ID 不一致,repository 更新失敗仍可能回報 learning_recorded。", @@ -466,7 +576,7 @@ _AI_AUTOMATION_PROGRAM_WORK_ITEMS: list[dict[str, Any]] = [ { "id": "AIA-P0-005", "priority": "P0", - "order": 9, + "order": 6, "status": "pending", "title": "完成 retry、rollback、incident closure 與 Telegram receipt", "problem": "apply、驗證、學習與通知尚未形成單一可重播閉環。", @@ -482,7 +592,7 @@ _AI_AUTOMATION_PROGRAM_WORK_ITEMS: list[dict[str, Any]] = [ { "id": "AIA-P0-006", "priority": "P0", - "order": 2, + "order": 7, "status": "in_progress", "title": "統一所有主機、服務、產品、網站、工具與套件資產總帳", "problem": "現有 inventory 分散,無法證明每個資產都具備 sensor、policy、executor 與 verifier。", @@ -498,7 +608,7 @@ _AI_AUTOMATION_PROGRAM_WORK_ITEMS: list[dict[str, Any]] = [ { "id": "AIA-P0-007", "priority": "P0", - "order": 10, + "order": 11, "status": "pending", "title": "以 production golden canary 證明自主閉環", "problem": "source/test/CD 綠燈不能證明 production AI Agent 已自主處理真實事件。", @@ -514,7 +624,7 @@ _AI_AUTOMATION_PROGRAM_WORK_ITEMS: list[dict[str, Any]] = [ { "id": "AIA-P0-008", "priority": "P0", - "order": 3, + "order": 8, "status": "in_progress", "title": "完成全資產資訊安全管理與 AISOC 自動回應閉環", "problem": "Wazuh、SIEM、工具安裝與 coverage UI 尚不能證明安全事件已完成 triage、containment、recovery 與 recurrence prevention。", @@ -532,7 +642,7 @@ _AI_AUTOMATION_PROGRAM_WORK_ITEMS: list[dict[str, Any]] = [ { "id": "AIA-P0-009", "priority": "P0", - "order": 4, + "order": 9, "status": "in_progress", "title": "移除所有 Gitea workflow 對 GitHub action supply chain 的依賴", "problem": "Gitea runner 仍會把 actions/checkout 等 action 從 github.com 抓入執行環境,GitHub freeze 尚未覆蓋 CI action resolution。", @@ -551,7 +661,7 @@ _AI_AUTOMATION_PROGRAM_WORK_ITEMS: list[dict[str, Any]] = [ { "id": "AIA-P0-010", "priority": "P0", - "order": 5, + "order": 10, "status": "in_progress", "title": "關閉 Gitea source-control 監控、完整備份、異地副本與還原演練", "problem": "Gitea workflow/Telegram receipt 已可讀,但 full-server DR 仍有 active gap,controlled restore drill 尚未形成 production receipt。", @@ -684,6 +794,23 @@ _AI_AUTOMATION_PROGRAM_WORK_ITEMS: list[dict[str, Any]] = [ ], "next_action": "建立 cross-product MCP/RAG integration registry 並接入 AI Loop health/readback。", }, + { + "id": "AIA-P1-008", + "priority": "P1", + "order": 15, + "status": "pending", + "title": "拆分控制面巨型模組並淘汰靜默失敗", + "problem": "decision、runtime readback、Telegram 與主要 UI 已形成超大模組;大量 broad exception、silent pass、缺 tenant context 與 fire-and-forget write 使閉環證據可能無聲遺失。", + "professional_practice": "bounded context、durable outbox、structured failure state、tenant context propagation、complexity budget", + "asset_scope_ids": ["services", "products", "websites", "tools", "observability_and_security"], + "acceptance": [ + "decision_manager、runtime control、Telegram gateway 與 Work Items 依 ownership boundary 拆分並維持契約測試", + "所有 completion-critical write 使用 awaited durable acknowledgement 或 transactional outbox", + "completion-critical broad exception 轉成可讀回 failure state,不得 silent pass", + "背景工作顯式傳遞 project_id、run_id 與 idempotency key", + ], + "next_action": "先拆 decision_manager._auto_execute 與 runtime receipt projection,並為 completion-critical background writes 加 durable outbox。", + }, ] _COMMANDER_INSERTED_REQUIREMENT_WORK_ITEMS: list[dict[str, Any]] = [ { @@ -7084,6 +7211,7 @@ def _apply_ai_automation_program_ledger(payload: dict[str, Any]) -> None: if isinstance(item, dict) ] professional_review = _load_ai_automation_professional_review() + codebase_review = copy.deepcopy(_AI_AUTOMATION_CODEBASE_REVIEW) review_findings = [ item for item in professional_review.get("findings", []) @@ -7162,6 +7290,7 @@ def _apply_ai_automation_program_ledger(payload: dict[str, Any]) -> None: ), "findings": review_findings, }, + "codebase_review": codebase_review, "scope_classes": scope_classes, "work_items": work_items, "summary": { @@ -7186,6 +7315,14 @@ def _apply_ai_automation_program_ledger(payload: dict[str, Any]) -> None: "scope_class_count": len(scope_classes), "tracked_asset_unit_count": tracked_asset_unit_count, "legacy_requirement_count": len(legacy_items), + "codebase_review_finding_count": len( + codebase_review.get("findings", []) + ), + "codebase_review_critical_finding_count": sum( + 1 + for item in codebase_review.get("findings", []) + if item.get("severity") == "critical" + ), "invalid_done_without_runtime_evidence_count": len(invalid_done_items), "next_work_item_id": str(next_item["id"]), "next_work_item_priority": str(next_item["priority"]), @@ -7233,6 +7370,9 @@ def _apply_ai_automation_program_ledger(payload: dict[str, Any]) -> None: summary["ai_automation_program_professional_review_finding_count"] = len( review_findings ) + summary["ai_automation_program_codebase_review_finding_count"] = len( + codebase_review.get("findings", []) + ) summary["ai_automation_program_required_outcome_metric_count"] = len( _AI_AUTOMATION_REQUIRED_OUTCOME_METRICS ) diff --git a/apps/api/src/services/awooop_ansible_check_mode_service.py b/apps/api/src/services/awooop_ansible_check_mode_service.py index c789e517d..052df2e3d 100644 --- a/apps/api/src/services/awooop_ansible_check_mode_service.py +++ b/apps/api/src/services/awooop_ansible_check_mode_service.py @@ -1105,15 +1105,40 @@ async def _record_runtime_stage_receipts( async with get_db_context(project_id) as db: updated = await db.execute( text(""" - UPDATE automation_operation_log + UPDATE automation_operation_log AS apply SET input = jsonb_set( - coalesce(input, '{}'::jsonb), + coalesce(apply.input, '{}'::jsonb), '{runtime_stage_receipts}', - CAST(:receipts AS jsonb), + ( + SELECT coalesce( + jsonb_agg( + deduplicated.receipt + ORDER BY deduplicated.stage_id + ), + '[]'::jsonb + ) + FROM ( + SELECT DISTINCT ON ( + merged.receipt ->> 'stage_id' + ) + merged.receipt, + merged.receipt ->> 'stage_id' AS stage_id + FROM jsonb_array_elements( + coalesce( + apply.input -> 'runtime_stage_receipts', + '[]'::jsonb + ) || CAST(:receipts AS jsonb) + ) WITH ORDINALITY AS merged(receipt, position) + WHERE merged.receipt ->> 'stage_id' IS NOT NULL + ORDER BY + merged.receipt ->> 'stage_id', + merged.position DESC + ) deduplicated + ), true ) - WHERE op_id = CAST(:apply_op_id AS uuid) - AND operation_type = 'ansible_apply_executed' + WHERE apply.op_id = CAST(:apply_op_id AS uuid) + AND apply.operation_type = 'ansible_apply_executed' """), { "receipts": json.dumps(receipts, ensure_ascii=False), diff --git a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py index c345699c8..8eb7f5dd0 100644 --- a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py +++ b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py @@ -917,10 +917,10 @@ def test_ai_automation_program_ledger_is_authoritative_and_runtime_strict(): assert program_summary["next_work_item_id"] == "AIA-P0-001" assert program_summary["mainline_p0_order"][:5] == [ "AIA-P0-001", - "AIA-P0-006", - "AIA-P0-008", - "AIA-P0-009", - "AIA-P0-010", + "AIA-P0-011", + "AIA-P0-002", + "AIA-P0-003", + "AIA-P0-004", ] assert program_summary["parallel_governance_p0_ids"] == ["AIA-P0-000"] assert program_summary["completion_percent"] == 0 @@ -939,14 +939,17 @@ def test_ai_automation_program_ledger_is_authoritative_and_runtime_strict(): assert work_item_by_id["AIA-P0-000"]["status"] == "in_progress" assert work_item_by_id["AIA-P0-000"]["mainline_blocking"] is False - runtime_critical_ids = {f"AIA-P0-{index:03d}" for index in range(1, 11)} + runtime_critical_ids = {f"AIA-P0-{index:03d}" for index in range(1, 12)} assert runtime_critical_ids.issubset(work_item_by_id) assert all( work_item_by_id[item_id]["status"] != "done" for item_id in runtime_critical_ids ) - assert work_item_by_id["AIA-P0-006"]["order"] == 2 - assert work_item_by_id["AIA-P0-008"]["order"] == 3 + assert work_item_by_id["AIA-P0-011"]["order"] == 2 + assert work_item_by_id["AIA-P0-011"]["status"] == "pending" + assert work_item_by_id["AIA-P0-002"]["order"] == 3 + assert work_item_by_id["AIA-P0-006"]["order"] == 7 + assert work_item_by_id["AIA-P0-008"]["order"] == 8 assert work_item_by_id["AIA-P0-008"]["status"] == "in_progress" assert work_item_by_id["AIA-P0-009"][ "observed_awoooi_external_action_reference_count" @@ -959,6 +962,12 @@ def test_ai_automation_program_ledger_is_authoritative_and_runtime_strict(): ) assert program["professional_review"]["finding_count"] == 9 assert program["professional_review"]["critical_finding_count"] == 3 + assert program["codebase_review"]["scope"]["reviewed_file_count"] == 977 + assert program["codebase_review"]["risk_metrics"][ + "broad_exception_handler_count" + ] == 715 + assert program_summary["codebase_review_finding_count"] == 6 + assert program_summary["codebase_review_critical_finding_count"] == 2 assert work_item_by_id["AIA-P1-002"]["status"] == "in_progress" assert "CPU" in work_item_by_id["AIA-P1-002"]["title"] assert work_item_by_id["AIA-P1-004"]["schedule"] == { @@ -970,6 +979,8 @@ def test_ai_automation_program_ledger_is_authoritative_and_runtime_strict(): assert "UI/UX" in work_item_by_id["AIA-P1-006"]["title"] assert work_item_by_id["AIA-P1-007"]["status"] == "pending" assert "MCP" in work_item_by_id["AIA-P1-007"]["title"] + assert work_item_by_id["AIA-P1-008"]["status"] == "pending" + assert "靜默失敗" in work_item_by_id["AIA-P1-008"]["title"] legacy_items = payload["commander_inserted_requirement_work_items"] assert len(legacy_items) == program_summary["legacy_requirement_count"] @@ -1851,8 +1862,8 @@ def test_ai_automation_live_closure_readbacks_close_node_receipts(): ] is True program = payload["ai_automation_program_ledger"] program_items = {item["id"]: item for item in program["work_items"]} - assert program["summary"]["next_work_item_id"] == "AIA-P0-006" - assert program["summary"]["completion_percent"] == 6 + assert program["summary"]["next_work_item_id"] == "AIA-P0-011" + assert program["summary"]["completion_percent"] == 5 assert program_items["AIA-P0-001"]["status"] == "done" assert program_items["AIA-P0-001"]["completion_evidence"] == { "source": "agent-autonomous-runtime-control", diff --git a/apps/api/tests/test_awooop_truth_chain_service.py b/apps/api/tests/test_awooop_truth_chain_service.py index b4a03a4b0..b5caebe77 100644 --- a/apps/api/tests/test_awooop_truth_chain_service.py +++ b/apps/api/tests/test_awooop_truth_chain_service.py @@ -1887,6 +1887,10 @@ def test_ansible_runtime_stage_receipts_are_same_run_and_public_safe() -> None: for receipt in [*pre, *post] ) assert inspect.iscoroutinefunction(_record_runtime_stage_receipts) + receipt_writer_source = inspect.getsource(_record_runtime_stage_receipts) + assert "jsonb_array_elements" in receipt_writer_source + assert "WITH ORDINALITY" in receipt_writer_source + assert "DISTINCT ON" in receipt_writer_source def test_ansible_context_receipts_reference_pre_decision_evidence_without_content() -> None: