Files
cleveragents-core/robot/helper_safety_profile_enforcement.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

401 lines
13 KiB
Python

"""Robot Framework helper for Safety Profile Enforcement smoke tests.
Provides a CLI-style interface for Robot to invoke safety profile
resolution and enforcement operations. Exit code 0 = success, 1 = failure.
Usage:
python robot/helper_safety_profile_enforcement.py resolve-precedence
python robot/helper_safety_profile_enforcement.py resolve-default
python robot/helper_safety_profile_enforcement.py enforce-unsafe-blocked
python robot/helper_safety_profile_enforcement.py enforce-unsafe-allowed
python robot/helper_safety_profile_enforcement.py enforce-category-blocked
python robot/helper_safety_profile_enforcement.py enforce-category-allowed
python robot/helper_safety_profile_enforcement.py enforce-checkpoint
python robot/helper_safety_profile_enforcement.py enforce-sandbox-blocked
python robot/helper_safety_profile_enforcement.py enforce-sandbox-allowed
python robot/helper_safety_profile_enforcement.py enforce-human-approval
python robot/helper_safety_profile_enforcement.py enforce-cost-limit
python robot/helper_safety_profile_enforcement.py enforce-retry-limit
python robot/helper_safety_profile_enforcement.py backward-compat
python robot/helper_safety_profile_enforcement.py context-summary
"""
from __future__ import annotations
import sys
from collections.abc import Callable
from pathlib import Path
# Ensure src is importable.
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.domain.models.core.safety_profile import ( # noqa: E402
DEFAULT_SAFETY_PROFILE,
SafetyProfile,
SafetyProfileProvenance,
resolve_safety_profile,
)
from cleveragents.domain.models.core.tool import ( # noqa: E402
Tool,
ToolCapability,
ToolSource,
)
from cleveragents.tool.context import ToolExecutionContext # noqa: E402
from cleveragents.tool.lifecycle import ( # noqa: E402
ToolCheckpointRequiredError,
ToolCostLimitExceededError,
ToolDescriptor,
ToolHumanApprovalRequiredError,
ToolResult,
ToolRetryLimitExceededError,
ToolRuntime,
ToolSafetyViolationError,
ToolSandboxRequiredError,
)
# ---------------------------------------------------------------------------
# Stub tool instance
# ---------------------------------------------------------------------------
class _StubInstance:
"""Minimal ToolInstance for integration smoke tests."""
def __init__(self, name: str) -> None:
self._name = name
def discover(self) -> ToolDescriptor:
return ToolDescriptor(name=self._name, description=f"stub {self._name}")
def activate(self, ctx: ToolExecutionContext) -> None:
pass
def execute(self, params: dict, ctx: ToolExecutionContext) -> ToolResult: # type: ignore[type-arg]
return ToolResult(success=True, data={"ok": True})
def deactivate(self, ctx: ToolExecutionContext) -> None:
pass
def _make_tool(name: str, *, unsafe: bool = False, read_only: bool = False) -> Tool:
return Tool(
name=name,
description=f"Test tool {name}",
source=ToolSource.CUSTOM,
code="pass",
capability=ToolCapability(read_only=read_only, unsafe=unsafe),
)
def _make_runtime() -> ToolRuntime:
rt = ToolRuntime()
for name, unsafe, ro in [
("test/safe", False, True),
("test/unsafe", True, False),
]:
t = _make_tool(name, unsafe=unsafe, read_only=ro)
rt.register_tool(t, _StubInstance(name))
return rt
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
def _cmd_resolve_precedence() -> int:
"""Test that plan-level profile wins over all others."""
plan = SafetyProfile(allow_unsafe_tools=True, require_sandbox=False)
action = SafetyProfile(allow_unsafe_tools=False, require_sandbox=True)
resolved, prov = resolve_safety_profile(plan_profile=plan, action_profile=action)
if prov != SafetyProfileProvenance.PLAN:
print(f"resolve-precedence-fail: expected PLAN, got {prov}")
return 1
if resolved.allow_unsafe_tools is not True:
print("resolve-precedence-fail: allow_unsafe_tools should be True")
return 1
print("resolve-precedence-ok")
return 0
def _cmd_resolve_default() -> int:
"""Test that all-None returns DEFAULT_SAFETY_PROFILE."""
resolved, prov = resolve_safety_profile()
if prov != SafetyProfileProvenance.GLOBAL:
print(f"resolve-default-fail: expected GLOBAL, got {prov}")
return 1
if resolved is not DEFAULT_SAFETY_PROFILE:
print("resolve-default-fail: not DEFAULT_SAFETY_PROFILE instance")
return 1
print("resolve-default-ok")
return 0
def _cmd_enforce_unsafe_blocked() -> int:
"""Test that unsafe tool is blocked by safety profile."""
rt = _make_runtime()
profile = SafetyProfile(
allow_unsafe_tools=False, require_sandbox=False, require_checkpoints=False
)
ctx = ToolExecutionContext(plan_id="robot-001", safety_profile=profile)
try:
rt.execute("test/unsafe", {}, ctx)
print("enforce-unsafe-blocked-fail: no error raised")
return 1
except ToolSafetyViolationError:
print("enforce-unsafe-blocked-ok")
return 0
def _cmd_enforce_unsafe_allowed() -> int:
"""Test that unsafe tool runs when profile allows it."""
rt = _make_runtime()
profile = SafetyProfile(
allow_unsafe_tools=True, require_sandbox=False, require_checkpoints=False
)
ctx = ToolExecutionContext(plan_id="robot-002", safety_profile=profile)
result = rt.execute("test/unsafe", {}, ctx)
if not result.success:
print("enforce-unsafe-allowed-fail: execution not successful")
return 1
print("enforce-unsafe-allowed-ok")
return 0
def _cmd_enforce_category_blocked() -> int:
"""Test that tool in wrong skill category is blocked."""
rt = _make_runtime()
profile = SafetyProfile(
allowed_skill_categories=["code"],
require_sandbox=False,
require_checkpoints=False,
)
ctx = ToolExecutionContext(
plan_id="robot-003",
safety_profile=profile,
metadata={"tool_skill_category": "deploy"},
)
try:
rt.execute("test/safe", {}, ctx)
print("enforce-category-blocked-fail: no error raised")
return 1
except ToolSafetyViolationError:
print("enforce-category-blocked-ok")
return 0
def _cmd_enforce_category_allowed() -> int:
"""Test that tool in allowed skill category runs."""
rt = _make_runtime()
profile = SafetyProfile(
allowed_skill_categories=["code"],
require_sandbox=False,
require_checkpoints=False,
)
ctx = ToolExecutionContext(
plan_id="robot-004",
safety_profile=profile,
metadata={"tool_skill_category": "code"},
)
result = rt.execute("test/safe", {}, ctx)
if not result.success:
print("enforce-category-allowed-fail: execution not successful")
return 1
print("enforce-category-allowed-ok")
return 0
def _cmd_enforce_checkpoint() -> int:
"""Test that safety profile require_checkpoints blocks non-checkpointable tools."""
rt = _make_runtime()
profile = SafetyProfile(require_checkpoints=True, require_sandbox=False)
ctx = ToolExecutionContext(plan_id="robot-005", safety_profile=profile)
try:
rt.execute("test/safe", {}, ctx)
print("enforce-checkpoint-fail: no error raised")
return 1
except ToolCheckpointRequiredError:
print("enforce-checkpoint-ok")
return 0
def _cmd_backward_compat() -> int:
"""Test that unsafe tools run when no safety profile on context."""
rt = _make_runtime()
ctx = ToolExecutionContext(plan_id="robot-006")
result = rt.execute("test/unsafe", {}, ctx)
if not result.success:
print("backward-compat-fail: execution not successful")
return 1
print("backward-compat-ok")
return 0
def _cmd_context_summary() -> int:
"""Test that context summary includes safety profile flag."""
profile = SafetyProfile(require_sandbox=False, require_checkpoints=False)
ctx = ToolExecutionContext(plan_id="robot-007", safety_profile=profile)
summary = ctx.as_summary()
if summary.get("has_safety_profile") is not True:
print(f"context-summary-fail: {summary}")
return 1
ctx2 = ToolExecutionContext(plan_id="robot-008")
summary2 = ctx2.as_summary()
if summary2.get("has_safety_profile") is not False:
print(f"context-summary-fail: {summary2}")
return 1
print("context-summary-ok")
return 0
def _cmd_enforce_sandbox_blocked() -> int:
"""Test that writing tool is blocked when sandbox required but missing."""
rt = ToolRuntime()
tool = Tool(
name="test/writer",
description="writes",
source=ToolSource.CUSTOM,
code="pass",
capability=ToolCapability(writes=True),
)
rt.register_tool(tool, _StubInstance("test/writer"))
profile = SafetyProfile(require_sandbox=True, require_checkpoints=False)
ctx = ToolExecutionContext(
plan_id="robot-009", safety_profile=profile, sandbox_id=None
)
try:
rt.execute("test/writer", {}, ctx)
print("enforce-sandbox-blocked-fail: no error raised")
return 1
except ToolSandboxRequiredError:
print("enforce-sandbox-blocked-ok")
return 0
def _cmd_enforce_sandbox_allowed() -> int:
"""Test that writing tool runs when sandbox is provided."""
rt = ToolRuntime()
tool = Tool(
name="test/writer",
description="writes",
source=ToolSource.CUSTOM,
code="pass",
capability=ToolCapability(writes=True),
)
rt.register_tool(tool, _StubInstance("test/writer"))
profile = SafetyProfile(require_sandbox=True, require_checkpoints=False)
ctx = ToolExecutionContext(
plan_id="robot-010",
safety_profile=profile,
sandbox_id="sandbox-001",
)
result = rt.execute("test/writer", {}, ctx)
if not result.success:
print("enforce-sandbox-allowed-fail: not successful")
return 1
print("enforce-sandbox-allowed-ok")
return 0
def _cmd_enforce_human_approval() -> int:
"""Test that tools are blocked when human approval is required."""
rt = _make_runtime()
profile = SafetyProfile(
require_human_approval=True,
require_sandbox=False,
require_checkpoints=False,
)
ctx = ToolExecutionContext(plan_id="robot-011", safety_profile=profile)
try:
rt.execute("test/safe", {}, ctx)
print("enforce-human-approval-fail: no error raised")
return 1
except ToolHumanApprovalRequiredError:
print("enforce-human-approval-ok")
return 0
def _cmd_enforce_cost_limit() -> int:
"""Test that tools are blocked when cost limit is exceeded."""
rt = _make_runtime()
profile = SafetyProfile(
max_cost_per_plan=10.0,
require_sandbox=False,
require_checkpoints=False,
)
ctx = ToolExecutionContext(
plan_id="robot-012",
safety_profile=profile,
accumulated_cost=10.0,
)
try:
rt.execute("test/safe", {}, ctx)
print("enforce-cost-limit-fail: no error raised")
return 1
except ToolCostLimitExceededError:
print("enforce-cost-limit-ok")
return 0
def _cmd_enforce_retry_limit() -> int:
"""Test that tools are blocked when retry limit is exceeded."""
rt = _make_runtime()
profile = SafetyProfile(
max_retries_per_step=3,
require_sandbox=False,
require_checkpoints=False,
)
ctx = ToolExecutionContext(
plan_id="robot-013",
safety_profile=profile,
step_retry_count=4,
)
try:
rt.execute("test/safe", {}, ctx)
print("enforce-retry-limit-fail: no error raised")
return 1
except ToolRetryLimitExceededError:
print("enforce-retry-limit-ok")
return 0
# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
_COMMANDS: dict[str, Callable[[], int]] = {
"resolve-precedence": _cmd_resolve_precedence,
"resolve-default": _cmd_resolve_default,
"enforce-unsafe-blocked": _cmd_enforce_unsafe_blocked,
"enforce-unsafe-allowed": _cmd_enforce_unsafe_allowed,
"enforce-category-blocked": _cmd_enforce_category_blocked,
"enforce-category-allowed": _cmd_enforce_category_allowed,
"enforce-checkpoint": _cmd_enforce_checkpoint,
"enforce-sandbox-blocked": _cmd_enforce_sandbox_blocked,
"enforce-sandbox-allowed": _cmd_enforce_sandbox_allowed,
"enforce-human-approval": _cmd_enforce_human_approval,
"enforce-cost-limit": _cmd_enforce_cost_limit,
"enforce-retry-limit": _cmd_enforce_retry_limit,
"backward-compat": _cmd_backward_compat,
"context-summary": _cmd_context_summary,
}
def main() -> int:
"""Entry point called by Robot Framework ``Run Process``."""
if len(sys.argv) < 2:
print("Usage: helper_safety_profile_enforcement.py <command>")
return 1
command = sys.argv[1]
handler = _COMMANDS.get(command)
if handler is None:
print(f"Unknown command: {command}")
return 1
return handler()
if __name__ == "__main__":
sys.exit(main())