diff --git a/apps/api/src/services/ai_agent_autonomous_runtime_control.py b/apps/api/src/services/ai_agent_autonomous_runtime_control.py index 8e97dc9d3..a06f9fc5b 100644 --- a/apps/api/src/services/ai_agent_autonomous_runtime_control.py +++ b/apps/api/src/services/ai_agent_autonomous_runtime_control.py @@ -7190,6 +7190,19 @@ async def _load_core_runtime_receipt_rows_direct( return None +def build_ai_agent_strict_runtime_completion_from_readback( + readback: Mapping[str, Any], +) -> dict[str, Any]: + """Project the canonical strict completion contract from one readback.""" + + payload = build_ai_agent_autonomous_runtime_control() + _attach_runtime_receipt_readback(payload, dict(readback)) + strict_runtime = payload.get("strict_runtime_completion") + if not isinstance(strict_runtime, Mapping): + return {} + return copy.deepcopy(dict(strict_runtime)) + + async def build_ai_agent_autonomous_runtime_control_with_live_readback( *, project_id: str = _DEFAULT_PROJECT_ID, diff --git a/apps/api/src/services/iwooos_security_asset_control_plane.py b/apps/api/src/services/iwooos_security_asset_control_plane.py index 68ed4315b..24396cd77 100644 --- a/apps/api/src/services/iwooos_security_asset_control_plane.py +++ b/apps/api/src/services/iwooos_security_asset_control_plane.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable +from collections.abc import Iterable, Mapping from datetime import UTC, datetime from typing import Any @@ -11,16 +11,27 @@ import structlog from sqlalchemy import text as _sql from src.db.base import get_db_context +from src.services.ai_agent_autonomous_runtime_control import ( + build_ai_agent_strict_runtime_completion_from_readback, + load_ai_agent_autonomous_runtime_receipt_readback, +) +from src.services.ai_automation_runtime_contract import ( + AI_AUTOMATION_REQUIRED_LOOP_STAGES, + AI_AUTOMATION_RUNTIME_CONTRACT_SCHEMA_VERSION, +) logger = structlog.get_logger(__name__) _SCHEMA_VERSION = "iwooos_security_asset_control_plane_v1" _QUERY_TIMEOUT_SECONDS = 12.0 _SOURCE_QUERY_TIMEOUT_SECONDS = 3.0 +_STRICT_RUNTIME_SOURCE_TIMEOUT_SECONDS = 8.5 _SOURCE_CONCURRENCY = 3 _CACHE_TTL_SECONDS = 30.0 _DISCOVERY_FRESHNESS_SECONDS = 2 * 60 * 60 _WINDOW_HOURS = 24 +_STRICT_RUNTIME_SCHEMA_VERSION = "ai_agent_strict_runtime_completion_v2" +_STRICT_RUNTIME_SOURCE_ID = "autonomous_strict_runtime" _ASSET_TYPES = ( "host", @@ -685,6 +696,126 @@ def _work_item( } +def _strict_runtime_completion_projection(row: Any) -> dict[str, Any]: + """Validate and reduce the authoritative same-run contract for public use.""" + + strict = row if isinstance(row, Mapping) else {} + expected_stage_ids = set(AI_AUTOMATION_REQUIRED_LOOP_STAGES) + required_stage_count = len(AI_AUTOMATION_REQUIRED_LOOP_STAGES) + reported_required_count = strict.get("required_stage_count") + reported_present_count = strict.get("present_stage_count") + missing_stage_ids = strict.get("missing_stage_ids") + mismatch_stage_ids = strict.get("run_id_mismatch_stage_ids") + uncorrelated_stage_ids = strict.get("uncorrelated_stage_ids") + stage_contracts = strict.get("stage_contracts") + + valid_counts = bool( + isinstance(reported_required_count, int) + and not isinstance(reported_required_count, bool) + and reported_required_count == required_stage_count + and isinstance(reported_present_count, int) + and not isinstance(reported_present_count, bool) + and 0 <= reported_present_count <= required_stage_count + ) + valid_missing = bool( + isinstance(missing_stage_ids, list) + and all(isinstance(stage_id, str) for stage_id in missing_stage_ids) + ) + valid_mismatch = bool( + isinstance(mismatch_stage_ids, list) + and all(isinstance(stage_id, str) for stage_id in mismatch_stage_ids) + ) + valid_uncorrelated = bool( + isinstance(uncorrelated_stage_ids, list) + and all(isinstance(stage_id, str) for stage_id in uncorrelated_stage_ids) + ) + valid_stage_contracts = False + if ( + isinstance(stage_contracts, list) + and len(stage_contracts) == required_stage_count + ): + stage_ids = { + str(stage.get("stage_id") or "") + for stage in stage_contracts + if isinstance(stage, Mapping) + } + valid_stage_contracts = bool( + stage_ids == expected_stage_ids + and all( + isinstance(stage, Mapping) + and stage.get("evidence_present") is True + and stage.get("same_run_correlation_proven") is True + and stage.get("completion_eligible") is True + for stage in stage_contracts + ) + ) + + contract_compatible = bool( + strict.get("schema_version") == _STRICT_RUNTIME_SCHEMA_VERSION + and strict.get("runtime_contract_schema_version") + == AI_AUTOMATION_RUNTIME_CONTRACT_SCHEMA_VERSION + and valid_counts + and valid_missing + and valid_mismatch + and valid_uncorrelated + and isinstance(stage_contracts, list) + and len(stage_contracts) == required_stage_count + ) + same_run_closed_loop_proven = bool( + contract_compatible + and valid_stage_contracts + and strict.get("completion_percent") == 100 + and reported_present_count == required_stage_count + and missing_stage_ids == [] + and strict.get("latest_flow_closed") is True + and strict.get("latest_loop_closed") is True + and strict.get("execution_loop_closed") is True + and strict.get("same_run_correlation") is True + and strict.get("full_trace_same_run_correlation_proven") is True + and bool(str(strict.get("automation_run_id") or "").strip()) + and mismatch_stage_ids == [] + and uncorrelated_stage_ids == [] + and strict.get("closed") is True + and strict.get("same_run_correlation_required") is True + and strict.get("metadata_only_counts_as_completion") is False + and strict.get("historical_aggregate_fallback_allowed") is False + ) + present_stage_count = reported_present_count if valid_counts else 0 + missing_stage_count = ( + len(missing_stage_ids) + if valid_missing + else required_stage_count - present_stage_count + ) + if not strict: + reason_code = "strict_runtime_source_unavailable" + elif not contract_compatible: + reason_code = "strict_runtime_contract_mismatch" + elif not same_run_closed_loop_proven: + reason_code = "strict_runtime_receipts_incomplete" + else: + reason_code = None + + return { + "schema_version": _STRICT_RUNTIME_SCHEMA_VERSION, + "runtime_contract_schema_version": ( + AI_AUTOMATION_RUNTIME_CONTRACT_SCHEMA_VERSION + ), + "contract_compatible": contract_compatible, + "completion_percent": 100 if same_run_closed_loop_proven else 0, + "required_stage_count": required_stage_count, + "present_stage_count": present_stage_count, + "missing_stage_count": missing_stage_count, + "same_run_correlation": same_run_closed_loop_proven, + "full_trace_same_run_correlation_proven": same_run_closed_loop_proven, + "execution_loop_closed": same_run_closed_loop_proven, + "closed": same_run_closed_loop_proven, + "reason_code": reason_code, + "cache_fallback_active": strict.get("_cache_fallback_active") is True, + "raw_run_identity_returned": False, + "raw_stage_receipts_returned": False, + } + + def build_iwooos_security_asset_control_plane( *, discovery_row: Any, @@ -696,6 +827,7 @@ def build_iwooos_security_asset_control_plane( ai_trace_row: Any, siem_row: Any, audit_row: Any, + strict_runtime_row: Any = None, source_health: Iterable[dict[str, Any]] | None = None, generated_at: datetime | None = None, ) -> dict[str, Any]: @@ -854,9 +986,10 @@ def build_iwooos_security_asset_control_plane( for row in automation_source if str(_value(row, "status", "")) == "failed" ) - # Aggregate stage receipts cannot prove that every stage belongs to one - # trace. Keep strict runtime completion closed until a same-run join exists. - same_run_closed_loop_proven = False + strict_runtime_completion = _strict_runtime_completion_projection( + strict_runtime_row + ) + same_run_closed_loop_proven = strict_runtime_completion["closed"] is True relationship_count = int(_value(relationship_row, "relationship_count", 0) or 0) orphan_asset_count = int( @@ -1160,7 +1293,9 @@ def build_iwooos_security_asset_control_plane( / len(security_program_domains) ) package_inventory_evidenced = by_type.get("package", 0) > 0 - strict_runtime_closure_percent = 100 if same_run_closed_loop_proven else 0 + strict_runtime_closure_percent = int( + strict_runtime_completion["completion_percent"] + ) completion_dimensions = [ { "dimension_id": "asset_scope", @@ -1280,7 +1415,8 @@ def build_iwooos_security_asset_control_plane( "rollback_receipt_count": rollback_receipts, "failed_operation_count": failed_operations, "same_run_closed_loop_proven": same_run_closed_loop_proven, - "same_run_closed_loop_count": 0, + "same_run_closed_loop_count": 1 if same_run_closed_loop_proven else 0, + "strict_runtime_completion": strict_runtime_completion, }, "security_audit": audit, "supply_chain": { @@ -1361,9 +1497,9 @@ def build_unavailable_iwooos_security_asset_control_plane( "source_status": "live_database_unavailable", "reason_code": reason_code, "summary": { - "source_count": 9, + "source_count": 10, "ready_source_count": 0, - "unavailable_source_count": 9, + "unavailable_source_count": 10, "managed_asset_count": 0, "expected_asset_type_count": len(_ASSET_TYPES), "present_asset_type_count": 0, @@ -1519,6 +1655,7 @@ def build_unavailable_iwooos_security_asset_control_plane( "failed_operation_count": 0, "same_run_closed_loop_proven": False, "same_run_closed_loop_count": 0, + "strict_runtime_completion": _strict_runtime_completion_projection(None), }, "security_audit": { "k8s_execution_audit_count_24h": 0, @@ -1573,6 +1710,7 @@ def build_unavailable_iwooos_security_asset_control_plane( "ai_decision_trace", "siem_runtime", "audit_runtime", + _STRICT_RUNTIME_SOURCE_ID, ) ], "work_items": [ @@ -1669,6 +1807,65 @@ async def _read_public_source( } +async def _read_strict_runtime_source() -> tuple[dict[str, Any], dict[str, Any]]: + """Read and project the canonical same-run receipt contract independently.""" + + try: + readback = await asyncio.wait_for( + load_ai_agent_autonomous_runtime_receipt_readback( + project_id="awoooi", + lookback_hours=_WINDOW_HOURS, + ), + timeout=_STRICT_RUNTIME_SOURCE_TIMEOUT_SECONDS, + ) + db_read_status = str(readback.get("db_read_status") or "") + if db_read_status not in {"ok", "partial"}: + return {}, { + "source_id": _STRICT_RUNTIME_SOURCE_ID, + "status": "unavailable", + "reason_code": "autonomous_strict_runtime_readback_unavailable", + } + strict_runtime = build_ai_agent_strict_runtime_completion_from_readback( + readback + ) + if ( + strict_runtime.get("schema_version") + != _STRICT_RUNTIME_SCHEMA_VERSION + or strict_runtime.get("runtime_contract_schema_version") + != AI_AUTOMATION_RUNTIME_CONTRACT_SCHEMA_VERSION + ): + return {}, { + "source_id": _STRICT_RUNTIME_SOURCE_ID, + "status": "unavailable", + "reason_code": "autonomous_strict_runtime_contract_mismatch", + } + strict_runtime["_cache_fallback_active"] = ( + readback.get("cache_fallback_active") is True + ) + return strict_runtime, { + "source_id": _STRICT_RUNTIME_SOURCE_ID, + "status": "ready", + "reason_code": None, + } + except TimeoutError: + logger.warning("iwooos_security_asset_control_plane_strict_runtime_timeout") + return {}, { + "source_id": _STRICT_RUNTIME_SOURCE_ID, + "status": "unavailable", + "reason_code": "autonomous_strict_runtime_query_timeout", + } + except Exception as exc: # noqa: BLE001 - redact public source failures + logger.warning( + "iwooos_security_asset_control_plane_strict_runtime_unavailable", + error_type=type(exc).__name__, + ) + return {}, { + "source_id": _STRICT_RUNTIME_SOURCE_ID, + "status": "unavailable", + "reason_code": "autonomous_strict_runtime_query_failed", + } + + class IwoooSSecurityAssetControlPlaneService: """Read aggregate production security state with a bounded DB timeout.""" @@ -2057,6 +2254,7 @@ class IwoooSSecurityAssetControlPlaneService: result_mode="one", default={}, ), + _read_strict_runtime_source(), ) ( @@ -2069,6 +2267,7 @@ class IwoooSSecurityAssetControlPlaneService: (ai_trace_row, ai_trace_health), (siem_row, siem_health), (audit_row, audit_health), + (strict_runtime_row, strict_runtime_health), ) = source_results source_health = [ discovery_health, @@ -2080,6 +2279,7 @@ class IwoooSSecurityAssetControlPlaneService: ai_trace_health, siem_health, audit_health, + strict_runtime_health, ] return build_iwooos_security_asset_control_plane( @@ -2092,6 +2292,7 @@ class IwoooSSecurityAssetControlPlaneService: ai_trace_row=ai_trace_row, siem_row=siem_row, audit_row=audit_row, + strict_runtime_row=strict_runtime_row, source_health=source_health, ) diff --git a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py index 6b2e00233..0d5cf28fc 100644 --- a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py +++ b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py @@ -16,6 +16,7 @@ from src.services.ai_agent_autonomous_runtime_control import ( _RUNTIME_TIMELINE_COUNTS_SQL, _runtime_stage_receipt_has_required_proof, build_ai_agent_autonomous_runtime_control, + build_ai_agent_strict_runtime_completion_from_readback, build_runtime_receipt_readback_from_rows, classify_deploy_control_plane_observation, ) @@ -1882,6 +1883,9 @@ def test_ai_agent_autonomous_runtime_control_uses_current_owner_directive(): assert strict["metadata_only_counts_as_completion"] is False assert strict["historical_aggregate_fallback_allowed"] is False assert len(strict["stage_contracts"]) == len(AI_AUTOMATION_REQUIRED_LOOP_STAGES) + assert build_ai_agent_strict_runtime_completion_from_readback( + data["runtime_receipt_readback"] + ) == strict assert data["current_policy"]["low_risk_controlled_apply_allowed"] is True assert data["current_policy"]["medium_risk_controlled_apply_allowed"] is True assert data["current_policy"]["high_risk_controlled_apply_allowed"] is True diff --git a/apps/api/tests/test_iwooos_security_asset_control_plane.py b/apps/api/tests/test_iwooos_security_asset_control_plane.py index 9e724ed12..12fa999fa 100644 --- a/apps/api/tests/test_iwooos_security_asset_control_plane.py +++ b/apps/api/tests/test_iwooos_security_asset_control_plane.py @@ -3,6 +3,7 @@ from __future__ import annotations # ruff: noqa: E402, I001 import asyncio +import copy import json import os from datetime import UTC, datetime, timedelta @@ -20,12 +21,51 @@ from src.services.iwooos_security_asset_control_plane import ( build_iwooos_security_asset_control_plane, build_unavailable_iwooos_security_asset_control_plane, ) +from src.services.ai_automation_runtime_contract import ( + AI_AUTOMATION_REQUIRED_LOOP_STAGES, + AI_AUTOMATION_RUNTIME_CONTRACT_SCHEMA_VERSION, +) def _row(**values): return SimpleNamespace(**values) +def _closed_strict_runtime() -> dict: + return { + "schema_version": "ai_agent_strict_runtime_completion_v2", + "runtime_contract_schema_version": ( + AI_AUTOMATION_RUNTIME_CONTRACT_SCHEMA_VERSION + ), + "completion_percent": 100, + "required_stage_count": len(AI_AUTOMATION_REQUIRED_LOOP_STAGES), + "present_stage_count": len(AI_AUTOMATION_REQUIRED_LOOP_STAGES), + "missing_stage_ids": [], + "latest_flow_closed": True, + "latest_loop_closed": True, + "execution_loop_closed": True, + "same_run_correlation": True, + "full_trace_same_run_correlation_proven": True, + "automation_run_id": "internal-run-identity", + "run_id_mismatch_stage_ids": [], + "uncorrelated_stage_ids": [], + "stage_contracts": [ + { + "stage_id": stage_id, + "evidence_present": True, + "same_run_correlation_proven": True, + "completion_eligible": True, + "evidence_source": "same_run_runtime_stage_receipt", + } + for stage_id in AI_AUTOMATION_REQUIRED_LOOP_STAGES + ], + "closed": True, + "same_run_correlation_required": True, + "metadata_only_counts_as_completion": False, + "historical_aggregate_fallback_allowed": False, + } + + def _ready_payload( source_health: list[dict] | None = None, *, @@ -33,6 +73,7 @@ def _ready_payload( discovery_error: str | None = None, automation_rows: list[SimpleNamespace] | None = None, runtime_receipt_counts: dict[str, int] | None = None, + strict_runtime: dict | None = None, ) -> dict: now = datetime(2026, 7, 11, 4, 0, tzinfo=UTC) discovery = _row( @@ -124,6 +165,7 @@ def _ready_payload( learning_writeback_count_24h=runtime_counts.get("learn", 0), mttr_minutes_24h=14.25, ), + strict_runtime_row=strict_runtime, audit_row=_row( k8s_audit_count_24h=2, k8s_audit_failure_count_24h=0, @@ -249,6 +291,92 @@ def test_projects_canonical_runtime_receipts_without_false_same_run_closure() -> assert work_items["AIA-P0-008-01"]["gap_count"] == 1 +def test_projects_exact_authoritative_same_run_runtime_closure() -> None: + payload = _ready_payload(strict_runtime=_closed_strict_runtime()) + + strict = payload["ai_automation"]["strict_runtime_completion"] + assert strict == { + "schema_version": "ai_agent_strict_runtime_completion_v2", + "runtime_contract_schema_version": ( + AI_AUTOMATION_RUNTIME_CONTRACT_SCHEMA_VERSION + ), + "contract_compatible": True, + "completion_percent": 100, + "required_stage_count": len(AI_AUTOMATION_REQUIRED_LOOP_STAGES), + "present_stage_count": len(AI_AUTOMATION_REQUIRED_LOOP_STAGES), + "missing_stage_count": 0, + "same_run_correlation": True, + "full_trace_same_run_correlation_proven": True, + "execution_loop_closed": True, + "closed": True, + "reason_code": None, + "cache_fallback_active": False, + "raw_run_identity_returned": False, + "raw_stage_receipts_returned": False, + } + assert payload["completion"]["strict_runtime_closure_percent"] == 100 + assert payload["ai_automation"]["same_run_closed_loop_proven"] is True + assert payload["ai_automation"]["same_run_closed_loop_count"] == 1 + assert all( + item["work_item_id"] != "AIA-P0-008-01" + for item in payload["work_items"] + ) + assert payload["completion"]["overall_percent"] == round( + sum( + dimension["completion_percent"] + for dimension in payload["completion"]["dimensions"] + ) + / payload["completion"]["dimension_count"] + ) + + public_text = json.dumps(payload, ensure_ascii=False) + assert "internal-run-identity" not in public_text + assert '"automation_run_id"' not in public_text + assert '"stage_contracts"' not in public_text + + +def test_strict_runtime_projection_fails_closed_on_contract_or_receipt_drift() -> None: + variants: list[dict] = [] + + bad_schema = _closed_strict_runtime() + bad_schema["schema_version"] = "ai_agent_strict_runtime_completion_v1" + variants.append(bad_schema) + + missing_stage = _closed_strict_runtime() + missing_stage["present_stage_count"] -= 1 + missing_stage["missing_stage_ids"] = [AI_AUTOMATION_REQUIRED_LOOP_STAGES[-1]] + variants.append(missing_stage) + + cross_run = _closed_strict_runtime() + cross_run["same_run_correlation"] = False + cross_run["run_id_mismatch_stage_ids"] = ["telegram_receipt"] + variants.append(cross_run) + + no_run_identity = _closed_strict_runtime() + no_run_identity["automation_run_id"] = "" + variants.append(no_run_identity) + + metadata_only = _closed_strict_runtime() + metadata_only["metadata_only_counts_as_completion"] = True + variants.append(metadata_only) + + invalid_stage_contract = copy.deepcopy(_closed_strict_runtime()) + invalid_stage_contract["stage_contracts"][0]["completion_eligible"] = False + variants.append(invalid_stage_contract) + + for strict_runtime in variants: + payload = _ready_payload(strict_runtime=strict_runtime) + strict = payload["ai_automation"]["strict_runtime_completion"] + assert strict["closed"] is False + assert strict["completion_percent"] == 0 + assert payload["completion"]["strict_runtime_closure_percent"] == 0 + assert payload["ai_automation"]["same_run_closed_loop_proven"] is False + assert any( + item["work_item_id"] == "AIA-P0-008-01" + for item in payload["work_items"] + ) + + def test_discovery_failure_exposes_only_allowlisted_collector_ids() -> None: raw_error = ( "RuntimeError: asset_collector_failures=domain_tls_inventory," @@ -612,6 +740,9 @@ def test_unavailable_payload_is_fixed_degraded_contract_without_raw_error() -> N assert payload["summary"]["overall_security_completion_percent"] == 0 assert payload["completion"]["overall_percent"] == 0 assert payload["completion"]["strict_runtime_closure_percent"] == 0 + assert payload["summary"]["source_count"] == 10 + assert payload["summary"]["unavailable_source_count"] == 10 + assert payload["ai_automation"]["strict_runtime_completion"]["closed"] is False assert payload["discovery"]["fresh"] is False assert payload["boundaries"]["raw_asset_identity_returned"] is False assert payload["boundaries"]["raw_event_payload_returned"] is False @@ -676,12 +807,25 @@ def test_service_uses_canonical_awoooi_project_scope(monkeypatch) -> None: "src.services.iwooos_security_asset_control_plane.get_db_context", fake_db_context, ) + + async def strict_runtime_unavailable(): + return {}, { + "source_id": "autonomous_strict_runtime", + "status": "unavailable", + "reason_code": "autonomous_strict_runtime_query_failed", + } + + monkeypatch.setattr( + "src.services.iwooos_security_asset_control_plane._read_strict_runtime_source", + strict_runtime_unavailable, + ) payload = asyncio.run(service.get_snapshot()) assert requested_projects == ["awoooi"] * 9 assert payload["status"] == "degraded" assert payload["source_status"] == "live_database_unavailable" - assert payload["summary"]["unavailable_source_count"] == 9 + assert payload["summary"]["source_count"] == 10 + assert payload["summary"]["unavailable_source_count"] == 10 def test_service_bounds_source_concurrency_for_database_budget(monkeypatch) -> None: @@ -719,11 +863,23 @@ def test_service_bounds_source_concurrency_for_database_budget(monkeypatch) -> N "src.services.iwooos_security_asset_control_plane.get_db_context", lambda project_id: DbContext(), ) + + async def strict_runtime_ready(): + return _closed_strict_runtime(), { + "source_id": "autonomous_strict_runtime", + "status": "ready", + "reason_code": None, + } + + monkeypatch.setattr( + "src.services.iwooos_security_asset_control_plane._read_strict_runtime_source", + strict_runtime_ready, + ) payload = asyncio.run(service._load_snapshot()) assert concurrency["maximum"] == 3 - assert payload["summary"]["source_count"] == 9 - assert payload["summary"]["ready_source_count"] == 9 + assert payload["summary"]["source_count"] == 10 + assert payload["summary"]["ready_source_count"] == 10 assert payload["summary"]["unavailable_source_count"] == 0 compliance_query = next( statement diff --git a/apps/web/src/components/iwooos/security-asset-control-plane-cockpit.tsx b/apps/web/src/components/iwooos/security-asset-control-plane-cockpit.tsx index b12c17689..07b5649e1 100644 --- a/apps/web/src/components/iwooos/security-asset-control-plane-cockpit.tsx +++ b/apps/web/src/components/iwooos/security-asset-control-plane-cockpit.tsx @@ -36,7 +36,7 @@ type Copy = { assetTypes: string; nist: string; siem: string; - aiLoop: string; + strictRun: string; verified: string; functions: string; framework: string; @@ -70,6 +70,7 @@ type Copy = { failedCollectors: string; failedCollectorsUnavailable: string; sameRunMissing: string; + sameRunVerified: string; loading: string; unavailable: string; refresh: string; @@ -87,7 +88,7 @@ const COPY: Record<"zh" | "en", Copy> = { assetTypes: "資產類型", nist: "NIST 控制", siem: "SIEM 閉環", - aiLoop: "AI 閉環", + strictRun: "同 run 閉環", verified: "24h 已驗證", functions: "治理六功能", framework: "完整資安框架", @@ -121,6 +122,7 @@ const COPY: Record<"zh" | "en", Copy> = { failedCollectors: "失敗 Collector", failedCollectorsUnavailable: "失敗原因待安全分類", sameRunMissing: "尚無同 run 閉環證據", + sameRunVerified: "18 段同 run 閉環已驗證", loading: "正在載入資安控制平面", unavailable: "Live readback 無法取得", refresh: "重新整理資安控制平面", @@ -136,7 +138,7 @@ const COPY: Record<"zh" | "en", Copy> = { assetTypes: "Asset types", nist: "NIST control", siem: "SIEM closure", - aiLoop: "AI closure", + strictRun: "Same-run closure", verified: "Verified 24h", functions: "Six functions", framework: "Security framework", @@ -170,6 +172,7 @@ const COPY: Record<"zh" | "en", Copy> = { failedCollectors: "Failed collectors", failedCollectorsUnavailable: "Failure detail awaiting safe classification", sameRunMissing: "No same-run closure evidence", + sameRunVerified: "18-stage same-run closure verified", loading: "Loading security control plane", unavailable: "Live readback unavailable", refresh: "Refresh security control plane", @@ -557,10 +560,10 @@ export function SecurityAssetControlPlaneCockpit({ icon={Radar} /> {copy.sameRunMissing} - ) : null} + ) : ( +
+
+ )}
diff --git a/apps/web/src/lib/__tests__/security-asset-control-plane.test.ts b/apps/web/src/lib/__tests__/security-asset-control-plane.test.ts index dfd88c5a3..b660b8b0d 100644 --- a/apps/web/src/lib/__tests__/security-asset-control-plane.test.ts +++ b/apps/web/src/lib/__tests__/security-asset-control-plane.test.ts @@ -14,8 +14,8 @@ function payload(): SecurityAssetControlPlanePayload { status: "ready", source_status: "live_database_aggregate", summary: { - source_count: 9, - ready_source_count: 9, + source_count: 10, + ready_source_count: 10, unavailable_source_count: 0, managed_asset_count: 24, expected_asset_type_count: 28, @@ -97,6 +97,24 @@ function payload(): SecurityAssetControlPlanePayload { failed_operation_count: 0, same_run_closed_loop_proven: false, same_run_closed_loop_count: 0, + strict_runtime_completion: { + schema_version: "ai_agent_strict_runtime_completion_v2", + runtime_contract_schema_version: + "ai_automation_runtime_completion_contract_v2", + contract_compatible: true, + completion_percent: 0, + required_stage_count: 18, + present_stage_count: 17, + missing_stage_count: 1, + same_run_correlation: false, + full_trace_same_run_correlation_proven: false, + execution_loop_closed: false, + closed: false, + reason_code: "strict_runtime_receipts_incomplete", + cache_fallback_active: false, + raw_run_identity_returned: false, + raw_stage_receipts_returned: false, + }, }, security_audit: { k8s_execution_audit_count_24h: 2, @@ -222,6 +240,20 @@ describe("security asset control plane projection", () => { expect(isSecurityAssetControlPlanePayload(unsafeCollectorIdentity)).toBe( false, ); + + const inconsistentStrictRuntime = { + ...safe, + ai_automation: { + ...safe.ai_automation, + strict_runtime_completion: { + ...safe.ai_automation.strict_runtime_completion, + closed: true, + }, + }, + }; + expect(isSecurityAssetControlPlanePayload(inconsistentStrictRuntime)).toBe( + false, + ); }); it("does not show healthy before same-run closure is proven", () => { diff --git a/apps/web/src/lib/security-asset-control-plane.ts b/apps/web/src/lib/security-asset-control-plane.ts index c6e7b5da9..d80a570d4 100644 --- a/apps/web/src/lib/security-asset-control-plane.ts +++ b/apps/web/src/lib/security-asset-control-plane.ts @@ -119,6 +119,24 @@ export type SecurityControlPlaneCertificateInventory = { private_key_material_read: false; }; +export type SecurityStrictRuntimeCompletion = { + schema_version: "ai_agent_strict_runtime_completion_v2"; + runtime_contract_schema_version: "ai_automation_runtime_completion_contract_v2"; + contract_compatible: boolean; + completion_percent: number; + required_stage_count: number; + present_stage_count: number; + missing_stage_count: number; + same_run_correlation: boolean; + full_trace_same_run_correlation_proven: boolean; + execution_loop_closed: boolean; + closed: boolean; + reason_code: string | null; + cache_fallback_active: boolean; + raw_run_identity_returned: false; + raw_stage_receipts_returned: false; +}; + export type SecurityAssetControlPlanePayload = { schema_version: "iwooos_security_asset_control_plane_v1"; generated_at: string; @@ -181,6 +199,7 @@ export type SecurityAssetControlPlanePayload = { failed_operation_count: number; same_run_closed_loop_proven: boolean; same_run_closed_loop_count: number; + strict_runtime_completion: SecurityStrictRuntimeCompletion; }; security_audit: { k8s_execution_audit_count_24h: number; @@ -255,6 +274,44 @@ export function isSecurityAssetControlPlanePayload( ) { return false; } + const strictRuntime = value.ai_automation.strict_runtime_completion; + if ( + !isRecord(strictRuntime) || + strictRuntime.schema_version !== "ai_agent_strict_runtime_completion_v2" || + strictRuntime.runtime_contract_schema_version !== + "ai_automation_runtime_completion_contract_v2" || + typeof strictRuntime.contract_compatible !== "boolean" || + typeof strictRuntime.completion_percent !== "number" || + typeof strictRuntime.required_stage_count !== "number" || + typeof strictRuntime.present_stage_count !== "number" || + typeof strictRuntime.missing_stage_count !== "number" || + typeof strictRuntime.same_run_correlation !== "boolean" || + typeof strictRuntime.full_trace_same_run_correlation_proven !== "boolean" || + typeof strictRuntime.execution_loop_closed !== "boolean" || + typeof strictRuntime.closed !== "boolean" || + typeof strictRuntime.cache_fallback_active !== "boolean" || + !( + strictRuntime.reason_code === null || + typeof strictRuntime.reason_code === "string" + ) || + strictRuntime.raw_run_identity_returned !== false || + strictRuntime.raw_stage_receipts_returned !== false || + strictRuntime.required_stage_count !== 18 || + strictRuntime.present_stage_count < 0 || + strictRuntime.present_stage_count > strictRuntime.required_stage_count || + strictRuntime.missing_stage_count !== + strictRuntime.required_stage_count - strictRuntime.present_stage_count || + value.ai_automation.same_run_closed_loop_proven !== strictRuntime.closed || + (strictRuntime.closed && + (!strictRuntime.contract_compatible || + strictRuntime.completion_percent !== 100 || + !strictRuntime.same_run_correlation || + !strictRuntime.full_trace_same_run_correlation_proven || + !strictRuntime.execution_loop_closed)) || + (!strictRuntime.closed && strictRuntime.completion_percent !== 0) + ) { + return false; + } if (value.supply_chain !== undefined) { if (!isRecord(value.supply_chain)) return false; if (