Files
cleveragents-core/benchmarks/security_readonly_bench.py
T
khyari hamza 2b09568cfa feat(security): enforce read-only actions
Tighten ToolRuntime._enforce_capabilities() to block ANY tool with
writes=True when plan_read_only is set, removing the not-cap.read_only
loophole that allowed certain write tools through. Tool name is now
always included in the ToolAccessDeniedError message.

Add read_only flag to ChangeSetCapture with ReadOnlyViolationError
raised when write-capable tools are wrapped on a read-only plan.

Add CLI fail-fast guards on plan execute and plan apply commands that
abort before calling the service layer if plan.read_only is True.

SkillContext.enforce_write_guard() already included tool name
correctly and required no changes.

Includes 18 Behave scenarios (90 steps), Robot integration tests,
ASV benchmarks, and docs/reference/read_only_actions.md.

ISSUES CLOSED: #322
2026-02-26 20:33:20 +00:00

165 lines
5.3 KiB
Python

"""ASV benchmarks for read-only enforcement layer throughput.
Measures the performance of:
- ToolRuntime._enforce_capabilities (allow and deny paths)
- SkillContext.enforce_write_guard (allow and deny paths)
- ChangeSetCapture.wrap_tool (allow and deny paths)
"""
from __future__ import annotations
import contextlib
import importlib
import sys
from pathlib import Path
# Ensure the local *source* tree is importable even when ASV has an
# older build of the package installed.
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
import cleveragents # noqa: E402
importlib.reload(cleveragents)
from cleveragents.domain.models.core.tool import ( # noqa: E402
Tool,
ToolCapability,
ToolSource,
ToolType,
)
from cleveragents.skills.context import ( # noqa: E402
SkillContext,
SkillExecutionError,
)
from cleveragents.tool.builtins.changeset import ( # noqa: E402
ChangeSetCapture,
ReadOnlyViolationError,
)
from cleveragents.tool.context import ToolExecutionContext # noqa: E402
from cleveragents.tool.lifecycle import ( # noqa: E402
ToolAccessDeniedError,
ToolRuntime,
)
from cleveragents.tool.runtime import ToolSpec # noqa: E402
# ---------------------------------------------------------------------------
# Fixtures shared across suites
# ---------------------------------------------------------------------------
_WRITE_TOOL = Tool(
name="bench/writer",
description="Benchmark write tool",
source=ToolSource.BUILTIN,
tool_type=ToolType.TOOL,
timeout=300,
capability=ToolCapability(writes=True, read_only=False),
)
_READ_TOOL = Tool(
name="bench/reader",
description="Benchmark read tool",
source=ToolSource.BUILTIN,
tool_type=ToolType.TOOL,
timeout=300,
capability=ToolCapability(writes=False, read_only=True),
)
_RO_CTX = ToolExecutionContext(plan_id="bench-ro", plan_read_only=True)
_RW_CTX = ToolExecutionContext(plan_id="bench-rw", plan_read_only=False)
_WRITE_SPEC = ToolSpec(
name="bench/writer",
description="Benchmark write spec",
capabilities=ToolCapability(writes=True, read_only=False),
handler=lambda inputs: {"ok": True},
)
_READ_SPEC = ToolSpec(
name="bench/reader",
description="Benchmark read spec",
capabilities=ToolCapability(writes=False, read_only=True),
handler=lambda inputs: {"ok": True},
)
# ---------------------------------------------------------------------------
# ToolRuntime._enforce_capabilities
# ---------------------------------------------------------------------------
class EnforceCapabilitiesSuite:
"""Benchmark ToolRuntime._enforce_capabilities."""
def time_allow_read_tool_on_readonly_plan(self) -> None:
"""Read-only tool on read-only plan (fast-path: no raise)."""
ToolRuntime._enforce_capabilities(_READ_TOOL, _RO_CTX)
def time_allow_write_tool_on_rw_plan(self) -> None:
"""Write tool on writable plan (fast-path: no raise)."""
ToolRuntime._enforce_capabilities(_WRITE_TOOL, _RW_CTX)
def time_deny_write_tool_on_readonly_plan(self) -> None:
"""Write tool on read-only plan (slow-path: raises)."""
with contextlib.suppress(ToolAccessDeniedError):
ToolRuntime._enforce_capabilities(_WRITE_TOOL, _RO_CTX)
# ---------------------------------------------------------------------------
# SkillContext.enforce_write_guard
# ---------------------------------------------------------------------------
class EnforceWriteGuardSuite:
"""Benchmark SkillContext.enforce_write_guard."""
def setup(self) -> None:
self.ro_ctx = SkillContext(
plan_id="bench-ro",
project_id="proj",
sandbox_path=Path("/tmp/sandbox"),
read_only=True,
)
self.rw_ctx = SkillContext(
plan_id="bench-rw",
project_id="proj",
sandbox_path=Path("/tmp/sandbox"),
read_only=False,
)
def time_allow_write_guard(self) -> None:
"""Writable context allows write (fast-path)."""
self.rw_ctx.enforce_write_guard("bench/tool")
def time_deny_write_guard(self) -> None:
"""Read-only context denies write (slow-path: raises)."""
with contextlib.suppress(SkillExecutionError):
self.ro_ctx.enforce_write_guard("bench/tool")
# ---------------------------------------------------------------------------
# ChangeSetCapture.wrap_tool
# ---------------------------------------------------------------------------
class ChangeSetCaptureSuite:
"""Benchmark ChangeSetCapture.wrap_tool."""
def setup(self) -> None:
self.ro_capture = ChangeSetCapture(plan_id="bench-ro", read_only=True)
self.rw_capture = ChangeSetCapture(plan_id="bench-rw", read_only=False)
def time_pass_through_read_spec(self) -> None:
"""Read-only spec passes through (no wrapping)."""
self.ro_capture.wrap_tool(_READ_SPEC)
def time_wrap_write_spec_on_rw(self) -> None:
"""Write spec wraps on writable capture."""
self.rw_capture.wrap_tool(_WRITE_SPEC)
def time_deny_write_spec_on_ro(self) -> None:
"""Write spec denied on read-only capture (raises)."""
with contextlib.suppress(ReadOnlyViolationError):
self.ro_capture.wrap_tool(_WRITE_SPEC)