Files
cleveragents-core/docs/api/guards.md
T
HAL9000 2b0bc485a4
CI / lint (pull_request) Successful in 30s
CI / typecheck (pull_request) Successful in 55s
CI / quality (pull_request) Successful in 52s
CI / security (pull_request) Successful in 1m5s
CI / build (pull_request) Successful in 23s
CI / helm (pull_request) Successful in 34s
CI / push-validation (pull_request) Successful in 20s
CI / e2e_tests (pull_request) Successful in 4m54s
CI / integration_tests (pull_request) Successful in 5m34s
CI / unit_tests (pull_request) Successful in 6m34s
CI / docker (pull_request) Successful in 16s
CI / coverage (pull_request) Successful in 13m26s
CI / status-check (pull_request) Successful in 1s
docs(a2a): add A2A protocol, guards, and automation profile documentation (v3.5.0)
- Add docs/api/a2a-protocol.md with JSON-RPC 2.0 wire format, facade session
  lifecycle, event queue publish/subscribe, stdio and HTTP transport modes,
  extension methods (_cleveragents/ namespace), standard A2A operations,
  Agent Card discovery, and CLI commands
- Add docs/api/guards.md with guard enforcement documentation covering denylist,
  allowlist, budget caps, tool call limits, write/apply approval gates,
  AutonomyGuardrailService, budget hierarchy, and GuardrailAuditTrail
- Add docs/api/automation-profiles.md with profile resolution precedence
  (plan > action > project > global), eight built-in profiles, confidence
  threshold semantics, safety profile sub-model, Python API, and CLI commands
- Update mkdocs.yml navigation to include new API docs
- Update CHANGELOG.md with documentation entries

Refs: #9010
2026-04-14 05:45:47 +00:00

12 KiB

Guards — Autonomy Guardrail Enforcement

Version: v3.5.0 (autonomy hardening milestone) ADR: ADR-017

Guards are runtime enforcement hooks that gate tool invocations and plan execution steps beyond the phase-transition thresholds defined by an Automation Profile. They provide a second layer of safety — even when a profile permits automatic execution, guards can block individual tool calls that exceed budget, call count, or denylist constraints.

Guards are evaluated by the AutonomyGuardrailService and the AutomationGuard model. Every enforcement decision is recorded in an immutable GuardrailAuditTrail persisted to plan metadata.


Guard Types

1. Denylist

The tool denylist prevents specific tools from being called automatically. Any tool on the denylist always requires human approval, regardless of the automation profile's confidence thresholds.

from cleveragents.domain.models.core.automation_guard import AutomationGuard

guard = AutomationGuard(
    tool_denylist=["shell_exec", "rm_file", "network_request"],
)

When a denylisted tool is invoked, the guard returns:

GuardResult(
    allowed=False,
    reason="Tool 'shell_exec' is on the denylist. "
           "Remediation: remove it from the denylist or request human approval.",
    requires_approval=True,
)

2. Allowlist

The tool allowlist restricts automatic execution to a specific set of tools. Any tool not on the allowlist requires human approval.

guard = AutomationGuard(
    tool_allowlist=["read_file", "search_code", "list_directory"],
)

3. Budget Cap (max_total_cost)

The budget cap halts automatic execution when the cumulative cost of tool invocations exceeds a threshold (in USD).

guard = AutomationGuard(
    max_total_cost=5.00,  # Stop after $5.00 of tool costs
)

When the budget is exceeded:

GuardResult(
    allowed=False,
    reason="Budget cap exceeded: $5.23 > $5.00. "
           "Remediation: raise max_total_cost, lower cost, or request human approval.",
    requires_approval=True,
)

4. Tool Call Limit (max_tool_calls_per_step)

The tool call limit caps the number of tool invocations per execution step. This prevents runaway loops and unbounded tool usage.

guard = AutomationGuard(
    max_tool_calls_per_step=10,  # Max 10 tool calls per step
)

When the limit is reached:

GuardResult(
    allowed=False,
    reason="Tool call limit reached: 10/10. "
           "Remediation: increase max_tool_calls_per_step, reduce tool usage, "
           "or request human approval.",
    requires_approval=True,
)

