- 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
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:
- Plan-level —
AutonomyGuardrails.tool_budget(checked first) - Session-level —
CostBudgetServicesession budget - Org-level —
CostBudgetServiceorg 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
- Automation Profiles — Profile resolution and autonomy control
- A2A Protocol — Protocol layer for plan execution
- ADR-017 — Automation profiles design
- ADR-041 — Safety profile extraction