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 5e1ef4106..bd1bb0038 100644 --- a/apps/api/src/services/ai_agent_autonomous_runtime_control.py +++ b/apps/api/src/services/ai_agent_autonomous_runtime_control.py @@ -7286,9 +7286,9 @@ def _attach_effective_executor_runtime_switches( if not isinstance(controls, Mapping): controls = {} dedicated_broker_ready = bool( - boundary_readback.get("status") == "verified_ready" - and boundary_readback.get("production_boundary_verified") is True + boundary_readback.get("production_boundary_verified") is True and boundary_readback.get("source_sha_matches_deployment") is True + and not list(boundary_readback.get("active_blockers") or []) and controls.get("broker_ssh_mount_present") is True and controls.get("broker_ssh_egress_allowlisted") is True and controls.get("broker_kubernetes_token_absent") is True 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 97e729ddd..ed5887cb7 100644 --- a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py +++ b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py @@ -33,9 +33,15 @@ def test_runtime_switches_distinguish_api_process_from_dedicated_broker() -> Non } } boundary = { - "status": "verified_ready", + "status": "network_boundary_verified_database_identity_in_progress", "production_boundary_verified": True, + "full_workload_boundary_verified": False, "source_sha_matches_deployment": True, + "active_blockers": [], + "full_boundary_active_blockers": [ + "database_capacity_receipt_stale", + "connection_budget_verified_not_verified", + ], "receipt_ref": ( "configmap:awoooi-prod/awoooi-executor-boundary-verification" ), @@ -72,6 +78,54 @@ def test_runtime_switches_distinguish_api_process_from_dedicated_broker() -> Non assert switches["effective_executor_readback_source"] is None +@pytest.mark.parametrize( + ("boundary_override", "control_override"), + [ + ({"production_boundary_verified": False}, {}), + ({"source_sha_matches_deployment": False}, {}), + ({"active_blockers": ["network_boundary_unverified"]}, {}), + ({}, {"broker_ssh_mount_present": False}), + ({}, {"broker_ssh_egress_allowlisted": False}), + ({}, {"broker_kubernetes_token_absent": False}), + ], +) +def test_dedicated_broker_readiness_fails_closed_on_its_own_boundary( + boundary_override: dict[str, object], + control_override: dict[str, object], +) -> None: + payload = {"runtime_switches": {}} + boundary: dict[str, object] = { + "status": "network_boundary_verified_database_identity_in_progress", + "production_boundary_verified": True, + "full_workload_boundary_verified": False, + "source_sha_matches_deployment": True, + "active_blockers": [], + "full_boundary_active_blockers": ["database_capacity_receipt_stale"], + "receipt_ref": "configmap:awoooi-prod/executor-boundary", + "verified_source_sha": "a" * 40, + "controls": { + "broker_ssh_mount_present": True, + "broker_ssh_egress_allowlisted": True, + "broker_kubernetes_token_absent": True, + }, + } + boundary.update(boundary_override) + controls = dict(boundary["controls"]) + controls.update(control_override) + boundary["controls"] = controls + + runtime_control_module._attach_effective_executor_runtime_switches( + payload, + boundary_readback=boundary, + ) + + switches = payload["runtime_switches"] + assert switches["dedicated_execution_broker_ready"] is False + assert switches["effective_ansible_executor_available"] is False + assert switches["effective_executor_readback_source"] is None + assert switches["effective_executor_verified_source_sha"] is None + + def _closed_retry_terminal_row() -> dict[str, object]: run_id = "retry-run-1" apply_op_id = "94925d5e-7d73-4ed2-82e2-a3f2d3026270" diff --git a/scripts/reboot-recovery/agent99-live-preflight.ps1 b/scripts/reboot-recovery/agent99-live-preflight.ps1 index 0efa45133..32b770bd4 100644 --- a/scripts/reboot-recovery/agent99-live-preflight.ps1 +++ b/scripts/reboot-recovery/agent99-live-preflight.ps1 @@ -456,7 +456,7 @@ $commandQueue = $queues | Where-Object { $_.relativePath -eq "queue" } | Select- $decision = Get-AgentLivePreflightDecision ` -AgentRootPresent ([bool](Test-Path $AgentRoot)) ` -Manifest $manifest ` - -ExpectedRuntimeFileCount 15 ` + -ExpectedRuntimeFileCount 16 ` -TaskFailureCount $taskFailures.Count ` -RelayListenerCount $relayListeners.Count ` -RequiredEvidenceStaleCount $requiredEvidenceStale.Count ` diff --git a/scripts/reboot-recovery/tests/test_agent99_live_preflight_decision.py b/scripts/reboot-recovery/tests/test_agent99_live_preflight_decision.py index eed38bdc6..1637b0635 100644 --- a/scripts/reboot-recovery/tests/test_agent99_live_preflight_decision.py +++ b/scripts/reboot-recovery/tests/test_agent99_live_preflight_decision.py @@ -36,7 +36,7 @@ def _base_case() -> dict[str, Any]: "exists": True, "sourceRevision": "a" * 40, "runtimeMatched": True, - "fileCount": 15, + "fileCount": 16, "mismatchCount": 0, "parseError": "", }, @@ -81,7 +81,7 @@ $case = @' $parameters = @{{ AgentRootPresent = [bool]$case.agentRootPresent Manifest = $case.manifest - ExpectedRuntimeFileCount = 15 + ExpectedRuntimeFileCount = 16 TaskFailureCount = [int]$case.taskFailureCount RelayListenerCount = [int]$case.relayListenerCount RequiredEvidenceStaleCount = [int]$case.requiredEvidenceStaleCount @@ -125,6 +125,7 @@ def test_warning_receipt_is_visible_without_weakening_integrity_blockers() -> No assert '$blockingReasons += "sre_alert_relay_not_listening"' in decision assert "deploymentEligible = [bool]($blockingReasons.Count -eq 0)" in decision assert "preflightGreen = [bool]$decision.deploymentEligible" in source + assert "-ExpectedRuntimeFileCount 16" in source assert "warningReasons = $warningReasons" in source assert "selfHealthPerformanceIssues = @(" in source assert 'if ($safeHost -notmatch "^[A-Za-z0-9.:-]{1,128}$")' in source @@ -263,6 +264,12 @@ def test_decision_behavior_when_powershell_is_available() -> None: ("runtime_manifest_identity_invalid",), (), ), + ( + _with_overrides(manifest={"fileCount": 15}), + False, + ("runtime_manifest_identity_invalid",), + (), + ), ( _with_overrides(relayListenerCount=0), False, diff --git a/scripts/reboot-recovery/tests/test_agent99_remote_atomic_deploy_transport_contract.py b/scripts/reboot-recovery/tests/test_agent99_remote_atomic_deploy_transport_contract.py index 3eee4a24e..5c45814d1 100644 --- a/scripts/reboot-recovery/tests/test_agent99_remote_atomic_deploy_transport_contract.py +++ b/scripts/reboot-recovery/tests/test_agent99_remote_atomic_deploy_transport_contract.py @@ -18,6 +18,7 @@ RECEIVER = ( / "scripts/reboot-recovery/agent99-remote-atomic-deploy-receiver.ps1" ) DEPLOYER = ROOT / "agent99-deploy.ps1" +LIVE_PREFLIGHT = ROOT / "scripts/reboot-recovery/agent99-live-preflight.ps1" EXPECTED_RUNTIME_FILES = ( "agent99-bootstrap.ps1", @@ -71,6 +72,7 @@ def test_sender_and_receiver_allow_exactly_the_deployer_runtime_bundle() -> None sender = SENDER.read_text(encoding="utf-8") receiver = RECEIVER.read_text(encoding="utf-8") deployer = DEPLOYER.read_text(encoding="utf-8") + live_preflight = LIVE_PREFLIGHT.read_text(encoding="utf-8") sender_files = _quoted_array(sender, "RUNTIME_FILES=(", ")") receiver_files = _quoted_array(receiver, "$FixedRuntimeFiles = @(", ")") @@ -79,6 +81,11 @@ def test_sender_and_receiver_allow_exactly_the_deployer_runtime_bundle() -> None assert sender_files == EXPECTED_RUNTIME_FILES assert receiver_files == EXPECTED_RUNTIME_FILES assert deployer_files == EXPECTED_RUNTIME_FILES + preflight_count = re.search( + r"-ExpectedRuntimeFileCount\s+(\d+)", live_preflight + ) + assert preflight_count is not None + assert int(preflight_count.group(1)) == len(EXPECTED_RUNTIME_FILES) assert "expectedRuntimeFileCount\": 16" in sender assert "$filesByName.Count -ne $FixedRuntimeFiles.Count" in receiver assert "required_runtime_file_missing" in receiver