5. Write Approval

The write approval flag requires human approval for any tool invocation that performs a write operation (file modification, database write, etc.).

guard = AutomationGuard(
    require_approval_for_writes=True,
)

6. Apply Approval

The apply approval flag requires human approval before the plan transitions to the Apply phase.

guard = AutomationGuard(
    require_approval_for_apply=True,
)

AutomationGuard Model

from cleveragents.domain.models.core.automation_guard import AutomationGuard, GuardResult

class AutomationGuard(BaseModel):
    max_tool_calls_per_step: int | None = None
    max_total_cost: float | None = None
    tool_allowlist: list[str] | None = None
    tool_denylist: list[str] | None = None
    require_approval_for_writes: bool = False
    require_approval_for_apply: bool = False

None values mean "no limit" / "no restriction".

GuardResult

class GuardResult(BaseModel):
    allowed: bool
    reason: str | None = None
    requires_approval: bool = False

Autonomy Guardrails (AutonomyGuardrails)

The AutonomyGuardrails model extends the guard framework with runtime execution constraints tracked per plan:

from cleveragents.domain.models.core.autonomy_guardrails import (
    AutonomyGuardrails,
    ActorLimits,
)

guardrails = AutonomyGuardrails(
    max_steps=100,                    # Max execution steps
    tool_budget=10.00,                # Max $10.00 in tool costs
    max_wall_clock_seconds=3600.0,    # Max 1 hour wall-clock time
    actor_limits=ActorLimits(
        max_tool_calls_per_invocation=20,  # Max 20 tool calls per actor invocation
        max_retries_per_failure=3,         # Max 3 retries per tool failure
    ),
    required_confirmations=["delete_database", "deploy_to_production"],
)

Guardrail Fields

Field Type Default Description
max_steps int | None None Maximum execution steps; None = unlimited
tool_budget float | None None Maximum cumulative tool cost (USD); None = unlimited
max_wall_clock_seconds float | None None Maximum wall-clock time in seconds; None = unlimited
actor_limits ActorLimits ActorLimits() Per-actor runtime limits
required_confirmations list[str] [] Operations requiring human confirmation
step_count int 0 Current step counter (tracked at runtime)
budget_spent float 0.0 Cumulative cost spent (tracked at runtime)
start_time str | None None ISO-8601 execution start time

ActorLimits

Field Type Default Description
max_tool_calls_per_invocation int | None None Max tool calls per actor invocation
max_retries_per_failure int | None None Max retries per tool failure

AutonomyGuardrailService

The AutonomyGuardrailService is the Application-layer service for enforcing guardrails during plan execution. It is thread-safe (all state mutations are protected by a reentrant lock).

from cleveragents.application.services.autonomy_guardrail_service import (
    AutonomyGuardrailService,
)
from cleveragents.domain.models.core.autonomy_guardrails import AutonomyGuardrails

service = AutonomyGuardrailService()

# Configure guardrails for a plan
service.configure_guardrails(
    plan_id="01HXRCF1...",
    guardrails=AutonomyGuardrails(
        max_steps=50,
        tool_budget=5.00,
    ),
)

Enforcement Methods

Method Returns Description
check_step_limit(plan_id, current_step) bool True if step is within limit
check_tool_budget(plan_id, tool_cost) bool True if cost is within budget; records cost if allowed
check_wall_clock(plan_id) bool True if within time limit
check_actor_tool_calls(plan_id, current_calls) bool True if within per-invocation call limit
check_retries_per_failure(plan_id, current_retries) bool True if within retry limit
check_confirmation_required(plan_id, operation) bool True if operation needs human confirmation

Budget Hierarchy

The service supports a multi-level budget hierarchy:

  1. Plan-levelAutonomyGuardrails.tool_budget (checked first)
  2. Session-levelCostBudgetService session budget
  3. Org-levelCostBudgetService org budget
from cleveragents.application.services.cost_budget_service import CostBudgetService

budget_service = CostBudgetService(...)
guardrail_service = AutonomyGuardrailService(cost_budget_service=budget_service)

