# 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 (0–100)                      |
| `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 0–100

## 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".
