"""Step definitions for autonomy guardrails and audit trail scenarios.""" from __future__ import annotations from datetime import UTC, datetime, timedelta from unittest.mock import MagicMock from behave import given, then, when from behave.runner import Context from cleveragents.application.services.autonomy_guardrail_service import ( _MAX_CONFIRMATIONS, _MAX_METADATA_ENTRIES, AutonomyGuardrailService, ) from cleveragents.application.services.plan_executor import ( PlanExecutor, ) from cleveragents.core.exceptions import PlanError from cleveragents.domain.models.core.autonomy_guardrails import ( ActorLimits, AutonomyGuardrails, GuardrailAuditEntry, GuardrailAuditTrail, GuardrailEventType, GuardrailResult, ) from cleveragents.domain.models.core.plan import ( PlanPhase, PlanTimestamps, ProcessingState, ) # ---- Model creation steps ---- @when( "I create autonomy guardrails with max_steps {max_steps:d}" " and tool_budget {budget:g}" ) def step_create_guardrails( context: Context, max_steps: int, budget: float, ) -> None: context.guardrails = AutonomyGuardrails( max_steps=max_steps, tool_budget=budget, ) @when("I create autonomy guardrails with defaults") def step_create_default_guardrails(context: Context) -> None: context.guardrails = AutonomyGuardrails() @when("I try to create guardrails with max_steps {max_steps:d}") def step_try_create_bad_max_steps( context: Context, max_steps: int, ) -> None: try: AutonomyGuardrails(max_steps=max_steps) context.guardrails_error = None except Exception as exc: context.guardrails_error = exc @when("I try to create guardrails with tool_budget {budget:g}") def step_try_create_bad_budget( context: Context, budget: float, ) -> None: try: AutonomyGuardrails(tool_budget=budget) context.guardrails_error = None except Exception as exc: context.guardrails_error = exc @then("the guardrails should have max_steps {value}") def step_check_max_steps(context: Context, value: str) -> None: if value == "None": assert context.guardrails.max_steps is None else: assert context.guardrails.max_steps == int(value) @then("the guardrails should have tool_budget {value}") def step_check_tool_budget(context: Context, value: str) -> None: if value == "None": assert context.guardrails.tool_budget is None else: assert context.guardrails.tool_budget == float(value) @then("the guardrails step_count should be {count:d}") def step_check_step_count(context: Context, count: int) -> None: assert context.guardrails.step_count == count @then("the guardrails budget_spent should be {amount:g}") def step_check_budget_spent(context: Context, amount: float) -> None: assert context.guardrails.budget_spent == amount @then("the guardrails should have empty required_confirmations") def step_check_empty_confirmations(context: Context) -> None: assert context.guardrails.required_confirmations == [] @then("a guardrails validation error should be raised") def step_check_validation_error(context: Context) -> None: assert context.guardrails_error is not None @then('the guardrails error should mention "{text}"') def step_check_error_message(context: Context, text: str) -> None: assert text in str(context.guardrails_error) # ---- Step limit checks ---- @given("guardrails with max_steps {max_steps:d}") def step_given_guardrails_max_steps( context: Context, max_steps: int, ) -> None: context.guardrails = AutonomyGuardrails(max_steps=max_steps) @given("guardrails with no step limit") def step_given_no_step_limit(context: Context) -> None: context.guardrails = AutonomyGuardrails() @when("I check step limit at step {step:d}") def step_check_step_limit(context: Context, step: int) -> None: context.guardrails.step_count = step allowed, reason = context.guardrails.check_step_limit() context.step_allowed = allowed context.step_reason = reason @then("the step check should be allowed") def step_check_allowed(context: Context) -> None: assert context.step_allowed is True @then("the step check should be denied") def step_check_denied(context: Context) -> None: assert context.step_allowed is False # ---- Budget checks ---- @given("guardrails with tool_budget {budget:g}") def step_given_guardrails_budget( context: Context, budget: float, ) -> None: context.guardrails = AutonomyGuardrails(tool_budget=budget) @given("guardrails with tool_budget {budget:g} and budget_spent {spent:g}") def step_given_guardrails_budget_spent( context: Context, budget: float, spent: float, ) -> None: context.guardrails = AutonomyGuardrails( tool_budget=budget, budget_spent=spent, ) @given("guardrails with no budget limit") def step_given_no_budget_limit(context: Context) -> None: context.guardrails = AutonomyGuardrails() @when("I check tool budget for cost {cost:g}") def step_check_budget(context: Context, cost: float) -> None: allowed, reason = context.guardrails.check_tool_budget(cost) context.budget_allowed = allowed context.budget_reason = reason # Provide a BudgetCheckResult for shared @then steps (#584) from cleveragents.domain.models.core.cost_budget import BudgetCheckResult context.budget_result = BudgetCheckResult(allowed=allowed, reason=reason or "") @when("I try to check tool budget for cost {cost:g}") def step_try_check_budget_negative( context: Context, cost: float, ) -> None: try: context.guardrails.check_tool_budget(cost) context.guardrails_error = None except Exception as exc: context.guardrails_error = exc # "the budget check should be allowed/denied" steps moved to # cost_budgets_steps.py (shared across both feature files, #584). # ---- Confirmation checks ---- @given('guardrails with required confirmations "{confirmations}"') def step_given_confirmations( context: Context, confirmations: str, ) -> None: context.guardrails = AutonomyGuardrails( required_confirmations=confirmations.split(","), ) @given("guardrails with no required confirmations") def step_given_no_confirmations(context: Context) -> None: context.guardrails = AutonomyGuardrails() @when('I check confirmation for operation "{operation}"') def step_check_confirmation(context: Context, operation: str) -> None: required, reason = context.guardrails.check_confirmation_required(operation) context.confirmation_required = required context.confirmation_reason = reason @then("confirmation should be required") def step_confirmation_required(context: Context) -> None: assert context.confirmation_required is True @then("confirmation should not be required") def step_confirmation_not_required(context: Context) -> None: assert context.confirmation_required is False # ---- Step counter and budget tracking ---- @when("I increment the step counter {count:d} times") def step_increment_counter(context: Context, count: int) -> None: for _ in range(count): context.guardrails.increment_step() @then("the step_count should be {count:d}") def step_verify_step_count(context: Context, count: int) -> None: assert context.guardrails.step_count == count @when("I record a tool cost of {cost:g}") def step_record_cost(context: Context, cost: float) -> None: context.guardrails.record_cost(cost) @then("the budget_spent should be {amount:g}") def step_verify_budget_spent(context: Context, amount: float) -> None: assert abs(context.guardrails.budget_spent - amount) < 0.001 @when("I try to record a negative cost") def step_try_record_negative_cost(context: Context) -> None: try: context.guardrails.record_cost(-5.0) context.guardrails_error = None except Exception as exc: context.guardrails_error = exc # ---- Audit trail ---- @given("an empty audit trail") def step_given_empty_trail(context: Context) -> None: context.audit_trail = GuardrailAuditTrail() @when("I add a step_allowed entry to the audit trail") def step_add_allowed_entry(context: Context) -> None: entry = GuardrailAuditEntry( event_type=GuardrailEventType.STEP_ALLOWED, guard_name="step_limit", result=GuardrailResult.ALLOWED, ) context.audit_trail.add_entry(entry) @when("I add a budget_blocked entry to the audit trail") def step_add_blocked_entry(context: Context) -> None: entry = GuardrailAuditEntry( event_type=GuardrailEventType.BUDGET_BLOCKED, guard_name="tool_budget", result=GuardrailResult.DENIED, reason="Budget exceeded", ) context.audit_trail.add_entry(entry) @then("the audit trail should have {count:d} entries") def step_check_trail_count(context: Context, count: int) -> None: trail = getattr(context, "audit_trail", None) if trail is None: trail = context.service_trail assert len(trail.entries) == count @then("the audit trail allowed_count should be {count:d}") def step_check_allowed_count(context: Context, count: int) -> None: assert context.audit_trail.allowed_count == count @then("the audit trail denied_count should be {count:d}") def step_check_denied_count(context: Context, count: int) -> None: assert context.audit_trail.denied_count == count @when( "I create an audit entry with event_type {event_type} and guard_name {guard_name}" ) def step_create_entry( context: Context, event_type: str, guard_name: str, ) -> None: context.audit_entry = GuardrailAuditEntry( event_type=GuardrailEventType(event_type), guard_name=guard_name, result=GuardrailResult.DENIED, reason="Test reason", ) @then('the audit entry event_type should be "{value}"') def step_check_entry_event_type(context: Context, value: str) -> None: assert context.audit_entry.event_type.value == value @then('the audit entry guard_name should be "{value}"') def step_check_entry_guard_name(context: Context, value: str) -> None: assert context.audit_entry.guard_name == value @then('the audit entry result should be "{value}"') def step_check_entry_result(context: Context, value: str) -> None: assert context.audit_entry.result.value == value @then("the audit entry should have a timestamp") def step_check_entry_timestamp(context: Context) -> None: assert context.audit_entry.timestamp is not None assert len(context.audit_entry.timestamp) > 0 # ---- Service steps ---- @given("an autonomy guardrail service") def step_given_service(context: Context) -> None: context.guardrail_service = AutonomyGuardrailService() @when('I configure guardrails for plan "{plan_id}" with max_steps {max_steps:d}') def step_configure_guardrails( context: Context, plan_id: str, max_steps: int, ) -> None: context.guardrail_service.configure_guardrails( plan_id, AutonomyGuardrails(max_steps=max_steps), ) @then('the service should return guardrails for plan "{plan_id}"') def step_service_has_guardrails( context: Context, plan_id: str, ) -> None: g = context.guardrail_service.get_guardrails(plan_id) assert g is not None context.service_guardrails = g @then('the service should return None for plan "{plan_id}"') def step_service_no_guardrails( context: Context, plan_id: str, ) -> None: g = context.guardrail_service.get_guardrails(plan_id) assert g is None @then("the service guardrails should have max_steps {value:d}") def step_service_max_steps(context: Context, value: int) -> None: assert context.service_guardrails.max_steps == value @given('an autonomy guardrail service with guardrails for plan "{plan_id}"') def step_given_service_with_guardrails( context: Context, plan_id: str, ) -> None: context.guardrail_service = AutonomyGuardrailService() context.guardrail_service.configure_guardrails( plan_id, AutonomyGuardrails(max_steps=10), ) @given('an autonomy guardrail service with budget guardrails for plan "{plan_id}"') def step_given_service_with_budget( context: Context, plan_id: str, ) -> None: context.guardrail_service = AutonomyGuardrailService() context.guardrail_service.configure_guardrails( plan_id, AutonomyGuardrails(tool_budget=100.0), ) @given( 'an autonomy guardrail service with confirmation guardrails for plan "{plan_id}"' ) def step_given_service_with_confirmations( context: Context, plan_id: str, ) -> None: context.guardrail_service = AutonomyGuardrailService() context.guardrail_service.configure_guardrails( plan_id, AutonomyGuardrails(required_confirmations=["deploy", "delete"]), ) @when('I check step limit via service for plan "{plan_id}" at step {step:d}') def step_service_check_step( context: Context, plan_id: str, step: int, ) -> None: context.step_result = context.guardrail_service.check_step_limit(plan_id, step) @when('I check tool budget via service for plan "{plan_id}" with cost {cost:g}') def step_service_check_budget( context: Context, plan_id: str, cost: float, ) -> None: context.budget_result = context.guardrail_service.check_tool_budget(plan_id, cost) @when('I check confirmation via service for plan "{plan_id}" operation "{operation}"') def step_service_check_confirmation( context: Context, plan_id: str, operation: str, ) -> None: context.confirmation_result = context.guardrail_service.check_confirmation_required( plan_id, operation ) @then('the audit trail for plan "{plan_id}" should have {count:d} entry') def step_service_trail_count( context: Context, plan_id: str, count: int, ) -> None: trail = context.guardrail_service.get_audit_trail(plan_id) context.service_trail = trail assert len(trail.entries) == count @then("the service guardrails budget_spent should be {amount:g}") def step_service_budget_spent(context: Context, amount: float) -> None: # Use the plan_id stored by the service budget step (plan-003) guardrails = context.guardrail_service.get_guardrails("plan-003") assert guardrails is not None, "No budget guardrails found for plan-003" assert abs(guardrails.budget_spent - amount) < 0.001 @then('the metadata for plan "{plan_id}" should contain the audit trail') def step_service_metadata(context: Context, plan_id: str) -> None: metadata = context.guardrail_service.get_audit_trail_as_metadata(plan_id) assert "guardrail_audit_trail" in metadata assert "autonomy_guardrails" in metadata @when('I load guardrails from metadata for plan "{plan_id}"') def step_load_metadata(context: Context, plan_id: str) -> None: metadata = { "autonomy_guardrails": { "max_steps": 20, "tool_budget": 50.0, "required_confirmations": [], "step_count": 0, "budget_spent": 0.0, }, "guardrail_audit_trail": {"entries": []}, } context.guardrail_service.load_from_metadata(plan_id, metadata) @when('I remove plan "{plan_id}" from the service') def step_remove_plan(context: Context, plan_id: str) -> None: context.guardrail_service.remove_plan(plan_id) @then("the step limit check should return True") def step_check_returns_true(context: Context) -> None: assert context.step_result is True @then("the tool budget check should return True") def step_budget_returns_true(context: Context) -> None: assert context.budget_result is True @then("the confirmation check should return False") def step_confirmation_returns_false(context: Context) -> None: assert context.confirmation_result is False @when("I try to configure guardrails with empty plan_id") def step_try_empty_plan_id(context: Context) -> None: try: context.guardrail_service.configure_guardrails( "", AutonomyGuardrails(), ) context.guardrails_error = None except Exception as exc: context.guardrails_error = exc @then("GuardrailEventType should have at least {count:d} members") def step_check_event_type_members( context: Context, count: int, ) -> None: assert len(GuardrailEventType) >= count @then("GuardrailResult should have exactly {count:d} members") def step_check_result_members(context: Context, count: int) -> None: assert len(GuardrailResult) == count # ---- Wall-clock time steps ---- @given("guardrails with no wall-clock limit") def step_given_no_wall_clock(context: Context) -> None: context.guardrails = AutonomyGuardrails() @given("guardrails with wall-clock limit {seconds:g} seconds started recently") def step_given_wall_clock_recent(context: Context, seconds: float) -> None: context.guardrails = AutonomyGuardrails(max_wall_clock_seconds=seconds) context.guardrails.mark_started() @given("guardrails with wall-clock limit that has already expired") def step_given_wall_clock_expired(context: Context) -> None: context.guardrails = AutonomyGuardrails(max_wall_clock_seconds=0.001) # Set start_time far in the past to guarantee expiry context.guardrails.start_time = ( datetime.now(tz=UTC) - timedelta(seconds=10) ).isoformat() @when("I check wall-clock time") def step_check_wall_clock(context: Context) -> None: allowed, reason = context.guardrails.check_wall_clock() context.wall_clock_allowed = allowed context.wall_clock_reason = reason @then("the wall-clock check should be allowed") def step_wall_clock_allowed(context: Context) -> None: assert context.wall_clock_allowed is True @then("the wall-clock check should be denied") def step_wall_clock_denied(context: Context) -> None: assert context.wall_clock_allowed is False @given('an autonomy guardrail service with wall-clock guardrails for plan "{plan_id}"') def step_given_service_wall_clock(context: Context, plan_id: str) -> None: context.guardrail_service = AutonomyGuardrailService() g = AutonomyGuardrails(max_wall_clock_seconds=600.0) g.mark_started() context.guardrail_service.configure_guardrails(plan_id, g) @when('I check wall-clock via service for plan "{plan_id}"') def step_service_check_wall_clock(context: Context, plan_id: str) -> None: context.wall_clock_result = context.guardrail_service.check_wall_clock(plan_id) # ---- Actor tool-call limit steps ---- @given("guardrails with actor tool-call limit {limit:d}") def step_given_actor_limit(context: Context, limit: int) -> None: context.guardrails = AutonomyGuardrails( actor_limits=ActorLimits(max_tool_calls_per_invocation=limit), ) @given("guardrails with no actor tool-call limit") def step_given_no_actor_limit(context: Context) -> None: context.guardrails = AutonomyGuardrails() @when("I check actor tool calls with {calls:d} current calls") def step_check_actor_calls(context: Context, calls: int) -> None: allowed, reason = context.guardrails.check_actor_tool_calls(calls) context.actor_allowed = allowed context.actor_reason = reason @then("the actor tool-call check should be allowed") def step_actor_allowed(context: Context) -> None: assert context.actor_allowed is True @then("the actor tool-call check should be denied") def step_actor_denied(context: Context) -> None: assert context.actor_allowed is False @given('an autonomy guardrail service with actor-limit guardrails for plan "{plan_id}"') def step_given_service_actor_limit(context: Context, plan_id: str) -> None: context.guardrail_service = AutonomyGuardrailService() context.guardrail_service.configure_guardrails( plan_id, AutonomyGuardrails( actor_limits=ActorLimits(max_tool_calls_per_invocation=10), ), ) @when('I check actor tool calls via service for plan "{plan_id}" with {calls:d} calls') def step_service_check_actor_calls( context: Context, plan_id: str, calls: int, ) -> None: context.actor_result = context.guardrail_service.check_actor_tool_calls( plan_id, calls ) # ---- Bounded audit trail eviction steps ---- @given("an audit trail with max_entries {max_entries:d}") def step_given_bounded_trail(context: Context, max_entries: int) -> None: context.audit_trail = GuardrailAuditTrail(max_entries=max_entries) @when("I add {count:d} step_allowed entries to the audit trail") def step_add_many_allowed_entries(context: Context, count: int) -> None: for _ in range(count): entry = GuardrailAuditEntry( event_type=GuardrailEventType.STEP_ALLOWED, guard_name="step_limit", result=GuardrailResult.ALLOWED, ) context.audit_trail.add_entry(entry) # ---- Edge-case steps ---- @when('I try to load metadata with oversized audit trail for plan "{plan_id}"') def step_try_load_oversized_trail(context: Context, plan_id: str) -> None: metadata = { "guardrail_audit_trail": { "entries": [{}] * (_MAX_METADATA_ENTRIES + 1), }, } try: context.guardrail_service.load_from_metadata(plan_id, metadata) context.guardrails_error = None except Exception as exc: context.guardrails_error = exc @when('I try to load metadata with oversized confirmations for plan "{plan_id}"') def step_try_load_oversized_confirmations( context: Context, plan_id: str, ) -> None: metadata = { "autonomy_guardrails": { "max_steps": 10, "tool_budget": 50.0, "required_confirmations": ["op"] * (_MAX_CONFIRMATIONS + 1), "step_count": 0, "budget_spent": 0.0, }, } try: context.guardrail_service.load_from_metadata(plan_id, metadata) context.guardrails_error = None except Exception as exc: context.guardrails_error = exc @when('I serialize and restore metadata for plan "{plan_id}"') def step_round_trip_metadata(context: Context, plan_id: str) -> None: metadata = context.guardrail_service.get_audit_trail_as_metadata(plan_id) context.round_trip_metadata = metadata # Create a fresh service and restore context.restored_service = AutonomyGuardrailService() context.restored_service.load_from_metadata(plan_id, metadata) @then('the restored guardrails for plan "{plan_id}" should match the original') def step_verify_round_trip(context: Context, plan_id: str) -> None: original = context.guardrail_service.get_guardrails(plan_id) restored = context.restored_service.get_guardrails(plan_id) assert original is not None assert restored is not None assert restored.max_steps == original.max_steps assert restored.tool_budget == original.tool_budget assert restored.required_confirmations == original.required_confirmations assert restored.step_count == original.step_count # Check audit trail was also restored original_trail = context.guardrail_service.get_audit_trail(plan_id) restored_trail = context.restored_service.get_audit_trail(plan_id) assert len(restored_trail.entries) == len(original_trail.entries) # ---- start_time validation steps ---- @when("I create guardrails with a valid start_time") def step_create_guardrails_valid_start_time(context: Context) -> None: context.guardrails = AutonomyGuardrails( start_time=datetime.now(tz=UTC).isoformat(), ) @when('I try to create guardrails with start_time "{value}"') def step_try_create_bad_start_time(context: Context, value: str) -> None: try: AutonomyGuardrails(start_time=value) context.guardrails_error = None except Exception as exc: context.guardrails_error = exc @then("the guardrails start_time should be set") def step_check_start_time_set(context: Context) -> None: assert context.guardrails.start_time is not None assert len(context.guardrails.start_time) > 0 @then("the guardrails start_time should be None") def step_check_start_time_none(context: Context) -> None: assert context.guardrails.start_time is None @given("guardrails with a malformed start_time bypassing validation") def step_given_malformed_start_time(context: Context) -> None: context.guardrails = AutonomyGuardrails(max_wall_clock_seconds=60.0) # Bypass Pydantic validation by setting directly via object.__setattr__ object.__setattr__(context.guardrails, "start_time", "not-a-date") # ---- Retry-per-failure check steps ---- @given("guardrails with retry limit {limit:d}") def step_given_retry_limit(context: Context, limit: int) -> None: context.guardrails = AutonomyGuardrails( actor_limits=ActorLimits(max_retries_per_failure=limit), ) @given("guardrails with no retry limit") def step_given_no_retry_limit(context: Context) -> None: context.guardrails = AutonomyGuardrails() @when("I check retries per failure with {retries:d} current retries") def step_check_retries(context: Context, retries: int) -> None: allowed, reason = context.guardrails.check_retries_per_failure(retries) context.retry_allowed = allowed context.retry_reason = reason @then("the retry check should be allowed") def step_retry_allowed(context: Context) -> None: assert context.retry_allowed is True @then("the retry check should be denied") def step_retry_denied(context: Context) -> None: assert context.retry_allowed is False @given('an autonomy guardrail service with retry-limit guardrails for plan "{plan_id}"') def step_given_service_retry_limit(context: Context, plan_id: str) -> None: context.guardrail_service = AutonomyGuardrailService() context.guardrail_service.configure_guardrails( plan_id, AutonomyGuardrails( actor_limits=ActorLimits(max_retries_per_failure=3), ), ) @when('I check retries via service for plan "{plan_id}" with {retries:d} retries') def step_service_check_retries( context: Context, plan_id: str, retries: int, ) -> None: context.retry_result = context.guardrail_service.check_retries_per_failure( plan_id, retries ) @then("the retry service check should return False") def step_retry_service_returns_false(context: Context) -> None: assert context.retry_result is False @given( 'a guardrail service with max_retries_per_failure {limit:d} for plan "{plan_id}"' ) def step_given_service_custom_retry_limit( context: Context, limit: int, plan_id: str ) -> None: context.guardrail_service = AutonomyGuardrailService() context.guardrail_service.configure_guardrails( plan_id, AutonomyGuardrails( actor_limits=ActorLimits(max_retries_per_failure=limit), ), ) @when('I check retries for plan "{plan_id}" incrementing from {start:d} to {end:d}') def step_check_retries_incrementing( context: Context, plan_id: str, start: int, end: int ) -> None: context.retry_results = [] for i in range(start, end + 1): allowed = context.guardrail_service.check_retries_per_failure(plan_id, i) context.retry_results.append((i, allowed)) @then("retry checks {allowed_csv} should be allowed") def step_retry_checks_allowed(context: Context, allowed_csv: str) -> None: expected = [int(x.strip()) for x in allowed_csv.split(",")] results_map = dict(context.retry_results) for idx in expected: assert results_map[idx] is True, ( f"Expected retry {idx} to be allowed, but it was denied" ) @then("retry checks {denied_csv} should be denied") def step_retry_checks_denied(context: Context, denied_csv: str) -> None: expected = [int(x.strip()) for x in denied_csv.split(",")] results_map = dict(context.retry_results) for idx in expected: assert results_map[idx] is False, ( f"Expected retry {idx} to be denied, but it was allowed" ) # ---- PlanExecutor guardrail integration helpers ---- def _make_guardrail_mock_plan( phase: PlanPhase = PlanPhase.EXECUTE, state: ProcessingState = ProcessingState.QUEUED, definition_of_done: str = "Step A\nStep B", decision_root_id: str = "root-001", ) -> MagicMock: """Create a mock plan suitable for PlanExecutor tests.""" plan = MagicMock() plan.phase = phase plan.state = state plan.definition_of_done = definition_of_done plan.decision_root_id = decision_root_id plan.timestamps = PlanTimestamps() plan.error_details = {} plan.changeset_id = None plan.sandbox_refs = [] plan.read_only = False plan.invariants = [] return plan def _make_guardrail_mock_lifecycle(plan: MagicMock) -> MagicMock: """Create a mock lifecycle service for PlanExecutor tests.""" lcs = MagicMock() lcs.get_plan.return_value = plan lcs.start_strategize = MagicMock() lcs.complete_strategize = MagicMock() lcs.fail_strategize = MagicMock() lcs.start_execute = MagicMock() lcs.complete_execute = MagicMock() lcs.fail_execute = MagicMock() lcs._commit_plan = MagicMock() return lcs # ---- PlanExecutor guardrail integration steps ---- @given("a plan executor with guardrail service and step limit {limit:d}") def step_given_executor_with_step_limit(context: Context, limit: int) -> None: service = AutonomyGuardrailService() service.configure_guardrails( "grd-plan-001", AutonomyGuardrails(max_steps=limit), ) context.guardrail_plan_id = "grd-plan-001" context.guardrail_exec_service = service @given("a plan executor with guardrail service and expired wall-clock") def step_given_executor_with_expired_wall_clock(context: Context) -> None: service = AutonomyGuardrailService() g = AutonomyGuardrails(max_wall_clock_seconds=0.001) g.start_time = (datetime.now(tz=UTC) - timedelta(seconds=10)).isoformat() service.configure_guardrails("grd-plan-002", g) context.guardrail_plan_id = "grd-plan-002" context.guardrail_exec_service = service @given("a plan executor without guardrail service") def step_given_executor_no_guardrails(context: Context) -> None: context.guardrail_plan_id = "grd-plan-003" context.guardrail_exec_service = None @given("a plan executor with guardrail service and no wall-clock limit") def step_given_executor_no_wall_clock(context: Context) -> None: service = AutonomyGuardrailService() service.configure_guardrails( "grd-plan-004", AutonomyGuardrails(max_steps=100), ) context.guardrail_plan_id = "grd-plan-004" context.guardrail_exec_service = service @given("a plan in execute phase with {count:d} decisions") def step_given_plan_in_execute_phase(context: Context, count: int) -> None: steps = "\n".join(f"Step {chr(65 + i)}" for i in range(count)) plan = _make_guardrail_mock_plan(definition_of_done=steps) lifecycle = _make_guardrail_mock_lifecycle(plan) context.guardrail_executor = PlanExecutor( lifecycle_service=lifecycle, guardrail_service=context.guardrail_exec_service, ) @when("I run execute on the plan") def step_run_execute(context: Context) -> None: try: context.execute_result = context.guardrail_executor.run_execute( context.guardrail_plan_id, ) context.execute_error = None except Exception as exc: context.execute_error = exc context.execute_result = None @then("execution should fail with a guardrail step limit error") def step_check_step_limit_error(context: Context) -> None: assert context.execute_error is not None assert isinstance(context.execute_error, PlanError) assert "step limit" in str(context.execute_error).lower() @then("execution should fail with a guardrail wall-clock error") def step_check_wall_clock_error(context: Context) -> None: assert context.execute_error is not None assert isinstance(context.execute_error, PlanError) assert "wall-clock" in str(context.execute_error).lower() @then("execution should succeed") def step_check_execution_success(context: Context) -> None: assert context.execute_error is None assert context.execute_result is not None @then("the guardrails start_time should have been set") def step_check_start_time_was_set(context: Context) -> None: service = context.guardrail_exec_service assert service is not None guardrails = service.get_guardrails(context.guardrail_plan_id) assert guardrails is not None assert guardrails.start_time is not None # ---- Additional validation edge-case steps ---- @when("I try to create actor limits with max_tool_calls_per_invocation {value:d}") def step_try_bad_max_tool_calls(context: Context, value: int) -> None: try: ActorLimits(max_tool_calls_per_invocation=value) context.guardrails_error = None except Exception as exc: context.guardrails_error = exc @when("I try to create actor limits with max_retries_per_failure {value:d}") def step_try_bad_max_retries(context: Context, value: int) -> None: try: ActorLimits(max_retries_per_failure=value) context.guardrails_error = None except Exception as exc: context.guardrails_error = exc @when("I try to create guardrails with max_wall_clock_seconds {value:d}") def step_try_bad_wall_clock(context: Context, value: int) -> None: try: AutonomyGuardrails(max_wall_clock_seconds=float(value)) context.guardrails_error = None except Exception as exc: context.guardrails_error = exc