Files
CoreRasurae 66b9a4279f
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 17s
CI / security (pull_request) Successful in 32s
CI / typecheck (pull_request) Successful in 35s
CI / unit_tests (pull_request) Successful in 1m55s
CI / docker (pull_request) Successful in 39s
CI / integration_tests (pull_request) Successful in 2m50s
CI / coverage (pull_request) Successful in 4m13s
CI / benchmark-regression (pull_request) Successful in 23m10s
CI / lint (push) Successful in 12s
CI / quality (push) Successful in 15s
CI / build (push) Successful in 15s
CI / security (push) Successful in 29s
CI / typecheck (push) Successful in 32s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 3m10s
CI / docker (push) Successful in 38s
CI / integration_tests (push) Successful in 4m1s
CI / coverage (push) Successful in 3m58s
CI / benchmark-publish (push) Successful in 13m21s
feat(security): add safety profile model and enforcement stubs
Add SafetyProfile domain model with configurable safety constraints
(checkpoint, human approval, cost limits, retry limits, skill
categories, sandbox requirement).  Integrate into Action model with
from_config/as_cli_dict support, and persist via LifecycleActionModel
safety_profile_json column.

Include resolve_safety_profile() stub that raises NotImplementedError
for local mode, signalling that real enforcement is deferred.

Add comprehensive test coverage:
- 20 BDD scenarios in features/safety_profile.feature
- 6 Robot Framework smoke tests
- 5 ASV benchmark suites (11 timing functions)

Add docs/reference/safety_profile.md reference documentation.

ISSUES CLOSED: #332
2026-03-02 17:44:18 +00:00

186 lines
6.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Safety Profile
## Overview
A **Safety Profile** defines constraints on plan execution to enforce security
and cost boundaries. It controls which skill categories are permitted, whether
sandboxes and checkpoints are required, whether unsafe tools may be invoked,
whether human approval is needed, and sets cost and retry limits.
When an Action carries both a `safety_profile` and an `automation_profile`,
the **SafetyProfile takes precedence** for overlapping safety flags
(`require_sandbox`, `require_checkpoints`, `allow_unsafe_tools`). The
AutomationProfile controls autonomy thresholds (confidence-based gating)
while the SafetyProfile enforces hard safety constraints.
## Schema
| Field | Type | Default | Description |
|-----------------------------|-----------------|---------|----------------------------------------------------------|
| `allowed_skill_categories` | `list[str]` | `[]` | Permitted skill categories (empty = all) |
| `require_sandbox` | `bool` | `True` | Require sandbox for execution |
| `require_checkpoints` | `bool` | `True` | Require checkpoints during execution |
| `require_human_approval` | `bool` | `False` | Require human approval per action step |
| `allow_unsafe_tools` | `bool` | `False` | Allow tools marked as unsafe to be invoked |
| `max_cost_per_plan` | `float \| None` | `None` | Max cost in USD per plan (None = unlimited) |
| `max_retries_per_step` | `int` | `3` | Max retry attempts per step (0100) |
| `max_total_cost` | `float \| None` | `None` | Max total cost in USD (None = unlimited) |
## Default Profile
The `DEFAULT_SAFETY_PROFILE` constant provides sensible defaults:
```python
DEFAULT_SAFETY_PROFILE = SafetyProfile(
allowed_skill_categories=[], # all categories allowed
require_sandbox=True,
require_checkpoints=True,
require_human_approval=False,
allow_unsafe_tools=False,
max_cost_per_plan=None, # no limit
max_retries_per_step=3,
max_total_cost=None, # no limit
)
```
The default profile is **immutable** (`frozen=True`). Attempting to modify
fields after construction raises a `ValidationError`.
## Cross-Field Validation
The following cross-field constraints are enforced:
- **`max_cost_per_plan <= max_total_cost`**: When both cost limits are set,
the per-plan cost must not exceed the total cost. Violating this raises a
`ValidationError`.
## Input Validation
- `allowed_skill_categories`: duplicates removed, blanks stripped. Non-list
input raises `ValueError`. Non-string items in the list raise `ValueError`.
- `max_cost_per_plan`: must be >= 0.0 if set
- `max_total_cost`: must be >= 0.0 if set
- `max_retries_per_step`: must be 0100
## Precedence over AutomationProfile
When an Action carries both a `SafetyProfile` and an `AutomationProfile`,
the overlapping safety flags are resolved as follows:
| Flag | Wins | Rationale |
|-----------------------|---------------|--------------------------------------------|
| `require_sandbox` | SafetyProfile | Hard safety constraint |
| `require_checkpoints` | SafetyProfile | Hard safety constraint |
| `allow_unsafe_tools` | SafetyProfile | Hard safety constraint |
Non-overlapping fields from each profile apply independently. The
AutomationProfile governs autonomy thresholds (e.g., confidence-based
gating for auto-execution), while the SafetyProfile enforces hard safety
boundaries.
## YAML Configuration
Safety profiles can be loaded from YAML:
```yaml
require_sandbox: false
require_checkpoints: true
require_human_approval: true
allow_unsafe_tools: false
max_cost_per_plan: 50.0
max_retries_per_step: 5
max_total_cost: 200.0
allowed_skill_categories:
- code
- test
- deploy
```
Load with:
```python
from cleveragents.domain.models.core.safety_profile import SafetyProfile
profile = SafetyProfile.from_yaml("path/to/safety.yaml")
# or from a dict:
profile = SafetyProfile.from_config(config_dict)
```
### YAML Error Handling
- **File not found**: `from_yaml()` raises `FileNotFoundError`
- **Non-dict content**: `from_yaml()` raises `ValueError` if the YAML root
is not a mapping
## Action Integration
Actions can include an optional `safety_profile` field:
```yaml
name: local/secure-deploy
description: Deploy with safety constraints
definition_of_done: Deployment verified
strategy_actor: local/default
execution_actor: local/default
safety_profile:
require_sandbox: true
require_checkpoints: true
require_human_approval: true
allow_unsafe_tools: false
max_cost_per_plan: 100.0
allowed_skill_categories:
- deploy
- test
```
## Resolution Precedence
Safety profiles are resolved at plan-use time following:
```
plan > action > project > global
```
The resolved profile is captured in a `SafetyProfileRef` with provenance:
| Provenance | Description |
|------------|---------------------------------------|
| `PLAN` | Profile set directly on the plan |
| `ACTION` | Inherited from the action template |
| `PROJECT` | Inherited from the project |
| `GLOBAL` | Global default profile |
## Stub Enforcement (Local Mode)
In the current local-mode implementation, `resolve_safety_profile()` raises
`NotImplementedError`. This signals that real enforcement is deferred to a
future server-mode release. The resolution logic is:
```python
from cleveragents.domain.models.core.safety_profile import resolve_safety_profile
try:
profile, provenance = resolve_safety_profile(
plan_profile=plan_sp,
action_profile=action_sp,
project_profile=project_sp,
global_profile=global_sp,
)
except NotImplementedError:
# Expected in local mode — enforcement not yet available
pass
```
## Persistence
Safety profiles are stored as JSON in the `safety_profile_json` column on the
`actions` table. Serialization uses `model_dump_json()` and deserialization
uses `SafetyProfile.model_validate()`.
The column is added by migration `c4_001_safety_profile_column`.
## Specification Reference
Based on `docs/specification.md` sections "Automation Profiles" and
"Guardrails".