diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 31fa0dbd2..44c1b2283 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -59,6 +59,9 @@ from src.services.backup_restore_drill_approval_package_template import ( from src.services.offsite_escrow_readiness_status import ( load_latest_offsite_escrow_readiness_status, ) +from src.services.runtime_surface_inventory import ( + load_latest_runtime_surface_inventory, +) from src.services.package_supply_chain_inventory import ( load_latest_package_supply_chain_inventory, ) @@ -476,6 +479,33 @@ async def get_automation_backlog_snapshot() -> dict[str, Any]: ) from exc +@router.get( + "/runtime-surface-inventory", + response_model=dict[str, Any], + summary="取得 Runtime surface 只讀盤點", + description=( + "讀取最新已提交的 API / Web / Worker / K8s runtime surface 盤點;" + "此端點不呼叫 live cluster、不碰 DB/Redis、不讀 Secret payload、" + "不執行 rollout/restart/scale/delete、不 patch K8s、不改 workflow、不改生產路由。" + ), +) +async def get_runtime_surface_inventory() -> dict[str, Any]: + """Return the latest read-only runtime surface inventory.""" + try: + return await asyncio.to_thread(load_latest_runtime_surface_inventory) + 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("runtime_surface_inventory_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Runtime surface 盤點快照無效", + ) from exc + + @router.get( "/backup-dr-target-inventory", response_model=dict[str, Any], diff --git a/apps/api/src/services/runtime_surface_inventory.py b/apps/api/src/services/runtime_surface_inventory.py new file mode 100644 index 000000000..9192d6864 --- /dev/null +++ b/apps/api/src/services/runtime_surface_inventory.py @@ -0,0 +1,172 @@ +""" +Runtime surface inventory snapshot. + +Loads the latest committed, read-only API / Web / Worker / K8s runtime surface +inventory. This module never calls the live cluster, reads Secret payloads, +executes rollouts, changes routing, or enables active scans. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from src.services.snapshot_paths import default_evaluations_dir + +_DEFAULT_EVALUATIONS_DIR = default_evaluations_dir(Path(__file__)) +_SNAPSHOT_PATTERN = "runtime_surface_inventory_*.json" +_SCHEMA_VERSION = "runtime_surface_inventory_v1" + + +def load_latest_runtime_surface_inventory( + evaluations_dir: Path | None = None, +) -> dict[str, Any]: + """Load the newest committed runtime surface inventory snapshot.""" + directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR + candidates = sorted(directory.glob(_SNAPSHOT_PATTERN)) + if not candidates: + raise FileNotFoundError(f"no runtime surface inventory snapshots found in {directory}") + + latest = candidates[-1] + with latest.open(encoding="utf-8") as handle: + payload = json.load(handle) + + if not isinstance(payload, dict): + raise ValueError(f"{latest}: expected JSON object") + _require_schema(payload, _SCHEMA_VERSION, str(latest)) + _require_read_only_boundaries(payload, str(latest)) + _require_operation_boundaries(payload, str(latest)) + _require_rollup_consistency(payload, str(latest)) + _require_operator_denials(payload, str(latest)) + _require_surface_evidence(payload, str(latest)) + return payload + + +def _require_schema(payload: dict[str, Any], expected: str, label: str) -> None: + actual = payload.get("schema_version") + if actual != expected: + raise ValueError(f"{label}: expected schema_version={expected}, got {actual!r}") + + +def _require_read_only_boundaries(payload: dict[str, Any], label: str) -> None: + program_status = payload.get("program_status") or {} + if program_status.get("read_only_mode") is not True: + raise ValueError(f"{label}: program_status.read_only_mode must be true") + + approval_boundaries = payload.get("approval_boundaries") or {} + allowed = sorted(key for key, value in approval_boundaries.items() if value is not False) + if allowed: + raise ValueError(f"{label}: approval boundaries must remain false: {allowed}") + + +def _require_operation_boundaries(payload: dict[str, Any], label: str) -> None: + boundaries = payload.get("operation_boundaries") or {} + if boundaries.get("read_only_api_allowed") is not True: + raise ValueError(f"{label}: read_only_api_allowed must be true") + + blocked_flags = { + "live_k8s_query_allowed", + "kubectl_allowed", + "rollout_allowed", + "restart_allowed", + "scale_allowed", + "delete_allowed", + "secret_read_allowed", + "secret_plaintext_allowed", + "active_scan_allowed", + "production_route_change_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: + surfaces = payload.get("runtime_surfaces") or [] + components = payload.get("source_runtime_components") or [] + rollups = payload.get("rollups") or {} + + if rollups.get("total_surfaces") != len(surfaces): + raise ValueError(f"{label}: rollups.total_surfaces must match runtime_surfaces") + + if rollups.get("by_kind") != _count_by(surfaces, "kind"): + raise ValueError(f"{label}: rollups.by_kind must match runtime_surfaces") + if rollups.get("by_status") != _count_by(surfaces, "status"): + raise ValueError(f"{label}: rollups.by_status must match runtime_surfaces") + if rollups.get("by_evidence_level") != _count_by(surfaces, "evidence_level"): + raise ValueError(f"{label}: rollups.by_evidence_level must match runtime_surfaces") + + action_required = sorted( + surface.get("surface_id") + for surface in surfaces + if surface.get("status") != "manifest_mapped" + ) + if sorted(rollups.get("action_required_surface_ids") or []) != action_required: + raise ValueError(f"{label}: rollups.action_required_surface_ids must match surfaces") + + secret_surfaces = sorted( + surface.get("surface_id") + for surface in surfaces + if surface.get("kind") == "secret" or surface.get("secret_exposure") != "none" + ) + if sorted(rollups.get("secret_surface_ids") or []) != secret_surfaces: + raise ValueError(f"{label}: rollups.secret_surface_ids must match secret surfaces") + + live_check_required = sorted( + surface.get("surface_id") + for surface in surfaces + if surface.get("live_check_status") == "required" + ) + if sorted(rollups.get("live_check_missing_surface_ids") or []) != live_check_required: + raise ValueError(f"{label}: rollups.live_check_missing_surface_ids must match surfaces") + + if rollups.get("total_source_components") != len(components): + raise ValueError(f"{label}: rollups.total_source_components must match source_runtime_components") + + bound_components = sum(1 for component in components if component.get("status") == "bound") + if rollups.get("source_components_with_runtime_binding") != bound_components: + raise ValueError( + f"{label}: rollups.source_components_with_runtime_binding must match source_runtime_components" + ) + + +def _require_operator_denials(payload: dict[str, Any], label: str) -> None: + contract = payload.get("operator_contract") or {} + must_not_interpret_as = set(contract.get("must_not_interpret_as") or []) + required_denials = { + "runtime 執行授權", + "rollout / restart / scale / delete 批准", + "Secret 已驗證或可讀取", + "Ingress / DNS 可修改", + } + if not required_denials.issubset(must_not_interpret_as): + raise ValueError(f"{label}: operator_contract.must_not_interpret_as is missing required denials") + + +def _require_surface_evidence(payload: dict[str, Any], label: str) -> None: + surfaces = payload.get("runtime_surfaces") or [] + missing_evidence = sorted( + surface.get("surface_id") + for surface in surfaces + if not surface.get("manifest_ref") or not surface.get("evidence_refs") + ) + if missing_evidence: + raise ValueError(f"{label}: runtime surfaces must include manifest_ref and evidence_refs: {missing_evidence}") + + plaintext_secret = sorted( + surface.get("surface_id") + for surface in surfaces + if surface.get("kind") == "secret" + and surface.get("secret_exposure") not in {"template_only", "name_only", "payload_redacted"} + ) + if plaintext_secret: + raise ValueError(f"{label}: secret surfaces must stay redacted: {plaintext_secret}") + + +def _count_by(items: list[dict[str, Any]], key: str) -> dict[str, int]: + counts: dict[str, int] = {} + for item in items: + value = item.get(key) + counts[value] = counts.get(value, 0) + 1 + return counts diff --git a/apps/api/tests/test_ai_agent_automation_backlog_snapshot_api.py b/apps/api/tests/test_ai_agent_automation_backlog_snapshot_api.py index 8b625e38e..018987f14 100644 --- a/apps/api/tests/test_ai_agent_automation_backlog_snapshot_api.py +++ b/apps/api/tests/test_ai_agent_automation_backlog_snapshot_api.py @@ -16,16 +16,16 @@ def test_ai_agent_automation_backlog_snapshot_endpoint_returns_committed_snapsho assert response.status_code == 200 data = response.json() assert data["schema_version"] == "ai_agent_automation_backlog_v1" - assert data["program_status"]["overall_completion_percent"] == 70 + assert data["program_status"]["overall_completion_percent"] == 74 assert data["program_status"]["read_only_mode"] is True - assert data["program_status"]["current_task_id"] == "P1-306" - assert data["program_status"]["next_task_id"] == "P1-001" + assert data["program_status"]["current_task_id"] == "P1-001" + assert data["program_status"]["next_task_id"] == "P1-002" assert data["rollups"]["total_items"] == len(data["backlog_items"]) == 23 assert data["rollups"]["by_priority"]["P1"] == 21 - assert data["rollups"]["by_status"]["done"] == 16 + assert data["rollups"]["by_status"]["done"] == 17 assert data["rollups"]["by_gate_status"]["read_only_allowed"] == 20 - assert data["progress_summary"]["overall_percent"] == 70 - assert data["progress_summary"]["done_items"] == 16 + assert data["progress_summary"]["overall_percent"] == 74 + assert data["progress_summary"]["done_items"] == 17 assert data["progress_summary"]["total_items"] == 23 assert data["item_approval_boundary_rollup"]["total_items"] == 23 assert data["item_approval_boundary_rollup"]["items_requiring_explicit_approval"] == [ @@ -44,6 +44,9 @@ def test_ai_agent_automation_backlog_snapshot_endpoint_returns_committed_snapsho assert any(item["item_id"] == "AUTO-P1-105" for item in data["backlog_items"]) assert any(item["item_id"] == "AUTO-P1-106" for item in data["backlog_items"]) assert any(item["item_id"] == "AUTO-P1-305" for item in data["backlog_items"]) + p1_001 = next(item for item in data["backlog_items"] if item["item_id"] == "AUTO-P1-001") + assert p1_001["status"] == "done" + assert "runtime_surface_inventory_2026-06-05.json" in p1_001["evidence_refs"][0] p1_306 = next(item for item in data["backlog_items"] if item["item_id"] == "AUTO-P1-306") assert p1_306["approval_boundary"]["mode"] == "read_only_allowed" assert "runtime_execution" in p1_306["approval_boundary"]["blocked_actions"] diff --git a/apps/api/tests/test_ai_agent_automation_inventory_snapshot_api.py b/apps/api/tests/test_ai_agent_automation_inventory_snapshot_api.py index 6f8aa959a..5269e465c 100644 --- a/apps/api/tests/test_ai_agent_automation_inventory_snapshot_api.py +++ b/apps/api/tests/test_ai_agent_automation_inventory_snapshot_api.py @@ -18,10 +18,10 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps assert data["schema_version"] == "ai_agent_automation_inventory_snapshot_v1" assert data["program_status"]["overall_completion_percent"] == 100 assert data["program_status"]["read_only_mode"] is True - assert data["program_status"]["current_task_id"] == "P1-306" - assert data["program_status"]["next_task_id"] == "P1-001" - assert data["task_approval_boundary_rollup"]["total_tasks"] == len(data["tasks"]) == 26 - assert data["task_approval_boundary_rollup"]["by_mode"]["read_only_allowed"] == 24 + assert data["program_status"]["current_task_id"] == "P1-001" + assert data["program_status"]["next_task_id"] == "P1-002" + assert data["task_approval_boundary_rollup"]["total_tasks"] == len(data["tasks"]) == 27 + assert data["task_approval_boundary_rollup"]["by_mode"]["read_only_allowed"] == 25 assert data["task_approval_boundary_rollup"]["tasks_requiring_explicit_approval"] == [ "P0-001", "P0-004", @@ -30,6 +30,9 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps assert data["approval_boundaries"]["paid_api_call_allowed"] is False assert data["approval_boundaries"]["production_routing_allowed"] is False assert any(asset["asset_id"] == "nemotron_candidate" for asset in data["assets"]) + p1_001 = next(task for task in data["tasks"] if task["task_id"] == "P1-001") + assert p1_001["status"] == "done" + assert p1_001["approval_boundary"]["mode"] == "read_only_allowed" assert any(task["task_id"] == "P1-204" for task in data["tasks"]) assert any(task["task_id"] == "P1-205" for task in data["tasks"]) assert any(task["task_id"] == "P1-206" for task in data["tasks"]) @@ -58,3 +61,4 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps assert any(evidence["evidence_id"] == "offsite_escrow_readiness_status_api" for evidence in data["evidence"]) assert any(evidence["evidence_id"] == "task_approval_boundary_ui" for evidence in data["evidence"]) assert any(evidence["evidence_id"] == "backlog_progress_summary_ui" for evidence in data["evidence"]) + assert any(evidence["evidence_id"] == "runtime_surface_inventory_api" for evidence in data["evidence"]) diff --git a/apps/api/tests/test_runtime_surface_inventory.py b/apps/api/tests/test_runtime_surface_inventory.py new file mode 100644 index 000000000..47581ef6d --- /dev/null +++ b/apps/api/tests/test_runtime_surface_inventory.py @@ -0,0 +1,228 @@ +from __future__ import annotations + +import json + +import pytest + +from src.services.runtime_surface_inventory import load_latest_runtime_surface_inventory + + +def test_load_latest_runtime_surface_inventory_reads_newest_file(tmp_path): + older = _snapshot(generated_at="2026-06-04T00:00:00+08:00", completion=40) + newer = _snapshot(generated_at="2026-06-05T00:00:00+08:00", completion=100) + (tmp_path / "runtime_surface_inventory_2026-06-04.json").write_text( + json.dumps(older), + encoding="utf-8", + ) + (tmp_path / "runtime_surface_inventory_2026-06-05.json").write_text( + json.dumps(newer), + encoding="utf-8", + ) + + loaded = load_latest_runtime_surface_inventory(tmp_path) + + assert loaded["generated_at"] == "2026-06-05T00:00:00+08:00" + assert loaded["program_status"]["overall_completion_percent"] == 100 + assert loaded["rollups"]["total_surfaces"] == 3 + assert loaded["operation_boundaries"]["kubectl_allowed"] is False + + +def test_runtime_surface_inventory_requires_read_only_mode(tmp_path): + snapshot = _snapshot() + snapshot["program_status"]["read_only_mode"] = False + (tmp_path / "runtime_surface_inventory_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="read_only_mode"): + load_latest_runtime_surface_inventory(tmp_path) + + +def test_runtime_surface_inventory_requires_blocked_operations(tmp_path): + snapshot = _snapshot() + snapshot["operation_boundaries"]["kubectl_allowed"] = True + (tmp_path / "runtime_surface_inventory_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="operation boundaries"): + load_latest_runtime_surface_inventory(tmp_path) + + +def test_runtime_surface_inventory_requires_rollup_consistency(tmp_path): + snapshot = _snapshot() + snapshot["rollups"]["by_kind"]["deployment"] = 99 + (tmp_path / "runtime_surface_inventory_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="by_kind"): + load_latest_runtime_surface_inventory(tmp_path) + + +def test_runtime_surface_inventory_requires_secret_redaction(tmp_path): + snapshot = _snapshot() + snapshot["runtime_surfaces"][2]["secret_exposure"] = "none" + (tmp_path / "runtime_surface_inventory_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="secret surfaces"): + load_latest_runtime_surface_inventory(tmp_path) + + +def test_runtime_surface_inventory_requires_operator_denials(tmp_path): + snapshot = _snapshot() + snapshot["operator_contract"]["must_not_interpret_as"] = ["runtime 執行授權"] + (tmp_path / "runtime_surface_inventory_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="must_not_interpret_as"): + load_latest_runtime_surface_inventory(tmp_path) + + +def test_runtime_surface_inventory_fails_when_missing(tmp_path): + with pytest.raises(FileNotFoundError): + load_latest_runtime_surface_inventory(tmp_path) + + +def _snapshot( + *, + generated_at: str = "2026-06-05T00:00:00+08:00", + completion: int = 100, +) -> dict: + return { + "schema_version": "runtime_surface_inventory_v1", + "generated_at": generated_at, + "program_status": { + "overall_completion_percent": completion, + "current_priority": "P1", + "current_task_id": "P1-001", + "next_task_id": "P1-002", + "read_only_mode": True, + }, + "source_refs": ["k8s/awoooi-prod/"], + "rollups": { + "total_surfaces": 3, + "by_kind": {"deployment": 1, "ingress": 1, "secret": 1}, + "by_status": {"manifest_mapped": 1, "action_required": 2}, + "by_evidence_level": { + "committed_manifest": 1, + "missing_manifest": 1, + "live_check_required": 1, + }, + "action_required_surface_ids": ["external_nginx_gateway_route", "awoooi_secrets"], + "secret_surface_ids": ["awoooi_secrets"], + "live_check_missing_surface_ids": ["external_nginx_gateway_route", "awoooi_secrets"], + "total_source_components": 1, + "source_components_with_runtime_binding": 1, + }, + "runtime_surfaces": [ + _surface( + "awoooi_api_deployment", + "deployment", + "manifest_mapped", + "committed_manifest", + "none", + "not_run", + ), + _surface( + "external_nginx_gateway_route", + "ingress", + "action_required", + "missing_manifest", + "none", + "required", + ), + _surface( + "awoooi_secrets", + "secret", + "action_required", + "live_check_required", + "template_only", + "required", + ), + ], + "source_runtime_components": [ + { + "component_id": "api_fastapi_app", + "display_name": "FastAPI app", + "source_ref": "apps/api/src/main.py", + "component_kind": "api_process", + "runtime_binding": "Deployment/awoooi-api", + "status": "bound", + "next_action": "只讀確認。", + } + ], + "evidence_gaps": [ + { + "gap_id": "external_gateway_manifest_gap", + "severity": "high", + "status": "action_required", + "summary": "缺外部 gateway manifest。", + "evidence_refs": ["k8s/awoooi-prod/"], + "next_action": "只讀補證據。", + } + ], + "operator_contract": { + "display_mode": "read_only_runtime_surface", + "must_not_interpret_as": [ + "runtime 執行授權", + "rollout / restart / scale / delete 批准", + "Secret 已驗證或可讀取", + "Ingress / DNS 可修改", + ], + "secret_display_policy": "只顯示 redacted metadata。", + }, + "operation_boundaries": { + "read_only_api_allowed": True, + "live_k8s_query_allowed": False, + "kubectl_allowed": False, + "rollout_allowed": False, + "restart_allowed": False, + "scale_allowed": False, + "delete_allowed": False, + "secret_read_allowed": False, + "secret_plaintext_allowed": False, + "active_scan_allowed": False, + "production_route_change_allowed": False, + }, + "approval_boundaries": { + "sdk_installation_allowed": False, + "paid_api_call_allowed": False, + "shadow_or_canary_allowed": False, + "production_routing_allowed": False, + "destructive_operation_allowed": False, + }, + } + + +def _surface( + surface_id: str, + kind: str, + status: str, + evidence_level: str, + secret_exposure: str, + live_check_status: str, +) -> dict: + return { + "surface_id": surface_id, + "display_name": surface_id, + "kind": kind, + "manifest_ref": "k8s/awoooi-prod/example.yaml", + "status": status, + "risk_level": "high", + "evidence_level": evidence_level, + "runtime_binding": "example runtime binding", + "health_contract": "example health contract", + "secret_exposure": secret_exposure, + "live_check_status": live_check_status, + "evidence_refs": ["k8s/awoooi-prod/example.yaml"], + "next_action": "只讀確認。", + } diff --git a/apps/api/tests/test_runtime_surface_inventory_api.py b/apps/api/tests/test_runtime_surface_inventory_api.py new file mode 100644 index 000000000..bc58ebe0f --- /dev/null +++ b/apps/api/tests/test_runtime_surface_inventory_api.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1.agents import router + + +def test_runtime_surface_inventory_endpoint_returns_committed_snapshot(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get("/api/v1/agents/runtime-surface-inventory") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "runtime_surface_inventory_v1" + assert data["program_status"]["overall_completion_percent"] == 100 + assert data["program_status"]["read_only_mode"] is True + assert data["program_status"]["current_task_id"] == "P1-001" + assert data["program_status"]["next_task_id"] == "P1-002" + assert data["rollups"]["total_surfaces"] == len(data["runtime_surfaces"]) == 22 + assert data["rollups"]["by_kind"]["deployment"] == 4 + assert data["rollups"]["by_kind"]["service"] == 2 + assert data["rollups"]["by_kind"]["ingress"] == 1 + assert data["rollups"]["by_kind"]["cronjob"] == 4 + assert data["rollups"]["by_kind"]["secret"] == 4 + assert data["rollups"]["by_status"]["action_required"] == 6 + assert data["rollups"]["secret_surface_ids"] == [ + "awoooi_secrets", + "awoooi_repair_ssh_key_secret", + "awoooi_repair_known_hosts_secret", + "ssh_mcp_key_secret", + ] + assert data["operation_boundaries"]["read_only_api_allowed"] is True + assert data["operation_boundaries"]["live_k8s_query_allowed"] is False + assert data["operation_boundaries"]["kubectl_allowed"] is False + assert data["operation_boundaries"]["secret_plaintext_allowed"] is False + assert data["operation_boundaries"]["production_route_change_allowed"] is False + assert data["approval_boundaries"]["runtime_execution_authorized"] is False + assert any(surface["surface_id"] == "awoooi_api_deployment" for surface in data["runtime_surfaces"]) + assert any( + surface["surface_id"] == "external_nginx_gateway_route" and surface["status"] == "action_required" + for surface in data["runtime_surfaces"] + ) + assert any( + surface["surface_id"] == "hpa_vpa_autoscaler_contract" and surface["live_check_status"] == "required" + for surface in data["runtime_surfaces"] + ) + assert "Secret 已驗證或可讀取" in data["operator_contract"]["must_not_interpret_as"] diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index dd0540fbf..dd8504aa9 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -3068,11 +3068,25 @@ "redacted_only": "僅脫敏", "read_only_allowed": "只讀允許", "ready_for_operator_review": "待人工審查", + "blocked_by_evidence": "證據不足阻擋", "dry_run_required": "需 dry-run", "cost_approval_required": "費用需批准", "dependency_approval_required": "依賴需批准", "production_change_blocked": "生產變更阻擋", - "shadow_canary_blocked": "Shadow / Canary 阻擋" + "shadow_canary_blocked": "Shadow / Canary 阻擋", + "manifest_mapped": "Manifest 已對應", + "missing": "缺 manifest", + "source_file": "來源檔", + "committed_manifest": "已提交 manifest", + "missing_manifest": "缺 manifest", + "live_check_required": "需 live 證據", + "not_run": "未執行", + "required": "必要", + "name_only": "只顯示名稱", + "template_only": "只顯示模板", + "payload_redacted": "Payload 已脫敏", + "bound": "已綁定", + "source_only": "僅來源" } }, "offsiteEscrow": { @@ -3105,6 +3119,55 @@ "production_routing_allowed": "生產路由禁止自動變更", "destructive_operation_allowed": "破壞性操作禁止自動執行" } + }, + "runtimeSurface": { + "title": "執行面只讀矩陣", + "source": "{generated} · {current} → {next}", + "componentsTitle": "來源元件", + "contractTitle": "不可誤讀合約", + "metrics": { + "total": "執行面", + "actionRequired": "需處置", + "secrets": "機密面", + "liveMissing": "待 Live 證據", + "boundComponents": "已綁定元件" + }, + "labels": { + "secret": "機密", + "live": "Live 證據" + }, + "kinds": { + "deployment": "Deployment", + "service": "Service", + "ingress": "Ingress / Route", + "cronjob": "CronJob", + "configmap": "ConfigMap", + "secret": "Secret", + "rbac": "RBAC", + "policy": "Policy", + "autoscaler": "Autoscaler", + "availability": "Availability" + }, + "values": { + "manifest_mapped": "Manifest 已映射", + "action_required": "需處置", + "blocked": "阻擋", + "missing": "缺失", + "committed_manifest": "已提交 manifest", + "source_file": "來源檔", + "missing_manifest": "缺 manifest", + "live_check_required": "需只讀 live 證據", + "none": "無", + "name_only": "只顯示名稱", + "template_only": "僅 template", + "payload_redacted": "payload 已遮蔽", + "not_run": "未執行", + "not_applicable": "不適用", + "required": "需要", + "bound": "已綁定", + "source_only": "僅來源", + "action_required_component": "元件需處置" + } } } }, diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index dd0540fbf..dd8504aa9 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -3068,11 +3068,25 @@ "redacted_only": "僅脫敏", "read_only_allowed": "只讀允許", "ready_for_operator_review": "待人工審查", + "blocked_by_evidence": "證據不足阻擋", "dry_run_required": "需 dry-run", "cost_approval_required": "費用需批准", "dependency_approval_required": "依賴需批准", "production_change_blocked": "生產變更阻擋", - "shadow_canary_blocked": "Shadow / Canary 阻擋" + "shadow_canary_blocked": "Shadow / Canary 阻擋", + "manifest_mapped": "Manifest 已對應", + "missing": "缺 manifest", + "source_file": "來源檔", + "committed_manifest": "已提交 manifest", + "missing_manifest": "缺 manifest", + "live_check_required": "需 live 證據", + "not_run": "未執行", + "required": "必要", + "name_only": "只顯示名稱", + "template_only": "只顯示模板", + "payload_redacted": "Payload 已脫敏", + "bound": "已綁定", + "source_only": "僅來源" } }, "offsiteEscrow": { @@ -3105,6 +3119,55 @@ "production_routing_allowed": "生產路由禁止自動變更", "destructive_operation_allowed": "破壞性操作禁止自動執行" } + }, + "runtimeSurface": { + "title": "執行面只讀矩陣", + "source": "{generated} · {current} → {next}", + "componentsTitle": "來源元件", + "contractTitle": "不可誤讀合約", + "metrics": { + "total": "執行面", + "actionRequired": "需處置", + "secrets": "機密面", + "liveMissing": "待 Live 證據", + "boundComponents": "已綁定元件" + }, + "labels": { + "secret": "機密", + "live": "Live 證據" + }, + "kinds": { + "deployment": "Deployment", + "service": "Service", + "ingress": "Ingress / Route", + "cronjob": "CronJob", + "configmap": "ConfigMap", + "secret": "Secret", + "rbac": "RBAC", + "policy": "Policy", + "autoscaler": "Autoscaler", + "availability": "Availability" + }, + "values": { + "manifest_mapped": "Manifest 已映射", + "action_required": "需處置", + "blocked": "阻擋", + "missing": "缺失", + "committed_manifest": "已提交 manifest", + "source_file": "來源檔", + "missing_manifest": "缺 manifest", + "live_check_required": "需只讀 live 證據", + "none": "無", + "name_only": "只顯示名稱", + "template_only": "僅 template", + "payload_redacted": "payload 已遮蔽", + "not_run": "未執行", + "not_applicable": "不適用", + "required": "需要", + "bound": "已綁定", + "source_only": "僅來源", + "action_required_component": "元件需處置" + } } } }, diff --git a/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx b/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx index efbe71ad2..4a5d07dc3 100644 --- a/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx +++ b/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx @@ -34,6 +34,7 @@ import { type BackupDrTargetInventorySnapshot, type BackupNotificationPolicySnapshot, type OffsiteEscrowReadinessStatusSnapshot, + type RuntimeSurfaceInventorySnapshot, } from '@/lib/api-client' function formatDateTime(value: string): string { @@ -205,6 +206,7 @@ export function AutomationInventoryTab() { const [backupReadiness, setBackupReadiness] = useState(null) const [backupPolicy, setBackupPolicy] = useState(null) const [offsiteEscrow, setOffsiteEscrow] = useState(null) + const [runtimeSurface, setRuntimeSurface] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(false) @@ -217,14 +219,16 @@ export function AutomationInventoryTab() { apiClient.getBackupDrReadinessMatrix(), apiClient.getBackupNotificationPolicy(), apiClient.getOffsiteEscrowReadinessStatus(), + apiClient.getRuntimeSurfaceInventory(), ]) - .then(([inventoryData, backlogData, targetData, readinessData, policyData, offsiteEscrowData]) => { + .then(([inventoryData, backlogData, targetData, readinessData, policyData, offsiteEscrowData, runtimeSurfaceData]) => { setSnapshot(inventoryData) setBacklog(backlogData) setBackupTargets(targetData) setBackupReadiness(readinessData) setBackupPolicy(policyData) setOffsiteEscrow(offsiteEscrowData) + setRuntimeSurface(runtimeSurfaceData) setError(false) }) .catch(() => setError(true)) @@ -297,6 +301,19 @@ export function AutomationInventoryTab() { }) }, [offsiteEscrow]) + const visibleRuntimeSurfaces = useMemo(() => { + if (!runtimeSurface) return [] + const priority = { missing: 0, action_required: 1, blocked: 2, manifest_mapped: 3 } as Record + return [...runtimeSurface.runtime_surfaces] + .sort((a, b) => { + const left = priority[a.status] ?? 4 + const right = priority[b.status] ?? 4 + if (left !== right) return left - right + return a.surface_id.localeCompare(b.surface_id) + }) + .slice(0, 8) + }, [runtimeSurface]) + if (loading) { return (
@@ -310,7 +327,7 @@ export function AutomationInventoryTab() { ) } - if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow) { + if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !runtimeSurface) { return (
@@ -357,6 +374,10 @@ export function AutomationInventoryTab() { const actionRequiredOffsiteCards = offsiteEscrow.rollups.by_readiness.action_required ?? 0 const blockedEscrowCards = offsiteEscrow.rollups.by_readiness.blocked ?? 0 const executionBlockedCards = offsiteEscrow.rollups.execution_blocked_card_ids.length + const runtimeActionRequired = runtimeSurface.rollups.action_required_surface_ids.length + const runtimeSecrets = runtimeSurface.rollups.secret_surface_ids.length + const runtimeLiveMissing = runtimeSurface.rollups.live_check_missing_surface_ids.length + const runtimeBoundComponents = runtimeSurface.rollups.source_components_with_runtime_binding const backlogProgressPercent = backlog.progress_summary.overall_percent const explicitApprovalItemCount = backlog.item_approval_boundary_rollup.items_requiring_explicit_approval.length const taskBoundaryCount = snapshot.task_approval_boundary_rollup.total_tasks @@ -389,6 +410,22 @@ export function AutomationInventoryTab() { } } + const runtimeKindLabel = (value: string) => { + try { + return t(`runtimeSurface.kinds.${value}` as never) + } catch { + return value + } + } + + const runtimeValueLabel = (value: string) => { + try { + return t(`runtimeSurface.values.${value}` as never) + } catch { + return value + } + } + return (
@@ -836,6 +873,110 @@ export function AutomationInventoryTab() {
+ +
+
+
+ + + {t('runtimeSurface.title')} + +
+
+ {t('runtimeSurface.source', { + generated: formatDateTime(runtimeSurface.generated_at), + current: runtimeSurface.program_status.current_task_id, + next: runtimeSurface.program_status.next_task_id, + })} +
+
+ +
+ } /> + 0 ? 'warn' : 'ok'} icon={} /> + } /> + 0 ? 'warn' : 'ok'} icon={} /> + } /> +
+ +
+
+ {visibleRuntimeSurfaces.map(surface => ( +
+
+
+ + {surface.display_name} + + +
+
+ + + + +
+
+ {surface.runtime_binding} +
+
+ {surface.health_contract} +
+
+ + +
+
+
+ ))} +
+ +
+
+ + {t('runtimeSurface.componentsTitle')} + + {runtimeSurface.source_runtime_components.slice(0, 5).map(component => ( +
+
+ + {component.display_name} + + +
+
+ {component.runtime_binding} +
+
+ ))} +
+ +
+ + {t('runtimeSurface.contractTitle')} + +
+ {runtimeSurface.operator_contract.secret_display_policy} +
+
+ {runtimeSurface.operator_contract.must_not_interpret_as.slice(0, 6).map(item => ( + + ))} +
+
+
+
+
+
+
@@ -925,6 +1066,9 @@ export function AutomationInventoryTab() { .automation-inventory-offsite-kpi-grid, .automation-inventory-offsite-grid, .automation-inventory-offsite-card-grid, + .automation-inventory-runtime-kpi-grid, + .automation-inventory-runtime-grid, + .automation-inventory-runtime-surface-grid, .automation-inventory-bottom-grid, .automation-inventory-task-grid { grid-template-columns: 1fr !important; diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 0f9ddd74f..603d2faca 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -262,6 +262,11 @@ export const apiClient = { return handleResponse(res) }, + async getRuntimeSurfaceInventory() { + const res = await fetch(`${API_BASE_URL}/agents/runtime-surface-inventory`) + return handleResponse(res) + }, + async getBackupDrTargetInventory() { const res = await fetch(`${API_BASE_URL}/agents/backup-dr-target-inventory`) return handleResponse(res) @@ -789,6 +794,69 @@ export interface AiAgentAutomationBacklogSnapshot { > } +export interface RuntimeSurfaceInventorySnapshot { + schema_version: 'runtime_surface_inventory_v1' + generated_at: string + program_status: { + overall_completion_percent: number + current_priority: 'P0' | 'P1' | 'P2' | 'P3' + current_task_id: string + next_task_id: string + read_only_mode: true + } + source_refs: string[] + rollups: { + total_surfaces: number + by_kind: Record + by_status: Record + by_evidence_level: Record + action_required_surface_ids: string[] + secret_surface_ids: string[] + live_check_missing_surface_ids: string[] + total_source_components: number + source_components_with_runtime_binding: number + } + runtime_surfaces: Array<{ + surface_id: string + display_name: string + kind: 'deployment' | 'service' | 'ingress' | 'cronjob' | 'configmap' | 'secret' | 'rbac' | 'policy' | 'autoscaler' | 'availability' + manifest_ref: string + status: 'manifest_mapped' | 'action_required' | 'blocked' | 'missing' + risk_level: 'low' | 'medium' | 'high' | 'critical' + evidence_level: 'committed_manifest' | 'source_file' | 'missing_manifest' | 'live_check_required' + runtime_binding: string + health_contract: string + secret_exposure: 'none' | 'name_only' | 'template_only' | 'payload_redacted' + live_check_status: 'not_run' | 'not_applicable' | 'required' + evidence_refs: string[] + next_action: string + }> + source_runtime_components: Array<{ + component_id: string + display_name: string + source_ref: string + component_kind: string + runtime_binding: string + status: 'bound' | 'action_required' | 'source_only' + next_action: string + }> + evidence_gaps: Array<{ + gap_id: string + severity: 'low' | 'medium' | 'high' | 'critical' + status: 'action_required' | 'blocked' | 'accepted' + summary: string + evidence_refs: string[] + next_action: string + }> + operator_contract: { + display_mode: 'read_only_runtime_surface' + must_not_interpret_as: string[] + secret_display_policy: string + } + operation_boundaries: Record + approval_boundaries: Record +} + export interface BackupDrTargetInventorySnapshot { schema_version: 'backup_dr_target_inventory_v1' generated_at: string diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 3ff908318..b602b99f2 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -43,6 +43,40 @@ - S4.9 真正能往前的下一步仍是 owner 以脫敏 metadata 回覆 5 題:public-only/local gap、org/user endpoint、110 adjacent scope、repo owner/canonical、legacy/inaccessible disposition。 - `P1-001 runtime surface inventory` 由另一個 AwoooP Session 繼續;本視窗推送後需同步 `gitea/main`,避免 LOGBOOK / workplan 衝突。 +## 2026-06-05|P1-001 執行面只讀矩陣本地完成 + +**背景**:接續 P1-305 / P1-306 任務批准邊界與進度彙總,本段推進 `P1-001`,把 API / Web / Worker / K8s runtime surface 從文件與 manifest 盤點成機器可讀 snapshot、只讀 API 與治理頁矩陣。這是 committed evidence 與只讀展示,不是 live cluster 查詢、Secret payload 讀取、runtime execution 或生產路由變更授權。 + +**本輪完成**: +- 新增 `runtime_surface_inventory_v1` schema 與 `docs/evaluations/runtime_surface_inventory_2026-06-05.json`。 +- 新增 `GET /api/v1/agents/runtime-surface-inventory` 與 runtime surface service guard,檢查 schema、只讀邊界、Secret redaction、operator contract 與 evidence gaps。 +- 治理頁 `/zh-TW/governance?tab=automation-inventory` 新增執行面只讀矩陣、來源元件、不可誤讀合約與 KPI。 +- 同步 automation backlog / inventory snapshot:current `P1-001`、next `P1-002`、backlog overall `74%`、P1 `81%`、done `17/23`、inventory tasks `27`。 + +**目前數字**: +- Runtime surfaces:`22`。 +- 需處置 surfaces:`6`。 +- Secret surfaces:`4`,僅允許 name/template/redacted metadata;不得讀取 payload。 +- 待 Live 證據 gaps:`6`。 +- Source components:`9/9` 已綁定 runtime surface。 + +**本地驗證進度**: +- JSON parse 通過。 +- Runtime surface / inventory / backlog 目標 pytest:`23 passed`。 +- Python py_compile 通過。 +- zh-TW / en i18n key 差異 `0`。 +- Web typecheck 通過。 +- Next production build 通過。 +- source-control-owner-response guard、security-mirror-progress guard 與 `git diff --check` 通過。 +- 本地 `http://127.0.0.1:3123/zh-TW/governance?tab=automation-inventory` desktop `1440x1000` 與 mobile `390x844` smoke 通過:`執行面只讀矩陣`、`來源元件`、`不可誤讀合約`、`P1-001`、`P1-002`、`22`、`74%` 可見;七個 agents API 皆 `200`;`horizontalOverflow=0`、overflowing elements `0`、禁止操作按鈕 `0`。 +- 本地截圖:`/tmp/awoooi-p1-001-runtime-surface-local-desktop.png`、`/tmp/awoooi-p1-001-runtime-surface-local-mobile.png`。 +- 本地 smoke 期間發現 `backupEvidence.statuses.blocked_by_evidence` 缺 i18n key,已補 zh-TW / en 鏡像並把同一狀態字典殘留的 `Source file` / `Committed manifest` 收斂為繁中顯示。 +- 正式環境 deploy 與 production smoke:本輪後續補上。 + +**邊界**: +- `runtime_execution_authorized=false`、`kubectl_allowed=false`、`rollout_allowed=false`、`restart_allowed=false`、`scale_allowed=false`、`delete_allowed=false`、`secret_plaintext_allowed=false`、`active_scan_allowed=false`、`production_route_change_allowed=false`。 +- 下一步:完成驗證後推正式環境,再進 `P1-002` Gitea 工作流程與 runner 健康合約。 + ## 2026-06-05|P1-305 / P1-306 任務批准邊界與進度彙總正式上線 **背景**:接續 P1-106 異地 / Escrow 準備度,本段收斂 AI Agent 自動化盤點的兩個可視化缺口:每個任務的批准邊界與 deterministic 進度百分比。這是 committed snapshot / API / 前台治理頁的只讀顯示,不是 runtime 執行授權、部署授權、owner response 接受或 active gate 提升。 diff --git a/docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md b/docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md index 1b664515c..561a01b47 100644 --- a/docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md +++ b/docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md @@ -10,10 +10,10 @@ |---|---:|---|---| | Agent 市場治理 | 72% | 進行中 | `agent_market_governance_snapshot_v1`、API、UI 分頁、每週觀察流程 | | Nemotron 實際整合應用 | 30% | 完整回放前仍被關卡擋下 | `blocked_needs_evidence`,下一關是 `refresh_source_evidence_then_5_record_smoke_only` | -| 工具 / 服務 / 套件 AI 自動化 | 100% | P0 已完成,P1 套件 / 供應鏈主線已完成;備份 / DR 主線已完成到異地 / escrow 準備度顯示;任務批准邊界與進度彙總細節已完成,下一主線是 P1-001 runtime surface 盤點 | 狀態分類、盤點 schema、權限矩陣、靜態盤點種子、只讀 API、UI 骨架、驗證、自動化待辦 schema / 快照 / API / 分組 UI、Backup / DR 目標盤點、準備度矩陣、備份通知政策、Backup / DR 證據 UI、復原演練批准包模板、異地 / escrow 準備度狀態、任務批准邊界、確定性進度彙總、Python 套件 / 供應鏈只讀基線、JS pnpm/npm 只讀基線、Docker build surface 只讀基線、CVE / license / drift 嚴重度政策、定期依賴漂移與外部資料來源檢查設計、依賴升級批准包模板已完成 | +| 工具 / 服務 / 套件 AI 自動化 | 74% | P0 已完成,P1 套件 / 供應鏈主線已完成;備份 / DR 主線已完成到異地 / escrow 準備度顯示;任務批准邊界、進度彙總與 P1-001 執行面只讀矩陣已完成,下一主線是 P1-002 Gitea 工作流程與 runner 健康合約 | 狀態分類、盤點 schema、權限矩陣、靜態盤點種子、只讀 API、UI 骨架、驗證、自動化待辦 schema / 快照 / API / 分組 UI、Backup / DR 目標盤點、準備度矩陣、備份通知政策、Backup / DR 證據 UI、復原演練批准包模板、異地 / escrow 準備度狀態、任務批准邊界、確定性進度彙總、Python 套件 / 供應鏈只讀基線、JS pnpm/npm 只讀基線、Docker build surface 只讀基線、CVE / license / drift 嚴重度政策、定期依賴漂移與外部資料來源檢查設計、依賴升級批准包模板、runtime_surface_inventory_v1 schema / snapshot / API / UI 已完成 | | 本工作清單與分析報告 | 100% | 已完成 | 本 MD 文件 | -整體計畫完成度:**100%**。 +AI Agent 自動化工作包目前完成度:**74%**。本工作清單文件本身完成度:**100%**。 完成度計算模型: @@ -866,7 +866,7 @@ UI: | ID | 狀態 | % | 負責 Agent | 任務 | 產出 | 關卡 | |---|---|---:|---|---|---|---| -| P1-001 | 待辦 | 0 | OpenClaw | 盤點 API / Web / Worker / K8s runtime surface | K8s / 服務矩陣 | 只讀 | +| P1-001 | 完成 | 100 | OpenClaw | 盤點 API / Web / Worker / K8s runtime surface | `runtime_surface_inventory_v1` / `GET /api/v1/agents/runtime-surface-inventory` / 執行面只讀矩陣 | 只讀;不得查 Secret payload、不得 rollout / restart / scale / delete | | P1-002 | 待辦 | 0 | Hermes | 盤點 Gitea 工作流程與 runner 健康合約 | 工作流程 / runner 矩陣 | 不修改工作流程 | | P1-003 | 待辦 | 0 | Hermes | 盤點 Prometheus / Alertmanager / SigNoz / Grafana 監控合約 | 可觀測性矩陣 | 只讀 | | P1-004 | 待辦 | 0 | OpenClaw | 盤點 AI Router / Ollama / Nemotron / Gemini provider 路徑 | 推理路由矩陣 | 不切 provider | @@ -1052,10 +1052,25 @@ UI: 下一步:P1-001 盤點 API / Web / Worker / K8s runtime surface。 ``` +本次同步: + +```text +進度:74%。 +目前優先級:P1。 +目前任務:P1-001 盤點 API / Web / Worker / K8s runtime surface。 +狀態變更:待辦 -> 完成。 +證據:runtime_surface_inventory_v1 schema / snapshot;GET /api/v1/agents/runtime-surface-inventory;治理頁執行面只讀矩陣;automation backlog 74%;inventory tasks 27。 +目前數字:runtime surfaces 22;action_required 6;Secret surfaces 4;live-check gaps 6;source components 9/9;backlog done 17/23;overall 74%;P1 81%。 +驗證:JSON parse 通過;runtime surface / inventory / backlog 目標 pytest 23 passed;Python py_compile 通過;zh-TW / en i18n key 差異 0;web typecheck 通過;Next production build 通過;source-control-owner-response guard、security-mirror-progress guard 與 git diff --check 通過;本地 `127.0.0.1:3123` desktop / mobile smoke 通過,七個 agents API 皆 200,`horizontalOverflow=0`,禁止操作按鈕 0;正式驗證待本輪推版後補上。 +阻擋:無;runtime execution、rollout、restart、scale、delete、Secret payload、active scan、production routing 仍全部禁止。 +下一步:P1-002 盤點 Gitea 工作流程與 runner 健康合約。 +``` + ## 13. 立即執行順序 -1. P1-001:盤點 API / Web / Worker / K8s runtime surface。 -2. P2 / P3 必須等 P1 runtime surface 可見且關卡穩定後再做。 +1. P1-002:盤點 Gitea 工作流程與 runner 健康合約。 +2. P1-003 / P1-004:接續盤點 Prometheus / Alertmanager / SigNoz / Grafana 監控合約,以及 AI Router / Ollama / Nemotron / Gemini provider 路徑。 +3. P2 / P3 必須等 P1 服務、監控與 provider runtime surface 可見且關卡穩定後再做。 ## 14. 目前風險 diff --git a/docs/evaluations/ai_agent_automation_backlog_2026-06-04.json b/docs/evaluations/ai_agent_automation_backlog_2026-06-04.json index d5146372e..96c934690 100644 --- a/docs/evaluations/ai_agent_automation_backlog_2026-06-04.json +++ b/docs/evaluations/ai_agent_automation_backlog_2026-06-04.json @@ -3,10 +3,10 @@ "generated_at": "2026-06-05T09:05:41+08:00", "source_inventory_snapshot_ref": "docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json", "program_status": { - "overall_completion_percent": 70, + "overall_completion_percent": 74, "current_priority": "P1", - "current_task_id": "P1-306", - "next_task_id": "P1-001", + "current_task_id": "P1-001", + "next_task_id": "P1-002", "read_only_mode": true }, "rollups": { @@ -17,8 +17,8 @@ "P3": 1 }, "by_status": { - "done": 16, - "planned": 7 + "done": 17, + "planned": 6 }, "by_gate_status": { "read_only_allowed": 20, @@ -223,26 +223,27 @@ { "item_id": "AUTO-P1-001", "priority": "P1", - "status": "planned", + "status": "done", "workstream_id": "WS3", "source_asset_id": "awoooi_k8s_prod", "source_signal_kind": "runtime_evidence_gap", "title": "盤點 API / Web / Worker / K8s runtime surface", "owner_agent": "openclaw", - "recommended_action": "建立只讀 runtime surface matrix,列出 Deployment、Service、Ingress、CronJob、ConfigMap、Secret 與對應健康證據。", + "recommended_action": "已建立只讀 runtime surface matrix,列出 Deployment、Service、Ingress、CronJob、ConfigMap、Secret 與對應 source / manifest 證據。", "action_class": "observe", "gate_status": "read_only_allowed", "risk_level": "high", "evidence_refs": [ - "k8s/awoooi-prod/", - "docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json" + "docs/evaluations/runtime_surface_inventory_2026-06-05.json", + "GET /api/v1/agents/runtime-surface-inventory", + "k8s/awoooi-prod/" ], "acceptance_criteria": [ "不執行 rollout、restart、scale、delete", "每個 runtime surface 都有來源檔或只讀檢查證據", "缺口列為 action-required,不直接修復" ], - "next_review": "P1-001", + "next_review": "P1-002", "approval_boundary": { "mode": "read_only_allowed", "display_summary": "只允許只讀盤點、顯示與批准包準備;不得直接執行寫入、部署、通知或外部呼叫。", @@ -1165,16 +1166,16 @@ ] }, "progress_summary": { - "overall_percent": 70, - "done_items": 16, - "planned_items": 7, + "overall_percent": 74, + "done_items": 17, + "planned_items": 6, "total_items": 23, "formula": "round(done_items / total_items * 100),只有 status=done 計入完成;planned/in_progress/blocked/deferred/rejected 不計入。", "by_priority": [ { "priority": "P1", - "completion_percent": 76, - "done_items": 16, + "completion_percent": 81, + "done_items": 17, "total_items": 21 }, { @@ -1202,10 +1203,10 @@ { "workstream_id": "WS3", "display_name": "監控自動化", - "completion_percent": 0, - "done_items": 0, + "completion_percent": 25, + "done_items": 1, "total_items": 4, - "next_task_id": "P1-001" + "next_task_id": "P1-002" }, { "workstream_id": "WS4", @@ -1245,7 +1246,7 @@ "completion_percent": 100, "done_items": 2, "total_items": 2, - "next_task_id": "P1-001" + "next_task_id": "P1-002" } ] } diff --git a/docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json b/docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json index e29424f39..8172ce2ac 100644 --- a/docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json +++ b/docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json @@ -4,8 +4,8 @@ "program_status": { "overall_completion_percent": 100, "current_priority": "P1", - "current_task_id": "P1-306", - "next_task_id": "P1-001", + "current_task_id": "P1-001", + "next_task_id": "P1-002", "read_only_mode": true }, "status_taxonomy": { @@ -167,28 +167,30 @@ "domain_id": "services", "display_name": "AWOOOI Worker 與排程器", "asset_type": "worker", - "status": "planned", + "status": "action_required", "gate_status": "read_only_allowed", "owner_agent": "openclaw", "risk_level": "high", "evidence_refs": [ - "apps/api/src/workers/" + "apps/api/src/workers/", + "docs/evaluations/runtime_surface_inventory_2026-06-05.json" ], - "next_action": "P1-001 盤點 worker 與排程器 runtime surface。" + "next_action": "P1-001 D0 已完成 committed manifest / source matrix;下一步 P1-002 對照 Gitea workflow / runner 健康。" }, { "asset_id": "awoooi_k8s_prod", "domain_id": "services", "display_name": "awoooi-prod K8s 工作負載", "asset_type": "k8s_workload", - "status": "planned", + "status": "action_required", "gate_status": "read_only_allowed", "owner_agent": "openclaw", "risk_level": "high", "evidence_refs": [ + "docs/evaluations/runtime_surface_inventory_2026-06-05.json", "k8s/awoooi-prod/" ], - "next_action": "P1-001 盤點 Deployment、Service、Ingress、CronJob、ConfigMap、Secret。" + "next_action": "P1-001 D0 已完成 committed manifest / source matrix;下一步 P1-002 對照 Gitea workflow / runner 健康。" }, { "asset_id": "awoooi_postgresql", @@ -467,9 +469,9 @@ { "workstream_id": "WS3", "display_name": "監控自動化", - "completion_percent": 20, - "status": "planned", - "next_task_id": "P1-001" + "completion_percent": 25, + "status": "in_progress", + "next_task_id": "P1-002" }, { "workstream_id": "WS4", @@ -502,9 +504,9 @@ { "workstream_id": "WS8", "display_name": "產品 UI", - "completion_percent": 90, + "completion_percent": 92, "status": "in_progress", - "next_task_id": "P1-001" + "next_task_id": "P1-002" } ], "tasks": [ @@ -765,6 +767,38 @@ ] } }, + { + "task_id": "P1-001", + "priority": "P1", + "status": "done", + "completion_percent": 100, + "owner_agent": "openclaw", + "title": "盤點 API / Web / Worker / K8s runtime surface", + "output": "docs/evaluations/runtime_surface_inventory_2026-06-05.json + GET /api/v1/agents/runtime-surface-inventory", + "gate_status": "read_only_allowed", + "next_action": "完成 D0 committed manifest / source matrix;下一步 P1-002 盤點 Gitea workflow 與 runner 健康合約。", + "approval_boundary": { + "mode": "read_only_allowed", + "display_summary": "只允許只讀盤點、顯示與批准包準備;不得直接執行寫入、部署、通知或外部呼叫。", + "allowed_actions": [ + "讀取 committed snapshot", + "整理只讀證據", + "顯示治理 UI" + ], + "blocked_actions": [ + "production_write", + "runtime_execution", + "destructive_operation", + "secret_plaintext_collection", + "unapproved_deploy", + "unapproved_external_call" + ], + "requires_operator_approval_for": [ + "任何非只讀操作", + "任何部署、排程、通知或外部呼叫變更" + ] + } + }, { "task_id": "P1-301", "priority": "P1", @@ -1636,6 +1670,18 @@ "kind": "doc", "ref": "docs/evaluations/ai_agent_automation_backlog_2026-06-04.json", "result": "backlog snapshot 已補 progress_summary 與 item_approval_boundary_rollup;百分比由 status=done / total_items 重算,不代表 runtime gate 提升。" + }, + { + "evidence_id": "runtime_surface_inventory_snapshot", + "kind": "runtime", + "ref": "docs/evaluations/runtime_surface_inventory_2026-06-05.json", + "result": "P1-001 D0 committed manifest / source runtime surface matrix 已建立,涵蓋 22 個 runtime surfaces 與 9 個 source components。" + }, + { + "evidence_id": "runtime_surface_inventory_api", + "kind": "api", + "ref": "GET /api/v1/agents/runtime-surface-inventory", + "result": "只讀 API 回傳 runtime_surface_inventory_v1;不查 live K8s、不讀 Secret payload、不提供執行按鈕。" } ], "approval_boundaries": { @@ -1646,10 +1692,10 @@ "destructive_operation_allowed": false }, "task_approval_boundary_rollup": { - "total_tasks": 26, + "total_tasks": 27, "by_mode": { "ready_for_operator_review": 1, - "read_only_allowed": 24, + "read_only_allowed": 25, "approval_required": 1 }, "tasks_requiring_explicit_approval": [ @@ -1665,6 +1711,7 @@ "P0-006", "P0-007", "P0-008", + "P1-001", "P1-101", "P1-102", "P1-103", diff --git a/docs/evaluations/runtime_surface_inventory_2026-06-05.json b/docs/evaluations/runtime_surface_inventory_2026-06-05.json new file mode 100644 index 000000000..3f74ecd69 --- /dev/null +++ b/docs/evaluations/runtime_surface_inventory_2026-06-05.json @@ -0,0 +1,648 @@ +{ + "schema_version": "runtime_surface_inventory_v1", + "generated_at": "2026-06-05T09:47:25+08:00", + "program_status": { + "overall_completion_percent": 100, + "current_priority": "P1", + "current_task_id": "P1-001", + "next_task_id": "P1-002", + "read_only_mode": true + }, + "source_refs": [ + "docs/schemas/runtime_surface_inventory_v1.schema.json", + "k8s/awoooi-prod/kustomization.yaml", + "k8s/awoooi-prod/05-deployment-web.yaml", + "k8s/awoooi-prod/06-deployment-api.yaml", + "k8s/awoooi-prod/08-deployment-worker.yaml", + "k8s/awoooi-prod/10-deployment-auto-repair-canary.yaml", + "k8s/awoooi-prod/13-cronjob-k3s-report.yaml", + "k8s/awoooi-prod/14-cronjob-weekly-report.yaml", + "k8s/awoooi-prod/15-cronjob-km-vectorize.yaml", + "k8s/awoooi-prod/12-cronjob-drift-scanner.yaml", + "k8s/awoooi-prod/03-secrets.example.yaml", + "k8s/awoooi-prod/04-repair-ssh-key-template.yaml", + "k8s/awoooi-prod/04-repair-known-hosts-template.yaml", + "k8s/awoooi-prod/04-ssh-mcp-secret.example.yaml", + "k8s/awoooi-prod/07-rbac.yaml", + "k8s/awoooi-prod/09-pdb.yaml", + "k8s/awoooi-prod/11-vpa.yaml", + "k8s/awoooi-prod/12-hpa.yaml", + "k8s/awoooi-prod/02-network-policy.yaml", + ".gitea/workflows/cd.yaml" + ], + "rollups": { + "total_surfaces": 22, + "by_kind": { + "deployment": 4, + "service": 2, + "ingress": 1, + "cronjob": 4, + "configmap": 2, + "secret": 4, + "rbac": 1, + "policy": 2, + "autoscaler": 1, + "availability": 1 + }, + "by_status": { + "manifest_mapped": 16, + "action_required": 6 + }, + "by_evidence_level": { + "committed_manifest": 17, + "live_check_required": 4, + "missing_manifest": 1 + }, + "action_required_surface_ids": [ + "external_nginx_gateway_route", + "awoooi_secrets", + "awoooi_repair_ssh_key_secret", + "awoooi_repair_known_hosts_secret", + "ssh_mcp_key_secret", + "hpa_vpa_autoscaler_contract" + ], + "secret_surface_ids": [ + "awoooi_secrets", + "awoooi_repair_ssh_key_secret", + "awoooi_repair_known_hosts_secret", + "ssh_mcp_key_secret" + ], + "live_check_missing_surface_ids": [ + "external_nginx_gateway_route", + "awoooi_secrets", + "awoooi_repair_ssh_key_secret", + "awoooi_repair_known_hosts_secret", + "ssh_mcp_key_secret", + "hpa_vpa_autoscaler_contract" + ], + "total_source_components": 9, + "source_components_with_runtime_binding": 9 + }, + "runtime_surfaces": [ + { + "surface_id": "awoooi_api_deployment", + "display_name": "AWOOOI API Deployment", + "kind": "deployment", + "manifest_ref": "k8s/awoooi-prod/06-deployment-api.yaml", + "status": "manifest_mapped", + "risk_level": "high", + "evidence_level": "committed_manifest", + "runtime_binding": "Deployment/awoooi-api replicas=2 image=192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER via kustomization images", + "health_contract": "liveness /api/v1/health/live;readiness /api/v1/health/ready;public health 由 CD 使用 https://awoooi.wooo.work/api/v1/health 驗證", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/06-deployment-api.yaml", + "k8s/awoooi-prod/kustomization.yaml", + ".gitea/workflows/cd.yaml" + ], + "next_action": "P1-002 盤點 runner / rollout evidence 時,只能讀取 rollout status 與健康證據,不得 restart、scale 或 patch。" + }, + { + "surface_id": "awoooi_web_deployment", + "display_name": "AWOOOI Web Deployment", + "kind": "deployment", + "manifest_ref": "k8s/awoooi-prod/05-deployment-web.yaml", + "status": "manifest_mapped", + "risk_level": "medium", + "evidence_level": "committed_manifest", + "runtime_binding": "Deployment/awoooi-web replicas=2 image=192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER via kustomization images", + "health_contract": "liveness /api/health;readiness /api/health;NEXT_PUBLIC_API_URL 必須為 https://awoooi.wooo.work", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/05-deployment-web.yaml", + "apps/web/src/app/api/health/route.ts", + "docs/HARD_RULES.md" + ], + "next_action": "前端 runtime 只允許讀取健康與 UI 證據;不得把 NodePort / internal IP 寫回 NEXT_PUBLIC bundle。" + }, + { + "surface_id": "awoooi_worker_deployment", + "display_name": "AWOOOI Signal Worker Deployment", + "kind": "deployment", + "manifest_ref": "k8s/awoooi-prod/08-deployment-worker.yaml", + "status": "manifest_mapped", + "risk_level": "high", + "evidence_level": "committed_manifest", + "runtime_binding": "Deployment/awoooi-worker replicas=1 command=python -m src.workers.signal_worker", + "health_contract": "liveness 檢查 /tmp/worker-healthy mtime;readiness /tmp/worker-ready;Redis Streams consumer group awoooi-workers", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/08-deployment-worker.yaml", + "apps/api/src/workers/signal_worker.py", + "apps/api/tests/test_runtime_bootstrap_guards.py" + ], + "next_action": "P1-002 / P1-003 只讀盤點 worker heartbeat 與 queue health,不得手動刪 Pod 或 rollout restart。" + }, + { + "surface_id": "auto_repair_canary_deployment", + "display_name": "Auto Repair Canary Deployment", + "kind": "deployment", + "manifest_ref": "k8s/awoooi-prod/10-deployment-auto-repair-canary.yaml", + "status": "manifest_mapped", + "risk_level": "medium", + "evidence_level": "committed_manifest", + "runtime_binding": "Deployment/awoooi-auto-repair-canary replicas=1;不承接流量、不掛 Secret、不暴露 Service", + "health_contract": "sleep loop 作為 live-fire target;只有批准後的低風險驗證可使用,不屬於本任務執行範圍", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/10-deployment-auto-repair-canary.yaml" + ], + "next_action": "維持只讀標記;任何 canary 操作仍需明確批准。" + }, + { + "surface_id": "awoooi_api_service", + "display_name": "AWOOOI API Service", + "kind": "service", + "manifest_ref": "k8s/awoooi-prod/06-deployment-api.yaml", + "status": "manifest_mapped", + "risk_level": "high", + "evidence_level": "committed_manifest", + "runtime_binding": "Service/awoooi-api-svc NodePort 32334 targetPort 8000 selector app=awoooi-api", + "health_contract": "內部 CronJob 使用 http://awoooi-api-svc.awoooi-prod.svc.cluster.local:8000;外部 health 走正式域名", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/06-deployment-api.yaml", + "k8s/awoooi-prod/15-cronjob-km-vectorize.yaml", + "k8s/awoooi-prod/12-cronjob-drift-scanner.yaml" + ], + "next_action": "只讀比對 Service selector / endpoint / public health,不得 patch Service 或改 NodePort。" + }, + { + "surface_id": "awoooi_web_service", + "display_name": "AWOOOI Web Service", + "kind": "service", + "manifest_ref": "k8s/awoooi-prod/05-deployment-web.yaml", + "status": "manifest_mapped", + "risk_level": "medium", + "evidence_level": "committed_manifest", + "runtime_binding": "Service/awoooi-web-svc NodePort 32335 targetPort 3000 selector app=awoooi-web", + "health_contract": "public route /zh-TW/governance?tab=automation-inventory;web container health /api/health", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/05-deployment-web.yaml", + "apps/web/src/app/api/health/route.ts", + ".gitea/workflows/cd.yaml" + ], + "next_action": "只讀確認 public route 與 UI smoke;不得修改 Service 或 public routing。" + }, + { + "surface_id": "external_nginx_gateway_route", + "display_name": "External Nginx / Domain Route", + "kind": "ingress", + "manifest_ref": "缺 Kubernetes Ingress manifest;由外部 gateway / public domain 與 NodePort health 證據承接", + "status": "action_required", + "risk_level": "high", + "evidence_level": "missing_manifest", + "runtime_binding": "https://awoooi.wooo.work -> external gateway -> NodePort 32334/32335;K8s repo 目前沒有 Ingress resource", + "health_contract": "CD 使用 public API health 與 production Playwright smoke;Ingress / gateway 設定仍需只讀外部路由盤點", + "secret_exposure": "none", + "live_check_status": "required", + "evidence_refs": [ + ".gitea/workflows/cd.yaml", + "docs/HARD_RULES.md", + "k8s/awoooi-prod/05-deployment-web.yaml", + "k8s/awoooi-prod/06-deployment-api.yaml" + ], + "next_action": "P1-002 只讀補 gateway / runner / DNS evidence;不得直接新增 Ingress 或改 Nginx。" + }, + { + "surface_id": "k3s_status_report_cronjob", + "display_name": "K3s Status Report CronJob", + "kind": "cronjob", + "manifest_ref": "k8s/awoooi-prod/13-cronjob-k3s-report.yaml", + "status": "manifest_mapped", + "risk_level": "medium", + "evidence_level": "committed_manifest", + "runtime_binding": "CronJob/k3s-status-report schedule=0 1 * * * timeZone=Asia/Taipei command=python -m src.services.k3s_monitor_service", + "health_contract": "Telegram daily K3s status;成功不得即時洗版,失敗才需 action-required", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/13-cronjob-k3s-report.yaml", + "apps/api/src/services/k3s_monitor_service.py" + ], + "next_action": "P1-002 讀取 CronJob last schedule / failure evidence;不得 create job 手動觸發。" + }, + { + "surface_id": "weekly_report_cronjob", + "display_name": "Weekly Report CronJob", + "kind": "cronjob", + "manifest_ref": "k8s/awoooi-prod/14-cronjob-weekly-report.yaml", + "status": "manifest_mapped", + "risk_level": "medium", + "evidence_level": "committed_manifest", + "runtime_binding": "CronJob/weekly-report schedule=0 10 * * 5 timeZone=Asia/Taipei command=python -m src.services.weekly_report_service", + "health_contract": "每週報告;Telegram success noise suppression 必須保留", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/14-cronjob-weekly-report.yaml", + "apps/api/src/services/weekly_report_service.py" + ], + "next_action": "只讀補 last run / failure-only notification evidence。" + }, + { + "surface_id": "km_vectorize_cronjob", + "display_name": "KM Vectorize CronJob", + "kind": "cronjob", + "manifest_ref": "k8s/awoooi-prod/15-cronjob-km-vectorize.yaml", + "status": "manifest_mapped", + "risk_level": "medium", + "evidence_level": "committed_manifest", + "runtime_binding": "CronJob/km-vectorize schedule=0 19 * * * timeZone=Asia/Taipei command=python /app/scripts/cron_km_vectorize.py", + "health_contract": "呼叫 in-cluster API Service;failedJobsHistoryLimit=0,失敗證據應進 AwoooP/KM governance", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/15-cronjob-km-vectorize.yaml", + "scripts/cron_km_vectorize.py" + ], + "next_action": "只讀確認 last schedule 與 failed row evidence,不得手動 vectorize 或重跑 job。" + }, + { + "surface_id": "drift_scanner_cronjob", + "display_name": "Config Drift Scanner CronJob", + "kind": "cronjob", + "manifest_ref": "k8s/awoooi-prod/12-cronjob-drift-scanner.yaml", + "status": "manifest_mapped", + "risk_level": "high", + "evidence_level": "committed_manifest", + "runtime_binding": "CronJob/drift-scanner schedule=0 * * * * command posts /api/v1/drift/internal/scan", + "health_contract": "只掃描 awoooi-prod;不得把 drift scan 當 active remediation 或自動 patch", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/12-cronjob-drift-scanner.yaml", + "apps/api/src/api/v1/drift.py" + ], + "next_action": "P1-003 盤點 drift scanner last run / alert mapping;不得修改 alert rule 或 workflow。" + }, + { + "surface_id": "awoooi_config_configmap", + "display_name": "AWOOOI ConfigMap", + "kind": "configmap", + "manifest_ref": "k8s/awoooi-prod/04-configmap.yaml", + "status": "manifest_mapped", + "risk_level": "high", + "evidence_level": "committed_manifest", + "runtime_binding": "ConfigMap/awoooi-config mounted via envFrom in API / Web / Worker / CronJob", + "health_contract": "承載非機密 endpoint / feature flags;任何 provider order 或 internal IP 曝露需獨立審查", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/04-configmap.yaml", + "k8s/awoooi-prod/06-deployment-api.yaml", + "k8s/awoooi-prod/05-deployment-web.yaml" + ], + "next_action": "P1-004 / P2 配置優化只能產 proposal;不得在本任務改 ConfigMap。" + }, + { + "surface_id": "service_registry_configmap", + "display_name": "Service Registry ConfigMap", + "kind": "configmap", + "manifest_ref": "k8s/awoooi-prod/15-service-registry-configmap.yaml", + "status": "manifest_mapped", + "risk_level": "high", + "evidence_level": "committed_manifest", + "runtime_binding": "ConfigMap/service-registry mounted at /app/ops/config/service-registry.yaml in API pod;CD 直接 apply", + "health_contract": "stateful_level BLOCK / CRITICAL_HITL / STANDARD_HITL / AUTO 是自動修復爆炸半徑守門依據", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/15-service-registry-configmap.yaml", + ".gitea/workflows/cd.yaml" + ], + "next_action": "只讀顯示服務分級;不得自動接受或修改 service stateful level。" + }, + { + "surface_id": "awoooi_secrets", + "display_name": "AWOOOI Secrets", + "kind": "secret", + "manifest_ref": "k8s/awoooi-prod/03-secrets.example.yaml + .gitea/workflows/cd.yaml Secret injection", + "status": "action_required", + "risk_level": "critical", + "evidence_level": "live_check_required", + "runtime_binding": "Secret/awoooi-secrets referenced by API / Web / Worker / CronJob envFrom;payload 永遠不得進 snapshot", + "health_contract": "只允許 metadata / key existence / redacted policy;禁止 secret plaintext read、base64 decode 或 log echo", + "secret_exposure": "template_only", + "live_check_status": "required", + "evidence_refs": [ + "k8s/awoooi-prod/03-secrets.example.yaml", + ".gitea/workflows/cd.yaml", + "docs/HARD_RULES.md" + ], + "next_action": "P1-002 只能補 Secret metadata existence / age evidence;不得讀 payload。" + }, + { + "surface_id": "awoooi_repair_ssh_key_secret", + "display_name": "Repair SSH Key Secret", + "kind": "secret", + "manifest_ref": "k8s/awoooi-prod/04-repair-ssh-key-template.yaml", + "status": "action_required", + "risk_level": "critical", + "evidence_level": "live_check_required", + "runtime_binding": "Secret/awoooi-repair-ssh-key mounted read-only into API pod /etc/repair-ssh", + "health_contract": "command= forced repair bot;只允許 template / metadata visibility;不得讀私鑰內容", + "secret_exposure": "template_only", + "live_check_status": "required", + "evidence_refs": [ + "k8s/awoooi-prod/04-repair-ssh-key-template.yaml", + "k8s/awoooi-prod/06-deployment-api.yaml" + ], + "next_action": "只讀確認 Secret 是否存在與 mount 是否健康;不得 ssh-keyscan、patch 或讀 key。" + }, + { + "surface_id": "awoooi_repair_known_hosts_secret", + "display_name": "Repair known_hosts Secret", + "kind": "secret", + "manifest_ref": "k8s/awoooi-prod/04-repair-known-hosts-template.yaml + .gitea/workflows/cd.yaml", + "status": "action_required", + "risk_level": "high", + "evidence_level": "live_check_required", + "runtime_binding": "Secret/awoooi-repair-known-hosts mounted read-only into API pod /etc/repair-known-hosts", + "health_contract": "CD 需四台主機指紋完整才 patch;本任務不執行 ssh-keyscan、不更新 Secret", + "secret_exposure": "template_only", + "live_check_status": "required", + "evidence_refs": [ + "k8s/awoooi-prod/04-repair-known-hosts-template.yaml", + ".gitea/workflows/cd.yaml" + ], + "next_action": "P1-002 只讀讀取 Secret metadata 與 CD evidence;不得重掃或 patch known_hosts。" + }, + { + "surface_id": "ssh_mcp_key_secret", + "display_name": "SSH MCP Key Secret", + "kind": "secret", + "manifest_ref": "k8s/awoooi-prod/04-ssh-mcp-secret.example.yaml", + "status": "action_required", + "risk_level": "critical", + "evidence_level": "live_check_required", + "runtime_binding": "Secret/ssh-mcp-key mounted read-only into API pod /run/secrets/ssh_mcp_key 與 /etc/ssh-mcp/known_hosts", + "health_contract": "MCP SSH 診斷需 redaction / audit;本任務不得讀 key、不得建立 SSH session、不得修復主機", + "secret_exposure": "template_only", + "live_check_status": "required", + "evidence_refs": [ + "k8s/awoooi-prod/04-ssh-mcp-secret.example.yaml", + "k8s/awoooi-prod/06-deployment-api.yaml" + ], + "next_action": "只讀確認 metadata 與 mount path;任何 MCP SSH 執行仍需批准與 audit。" + }, + { + "surface_id": "executor_rbac", + "display_name": "Executor RBAC", + "kind": "rbac", + "manifest_ref": "k8s/awoooi-prod/07-rbac.yaml", + "status": "manifest_mapped", + "risk_level": "critical", + "evidence_level": "committed_manifest", + "runtime_binding": "ServiceAccount/awoooi-executor + ClusterRole/awoooi-executor-role + ClusterRoleBinding", + "health_contract": "允許讀取多類 K8s 資源;寫入權限限 pods delete、deployments patch、scale 等高風險操作,必須由 approval gate 阻擋", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/07-rbac.yaml", + "apps/api/src/services/executor.py" + ], + "next_action": "P2 安全執行關卡前只做只讀 RBAC diff;不得修改 ClusterRole。" + }, + { + "surface_id": "namespace_quota_limits", + "display_name": "Namespace Quota / LimitRange", + "kind": "policy", + "manifest_ref": "k8s/awoooi-prod/01-namespace-quota.yaml", + "status": "manifest_mapped", + "risk_level": "medium", + "evidence_level": "committed_manifest", + "runtime_binding": "Namespace/awoooi-prod + ResourceQuota/awoooi-prod-quota + LimitRange/awoooi-prod-limits", + "health_contract": "保護 namespace resource blast radius;任何 limit 調整屬 P2 proposal,不在本任務執行", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/01-namespace-quota.yaml" + ], + "next_action": "只讀對齊 quota usage / HPA 建議;不得 patch quota。" + }, + { + "surface_id": "network_policy_contract", + "display_name": "NetworkPolicy Contract", + "kind": "policy", + "manifest_ref": "k8s/awoooi-prod/02-network-policy.yaml", + "status": "manifest_mapped", + "risk_level": "critical", + "evidence_level": "committed_manifest", + "runtime_binding": "NetworkPolicy/default-deny-all + allow-nginx-ingress + allow-required-egress;kustomization 註記由 CD 單獨 apply", + "health_contract": "預設拒絕;允許 Nginx gateway、K3s NodePort、Ollama proxy、monitoring、K8s API 與必要服務 egress", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/02-network-policy.yaml", + "k8s/awoooi-prod/kustomization.yaml", + "docs/HARD_RULES.md" + ], + "next_action": "P1-003 / P2 只讀盤點 egress / alert chain evidence;不得 patch NetworkPolicy。" + }, + { + "surface_id": "hpa_vpa_autoscaler_contract", + "display_name": "HPA / VPA Autoscaler Contract", + "kind": "autoscaler", + "manifest_ref": "k8s/awoooi-prod/12-hpa.yaml + k8s/awoooi-prod/11-vpa.yaml", + "status": "action_required", + "risk_level": "high", + "evidence_level": "committed_manifest", + "runtime_binding": "HPA api/web/worker 與 VPA api/web/worker manifests 存在,但 kustomization resources 未列入;需只讀確認實際 apply 路徑與 runtime 狀態", + "health_contract": "HPA/VPA 只能提供建議或已批准擴縮容;不得自動提升 runtime gate", + "secret_exposure": "none", + "live_check_status": "required", + "evidence_refs": [ + "k8s/awoooi-prod/12-hpa.yaml", + "k8s/awoooi-prod/11-vpa.yaml", + "k8s/awoooi-prod/kustomization.yaml" + ], + "next_action": "P2 配置優化前只讀確認 HPA/VPA 是否實際存在;不得 apply 或 patch autoscaler。" + }, + { + "surface_id": "pod_disruption_budget", + "display_name": "PodDisruptionBudget", + "kind": "availability", + "manifest_ref": "k8s/awoooi-prod/09-pdb.yaml", + "status": "manifest_mapped", + "risk_level": "medium", + "evidence_level": "committed_manifest", + "runtime_binding": "PDB/api minAvailable=1;PDB/web minAvailable=1;PDB/worker maxUnavailable=1", + "health_contract": "節點維護與 rollout availability guard;不得在本任務變更 maxUnavailable / minAvailable", + "secret_exposure": "none", + "live_check_status": "not_run", + "evidence_refs": [ + "k8s/awoooi-prod/09-pdb.yaml", + "k8s/awoooi-prod/kustomization.yaml" + ], + "next_action": "只讀確認 PDB health 與 rollout evidence;不得修改可用性策略。" + } + ], + "source_runtime_components": [ + { + "component_id": "api_fastapi_app", + "display_name": "FastAPI app", + "source_ref": "apps/api/src/main.py", + "component_kind": "api_process", + "runtime_binding": "Deployment/awoooi-api container api", + "status": "bound", + "next_action": "只讀 health / route inventory。" + }, + { + "component_id": "api_health_routes", + "display_name": "API health routes", + "source_ref": "apps/api/src/routes/health.py", + "component_kind": "health_endpoint", + "runtime_binding": "Deployment/awoooi-api probes /api/v1/health/live and /api/v1/health/ready", + "status": "bound", + "next_action": "只讀確認 probe 與 public health 契約。" + }, + { + "component_id": "web_next_app", + "display_name": "Next.js app", + "source_ref": "apps/web/src/app/", + "component_kind": "web_process", + "runtime_binding": "Deployment/awoooi-web container web", + "status": "bound", + "next_action": "只讀 UI smoke;不得改 public API route。" + }, + { + "component_id": "web_health_route", + "display_name": "Web health route", + "source_ref": "apps/web/src/app/api/health/route.ts", + "component_kind": "health_endpoint", + "runtime_binding": "Deployment/awoooi-web probes /api/health", + "status": "bound", + "next_action": "只讀確認 web health。" + }, + { + "component_id": "signal_worker", + "display_name": "Signal Worker", + "source_ref": "apps/api/src/workers/signal_worker.py", + "component_kind": "worker_process", + "runtime_binding": "Deployment/awoooi-worker command python -m src.workers.signal_worker", + "status": "bound", + "next_action": "只讀 queue / heartbeat evidence。" + }, + { + "component_id": "k3s_monitor_service", + "display_name": "K3s monitor service", + "source_ref": "apps/api/src/services/k3s_monitor_service.py", + "component_kind": "scheduled_report", + "runtime_binding": "CronJob/k3s-status-report", + "status": "bound", + "next_action": "只讀 last schedule / failure evidence。" + }, + { + "component_id": "weekly_report_service", + "display_name": "Weekly report service", + "source_ref": "apps/api/src/services/weekly_report_service.py", + "component_kind": "scheduled_report", + "runtime_binding": "CronJob/weekly-report", + "status": "bound", + "next_action": "保留 success-noise suppression。" + }, + { + "component_id": "km_vectorize_script", + "display_name": "KM vectorize script", + "source_ref": "scripts/cron_km_vectorize.py", + "component_kind": "scheduled_job", + "runtime_binding": "CronJob/km-vectorize", + "status": "bound", + "next_action": "只讀確認 vectorize report;不得手動重跑。" + }, + { + "component_id": "drift_internal_scan", + "display_name": "Drift internal scan endpoint", + "source_ref": "apps/api/src/api/v1/drift.py", + "component_kind": "internal_endpoint", + "runtime_binding": "CronJob/drift-scanner POST /api/v1/drift/internal/scan", + "status": "bound", + "next_action": "只讀盤點 drift evidence;不得自動 remediation。" + } + ], + "evidence_gaps": [ + { + "gap_id": "external_gateway_manifest_gap", + "severity": "high", + "status": "action_required", + "summary": "正式域名健康證據存在,但 repo 內沒有 Kubernetes Ingress manifest;外部 Nginx / gateway 路由仍需只讀盤點。", + "evidence_refs": [ + ".gitea/workflows/cd.yaml", + "k8s/awoooi-prod/05-deployment-web.yaml", + "k8s/awoooi-prod/06-deployment-api.yaml" + ], + "next_action": "P1-002 盤點 gateway / runner / DNS evidence,不直接改 Nginx 或 Ingress。" + }, + { + "gap_id": "secret_metadata_live_check_gap", + "severity": "critical", + "status": "action_required", + "summary": "四個 Secret surface 只能以 template / metadata 顯示;仍缺只讀 metadata existence / age evidence,禁止讀 payload。", + "evidence_refs": [ + "k8s/awoooi-prod/03-secrets.example.yaml", + "k8s/awoooi-prod/04-repair-ssh-key-template.yaml", + "k8s/awoooi-prod/04-repair-known-hosts-template.yaml", + "k8s/awoooi-prod/04-ssh-mcp-secret.example.yaml" + ], + "next_action": "P1-002 只讀補 Secret metadata,不 base64 decode、不輸出明文。" + }, + { + "gap_id": "autoscaler_apply_path_gap", + "severity": "medium", + "status": "action_required", + "summary": "HPA / VPA manifest 存在,但未列入 kustomization resources;需只讀確認實際 apply path 與 runtime 狀態。", + "evidence_refs": [ + "k8s/awoooi-prod/12-hpa.yaml", + "k8s/awoooi-prod/11-vpa.yaml", + "k8s/awoooi-prod/kustomization.yaml" + ], + "next_action": "P2 配置優化前只讀確認,不在本任務 apply 或 patch。" + } + ], + "operator_contract": { + "display_mode": "read_only_runtime_surface", + "must_not_interpret_as": [ + "runtime 執行批准", + "rollout / restart / scale 批准", + "Secret payload 可讀批准", + "生產路由變更批准", + "HPA / VPA apply 批准", + "active scan 或 auto remediation 批准", + "runtime 執行授權", + "rollout / restart / scale / delete 批准", + "Secret 已驗證或可讀取", + "Ingress / DNS 可修改" + ], + "secret_display_policy": "只顯示 template、metadata、引用路徑與 redacted policy;禁止 Secret 明文、base64 decode、token、key、password 或私鑰內容進入 snapshot/API/UI/log。" + }, + "operation_boundaries": { + "read_only_api_allowed": true, + "live_k8s_query_allowed": false, + "kubectl_allowed": false, + "rollout_allowed": false, + "restart_allowed": false, + "scale_allowed": false, + "delete_allowed": false, + "secret_read_allowed": false, + "secret_plaintext_allowed": false, + "active_scan_allowed": false, + "production_route_change_allowed": false + }, + "approval_boundaries": { + "runtime_execution_authorized": false, + "action_buttons_allowed": false, + "secret_value_collection_allowed": false, + "host_change_authorized": false, + "workflow_modification_authorized": false, + "production_routing_authorized": false, + "destructive_operation_allowed": false + } +} diff --git a/docs/schemas/runtime_surface_inventory_v1.schema.json b/docs/schemas/runtime_surface_inventory_v1.schema.json new file mode 100644 index 000000000..6a4b238cb --- /dev/null +++ b/docs/schemas/runtime_surface_inventory_v1.schema.json @@ -0,0 +1,150 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:runtime-surface-inventory-v1", + "title": "AWOOOI Runtime surface 只讀盤點 v1", + "description": "以 repo 內 committed manifest 與 source file 建立 API / Web / Worker / K8s runtime surface 矩陣;不查 live cluster、不讀 Secret payload、不執行 rollout/restart/scale/delete。", + "type": "object", + "required": [ + "schema_version", + "generated_at", + "program_status", + "source_refs", + "rollups", + "runtime_surfaces", + "source_runtime_components", + "evidence_gaps", + "operator_contract", + "operation_boundaries", + "approval_boundaries" + ], + "properties": { + "schema_version": { "type": "string", "const": "runtime_surface_inventory_v1" }, + "generated_at": { "type": "string", "minLength": 1 }, + "program_status": { + "type": "object", + "required": ["overall_completion_percent", "current_priority", "current_task_id", "next_task_id", "read_only_mode"], + "properties": { + "overall_completion_percent": { "type": "integer", "minimum": 0, "maximum": 100 }, + "current_priority": { "type": "string", "enum": ["P0", "P1", "P2", "P3"] }, + "current_task_id": { "type": "string", "minLength": 1 }, + "next_task_id": { "type": "string", "minLength": 1 }, + "read_only_mode": { "type": "boolean", "const": true } + }, + "additionalProperties": false + }, + "source_refs": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "rollups": { + "type": "object", + "required": [ + "total_surfaces", + "by_kind", + "by_status", + "by_evidence_level", + "action_required_surface_ids", + "secret_surface_ids", + "live_check_missing_surface_ids", + "total_source_components", + "source_components_with_runtime_binding" + ], + "properties": { + "total_surfaces": { "type": "integer", "minimum": 0 }, + "by_kind": { "type": "object", "additionalProperties": { "type": "integer", "minimum": 0 } }, + "by_status": { "type": "object", "additionalProperties": { "type": "integer", "minimum": 0 } }, + "by_evidence_level": { "type": "object", "additionalProperties": { "type": "integer", "minimum": 0 } }, + "action_required_surface_ids": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "secret_surface_ids": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "live_check_missing_surface_ids": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "total_source_components": { "type": "integer", "minimum": 0 }, + "source_components_with_runtime_binding": { "type": "integer", "minimum": 0 } + }, + "additionalProperties": false + }, + "runtime_surfaces": { + "type": "array", + "items": { + "type": "object", + "required": [ + "surface_id", + "display_name", + "kind", + "manifest_ref", + "status", + "risk_level", + "evidence_level", + "runtime_binding", + "health_contract", + "secret_exposure", + "live_check_status", + "evidence_refs", + "next_action" + ], + "properties": { + "surface_id": { "type": "string", "minLength": 1 }, + "display_name": { "type": "string", "minLength": 1 }, + "kind": { "type": "string", "enum": ["deployment", "service", "ingress", "cronjob", "configmap", "secret", "rbac", "policy", "autoscaler", "availability"] }, + "manifest_ref": { "type": "string", "minLength": 1 }, + "status": { "type": "string", "enum": ["manifest_mapped", "action_required", "blocked", "missing"] }, + "risk_level": { "type": "string", "enum": ["low", "medium", "high", "critical"] }, + "evidence_level": { "type": "string", "enum": ["committed_manifest", "source_file", "missing_manifest", "live_check_required"] }, + "runtime_binding": { "type": "string", "minLength": 1 }, + "health_contract": { "type": "string", "minLength": 1 }, + "secret_exposure": { "type": "string", "enum": ["none", "name_only", "template_only", "payload_redacted"] }, + "live_check_status": { "type": "string", "enum": ["not_run", "not_applicable", "required"] }, + "evidence_refs": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "next_action": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + } + }, + "source_runtime_components": { + "type": "array", + "items": { + "type": "object", + "required": ["component_id", "display_name", "source_ref", "component_kind", "runtime_binding", "status", "next_action"], + "properties": { + "component_id": { "type": "string", "minLength": 1 }, + "display_name": { "type": "string", "minLength": 1 }, + "source_ref": { "type": "string", "minLength": 1 }, + "component_kind": { "type": "string", "minLength": 1 }, + "runtime_binding": { "type": "string", "minLength": 1 }, + "status": { "type": "string", "enum": ["bound", "action_required", "source_only"] }, + "next_action": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + } + }, + "evidence_gaps": { + "type": "array", + "items": { + "type": "object", + "required": ["gap_id", "severity", "status", "summary", "evidence_refs", "next_action"], + "properties": { + "gap_id": { "type": "string", "minLength": 1 }, + "severity": { "type": "string", "enum": ["low", "medium", "high", "critical"] }, + "status": { "type": "string", "enum": ["action_required", "blocked", "accepted"] }, + "summary": { "type": "string", "minLength": 1 }, + "evidence_refs": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "next_action": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + } + }, + "operator_contract": { + "type": "object", + "required": ["display_mode", "must_not_interpret_as", "secret_display_policy"], + "properties": { + "display_mode": { "type": "string", "const": "read_only_runtime_surface" }, + "must_not_interpret_as": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "secret_display_policy": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + }, + "operation_boundaries": { "type": "object", "additionalProperties": { "type": "boolean" } }, + "approval_boundaries": { "type": "object", "additionalProperties": { "type": "boolean", "const": false } } + }, + "additionalProperties": false +} diff --git a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md index 2a32be4c4..58b803336 100644 --- a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md +++ b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md @@ -3461,3 +3461,21 @@ Phase 6 完成後 2. P2 / P3 必須等 P1 runtime surface 可見且關卡穩定後再做。 **裁決:** P1-305 / P1-306 只完成 read-only metadata 與 UI 顯示,不代表任務可自動批准、自動執行、自動合併、自動部署或改動 runtime。所有非只讀操作仍需 OpenClaw 仲裁與人工批准。 + +### 2026-06-05 下午 (台北) — P1-001 執行面只讀矩陣本地完成 + +**觸發**:統帥批准繼續,要求依工作清單優先順序推進,並同步完成度、工作狀態與正式環境推版。 + +**已推進:** +- P1-001:新增 `runtime_surface_inventory_v1` schema 與 `docs/evaluations/runtime_surface_inventory_2026-06-05.json`,用 committed manifest / source refs 盤點 API、Web、Worker、K8s Service、Route、CronJob、ConfigMap、Secret surface、RBAC、policy、autoscaler 與 availability。 +- P1-001:新增 `GET /api/v1/agents/runtime-surface-inventory` 只讀 API 與 service guard,強制拒絕把 snapshot 誤讀成 runtime execution、rollout / restart / scale / delete、Secret payload、active scan 或 production routing 授權。 +- P1-001:治理頁 `/zh-TW/governance?tab=automation-inventory` 新增執行面只讀矩陣、來源元件、不可誤讀合約與 KPI;不新增任何執行按鈕。 +- 目前數字:runtime surfaces `22`;需處置 `6`;Secret surfaces `4`;待 Live 證據 `6`;source components `9/9`;automation backlog done `17/23`、overall `74%`、P1 `81%`;inventory tasks `27`。 +- 本地驗證:JSON parse 通過;runtime surface / inventory / backlog 目標 pytest `23 passed`;Python py_compile 通過;zh-TW / en i18n key 差異 `0`;web typecheck 通過;Next production build 通過;source-control-owner-response guard、security-mirror-progress guard 與 `git diff --check` 通過;`127.0.0.1:3123` desktop `1440x1000` / mobile `390x844` smoke 通過,七個 agents API 皆 `200`,`horizontalOverflow=0`,禁止操作按鈕 `0`。 +- 本地 smoke 期間發現同頁 `backupEvidence.statuses.blocked_by_evidence` 缺 i18n key,已補 zh-TW / en 鏡像;這是同頁既有狀態字典缺口,不改 runtime 邏輯。 + +**下一步:** +1. 推送正式環境並重驗 production API、治理頁 desktop / mobile 與 deploy marker。 +2. P1-002:盤點 Gitea 工作流程與 runner 健康合約。 + +**裁決:** P1-001 只完成 read-only runtime surface 可視化,不代表 live cluster 查詢、Secret payload 讀取、Ingress / DNS 修改、HPA / VPA apply、rollout、restart、scale、delete 或任何 active remediation 已批准。