b4b96d213c
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / build (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 29s
CI / security (pull_request) Successful in 34s
CI / typecheck (pull_request) Successful in 39s
CI / unit_tests (pull_request) Successful in 2m19s
CI / docker (pull_request) Successful in 40s
CI / integration_tests (pull_request) Successful in 3m1s
CI / coverage (pull_request) Successful in 7m20s
CI / lint (push) Successful in 13s
CI / quality (push) Successful in 16s
CI / build (push) Successful in 18s
CI / security (push) Successful in 31s
CI / typecheck (push) Successful in 35s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m10s
CI / docker (push) Successful in 39s
CI / integration_tests (push) Successful in 3m4s
CI / coverage (push) Successful in 4m45s
CI / benchmark-publish (push) Successful in 14m51s
CI / benchmark-regression (pull_request) Successful in 26m49s
Implement safety profile resolution and enforcement in the tool
execution pipeline, replacing the NotImplementedError stub with
working precedence logic and runtime safety checks.
Core changes:
- resolve_safety_profile() now resolves plan > action > project >
global precedence, returning the highest-priority non-None profile
(or DEFAULT_SAFETY_PROFILE with GLOBAL provenance when all None)
- ToolExecutionContext gains an optional safety_profile field
- ToolRuntime._enforce_capabilities() extended with three new checks:
* Unsafe tool gating: blocks tools with unsafe=True when profile
has allow_unsafe_tools=False (ToolSafetyViolationError)
* Skill category allow-list: blocks tools whose skill category
is not in allowed_skill_categories (ToolSafetyViolationError)
* Checkpoint requirement: OR-combines ctx.require_checkpoints
with safety_profile.require_checkpoints
- New ToolSafetyViolationError in tool error hierarchy
Test coverage:
- 30 updated BDD scenarios in safety_profile.feature (resolve
precedence replaces NotImplementedError stub test)
- 24 new BDD scenarios in safety_profile_enforcement.feature
- 9 Robot Framework integration smoke tests
- 4 ASV benchmark suites (construction, serialization, resolution,
provenance enum)
All nox sessions pass (typecheck 0 errors, unit_tests 7735 scenarios
0 failures, coverage 97%, integration_tests 9/9 passed, benchmarks
complete).
ISSUES CLOSED: #345
229 lines
10 KiB
Gherkin
229 lines
10 KiB
Gherkin
Feature: Safety Profile Domain Model
|
|
As a developer
|
|
I want safety profile domain models with constraint validation
|
|
So that plan execution safety can be configured and enforced
|
|
|
|
# ---- Default construction ----
|
|
|
|
Scenario: Default safety profile has correct defaults
|
|
When I create a default safety profile
|
|
Then the safety profile should be created
|
|
And the safety require_sandbox should be true
|
|
And the safety require_checkpoints should be true
|
|
And the safety require_human_approval should be false
|
|
And the safety allow_unsafe_tools should be false
|
|
And the safety max_retries_per_step should be 3
|
|
And the safety max_cost_per_plan should be none
|
|
And the safety max_total_cost should be none
|
|
And the safety allowed_skill_categories should be empty
|
|
|
|
# ---- Field parsing ----
|
|
|
|
Scenario: Safety profile accepts allowed_skill_categories
|
|
When I create a safety profile with categories "code,test,deploy"
|
|
Then the safety profile should be created
|
|
And the safety allowed_skill_categories count should be 3
|
|
|
|
Scenario: Safety profile deduplicates categories
|
|
When I create a safety profile with categories "code,test,code"
|
|
Then the safety profile should be created
|
|
And the safety allowed_skill_categories count should be 2
|
|
|
|
Scenario: Safety profile strips blank categories
|
|
When I create a safety profile with categories "code, ,test, "
|
|
Then the safety profile should be created
|
|
And the safety allowed_skill_categories count should be 2
|
|
|
|
# ---- Constraint validation ----
|
|
|
|
Scenario: Safety profile rejects negative max_cost_per_plan
|
|
When I try to create a safety profile with max_cost_per_plan -1.0
|
|
Then a safety validation error should be raised
|
|
|
|
Scenario: Safety profile rejects negative max_total_cost
|
|
When I try to create a safety profile with max_total_cost -5.0
|
|
Then a safety validation error should be raised
|
|
|
|
Scenario: Safety profile rejects negative max_retries_per_step
|
|
When I try to create a safety profile with max_retries_per_step -1
|
|
Then a safety validation error should be raised
|
|
|
|
Scenario: Safety profile rejects max_retries_per_step above 100
|
|
When I try to create a safety profile with max_retries_per_step 101
|
|
Then a safety validation error should be raised
|
|
|
|
Scenario: Safety profile accepts zero max_cost_per_plan
|
|
When I create a safety profile with max_cost_per_plan 0.0
|
|
Then the safety profile should be created
|
|
And the safety max_cost_per_plan should be 0.0
|
|
|
|
Scenario: Safety profile accepts zero max_retries_per_step
|
|
When I create a safety profile with max_retries_per_step 0
|
|
Then the safety profile should be created
|
|
And the safety max_retries_per_step should be 0
|
|
|
|
# ---- Cross-field validation ----
|
|
|
|
Scenario: Safety profile rejects max_cost_per_plan exceeding max_total_cost
|
|
When I try to create a safety profile with cost 1000.0 and total 10.0
|
|
Then a safety validation error should be raised
|
|
|
|
Scenario: Safety profile accepts max_cost_per_plan equal to max_total_cost
|
|
When I create a safety profile with cost 100.0 and total 100.0
|
|
Then the safety profile should be created
|
|
And the safety max_cost_per_plan should be 100.0
|
|
And the safety max_total_cost should be 100.0
|
|
|
|
# ---- Category type guard ----
|
|
|
|
Scenario: Safety profile rejects non-string categories
|
|
When I try to create a safety profile with non-string categories
|
|
Then a safety validation error should be raised
|
|
|
|
Scenario: Safety profile rejects non-list categories
|
|
When I try to create a safety profile with non-list categories
|
|
Then a safety validation error should be raised
|
|
|
|
# ---- Frozen immutability ----
|
|
|
|
Scenario: Safety profile is immutable (frozen)
|
|
When I create a default safety profile
|
|
And I try to assign max_retries_per_step -1 on the safety profile
|
|
Then a safety validation error should be raised
|
|
|
|
# ---- from_config factory ----
|
|
|
|
Scenario: Safety profile from config loads correctly
|
|
When I load a safety profile from config with require_sandbox false
|
|
Then the safety profile should be created
|
|
And the safety require_sandbox should be false
|
|
|
|
Scenario: Safety profile from config with all fields
|
|
When I load a safety profile from full config
|
|
Then the safety profile should be created
|
|
And the safety require_sandbox should be false
|
|
And the safety require_checkpoints should be false
|
|
And the safety require_human_approval should be true
|
|
And the safety allow_unsafe_tools should be true
|
|
And the safety max_cost_per_plan should be 50.0
|
|
And the safety max_retries_per_step should be 5
|
|
And the safety max_total_cost should be 200.0
|
|
|
|
# ---- DEFAULT_SAFETY_PROFILE constant ----
|
|
|
|
Scenario: DEFAULT_SAFETY_PROFILE has sensible defaults
|
|
When I load the default safety profile constant
|
|
Then the safety profile should be created
|
|
And the safety require_sandbox should be true
|
|
And the safety require_checkpoints should be true
|
|
And the safety require_human_approval should be false
|
|
And the safety allow_unsafe_tools should be false
|
|
And the safety max_retries_per_step should be 3
|
|
|
|
# ---- SafetyProfileRef ----
|
|
|
|
Scenario: SafetyProfileRef stores name and provenance
|
|
When I create a safety profile ref with name "strict" and provenance "plan"
|
|
Then the safety profile ref should have name "strict"
|
|
And the safety profile ref should have provenance "plan"
|
|
|
|
Scenario: SafetyProfileRef rejects empty name
|
|
When I try to create a safety profile ref with empty name
|
|
Then a safety validation error should be raised
|
|
|
|
# ---- resolve_safety_profile precedence ----
|
|
|
|
Scenario: resolve_safety_profile returns plan-level when all levels set
|
|
Given a plan safety profile with allow_unsafe_tools true
|
|
And an action safety profile with allow_unsafe_tools false
|
|
And a project safety profile with allow_unsafe_tools false
|
|
And a global safety profile with allow_unsafe_tools false
|
|
When I resolve the safety profile
|
|
Then the resolved provenance should be "plan"
|
|
And the resolved profile allow_unsafe_tools should be true
|
|
|
|
Scenario: resolve_safety_profile returns action-level when no plan-level
|
|
Given an action safety profile with allow_unsafe_tools true
|
|
And a project safety profile with allow_unsafe_tools false
|
|
And a global safety profile with allow_unsafe_tools false
|
|
When I resolve the safety profile
|
|
Then the resolved provenance should be "action"
|
|
And the resolved profile allow_unsafe_tools should be true
|
|
|
|
Scenario: resolve_safety_profile returns project-level when no plan or action
|
|
Given a project safety profile with allow_unsafe_tools true
|
|
And a global safety profile with allow_unsafe_tools false
|
|
When I resolve the safety profile
|
|
Then the resolved provenance should be "project"
|
|
And the resolved profile allow_unsafe_tools should be true
|
|
|
|
Scenario: resolve_safety_profile returns global-level when only global set
|
|
Given a global safety profile with allow_unsafe_tools true
|
|
When I resolve the safety profile
|
|
Then the resolved provenance should be "global"
|
|
And the resolved profile allow_unsafe_tools should be true
|
|
|
|
Scenario: resolve_safety_profile returns default when all levels are None
|
|
When I resolve the safety profile with no levels
|
|
Then the resolved provenance should be "global"
|
|
And the resolved profile allow_unsafe_tools should be false
|
|
And the resolved profile require_sandbox should be true
|
|
|
|
Scenario: resolve_safety_profile preserves all 8 fields from plan-level
|
|
Given a full plan safety profile with all fields customized
|
|
And a full action safety profile with defaults
|
|
When I resolve the safety profile
|
|
Then the resolved provenance should be "plan"
|
|
And the resolved profile require_sandbox should be false
|
|
And the resolved profile require_checkpoints should be false
|
|
And the resolved profile allow_unsafe_tools should be true
|
|
And the resolved profile require_human_approval should be true
|
|
And the resolved profile max_cost_per_plan should be 50.0
|
|
And the resolved profile max_retries_per_step should be 7
|
|
And the resolved profile max_total_cost should be 200.0
|
|
And the resolved profile allowed_skill_categories should be "code,test"
|
|
|
|
Scenario: resolve_safety_profile preserves all 8 fields from action-level
|
|
Given a full action safety profile with all fields customized
|
|
When I resolve the safety profile
|
|
Then the resolved provenance should be "action"
|
|
And the resolved profile require_sandbox should be false
|
|
And the resolved profile require_checkpoints should be false
|
|
And the resolved profile allow_unsafe_tools should be true
|
|
And the resolved profile require_human_approval should be true
|
|
And the resolved profile max_cost_per_plan should be 50.0
|
|
And the resolved profile max_retries_per_step should be 7
|
|
And the resolved profile max_total_cost should be 200.0
|
|
And the resolved profile allowed_skill_categories should be "code,test"
|
|
|
|
# ---- model_dump round-trip ----
|
|
|
|
Scenario: Safety profile model_dump produces valid dict
|
|
When I create a safety profile and dump it
|
|
Then the safety dump should have key "require_sandbox"
|
|
Then the safety dump should have key "require_checkpoints"
|
|
And the safety dump should have key "max_retries_per_step"
|
|
And the safety dump should have key "allow_unsafe_tools"
|
|
And the safety dump require_sandbox should be true
|
|
|
|
# ---- Action with safety profile ----
|
|
|
|
Scenario: Action accepts safety profile in from_config
|
|
When I create an action with safety profile from config
|
|
Then the action safety profile should not be none
|
|
And the action safety profile require_sandbox should be false
|
|
|
|
Scenario: Action as_cli_dict includes safety profile
|
|
When I create an action with safety profile from config
|
|
Then the action cli dict should contain safety_profile key
|
|
|
|
# ---- from_yaml edge cases ----
|
|
|
|
Scenario: Safety profile from_yaml rejects non-existent file
|
|
When I try to load a safety profile from a non-existent yaml file
|
|
Then a safety FileNotFoundError should be raised
|
|
|
|
Scenario: Safety profile from_yaml rejects non-dict content
|
|
When I try to load a safety profile from yaml with non-dict content
|
|
Then a safety ValueError should be raised
|