Files
cleveragents-core/benchmarks/safety_profile_bench.py
T
Luis Mendes 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
feat(security): add safety profile enforcement
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
2026-03-03 22:21:14 +00:00

191 lines
6.1 KiB
Python

"""ASV benchmarks for Safety Profile domain model operations.
Measures the performance of:
- SafetyProfile construction (Pydantic validation)
- SafetyProfile serialization (model_dump / model_validate)
- resolve_safety_profile precedence resolution
- SafetyProfileProvenance enum operations
"""
from __future__ import annotations
import sys
from pathlib import Path
try:
from cleveragents.domain.models.core.safety_profile import (
DEFAULT_SAFETY_PROFILE,
SafetyProfile,
SafetyProfileProvenance,
resolve_safety_profile,
)
except ModuleNotFoundError:
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from cleveragents.domain.models.core.safety_profile import (
DEFAULT_SAFETY_PROFILE,
SafetyProfile,
SafetyProfileProvenance,
resolve_safety_profile,
)
# ---------------------------------------------------------------------------
# Module-level helpers
# ---------------------------------------------------------------------------
def _make_profile(**kwargs: object) -> SafetyProfile:
"""Create a SafetyProfile with sensible defaults for benchmarks."""
return SafetyProfile(**kwargs) # type: ignore[arg-type]
_FULL_CONFIG = {
"allowed_skill_categories": ["code", "test", "deploy"],
"require_sandbox": False,
"require_checkpoints": False,
"require_human_approval": True,
"allow_unsafe_tools": True,
"max_cost_per_plan": 100.0,
"max_retries_per_step": 5,
"max_total_cost": 500.0,
}
# ---------------------------------------------------------------------------
# Construction suite
# ---------------------------------------------------------------------------
class ConstructionSuite:
"""Benchmark SafetyProfile construction."""
def time_default_construction(self) -> None:
"""Benchmark creating a default SafetyProfile."""
SafetyProfile()
def time_full_construction(self) -> None:
"""Benchmark creating a fully-populated SafetyProfile."""
SafetyProfile(
allowed_skill_categories=["code", "test", "deploy"],
require_sandbox=False,
require_checkpoints=False,
require_human_approval=True,
allow_unsafe_tools=True,
max_cost_per_plan=100.0,
max_retries_per_step=5,
max_total_cost=500.0,
)
def time_from_config(self) -> None:
"""Benchmark SafetyProfile.from_config factory."""
SafetyProfile.from_config(_FULL_CONFIG)
# ---------------------------------------------------------------------------
# Serialization suite
# ---------------------------------------------------------------------------
class SerializationSuite:
"""Benchmark SafetyProfile serialization round-trips."""
def setup(self) -> None:
"""Create profiles for serialization benchmarks."""
self.profile = _make_profile(
allowed_skill_categories=["code", "test"],
max_cost_per_plan=50.0,
max_total_cost=200.0,
)
self.dump = self.profile.model_dump()
def time_model_dump(self) -> None:
"""Benchmark model_dump serialization."""
self.profile.model_dump()
def time_model_dump_json(self) -> None:
"""Benchmark model_dump_json serialization."""
self.profile.model_dump_json()
def time_model_validate(self) -> None:
"""Benchmark model_validate deserialization."""
SafetyProfile.model_validate(self.dump)
# ---------------------------------------------------------------------------
# Resolution suite
# ---------------------------------------------------------------------------
class ResolutionSuite:
"""Benchmark resolve_safety_profile precedence resolution."""
def setup(self) -> None:
"""Create profiles for resolution benchmarks."""
self.plan = SafetyProfile(allow_unsafe_tools=True, require_sandbox=False)
self.action = SafetyProfile(allow_unsafe_tools=False)
self.project = SafetyProfile(max_cost_per_plan=100.0)
self.global_ = SafetyProfile()
def time_resolve_all_levels(self) -> None:
"""Benchmark resolution with all four levels populated."""
resolve_safety_profile(
plan_profile=self.plan,
action_profile=self.action,
project_profile=self.project,
global_profile=self.global_,
)
def time_resolve_global_only(self) -> None:
"""Benchmark resolution with only global level."""
resolve_safety_profile(global_profile=self.global_)
def time_resolve_none(self) -> None:
"""Benchmark resolution with all None (falls back to default)."""
resolve_safety_profile()
def time_resolve_action_level(self) -> None:
"""Benchmark resolution with action and lower levels."""
resolve_safety_profile(
action_profile=self.action,
project_profile=self.project,
global_profile=self.global_,
)
# ---------------------------------------------------------------------------
# Enum suite
# ---------------------------------------------------------------------------
class DefaultProfileSuite:
"""Benchmark DEFAULT_SAFETY_PROFILE operations."""
def time_default_access(self) -> None:
"""Benchmark accessing the DEFAULT_SAFETY_PROFILE constant."""
_ = DEFAULT_SAFETY_PROFILE
def time_default_model_dump(self) -> None:
"""Benchmark serializing the default profile."""
DEFAULT_SAFETY_PROFILE.model_dump()
# ---------------------------------------------------------------------------
# Enum suite
# ---------------------------------------------------------------------------
class ProvenanceEnumSuite:
"""Benchmark SafetyProfileProvenance enum operations."""
def time_enum_access(self) -> None:
"""Benchmark direct enum member access."""
_ = SafetyProfileProvenance.PLAN
def time_enum_from_value(self) -> None:
"""Benchmark enum construction from string value."""
SafetyProfileProvenance("plan")
def time_enum_iteration(self) -> None:
"""Benchmark enum iteration."""
list(SafetyProfileProvenance)