# Associate plan with session for hierarchy checks
guardrail_service.associate_plan_with_session(
    plan_id="01HXRCF1...",
    session_id="session-abc",
)

# Check full budget hierarchy
result = guardrail_service.check_budget_hierarchy(
    plan_id="01HXRCF1...",
    tool_cost=0.05,
)
if not result.allowed:
    print(f"Budget exceeded at {result.exceeded_level}: {result.reason}")

Guard Evaluation via AutomationProfileService

Guards can also be evaluated through the AutomationProfileService, which resolves the active automation profile and delegates to AutomationProfile.check_guard:

from cleveragents.application.services.automation_profile_service import (
    AutomationProfileService,
)
from cleveragents.domain.models.core.automation_guard import GuardScope

service = AutomationProfileService()

result = service.evaluate_guard(
    profile_name="supervised",
    tool_name="shell_exec",
    context={
        "is_write": True,
        "cost_so_far": 2.50,
        "calls_so_far": 5,
        "scope": GuardScope.PLAN,
    },
)

if not result.allowed:
    print(f"Guard blocked: {result.reason}")
    if result.requires_approval:
        # Prompt user for approval
        pass

Audit Trail

Every guardrail enforcement decision is recorded in a GuardrailAuditTrail persisted to plan metadata. The trail is bounded (default 10,000 entries) with oldest-first eviction.

from cleveragents.domain.models.core.autonomy_guardrails import (
    GuardrailAuditEntry,
    GuardrailAuditTrail,
    GuardrailEventType,
    GuardrailResult,
)

# Retrieve the audit trail for a plan
trail = service.get_audit_trail(plan_id="01HXRCF1...")
print(f"Allowed: {trail.allowed_count}, Denied: {trail.denied_count}")

for entry in trail.entries[-10:]:  # Last 10 entries
    print(f"[{entry.timestamp}] {entry.guard_name}: {entry.result.value}{entry.reason}")

Audit Entry Fields

Field Type Description
timestamp str ISO-8601 UTC timestamp
event_type GuardrailEventType Type of enforcement event
guard_name str Name of the guard evaluated
result GuardrailResult "allowed" or "denied"
reason str | None Human-readable decision reason
context dict Additional context (cost, step count, etc.)

Event Types

Event Type Description
step_allowed / step_blocked Step limit check
budget_allowed / budget_blocked Budget cap check
time_allowed / time_blocked Wall-clock limit check
actor_limit_allowed / actor_limit_blocked Per-actor tool call limit
retry_allowed / retry_blocked Retry limit check
confirmation_granted / confirmation_required Human confirmation gate

Persisting Guardrail State

Guardrail state can be serialized to and restored from plan metadata:

# Serialize to metadata
metadata = service.get_audit_trail_as_metadata(plan_id="01HXRCF1...")
# metadata contains "guardrail_audit_trail" and "autonomy_guardrails" keys

# Restore from metadata
service.load_from_metadata(plan_id="01HXRCF1...", metadata=metadata)

CLI Commands

# View guard configuration for a plan
agents plan guard show <PLAN_ID>

# View guardrail audit trail
agents plan guard audit <PLAN_ID>

# Set plan-level budget cap
agents plan guard set <PLAN_ID> --max-cost 10.00

# Set tool call limit
agents plan guard set <PLAN_ID> --max-tool-calls 20

# Add tool to denylist
agents plan guard denylist add <PLAN_ID> shell_exec

# Remove tool from denylist
agents plan guard denylist remove <PLAN_ID> shell_exec

Configuration Example

Guards are typically configured as part of an automation profile's safety settings. The following YAML shows a custom profile with guards:

name: strict-review
decompose_task: 0.0
create_tool: 0.8
select_tool: 1.0
edit_code: 0.9
execute_command: 1.0
safety:
  require_sandbox: true
  require_checkpoints: true
  allow_unsafe_tools: false
  require_human_approval: false
  max_cost_per_plan: 5.00
  max_retries_per_step: 2
guard:
  max_tool_calls_per_step: 15
  max_total_cost: 5.00
  tool_denylist:
    - shell_exec
    - network_request
  require_approval_for_writes: true
  require_approval_for_apply: true

See Also