Files
freemo 48cff5cfe0
CI / build (push) Successful in 18s
CI / lint (push) Failing after 31s
CI / helm (push) Successful in 33s
CI / typecheck (push) Successful in 50s
CI / security (push) Failing after 51s
CI / coverage (push) Has been skipped
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Failing after 1m50s
CI / docker (push) Has been skipped
CI / quality (push) Successful in 3m43s
CI / integration_tests (push) Has been cancelled
CI / e2e_tests (push) Has been cancelled
CI / benchmark-publish (push) Has been cancelled
CI / status-check (push) Has been cancelled
refactor(cli): rename plan lifecycle-list and lifecycle-apply to match specification
Renames `plan lifecycle-list` to `plan list` and `plan lifecycle-apply` to `plan apply` to align with the specification's canonical command names. Removes legacy V2 plan commands that occupied those names.

- Renamed CLI command registrations from lifecycle-list/lifecycle-apply to list/apply
- Removed legacy V2 apply and list commands (~200 lines)
- Updated apply shortcut in main.py to delegate to v3 lifecycle
- Added defensive null check for plan existence in apply command
- Updated 63+ test, doc, and benchmark files for consistency

Closes #881

Co-authored-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
Co-committed-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
2026-04-02 19:09:04 +00:00

3.2 KiB

Security: Read-Only Action Enforcement

This document describes the multi-layer read-only enforcement in CleverAgents, ensuring that plans marked read_only=True never produce side-effects.

Background

A read-only plan is created when the originating Action has read_only=True. The flag propagates through the execution layers:

Plan.read_only  ->  CLI fail-fast guard (plan execute / plan apply)
                ->  ExecuteStubActor.execute(read_only=...) -> ChangeSetCapture
                ->  ToolRuntime._enforce_capabilities (via ToolExecutionContext)
                ->  SkillContext.enforce_write_guard

Enforcement Layers

1. ToolRuntime._enforce_capabilities

Location: src/cleveragents/tool/lifecycle.py

When a tool is about to execute, _enforce_capabilities() checks:

if ctx.plan_read_only and cap.writes:
    raise ToolAccessDeniedError(
        f"Tool '{tool.name}' has writes=True but plan "
        f"'{ctx.plan_id}' is read-only"
    )

Any tool with ToolCapability.writes=True is blocked regardless of cap.read_only. The error always includes the tool name for auditability.

2. SkillContext.enforce_write_guard

Location: src/cleveragents/skills/context.py

Skills call enforce_write_guard(tool_name) before executing write operations. When read_only=True:

raise SkillExecutionError(
    SkillError(
        error_type=SkillErrorType.PERMISSION_DENIED,
        message=f"Write operation denied: tool '{tool_name}' cannot "
                f"write in read-only context (plan={self.plan_id})",
        ...
    )
)

3. ChangeSetCapture (wired via ExecuteStubActor)

Location: src/cleveragents/tool/builtins/changeset.py

ChangeSetCapture accepts a read_only keyword argument. When read_only=True, wrap_tool() raises ReadOnlyViolationError for any tool spec with writes=True:

raise ReadOnlyViolationError(
    f"Cannot wrap write-capable tool '{tool_spec.name}' "
    f"in read-only ChangeSetCapture (plan={self._plan_id})"
)

Read-only tool specs pass through unchanged.

ExecuteStubActor.execute() propagates the plan's read_only flag into ChangeSetCapture, and PlanExecutor._run_execute_with_stub() reads plan.read_only from the plan model.

4. CLI Fail-Fast Guards

Location: src/cleveragents/cli/commands/plan.py

Both plan execute and plan apply check plan.read_only before calling the lifecycle service. If the plan is read-only, the CLI prints an error and aborts without making any state transitions.

Error Types

Layer Error Class Module
ToolRuntime ToolAccessDeniedError tool.lifecycle
SkillContext SkillExecutionError skills.context
ChangeSet ReadOnlyViolationError tool.builtins.changeset
CLI typer.Abort cli.commands.plan

Testing

BDD scenarios covering all layers are in:

  • features/security_readonly.feature (18 scenarios)
  • features/steps/security_readonly_steps.py

Run with:

nox -s unit_tests