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
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
401 lines
13 KiB
Python
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())
|