#!/usr/bin/env python3 """Helper script for security_readonly Robot Framework smoke tests. Each sub-command exercises one enforcement layer and prints a sentinel on success so the .robot file can assert via ``Should Contain``. """ from __future__ import annotations import sys from pathlib import Path # Ensure local 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.tool import ( # noqa: E402 Tool, ToolCapability, ToolSource, ToolType, ) from cleveragents.skills.context import SkillContext, SkillExecutionError # noqa: E402 from cleveragents.tool.builtins.changeset import ( # noqa: E402 ChangeSetCapture, ReadOnlyViolationError, ) from cleveragents.tool.context import ToolExecutionContext # noqa: E402 from cleveragents.tool.lifecycle import ToolAccessDeniedError, ToolRuntime # noqa: E402 from cleveragents.tool.runtime import ToolSpec # noqa: E402 def _make_tool(name: str, writes: bool, read_only: bool) -> Tool: return Tool( name=name, description=f"Test {name}", source=ToolSource.BUILTIN, tool_type=ToolType.TOOL, timeout=300, capability=ToolCapability(read_only=read_only, writes=writes), ) def _make_spec(name: str, writes: bool, read_only: bool) -> ToolSpec: return ToolSpec( name=name, description=f"Test spec {name}", capabilities=ToolCapability(read_only=read_only, writes=writes), handler=lambda inputs: {"ok": True}, ) def runtime_blocks_write() -> None: tool = _make_tool("builtin/file-write", writes=True, read_only=False) ctx = ToolExecutionContext(plan_id="plan-ro", plan_read_only=True) try: ToolRuntime._enforce_capabilities(tool, ctx) print("FAIL: expected ToolAccessDeniedError") sys.exit(1) except ToolAccessDeniedError: print("runtime-blocks-write-ok") def runtime_allows_readonly() -> None: tool = _make_tool("builtin/search", writes=False, read_only=True) ctx = ToolExecutionContext(plan_id="plan-ro", plan_read_only=True) ToolRuntime._enforce_capabilities(tool, ctx) print("runtime-allows-readonly-ok") def skillctx_blocks_write() -> None: sctx = SkillContext( plan_id="plan-ro", project_id="proj", sandbox_path=Path("/tmp/sandbox"), read_only=True, ) try: sctx.enforce_write_guard("skill/writer") print("FAIL: expected SkillExecutionError") sys.exit(1) except SkillExecutionError: print("skillctx-blocks-write-ok") def changeset_rejects_write() -> None: cap = ChangeSetCapture(plan_id="plan-ro", read_only=True) spec = _make_spec("test/writer", writes=True, read_only=False) try: cap.wrap_tool(spec) print("FAIL: expected ReadOnlyViolationError") sys.exit(1) except ReadOnlyViolationError: print("changeset-rejects-write-ok") def changeset_passes_readonly() -> None: cap = ChangeSetCapture(plan_id="plan-ro", read_only=True) spec = _make_spec("test/reader", writes=False, read_only=True) result = cap.wrap_tool(spec) assert result.name == "test/reader" print("changeset-passes-readonly-ok") def error_contains_toolname() -> None: tool = _make_tool("git/push", writes=True, read_only=False) ctx = ToolExecutionContext(plan_id="plan-ro", plan_read_only=True) try: ToolRuntime._enforce_capabilities(tool, ctx) print("FAIL: expected ToolAccessDeniedError") sys.exit(1) except ToolAccessDeniedError as exc: msg = str(exc) assert "git/push" in msg, f"Tool name missing from error: {msg}" print("error-contains-toolname-ok") _COMMANDS = { "runtime-blocks-write": runtime_blocks_write, "runtime-allows-readonly": runtime_allows_readonly, "skillctx-blocks-write": skillctx_blocks_write, "changeset-rejects-write": changeset_rejects_write, "changeset-passes-readonly": changeset_passes_readonly, "error-contains-toolname": error_contains_toolname, } if __name__ == "__main__": if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS: print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>") sys.exit(1) _COMMANDS[sys.argv[1]]()