a8d91b94a7
CI / build (push) Successful in 16s
CI / helm (push) Successful in 22s
CI / lint (push) Successful in 39s
CI / quality (push) Successful in 3m43s
CI / typecheck (push) Successful in 3m57s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 4m8s
CI / unit_tests (push) Successful in 9m13s
CI / docker (push) Successful in 1m34s
CI / e2e_tests (push) Successful in 15m20s
CI / coverage (push) Successful in 12m28s
CI / integration_tests (push) Successful in 21m12s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Successful in 28m30s
## Summary
- Enforced autonomy guard behavior for denylist/allowlist checks, budget caps, tool-call limits, and write/apply approval gates.
- Added scope-aware guard evaluation (plan vs subplan) using `GuardScope(StrEnum)` for type-safe scope handling.
- Extracted remediation guidance strings as module-level constants (`REMEDIATION_DENYLIST`, `REMEDIATION_ALLOWLIST`, etc.) for consistency and testability.
- Added Behave coverage to validate guard remediation messaging and subplan-scoped tool-call limit behavior.
## Review Fix Round (v2)
Addressed review #2910 by @freemo:
1. **Reverted `resource_dag.robot`** — Unrelated Robot SQLite/cycle-detection changes removed from this commit; will be submitted as a separate PR.
2. **`scope` parameter → `GuardScope` enum** — Created `GuardScope(StrEnum)` in `automation_guard.py` with `PLAN` and `SUBPLAN` members. Updated `check_guard()` signature and all call sites.
3. **Removed redundant `scope_label`** — Now uses `scope.value` directly.
4. **Extracted remediation constants** — Guidance strings moved to module-level constants in `automation_guard.py`.
## Validation
- `nox -s lint` — passed
- `nox -s typecheck` — passed (0 errors, 0 warnings)
- `nox -s unit_tests` — passed (508 features, 12989 scenarios, 0 failures)
- `nox -s coverage_report` — passed (97% coverage)
- Rebased onto latest `master` (532ea100)
Closes #853
Reviewed-on: #1204
Co-authored-by: Brent Edwards <brent.edwards@cleverthis.com>
Co-committed-by: Brent Edwards <brent.edwards@cleverthis.com>
828 lines
30 KiB
Gherkin
828 lines
30 KiB
Gherkin
Feature: Consolidated Automation Profile
|
|
Combined scenarios from: automation_level_removed, automation_profile, automation_profile_crud_coverage, automation_profile_service, automation_profiles_guards
|
|
|
|
# ============================================================
|
|
# Originally from: automation_level_removed.feature
|
|
# Feature: automation_level legacy removal
|
|
# ============================================================
|
|
|
|
Scenario: Plan creation uses automation_profile only
|
|
Given I have a plan lifecycle service with automation level support
|
|
Given I have an available action for automation tests
|
|
When I use the action without explicit automation level on project "proj-001"
|
|
Then the automated plan phase should be "strategize"
|
|
|
|
|
|
Scenario: Plan display shows automation_profile not automation_level
|
|
Given I have a plan lifecycle service with automation level support
|
|
Given I have an available action for automation tests
|
|
When I use the action without explicit automation level on project "proj-001"
|
|
Then the automated plan phase should be "strategize"
|
|
|
|
|
|
Scenario: Pause sets manual automation profile
|
|
Given I have a plan lifecycle service with automation level support
|
|
Given I have an available action for automation tests
|
|
When I use the action without explicit automation level on project "proj-002"
|
|
And I pause the plan
|
|
Then the plan automation level should be "manual"
|
|
|
|
|
|
Scenario: Resume sets auto automation profile
|
|
Given I have a plan lifecycle service with automation level support
|
|
Given I have a paused plan that was previously "manual" in strategize phase
|
|
When I resume the plan without explicit level
|
|
Then the automated plan phase should be "strategize"
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: automation_profile.feature
|
|
# Feature: Automation Profile Domain Model
|
|
# ============================================================
|
|
|
|
Scenario: Profile accepts threshold of 0.0
|
|
When I create a profile with decompose_task 0.0
|
|
Then the profile model should be created
|
|
And the profile decompose_task should be 0.0
|
|
|
|
|
|
Scenario: Profile accepts threshold of 0.5
|
|
When I create a profile with decompose_task 0.5
|
|
Then the profile model should be created
|
|
And the profile decompose_task should be 0.5
|
|
|
|
|
|
Scenario: Profile accepts threshold of 1.0
|
|
When I create a profile with decompose_task 1.0
|
|
Then the profile model should be created
|
|
And the profile decompose_task should be 1.0
|
|
|
|
# ---- Profile validation: invalid thresholds ----
|
|
|
|
|
|
Scenario: Profile rejects threshold below 0.0
|
|
When I try to create a profile with decompose_task -0.1
|
|
Then a profile validation error should be raised
|
|
And the profile error should mention "greater than or equal"
|
|
|
|
|
|
Scenario: Profile rejects threshold above 1.0
|
|
When I try to create a profile with decompose_task 1.1
|
|
Then a profile validation error should be raised
|
|
And the profile error should mention "less than or equal"
|
|
|
|
|
|
Scenario: Profile rejects create_tool below 0.0
|
|
When I try to create a profile with create_tool -0.5
|
|
Then a profile validation error should be raised
|
|
|
|
|
|
Scenario: Profile rejects create_tool above 1.0
|
|
When I try to create a profile with create_tool 2.0
|
|
Then a profile validation error should be raised
|
|
|
|
|
|
Scenario: Profile rejects select_tool below 0.0
|
|
When I try to create a profile with select_tool -0.01
|
|
Then a profile validation error should be raised
|
|
|
|
|
|
Scenario: Profile rejects edit_code above 1.0
|
|
When I try to create a profile with edit_code 1.5
|
|
Then a profile validation error should be raised
|
|
|
|
|
|
Scenario: Profile rejects execute_command below 0.0
|
|
When I try to create a profile with execute_command -1.0
|
|
Then a profile validation error should be raised
|
|
|
|
|
|
Scenario: Profile rejects create_file above 1.0
|
|
When I try to create a profile with create_file 9.9
|
|
Then a profile validation error should be raised
|
|
|
|
|
|
Scenario: Profile rejects delete_content below 0.0
|
|
When I try to create a profile with delete_content -0.001
|
|
Then a profile validation error should be raised
|
|
|
|
|
|
Scenario: Profile rejects access_network above 1.0
|
|
When I try to create a profile with access_network 1.01
|
|
Then a profile validation error should be raised
|
|
|
|
|
|
Scenario: Profile rejects install_dependency below 0.0
|
|
When I try to create a profile with install_dependency -0.5
|
|
Then a profile validation error should be raised
|
|
|
|
|
|
Scenario: Profile rejects modify_config above 1.0
|
|
When I try to create a profile with modify_config 100.0
|
|
Then a profile validation error should be raised
|
|
|
|
|
|
Scenario: Profile rejects approve_plan below 0.0
|
|
When I try to create a profile with approve_plan -0.1
|
|
Then a profile validation error should be raised
|
|
|
|
# ---- Built-in profiles load correctly ----
|
|
|
|
|
|
Scenario: Built-in manual profile loads with expected values
|
|
When I load the built-in profile "manual"
|
|
Then the profile model should be created
|
|
And the profile decompose_task should be 1.0
|
|
And the profile create_tool should be 1.0
|
|
And the profile select_tool should be 1.0
|
|
And the profile safety require_sandbox should be true
|
|
And the profile safety require_checkpoints should be true
|
|
And the profile safety allow_unsafe_tools should be false
|
|
|
|
|
|
Scenario: Built-in review profile loads with expected values
|
|
When I load the built-in profile "review"
|
|
Then the profile model should be created
|
|
And the profile decompose_task should be 0.0
|
|
And the profile create_tool should be 0.0
|
|
And the profile select_tool should be 1.0
|
|
And the profile install_dependency should be 0.0
|
|
|
|
|
|
Scenario: Built-in supervised profile loads with expected values
|
|
When I load the built-in profile "supervised"
|
|
Then the profile model should be created
|
|
And the profile decompose_task should be 0.0
|
|
And the profile create_tool should be 1.0
|
|
And the profile edit_code should be 0.0
|
|
And the profile install_dependency should be 1.0
|
|
|
|
|
|
Scenario: Built-in cautious profile loads with expected values
|
|
When I load the built-in profile "cautious"
|
|
Then the profile model should be created
|
|
And the profile decompose_task should be 0.7
|
|
And the profile create_tool should be 0.7
|
|
And the profile edit_code should be 0.6
|
|
And the profile execute_command should be 0.8
|
|
And the profile create_file should be 0.7
|
|
And the profile delete_content should be 0.8
|
|
And the profile access_network should be 0.9
|
|
And the profile install_dependency should be 0.7
|
|
And the profile approve_plan should be 0.6
|
|
|
|
|
|
Scenario: Built-in trusted profile loads with expected values
|
|
When I load the built-in profile "trusted"
|
|
Then the profile model should be created
|
|
And the profile decompose_task should be 0.0
|
|
And the profile create_tool should be 0.0
|
|
And the profile select_tool should be 1.0
|
|
And the profile delete_content should be 1.0
|
|
And the profile access_network should be 1.0
|
|
|
|
|
|
Scenario: Built-in auto profile loads with expected values
|
|
When I load the built-in profile "auto"
|
|
Then the profile model should be created
|
|
And the profile decompose_task should be 0.0
|
|
And the profile select_tool should be 1.0
|
|
And the profile access_network should be 1.0
|
|
And the profile delete_content should be 0.0
|
|
|
|
|
|
Scenario: Built-in ci profile loads with expected values
|
|
When I load the built-in profile "ci"
|
|
Then the profile model should be created
|
|
And the profile decompose_task should be 0.0
|
|
And the profile select_tool should be 0.0
|
|
And the profile safety require_sandbox should be true
|
|
And the profile safety allow_unsafe_tools should be false
|
|
|
|
|
|
Scenario: Built-in full-auto profile loads with expected values
|
|
When I load the built-in profile "full-auto"
|
|
Then the profile model should be created
|
|
And the profile decompose_task should be 0.0
|
|
And the profile select_tool should be 0.0
|
|
And the profile safety require_sandbox should be false
|
|
And the profile safety require_checkpoints should be false
|
|
And the profile safety allow_unsafe_tools should be true
|
|
|
|
# ---- All 8 built-in profiles exist ----
|
|
|
|
|
|
Scenario: All 8 built-in profiles are registered
|
|
Then there should be 8 built-in profiles
|
|
And built-in profile "manual" should exist
|
|
And built-in profile "review" should exist
|
|
And built-in profile "supervised" should exist
|
|
And built-in profile "cautious" should exist
|
|
And built-in profile "trusted" should exist
|
|
And built-in profile "auto" should exist
|
|
And built-in profile "ci" should exist
|
|
And built-in profile "full-auto" should exist
|
|
|
|
# ---- Custom profile from YAML dict loads correctly ----
|
|
|
|
|
|
Scenario: Custom profile from YAML config loads correctly
|
|
When I load a profile from config with name "acme/strict" and select_tool 0.8
|
|
Then the profile model should be created
|
|
And the profile name should be "acme/strict"
|
|
And the profile select_tool should be 0.8
|
|
|
|
|
|
Scenario: Profile from config missing name raises error
|
|
When I try to load a profile from config missing name
|
|
Then a profile config error should be raised with "name"
|
|
|
|
# ---- Name format validation ----
|
|
|
|
|
|
Scenario: Profile name accepts bare name
|
|
When I create a profile with name "manual"
|
|
Then the profile model should be created
|
|
And the profile name should be "manual"
|
|
|
|
|
|
Scenario: Profile name accepts namespaced name
|
|
When I create a profile with name "acme/strict"
|
|
Then the profile model should be created
|
|
And the profile name should be "acme/strict"
|
|
|
|
|
|
Scenario: Profile name rejects spaces
|
|
When I try to create a profile with invalid name "bad name"
|
|
Then a profile validation error should be raised
|
|
And the profile error should mention "Profile name"
|
|
|
|
|
|
Scenario: Profile name rejects empty string
|
|
When I try to create a profile with empty name
|
|
Then a profile validation error should be raised
|
|
|
|
|
|
Scenario: Profile name rejects double slash
|
|
When I try to create a profile with invalid name "bad//name"
|
|
Then a profile validation error should be raised
|
|
|
|
|
|
Scenario: Profile name rejects trailing slash
|
|
When I try to create a profile with invalid name "bad/"
|
|
Then a profile validation error should be raised
|
|
|
|
# ---- get_builtin_profile helper ----
|
|
|
|
|
|
Scenario: get_builtin_profile returns correct profile
|
|
When I call get_builtin_profile with "ci"
|
|
Then the profile model should be created
|
|
And the profile name should be "ci"
|
|
|
|
|
|
Scenario: get_builtin_profile raises for unknown name
|
|
When I try to call get_builtin_profile with "nonexistent"
|
|
Then a profile key error should be raised
|
|
|
|
# ---- Schema version ----
|
|
|
|
|
|
Scenario: Profile has default schema version
|
|
When I create a profile with name "test-profile"
|
|
Then the profile schema_version should be "1.0"
|
|
|
|
|
|
Scenario: Profile accepts custom schema version
|
|
When I create a profile with schema_version "2.0"
|
|
Then the profile schema_version should be "2.0"
|
|
|
|
# ---- Safety field defaults ----
|
|
|
|
|
|
Scenario: Profile safety fields have correct defaults
|
|
When I create a profile with name "test-defaults"
|
|
Then the profile safety require_sandbox should be true
|
|
And the profile safety require_checkpoints should be true
|
|
And the profile safety allow_unsafe_tools should be false
|
|
|
|
# ---- Description field ----
|
|
|
|
|
|
Scenario: Profile description defaults to empty string
|
|
When I create a profile with name "no-desc"
|
|
Then the profile description should be empty
|
|
|
|
|
|
Scenario: Profile description accepts custom value
|
|
When I create a profile with description "My custom profile"
|
|
Then the profile description should be "My custom profile"
|
|
|
|
# ---- validate_assignment enforcement ----
|
|
|
|
|
|
Scenario: Assigning invalid threshold raises error
|
|
When I create a profile with name "assign-test"
|
|
And I try to assign decompose_task 1.5 on the profile
|
|
Then a profile validation error should be raised
|
|
|
|
# ---- Built-in profile retry and checkpoint values ----
|
|
|
|
|
|
Scenario: Manual profile has modify_config 1.0
|
|
When I load the built-in profile "manual"
|
|
Then the profile modify_config should be 1.0
|
|
And the profile approve_plan should be 1.0
|
|
|
|
|
|
Scenario: Review profile has modify_config 0.0
|
|
When I load the built-in profile "review"
|
|
Then the profile modify_config should be 0.0
|
|
And the profile approve_plan should be 1.0
|
|
|
|
|
|
Scenario: Cautious profile has modify_config 0.0
|
|
When I load the built-in profile "cautious"
|
|
Then the profile modify_config should be 0.0
|
|
|
|
# ---- Model dump and round-trip ----
|
|
|
|
|
|
Scenario: Profile model_dump produces valid dict
|
|
When I create a profile and dump it with select_tool 0.5
|
|
Then the profile model dump should have key "name"
|
|
And the profile model dump should have key "select_tool"
|
|
And the profile model dump select_tool should be 0.5
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: automation_profile_crud_coverage.feature
|
|
# Feature: Automation Profile CRUD operations with repository
|
|
# ============================================================
|
|
|
|
Scenario: Get custom profile from repository
|
|
Given a profile service with a mock repository
|
|
And the mock repository contains a profile named "acme/custom"
|
|
When I fetch profile "acme/custom"
|
|
Then the fetched profile name should be "acme/custom"
|
|
|
|
# ---- list_profiles with repository (lines 221-222) ----
|
|
|
|
|
|
Scenario: List profiles includes custom profiles from repository
|
|
Given a profile service with a mock repository
|
|
And the mock repository contains a profile named "acme/custom"
|
|
When I list all available profiles
|
|
Then the available profiles should include "manual"
|
|
And the available profiles should include "full-auto"
|
|
And the available profiles should include "acme/custom"
|
|
|
|
# ---- create_profile valid (lines 239-248) ----
|
|
|
|
|
|
Scenario: Create a custom profile with valid config
|
|
Given a profile service with a mock repository
|
|
When I create a profile with name "team/deploy" and description "Deploy profile"
|
|
Then the created profile name should be "team/deploy"
|
|
And the created profile description should be "Deploy profile"
|
|
And the mock repository should contain "team/deploy"
|
|
|
|
# ---- create_profile builtin name raises ValidationError (lines 240-241) ----
|
|
|
|
|
|
Scenario: Create profile with builtin name raises ValidationError
|
|
Given a profile service with a mock repository
|
|
When I try to create a profile with builtin name "manual"
|
|
Then a crud validation error should be raised
|
|
And the crud validation error message should mention "manual"
|
|
|
|
# ---- update_profile valid (lines 269-282) ----
|
|
|
|
|
|
Scenario: Update a custom profile with valid config
|
|
Given a profile service with a mock repository
|
|
And the mock repository contains a profile named "acme/custom"
|
|
When I update profile "acme/custom" with description "Updated description"
|
|
Then the updated profile name should be "acme/custom"
|
|
And the updated profile description should be "Updated description"
|
|
And the mock repository should contain "acme/custom"
|
|
|
|
# ---- update_profile builtin name raises ValidationError (line 269-270) ----
|
|
|
|
|
|
Scenario: Update builtin profile raises ValidationError
|
|
Given a profile service with a mock repository
|
|
When I try to update builtin profile "auto"
|
|
Then a crud validation error should be raised
|
|
And the crud validation error message should mention "auto"
|
|
|
|
# ---- update_profile without repo raises NotFoundError (lines 275-276) ----
|
|
|
|
|
|
Scenario: Update profile without repository raises NotFoundError
|
|
Given a profile service without a repository
|
|
When I try to update profile "acme/custom" without repo
|
|
Then a crud not found error should be raised
|
|
And the crud not found error message should mention "acme/custom"
|
|
|
|
# ---- delete_profile valid (lines 294-304) ----
|
|
|
|
|
|
Scenario: Delete a custom profile with valid name
|
|
Given a profile service with a mock repository
|
|
And the mock repository contains a profile named "acme/custom"
|
|
When I delete profile "acme/custom"
|
|
Then the mock repository should not contain "acme/custom"
|
|
|
|
# ---- delete_profile builtin name raises ValidationError (lines 294-295) ----
|
|
|
|
|
|
Scenario: Delete builtin profile raises ValidationError
|
|
Given a profile service with a mock repository
|
|
When I try to delete builtin profile "supervised"
|
|
Then a crud validation error should be raised
|
|
And the crud validation error message should mention "supervised"
|
|
|
|
# ---- delete_profile without repo raises NotFoundError (lines 298-299) ----
|
|
|
|
|
|
Scenario: Delete profile without repository raises NotFoundError
|
|
Given a profile service without a repository
|
|
When I try to delete profile "acme/custom" without repo
|
|
Then a crud not found error should be raised
|
|
And the crud not found error message should mention "acme/custom"
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: automation_profile_service.feature
|
|
# Feature: Automation Profile Service
|
|
# ============================================================
|
|
|
|
Scenario: Plan-level profile takes highest precedence
|
|
Given an automation profile service with global default "manual"
|
|
When I resolve profile with plan "full-auto" action "auto" project "supervised"
|
|
Then the resolved profile name should be "full-auto"
|
|
|
|
|
|
Scenario: Action-level profile used when plan is not set
|
|
Given an automation profile service with global default "manual"
|
|
When I resolve profile with plan None action "auto" project "supervised"
|
|
Then the resolved profile name should be "auto"
|
|
|
|
|
|
Scenario: Project-level profile used when plan and action are not set
|
|
Given an automation profile service with global default "manual"
|
|
When I resolve profile with plan None action None project "supervised"
|
|
Then the resolved profile name should be "supervised"
|
|
|
|
|
|
Scenario: Global default used when no level is set
|
|
Given an automation profile service with global default "cautious"
|
|
When I resolve profile with plan None action None project None
|
|
Then the resolved profile name should be "cautious"
|
|
|
|
|
|
Scenario: Default global is manual when nothing configured
|
|
Given an automation profile service with no configuration
|
|
When I resolve profile with plan None action None project None
|
|
Then the resolved profile name should be "manual"
|
|
|
|
# ---- Missing profile errors ----
|
|
|
|
|
|
Scenario: Missing profile raises NotFoundError
|
|
Given an automation profile service with global default "manual"
|
|
When I try to get profile "nonexistent-profile"
|
|
Then a profile not found error should be raised
|
|
And the profile service error should mention "nonexistent-profile"
|
|
|
|
|
|
Scenario: Missing profile in resolve raises NotFoundError
|
|
Given an automation profile service with global default "does-not-exist"
|
|
When I try to resolve profile with all None
|
|
Then a profile not found error should be raised
|
|
|
|
# ---- Environment variable override ----
|
|
|
|
|
|
Scenario: Env var overrides empty global default
|
|
Given an automation profile service with no configuration
|
|
And the env var CLEVERAGENTS_AUTOMATION_PROFILE is set to "ci"
|
|
When I resolve profile with plan None action None project None
|
|
Then the resolved profile name should be "ci"
|
|
|
|
|
|
Scenario: Explicit global default takes precedence over env var
|
|
Given an automation profile service with global default "review"
|
|
And the env var CLEVERAGENTS_AUTOMATION_PROFILE is set to "ci"
|
|
When I resolve profile with plan None action None project None
|
|
Then the resolved profile name should be "review"
|
|
|
|
# ---- CRUD via repository ----
|
|
|
|
|
|
Scenario: List profiles includes built-ins
|
|
Given an automation profile service with global default "manual"
|
|
When I list all profiles
|
|
Then the profile list should include "manual"
|
|
And the profile list should include "full-auto"
|
|
And the profile list should have at least 8 entries
|
|
|
|
# ---- Get built-in profile ----
|
|
|
|
|
|
Scenario: Get built-in profile by name
|
|
Given an automation profile service with global default "manual"
|
|
When I get profile "auto"
|
|
Then the retrieved profile name should be "auto"
|
|
And the retrieved profile select_tool should be 1.0
|
|
|
|
# ---- Profile threshold checks ----
|
|
|
|
|
|
Scenario: Manual profile has all thresholds at 1.0
|
|
Given an automation profile service with global default "manual"
|
|
When I get profile "manual"
|
|
Then the retrieved profile decompose_task should be 1.0
|
|
And the retrieved profile create_tool should be 1.0
|
|
And the retrieved profile select_tool should be 1.0
|
|
|
|
|
|
Scenario: Full-auto profile has all thresholds at 0.0
|
|
Given an automation profile service with global default "manual"
|
|
When I get profile "full-auto"
|
|
Then the retrieved profile decompose_task should be 0.0
|
|
And the retrieved profile create_tool should be 0.0
|
|
And the retrieved profile select_tool should be 0.0
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: automation_profiles_guards.feature
|
|
# Feature: Automation Profile Guards
|
|
# ============================================================
|
|
|
|
Scenario: Guard allows tool within call limits
|
|
Given a profile with max_tool_calls_per_step 5
|
|
When I check guard for tool "read_file" with 3 calls so far
|
|
Then the guard result should be allowed
|
|
|
|
|
|
Scenario: Guard allows tool with no guards configured
|
|
Given a profile with no guards
|
|
When I check guard for tool "any_tool"
|
|
Then the guard result should be allowed
|
|
|
|
# ---- Guard blocks tool exceeding max_tool_calls ----
|
|
|
|
|
|
Scenario: Guard blocks tool exceeding max_tool_calls
|
|
Given a profile with max_tool_calls_per_step 3
|
|
When I check guard for tool "read_file" with 3 calls so far
|
|
Then the guard result should not be allowed
|
|
And the guard reason should mention "limit"
|
|
And the guard should require approval
|
|
|
|
|
|
Scenario: Guard blocks tool at exact max_tool_calls boundary
|
|
Given a profile with max_tool_calls_per_step 0
|
|
When I check guard for tool "read_file" with 0 calls so far
|
|
Then the guard result should not be allowed
|
|
|
|
# ---- Guard blocks tool on denylist ----
|
|
|
|
|
|
Scenario: Guard blocks tool on denylist
|
|
Given a profile with denylist "shell_exec,file_delete"
|
|
When I check guard for tool "shell_exec"
|
|
Then the guard result should not be allowed
|
|
And the guard reason should mention "denylist"
|
|
And the guard should require approval
|
|
|
|
|
|
Scenario: Guard allows tool not on denylist
|
|
Given a profile with denylist "shell_exec,file_delete"
|
|
When I check guard for tool "read_file"
|
|
Then the guard result should be allowed
|
|
|
|
# ---- Guard allows tool on allowlist ----
|
|
|
|
|
|
Scenario: Guard allows tool on allowlist
|
|
Given a profile with allowlist "read_file,list_dir"
|
|
When I check guard for tool "read_file"
|
|
Then the guard result should be allowed
|
|
|
|
|
|
Scenario: Guard blocks tool not on allowlist
|
|
Given a profile with allowlist "read_file,list_dir"
|
|
When I check guard for tool "write_file"
|
|
Then the guard result should not be allowed
|
|
And the guard reason should mention "allowlist"
|
|
|
|
# ---- Guard requires approval for writes ----
|
|
|
|
|
|
Scenario: Guard requires approval for write operations
|
|
Given a profile with require_approval_for_writes true
|
|
When I check guard for tool "write_file" with is_write true
|
|
Then the guard result should not be allowed
|
|
And the guard reason should mention "Write"
|
|
And the guard should require approval
|
|
|
|
|
|
Scenario: Guard allows non-write operations when writes require approval
|
|
Given a profile with require_approval_for_writes true
|
|
When I check guard for tool "read_file" with is_write false
|
|
Then the guard result should be allowed
|
|
|
|
# ---- Guard requires approval for apply ----
|
|
|
|
|
|
Scenario: Guard requires approval for apply phase
|
|
Given a profile with require_approval_for_apply true
|
|
When I check guard for tool "__apply__"
|
|
Then the guard result should not be allowed
|
|
And the guard reason should mention "Apply"
|
|
And the guard should require approval
|
|
|
|
|
|
Scenario: Guard allows non-apply tools when apply requires approval
|
|
Given a profile with require_approval_for_apply true
|
|
When I check guard for tool "read_file"
|
|
Then the guard result should be allowed
|
|
|
|
# ---- Cost budget enforcement ----
|
|
|
|
|
|
Scenario: Guard blocks tool when cost budget exceeded
|
|
Given a profile with max_total_cost 10.0
|
|
When I check guard for tool "expensive_tool" with cost 10.0
|
|
Then the guard result should not be allowed
|
|
And the guard reason should mention "Cost"
|
|
And the guard should require approval
|
|
|
|
|
|
Scenario: Guard allows tool within cost budget
|
|
Given a profile with max_total_cost 10.0
|
|
When I check guard for tool "cheap_tool" with cost 5.0
|
|
Then the guard result should be allowed
|
|
|
|
# ---- Effective profile resolution ----
|
|
|
|
|
|
Scenario: Effective profile resolves plan-level first
|
|
Given a profile service with profiles "cautious" and "manual"
|
|
When I resolve effective profile with plan "cautious"
|
|
Then the resolved guard profile name should be "cautious"
|
|
|
|
|
|
Scenario: Effective profile falls back to action level
|
|
Given a profile service with profiles "cautious" and "manual"
|
|
When I resolve effective profile with action "cautious"
|
|
Then the resolved guard profile name should be "cautious"
|
|
|
|
|
|
Scenario: Effective profile falls back to default
|
|
Given a profile service with profiles "cautious" and "manual"
|
|
When I resolve effective profile with default "cautious"
|
|
Then the resolved guard profile name should be "cautious"
|
|
|
|
|
|
Scenario: Effective profile uses global when no overrides
|
|
Given a profile service with global default "manual"
|
|
When I resolve effective profile with no overrides
|
|
Then the resolved guard profile name should be "manual"
|
|
|
|
# ---- Custom profile YAML loading ----
|
|
|
|
|
|
Scenario: Load custom profile from YAML file
|
|
Given a YAML profile file with guards
|
|
When I load the profile from YAML
|
|
Then the loaded profile should have guards
|
|
And the loaded profile guards max_tool_calls_per_step should be 10
|
|
|
|
|
|
Scenario: Load custom profile from YAML via service
|
|
Given a YAML profile file with guards
|
|
When I load the profile via service from_yaml
|
|
Then the loaded profile should have guards
|
|
|
|
# ---- Built-in profile immutability ----
|
|
|
|
|
|
Scenario: Built-in profiles cannot be updated
|
|
Given a profile service
|
|
When I try to update built-in profile "manual"
|
|
Then a profile service validation error should be raised
|
|
|
|
|
|
Scenario: Built-in profiles cannot be deleted
|
|
Given a profile service
|
|
When I try to delete built-in profile "manual"
|
|
Then a profile service validation error should be raised
|
|
|
|
# ---- Guard model validation ----
|
|
|
|
|
|
Scenario: AutomationGuard rejects negative max_tool_calls
|
|
When I try to create a guard with max_tool_calls -1
|
|
Then a guard model validation error should be raised
|
|
|
|
|
|
Scenario: AutomationGuard rejects negative max_total_cost
|
|
When I try to create a guard with max_total_cost -1.0
|
|
Then a guard model validation error should be raised
|
|
|
|
|
|
Scenario: AutomationGuard accepts zero max_tool_calls
|
|
When I create a guard with max_tool_calls 0
|
|
Then the guard should be created
|
|
|
|
|
|
Scenario: AutomationGuard accepts None values
|
|
When I create a guard with defaults
|
|
Then the guard max_tool_calls_per_step should be None
|
|
And the guard max_total_cost should be None
|
|
|
|
# ---- GuardResult model ----
|
|
|
|
|
|
Scenario: GuardResult allowed with no reason
|
|
When I create a guard result allowed true
|
|
Then the guard result allowed should be true
|
|
And the guard result reason should be None
|
|
|
|
|
|
Scenario: GuardResult denied with reason
|
|
When I create a guard result denied with reason "blocked"
|
|
Then the guard result allowed should be false
|
|
And the guard result reason should be "blocked"
|
|
And the guard result requires_approval should be true
|
|
|
|
# ---- Service evaluate_guard ----
|
|
|
|
|
|
Scenario: Service evaluate_guard delegates to profile
|
|
Given a profile service
|
|
When I evaluate guard for profile "full-auto" tool "any_tool"
|
|
Then the guard result should be allowed
|
|
|
|
|
|
Scenario: Service evaluate_guard rejects empty profile name
|
|
Given a profile service
|
|
When I try to evaluate guard with empty profile name
|
|
Then a profile service validation error should be raised
|
|
|
|
|
|
Scenario: Service evaluate_guard rejects empty tool name
|
|
Given a profile service
|
|
When I try to evaluate guard with empty tool name
|
|
Then a profile service validation error should be raised
|
|
|
|
# ---- Profile with guards from config dict ----
|
|
|
|
|
|
Scenario: Profile from config dict with guards
|
|
When I create a profile from config with guards
|
|
Then the profile should have guards configured
|
|
And the profile guards should have denylist
|
|
|
|
# ---- check_guard argument validation ----
|
|
|
|
|
|
Scenario: check_guard rejects negative cost_so_far
|
|
Given a profile with max_total_cost 10.0
|
|
When I try to check guard with negative cost
|
|
Then a guard argument error should be raised
|
|
|
|
|
|
Scenario: check_guard rejects negative calls_so_far
|
|
Given a profile with max_tool_calls_per_step 5
|
|
When I try to check guard with negative calls
|
|
Then a guard argument error should be raised
|
|
|
|
Scenario: Guard denylist message includes remediation guidance
|
|
Given a profile with denylist "shell_exec"
|
|
When I check guard for tool "shell_exec"
|
|
Then the guard result should not be allowed
|
|
And the guard reason should mention "Remediation"
|
|
|
|
Scenario: Guard budget message includes remediation guidance
|
|
Given a profile with max_total_cost 10.0
|
|
When I check guard for tool "expensive_tool" with cost 10.0
|
|
Then the guard result should not be allowed
|
|
And the guard reason should mention "Remediation"
|
|
|
|
Scenario: Guard tool-call limit can be evaluated per subplan scope
|
|
Given a profile with max_tool_calls_per_step 2
|
|
When I check guard for tool "read_file" with 2 calls so far in "subplan" scope
|
|
Then the guard result should not be allowed
|
|
And the guard reason should mention "Subplan"
|
|
|
|
Scenario: Guard write approval message includes remediation guidance
|
|
Given a profile with require_approval_for_writes true
|
|
When I check guard for tool "write_file" with is_write true
|
|
Then the guard result should not be allowed
|
|
And the guard reason should mention "Remediation"
|