10 KiB
adr_number, title, status_history, tier, authors, superseded_by, related_adrs, acceptance
| adr_number | title | status_history | tier | authors | superseded_by | related_adrs | acceptance | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 41 | Safety Profile Extraction |
|
3 |
|
null |
|
|
Context
Issue #332 introduced a standalone SafetyProfile domain model to capture hard safety constraints (sandbox requirements, checkpoint enforcement, unsafe-tool gating, skill category restrictions, cost/retry limits, and human-approval flags). However, the original specification placed three of these fields (require_sandbox, require_checkpoints, allow_unsafe_tools) directly on the AutomationProfile model alongside its 11 confidence thresholds.
Implementing SafetyProfile as a separate, parallel model created overlapping authority: both AutomationProfile and SafetyProfile defined the same three boolean safety fields, with no spec-defined resolution when their values conflicted. The documentation attempted to resolve this by stating "SafetyProfile takes precedence for overlapping safety flags," but this precedence rule existed only in the new docs, not in the specification.
This dual-model overlap was identified during implementation review (see #332 comment).
Decision Drivers
- Three safety booleans (
require_sandbox,require_checkpoints,allow_unsafe_tools) existed on bothAutomationProfileand standaloneSafetyProfile, creating overlapping authority with no spec-defined conflict resolution - Safety constraints (binary invariants like sandbox enforcement) are fundamentally different from autonomy thresholds (confidence-gated behavior) and should be cleanly separated
SafetyProfilemust be usable standalone on Actions for lightweight safety-only configurations without requiring a full automation profile- New safety-specific fields (cost limits, retry caps, skill category restrictions, human-approval flags) need a dedicated home that does not bloat
AutomationProfile - Resolution precedence (plan > action > project > global) must apply to safety constraints identically to automation profiles
Decision
CleverAgents extracts all safety constraints into a SafetyProfile sub-model that is composed within AutomationProfile via a safety field. The three previously top-level safety booleans (require_sandbox, require_checkpoints, allow_unsafe_tools) move from AutomationProfile's top level into the composed SafetyProfile, which also adds new safety-specific fields (require_human_approval, allowed_skill_categories, max_cost_per_plan, max_retries_per_step, max_total_cost).
SafetyProfile may also be attached directly to an Action (via safety_profile) when only safety constraints — without full autonomy thresholds — are needed.
Design
SafetyProfile Model
SafetyProfile is a frozen Pydantic BaseModel with the following fields:
| Field | Type | Default | Description |
|---|---|---|---|
require_sandbox |
bool |
True |
Whether sandbox isolation is required for execution |
require_checkpoints |
bool |
True |
Whether checkpoints are required before writes |
allow_unsafe_tools |
bool |
False |
Whether tools marked as unsafe may be invoked |
require_human_approval |
bool |
False |
Whether human approval is required before each action step |
allowed_skill_categories |
list[str] |
[] |
Skill categories permitted for execution (empty = all allowed) |
max_cost_per_plan |
float | None |
None |
Maximum cost in USD per plan execution |
max_total_cost |
float | None |
None |
Maximum total cost in USD across all plans |
max_retries_per_step |
int |
3 |
Maximum retry attempts per action step (range: 0–100) |
Cross-field validation enforces max_cost_per_plan <= max_total_cost when both are set.
Composition in AutomationProfile
AutomationProfile gains a safety: SafetyProfile field. The three previously top-level booleans (require_sandbox, require_checkpoints, allow_unsafe_tools) are removed from AutomationProfile's top level and accessed exclusively via the safety sub-model:
# Before (flat, overlapping):
profile.require_sandbox # on AutomationProfile
safety.require_sandbox # on separate SafetyProfile — conflict
# After (composed, single source of truth):
profile.safety.require_sandbox # SafetyProfile is the sole authority
All eight built-in profiles compose their safety constraints via this nested structure.
Standalone Use on Action
The Action model may carry a safety_profile: SafetyProfile | None field for cases where safety constraints are needed without full autonomy thresholds. When a plan is created from such an action, the safety profile is available for enforcement independently of the automation profile.
Resolution Precedence
Safety profile resolution follows the same precedence as automation profiles: plan > action > project > global. The SafetyProfileRef captures provenance (which level the profile was resolved from) via the SafetyProfileProvenance enum (PLAN, ACTION, PROJECT, GLOBAL).
Relationship to Automation Guards
SafetyProfile.max_total_cost sets a plan-level budget cap (broad scope), while AutomationGuard.max_total_cost sets a per-invocation budget cap (narrow scope). These operate at different granularities and both may be active simultaneously — the tighter constraint takes precedence at any given point.
Constraints
SafetyProfileis frozen (immutable) once constructed.max_cost_per_planmust not exceedmax_total_costwhen both are set.max_retries_per_stepmust be in the range [0, 100].allowed_skill_categoriesentries are trimmed, deduplicated, and must be non-empty strings.- All eight built-in automation profiles must compose a
SafetyProfilevia thesafetyfield. - The three safety booleans must not appear as top-level fields on
AutomationProfile— they exist only on the composedSafetyProfile.
Consequences
Positive
- Single source of truth: Safety constraints live in one place (
SafetyProfile), eliminating the dual-authority problem betweenAutomationProfileand a separateSafetyProfilemodel. - Standalone usability:
SafetyProfilecan be used on its own (e.g., onAction.safety_profile) without requiring a full automation profile, enabling lightweight safety-only configurations. - Clean separation of concerns: Autonomy thresholds (confidence-gated behavior) and safety constraints (binary invariants) are clearly separated within the profile structure.
- Extensibility: New safety constraints can be added to
SafetyProfilewithout modifyingAutomationProfile's threshold fields.
Negative
- Breaking change: Code that previously accessed
profile.require_sandboxmust now useprofile.safety.require_sandbox. All built-in profiles, tests, and serialization must be updated. - YAML schema change: Custom profile YAML files must nest safety fields under a
safety:key. Existing flat-format profiles require migration. - Slightly deeper nesting: Accessing safety fields requires one additional level of attribute access.
Risks
- Migration burden: Existing custom profiles in user configurations will break until updated to the nested format. A migration guide or backward-compatibility shim may be needed.
- Serialization impact: Persistence mappings (e.g.,
safety_profile_jsoncolumn onLifecycleActionModel) must serialize the composed structure correctly.
Alternatives Considered
Inheritance (AutomationProfile extends SafetyProfile) — This would give AutomationProfile all SafetyProfile fields via inheritance. Rejected because it conflates the identity of the two models (an AutomationProfile IS-A SafetyProfile) and would add the new cost/retry/category fields to AutomationProfile's top level, bloating it further. Composition ("has-a") is cleaner than inheritance ("is-a") here.
Shared mixin base class — Extract the 3 overlapping booleans into a SafetyConstraintsMixin that both models inherit from. Rejected because it still results in two separate models with the same fields, just sourced from a common parent — the dual-authority problem remains.
Keep flat (no extraction) — Leave the safety booleans on AutomationProfile and add the new fields there too. Rejected because it makes AutomationProfile a grab-bag of unrelated concerns and prevents standalone safety-only use on Actions.
Compliance
- Composition tests: Tests verify that
AutomationProfile.safetyis aSafetyProfileinstance with correct defaults. - Built-in profile tests: Tests verify all eight built-in profiles correctly compose their safety constraints via the
safetyfield. - Standalone tests: Tests verify that
SafetyProfilecan be created, validated, and serialized independently ofAutomationProfile. - Cross-field validation tests: Tests verify that
max_cost_per_plan > max_total_costraisesValueError. - BDD scenarios: Behave features exercise safety profile parsing, constraint validation, YAML loading, and plan-level resolution.
- Persistence tests: Tests verify correct JSON serialization/deserialization of the composed
safetyfield in the database.