# 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
