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
164 lines
6.3 KiB
Markdown
164 lines
6.3 KiB
Markdown
# Safety Profiles
|
|
|
|
Safety profiles control hard safety constraints for plan execution. Unlike
|
|
autonomy thresholds (which are confidence-dependent), safety constraints are
|
|
binary invariants enforced regardless of the system's confidence level.
|
|
|
|
## Overview
|
|
|
|
A `SafetyProfile` is a composed sub-model of `AutomationProfile` that groups
|
|
all hard safety constraints. It can also be attached directly to an `Action`
|
|
when only safety constraints are needed without full autonomy thresholds.
|
|
|
|
## Fields
|
|
|
|
| Field | Type | Default | Description |
|
|
|-------|------|---------|-------------|
|
|
| `require_sandbox` | `bool` | `true` | Require sandbox isolation for execution |
|
|
| `require_checkpoints` | `bool` | `true` | Require checkpointing before writes |
|
|
| `allow_unsafe_tools` | `bool` | `false` | Allow tools marked `unsafe` |
|
|
| `require_human_approval` | `bool` | `false` | Require human approval per action step |
|
|
| `allowed_skill_categories` | `list[str]` | `[]` | Skill categories allowed (empty = all) |
|
|
| `max_cost_per_plan` | `float \| None` | `None` | Max cost in USD per plan |
|
|
| `max_retries_per_step` | `int` | `3` | Max retries per action step (0-100) |
|
|
| `max_total_cost` | `float \| None` | `None` | Max total cost across all plans |
|
|
|
|
## Resolution Precedence
|
|
|
|
Safety profiles are resolved at `plan use` time using this precedence
|
|
(highest to lowest):
|
|
|
|
1. **Plan-level** -- set via `--automation-profile` on `agents plan use`
|
|
2. **Action-level** -- set on the action via `--automation-profile`
|
|
3. **Project-level** -- set via `agents config set core.automation-profile`
|
|
4. **Global-level** -- set via `agents config set core.automation-profile`
|
|
|
|
Once resolved, the profile is **locked to the plan** -- subsequent changes
|
|
to project or global profiles do not affect running plans.
|
|
|
|
When all levels are `None`, the `DEFAULT_SAFETY_PROFILE` is returned with
|
|
`GLOBAL` provenance.
|
|
|
|
```python
|
|
from cleveragents.domain.models.core.safety_profile import (
|
|
SafetyProfile,
|
|
resolve_safety_profile,
|
|
)
|
|
|
|
plan_safety = SafetyProfile(allow_unsafe_tools=True, require_sandbox=False)
|
|
resolved, provenance = resolve_safety_profile(plan_profile=plan_safety)
|
|
# provenance == SafetyProfileProvenance.PLAN
|
|
```
|
|
|
|
## Enforcement
|
|
|
|
Safety profile constraints are enforced at tool activation and execution
|
|
time in `ToolRuntime._enforce_capabilities()`. The checks are evaluated
|
|
in order; the first failing check raises the corresponding error.
|
|
|
|
### 1. Read-Only Plans
|
|
|
|
Read-only plan enforcement (`plan_read_only`) is independent of the safety
|
|
profile and always applies: tools with `writes=True` are blocked with
|
|
`ToolAccessDeniedError`.
|
|
|
|
### 2. Checkpoint Requirements
|
|
|
|
When `require_checkpoints=True` (from either the context flag or the safety
|
|
profile), tools without `capability.checkpointable=True` are blocked with
|
|
a `ToolCheckpointRequiredError`.
|
|
|
|
### 3. Unsafe Tool Gating
|
|
|
|
Tools with `capability.unsafe=True` are blocked unless the safety profile
|
|
has `allow_unsafe_tools=True`. When blocked, a `ToolSafetyViolationError`
|
|
is raised.
|
|
|
|
### 4. Skill Category Allow-List
|
|
|
|
When `allowed_skill_categories` is non-empty, only tools belonging to a
|
|
listed category may execute. The tool's skill category is carried in
|
|
`ToolExecutionContext.metadata["tool_skill_category"]`. An empty list means
|
|
all categories are allowed. If the metadata key is missing when the
|
|
allow-list is non-empty, a `ToolSafetyViolationError` is raised with a
|
|
clear message indicating the missing metadata.
|
|
|
|
### 5. Sandbox Requirement
|
|
|
|
When `require_sandbox=True`, tools with `capability.writes=True` require a
|
|
`sandbox_id` to be set on the `ToolExecutionContext`. If no sandbox is
|
|
available, a `ToolSandboxRequiredError` is raised. Read-only tools are not
|
|
affected by this check since they do not modify resources.
|
|
|
|
### 6. Human Approval
|
|
|
|
When `require_human_approval=True`, all tool executions require that
|
|
`ToolExecutionContext.metadata["human_approved"]` is set to `True`. If
|
|
approval has not been recorded, a `ToolHumanApprovalRequiredError` is
|
|
raised. The higher-level orchestrator is responsible for obtaining approval
|
|
and setting this metadata before tool execution.
|
|
|
|
### 7. Cost Limits
|
|
|
|
When `max_cost_per_plan` is set, tools are blocked with a
|
|
`ToolCostLimitExceededError` if `ToolExecutionContext.accumulated_cost`
|
|
meets or exceeds the limit. Similarly, `max_total_cost` is checked against
|
|
`ToolExecutionContext.total_accumulated_cost`. The caller is responsible
|
|
for updating these cost fields after each tool execution.
|
|
|
|
### 8. Retry Limit
|
|
|
|
When `max_retries_per_step` is set, tools are blocked with a
|
|
`ToolRetryLimitExceededError` if `ToolExecutionContext.step_retry_count`
|
|
exceeds the limit. The caller is responsible for incrementing the retry
|
|
count for each retry attempt.
|
|
|
|
## Error Hierarchy
|
|
|
|
```
|
|
ToolRuntimeError
|
|
|-- ToolAccessDeniedError (read-only plan violation)
|
|
|-- ToolCheckpointRequiredError (checkpoint requirement violation)
|
|
|-- ToolSafetyViolationError (unsafe tool or category violation)
|
|
|-- ToolSandboxRequiredError (sandbox requirement violation)
|
|
|-- ToolHumanApprovalRequiredError (human approval not granted)
|
|
|-- ToolCostLimitExceededError (cost limit exceeded)
|
|
|-- ToolRetryLimitExceededError (retry limit exceeded)
|
|
|-- ToolNotActivatedError
|
|
|-- ToolActivationError
|
|
|-- ToolExecutionError
|
|
+-- ToolDeactivationError
|
|
```
|
|
|
|
## Backward Compatibility
|
|
|
|
When no `SafetyProfile` is attached to the `ToolExecutionContext` (i.e.,
|
|
`safety_profile=None`), the safety-profile-specific checks (unsafe tool
|
|
gating, skill category, sandbox requirement, human approval, cost limits,
|
|
and retry limits) are skipped. Only the pre-existing `plan_read_only` and
|
|
`require_checkpoints` context flags apply.
|
|
|
|
## Default Profile
|
|
|
|
The `DEFAULT_SAFETY_PROFILE` constant provides sensible defaults matching
|
|
the specification:
|
|
|
|
```python
|
|
DEFAULT_SAFETY_PROFILE = SafetyProfile(
|
|
allowed_skill_categories=[],
|
|
require_sandbox=True,
|
|
require_checkpoints=True,
|
|
require_human_approval=False,
|
|
allow_unsafe_tools=False,
|
|
max_cost_per_plan=None,
|
|
max_retries_per_step=3,
|
|
max_total_cost=None,
|
|
)
|
|
```
|
|
|
|
## Related
|
|
|
|
- [ADR-041: Safety Profile Extraction](../adr/ADR-041-safety-profile-extraction.md)
|
|
- [Specification: Automation Profiles](../specification.md) -- Profile Precedence section
|
|
- [Specification: Tool Capability Metadata](../specification.md) -- `unsafe` field
|