Compare commits

...

1 Commits

Author SHA1 Message Date
HAL9000 2b0bc485a4 docs(a2a): add A2A protocol, guards, and automation profile documentation (v3.5.0)
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
- 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
5 changed files with 1398 additions and 0 deletions
+11
View File
@@ -5,6 +5,17 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
### Added
- **A2A Protocol Guide** (`docs/api/a2a-protocol.md`): Comprehensive documentation for the A2A (Agent-to-Agent) protocol covering 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. Covers v3.5.0 and v3.8.0 features. Refs #9010
- **Guards Documentation** (`docs/api/guards.md`): Documentation for autonomy guardrail enforcement covering denylist, allowlist, budget caps (`max_total_cost`), tool call limits (`max_tool_calls_per_step`), write/apply approval gates, `AutonomyGuardrailService` enforcement methods, budget hierarchy (plan > session > org), and the `GuardrailAuditTrail` for immutable enforcement records. Refs #9010
- **Automation Profiles Documentation** (`docs/api/automation-profiles.md`): Documentation for automation profiles covering the eight built-in profiles (`manual`, `review`, `supervised`, `cautious`, `trusted`, `auto`, `ci`, `full-auto`), confidence threshold semantics, safety profile sub-model, profile resolution precedence (plan > action > project > global), `AutomationProfileService` Python API, YAML configuration, and CLI commands. Refs #9010
- **mkdocs.yml navigation**: Added `A2A Protocol Guide`, `Guards`, and `Automation Profiles` entries to the API Reference section.
### Fixed
- **Automation Profile Silent Fallback** (#8232): `_resolve_profile_for_plan` in
+607
View File
@@ -0,0 +1,607 @@
# A2A Protocol — Agent-to-Agent Communication
> **Version:** v3.5.0 / v3.8.0 (JSON-RPC 2.0 wire format)
> **ADRs:** [ADR-026](../adr/ADR-026-agent-client-protocol.md) · [ADR-047](../adr/ADR-047-acp-standard-adoption.md)
The **Agent-to-Agent (A2A) Protocol** is the **sole** communication boundary between all
CleverAgents clients (CLI, TUI, IDE plugin) and the Application-layer services. Every
operation — from creating a session to executing a plan — flows through A2A. No
Presentation-layer module is permitted to bypass A2A and access Domain or Infrastructure
services directly.
CleverAgents adopts the external [A2A open standard](https://a2a-protocol.org) (Linux
Foundation, Apache 2.0) built on **JSON-RPC 2.0**, with platform-specific operations
exposed via the `_cleveragents/` extension namespace.
---
## Overview
```
+------------------------------------------------------------------+
| Presentation Layer |
| CLI · TUI · IDE Plugin |
+--------------------------------+---------------------------------+
| A2A (JSON-RPC 2.0)
| stdio (local) or HTTP (server)
+--------------------------------v---------------------------------+
| A2A Boundary |
| A2aLocalFacade (local) · A2A SDK HTTP transport (server) |
+--------------------------------+---------------------------------+
|
+--------------------------------v---------------------------------+
| Application Layer |
| SessionService · PlanLifecycleService · ToolRegistry · ... |
+------------------------------------------------------------------+
```
---
## JSON-RPC 2.0 Wire Format
All A2A messages use the [JSON-RPC 2.0](https://www.jsonrpc.org/specification) envelope.
### Request
```json
{
"jsonrpc": "2.0",
"id": "01HXRCF1ABCDE12345678901AB",
"method": "_cleveragents/plan/status",
"params": {
"plan_id": "01HXRCF1ABCDE12345678901AB"
}
}
```
| Field | Type | Description |
|-------|------|-------------|
| `jsonrpc` | `"2.0"` | Always the literal string `"2.0"` |
| `id` | `string` | Request correlation ID (ULID recommended); auto-generated if omitted |
| `method` | `string` | Operation name — standard A2A or `_cleveragents/` extension |
| `params` | `object` | Operation parameters (may be empty `{}`) |
### Successful Response
```json
{
"jsonrpc": "2.0",
"id": "01HXRCF1ABCDE12345678901AB",
"result": {
"plan_id": "01HXRCF1ABCDE12345678901AB",
"phase": "execute",
"state": "running"
}
}
```
### Error Response
```json
{
"jsonrpc": "2.0",
"id": "01HXRCF1ABCDE12345678901AB",
"error": {
"code": -32001,
"message": "Plan not found",
"data": { "plan_id": "01HXRCF1ABCDE12345678901AB" }
}
}
```
`result` and `error` are **mutually exclusive** — exactly one must be present.
### Error Codes
| Code | Meaning | Domain Exception |
|------|---------|-----------------|
| `-32700` | Parse error (malformed JSON) | — |
| `-32600` | Invalid request | — |
| `-32601` | Method not found | `A2aOperationNotFoundError` |
| `-32602` | Invalid params | `ValidationError` |
| `-32603` | Internal error | Any unhandled `Exception` |
| `-32001` | Entity not found | `ResourceNotFoundError` |
| `-32002` | Authentication required | `AuthenticationError` |
| `-32003` | Authorization forbidden | `AuthorizationError` |
| `-32004` | Invalid state | `BusinessRuleViolation` |
| `-32005` | Already exists | `DuplicateEntityError` |
| `-32006` | Budget exceeded | `BudgetExceededError` |
| `-32007` | Version mismatch | `A2aVersionMismatchError` |
| `-32008` | Plan error | `PlanError` |
Application-specific codes use the range `-32001` to `-32099`.
---
## Python Models
```python
from cleveragents.a2a.models import A2aRequest, A2aResponse, A2aErrorDetail, A2aEvent
```
### `A2aRequest`
```python
class A2aRequest(BaseModel):
jsonrpc: str = "2.0" # Always "2.0"; validated
id: str = "" # Auto-generated ULID if empty
method: str # Required; must not be blank
params: dict[str, Any] = {}
```
### `A2aResponse`
```python
class A2aResponse(BaseModel):
jsonrpc: str = "2.0"
id: str # Echoes the request id
result: dict[str, Any] | None = None # Success path
error: A2aErrorDetail | None = None # Error path
# Exactly one of result/error must be set (enforced by validator)
```
### `A2aErrorDetail`
```python
class A2aErrorDetail(BaseModel):
code: int # JSON-RPC 2.0 integer error code
message: str # Human-readable description
data: dict[str, Any] = {} # Optional structured context
```
### `A2aEvent`
```python
class A2aEvent(BaseModel):
event_id: str # Auto-generated ULID
event_type: str # e.g. "TaskStatusUpdateEvent"
plan_id: str | None = None
data: dict[str, Any] = {}
timestamp: str # ISO-8601 UTC; auto-set
```
> **Breaking change (v3.7.0+):** Fields were renamed to comply with JSON-RPC 2.0.
> `a2a_version` → `jsonrpc`, `request_id` → `id`, `operation` → `method`,
> `data` (success) → `result`. See [CHANGELOG](../../CHANGELOG.md).
---
## Transport Modes
A2A is transport-agnostic. CleverAgents supports two transports with **identical
operational semantics**.
### Local Mode — JSON-RPC 2.0 over stdio
The client spawns the agent as a subprocess. JSON-RPC messages flow over
`stdin`/`stdout`. The `A2aLocalFacade` routes operations to in-process
Application-layer services. No network, no authentication.
```
CLI Process --stdin/stdout--> Agent Subprocess
|
+-- A2aLocalFacade --> ServiceFacade
```
```python
from cleveragents.a2a import A2aLocalFacade, A2aRequest
facade = A2aLocalFacade(services={
"session_service": session_svc,
"plan_lifecycle_service": plan_svc,
"tool_registry": tool_registry,
"event_queue": event_queue,
})
response = facade.dispatch(
A2aRequest(method="session.create", params={"actor_name": "openai/gpt-4o"})
)
session_id = response.result["session_id"]
```
### Server Mode — JSON-RPC 2.0 over HTTP
The client connects to the CleverAgents server via the A2A SDK HTTP transport.
Authentication uses the security schemes declared in the Agent Card.
```
CLI --HTTP--> CleverAgents A2A Server --> ServiceFacade
```
The `A2A-Version` HTTP header communicates the protocol version. The Agent Card
at `/.well-known/agent.json` advertises supported security schemes (OAuth2, API
key, HTTP bearer).
---
## A2aLocalFacade — Session Lifecycle
The `A2aLocalFacade` is the primary entry point in local mode.
### Constructor
```python
from cleveragents.a2a.facade import A2aLocalFacade
facade = A2aLocalFacade(services={
"session_service": session_service, # SessionService
"plan_lifecycle_service": plan_lifecycle_service, # PlanLifecycleService
"tool_registry": tool_registry, # ToolRegistry
"resource_registry_service": resource_registry_svc, # ResourceRegistryService
"event_queue": event_queue, # A2aEventQueue
})
```
All service keys are optional. Missing services return safe stub responses so
the facade never crashes due to incomplete wiring.
### Session Create
```python
response = facade.dispatch(A2aRequest(
method="session.create",
params={"actor_name": "openai/gpt-4o"}
))
# response.result == {"session_id": "01HXRCF1...", "status": "created"}
```
If `session_id` is already supplied in `params`, the call is idempotent and
returns the existing ID without creating a duplicate (fixes Forgejo #1141).
### Session Close
```python
response = facade.dispatch(A2aRequest(
method="session.close",
params={"session_id": "01HXRCF1..."}
))
# response.result == {"status": "closed"}
```
Session close automatically stops any active devcontainers associated with the
session (best-effort; failures are logged but never propagated).
### Plan Lifecycle via Extension Methods
```python
# Create (use) a plan
response = facade.dispatch(A2aRequest(
method="_cleveragents/plan/use",
params={"action_name": "refactor-auth", "arguments": {"target": "src/auth"}}
))
plan_id = response.result["plan_id"]
# Execute the plan
response = facade.dispatch(A2aRequest(
method="_cleveragents/plan/execute",
params={"plan_id": plan_id}
))
# Check status
response = facade.dispatch(A2aRequest(
method="_cleveragents/plan/status",
params={"plan_id": plan_id}
))
# response.result == {"plan_id": "...", "phase": "execute", "state": "running"}
# Apply the plan
response = facade.dispatch(A2aRequest(
method="_cleveragents/plan/apply",
params={"plan_id": plan_id}
))
```
---
## Extension Methods (`_cleveragents/`)
CleverAgents platform operations use the `_cleveragents/` namespace per the A2A
extensibility mechanism (declared in the Agent Card under
`urn:cleveragents:extensions:v1`).
### Plan Lifecycle
| Method | Required Params | Description |
|--------|----------------|-------------|
| `_cleveragents/plan/use` | `action_name` | Create a plan from an action template |
| `_cleveragents/plan/execute` | `plan_id` | Transition plan to Execute phase |
| `_cleveragents/plan/apply` | `plan_id` | Transition plan to Apply phase |
| `_cleveragents/plan/cancel` | `plan_id` | Cancel a running plan |
| `_cleveragents/plan/status` | `plan_id` | Query plan phase and state |
| `_cleveragents/plan/tree` | `plan_id` | Retrieve the decision tree |
| `_cleveragents/plan/explain` | `plan_id`, `decision_id` | Explain a decision |
| `_cleveragents/plan/correct` | `plan_id` | Inject a correction |
| `_cleveragents/plan/diff` | `plan_id` | Retrieve plan diff |
| `_cleveragents/plan/artifacts` | `plan_id` | List plan artifacts |
| `_cleveragents/plan/prompt` | `plan_id`, `guidance` | Inject guidance into execution |
| `_cleveragents/plan/rollback` | `plan_id` | Roll back plan changes |
| `_cleveragents/plan/list` | _(none)_ | List all plans |
### Registry
| Method | Optional Params | Description |
|--------|----------------|-------------|
| `_cleveragents/registry/tool/list` | `namespace` | List available tools |
| `_cleveragents/registry/resource/list` | `type_name` | List registered resources |
| `_cleveragents/registry/actor/list` | — | List registered actors |
| `_cleveragents/registry/skill/list` | — | List registered skills |
| `_cleveragents/registry/action/list` | — | List registered actions |
| `_cleveragents/registry/project/list` | — | List registered projects |
### Context
| Method | Description |
|--------|-------------|
| `_cleveragents/context/show` | Show assembled context |
| `_cleveragents/context/inspect` | Inspect context tiers |
| `_cleveragents/context/simulate` | Simulate context assembly |
| `_cleveragents/context/set` | Override context values |
### Health & Diagnostics
| Method | Description |
|--------|-------------|
| `_cleveragents/health/check` | Health check |
| `_cleveragents/diagnostics/run` | Run diagnostics |
### Sync & Namespace (stubs)
| Method | Description |
|--------|-------------|
| `_cleveragents/sync/pull` | Pull remote changes |
| `_cleveragents/sync/push` | Push local changes |
| `_cleveragents/sync/status` | Sync status |
| `_cleveragents/namespace/list` | List namespaces |
| `_cleveragents/namespace/show` | Show namespace detail |
| `_cleveragents/namespace/members` | List namespace members |
### Legacy Operations (Deprecated)
The following proprietary operation names are kept for backward compatibility
but are deprecated in favour of the `_cleveragents/` extension methods:
| Legacy Name | Replacement |
|-------------|-------------|
| `session.create` | `session.create` (unchanged) |
| `session.close` | `session.close` (unchanged) |
| `plan.create` | `_cleveragents/plan/use` |
| `plan.execute` | `_cleveragents/plan/execute` |
| `plan.status` | `_cleveragents/plan/status` |
| `plan.diff` | `_cleveragents/plan/diff` |
| `plan.apply` | `_cleveragents/plan/apply` |
| `registry.list_tools` | `_cleveragents/registry/tool/list` |
| `registry.list_resources` | `_cleveragents/registry/resource/list` |
| `context.get` | `_cleveragents/context/show` |
| `event.subscribe` | `_cleveragents/event/subscribe` |
---
## Event Queue — Publish / Subscribe
The `A2aEventQueue` provides in-memory pub/sub for plan and session events in
local mode. In server mode, events are delivered via Server-Sent Events (SSE).
```python
from cleveragents.a2a.events import A2aEventQueue, TASK_STATUS_UPDATE
from cleveragents.a2a.models import A2aEvent
queue = A2aEventQueue()
# Subscribe with a callback
def on_event(event: A2aEvent) -> None:
print(f"[{event.event_type}] plan={event.plan_id} data={event.data}")
sub_id = queue.subscribe_local(on_event)
# Publish an event
queue.publish(A2aEvent(
event_type=TASK_STATUS_UPDATE,
plan_id="01HXRCF1...",
data={"state": "working", "phase": "execute"},
))
# Retrieve recent events
events = queue.get_events(limit=50)
# Unsubscribe
queue.unsubscribe(sub_id)
# Shutdown
queue.close()
```
### Event Types
| Constant | Value | Emitted When |
|----------|-------|-------------|
| `TASK_STATUS_UPDATE` | `"TaskStatusUpdateEvent"` | Plan phase or state changes |
| `TASK_ARTIFACT_UPDATE` | `"TaskArtifactUpdateEvent"` | Agent produces output artifacts |
### SSE Wire Format
In server mode, events are formatted as Server-Sent Events with a JSON-RPC 2.0
notification payload:
```
event: TaskStatusUpdateEvent
id: 01HXRCF1ABCDE12345678901AB
data: {"jsonrpc":"2.0","method":"task/statusUpdate","params":{"taskId":"01HXRCF1...","state":"working"}}
```
### EventBusBridge
The `EventBusBridge` subscribes to the internal domain `EventBus` and translates
domain events into `A2aEvent` instances:
```python
from cleveragents.a2a.events import EventBusBridge
bridge = EventBusBridge(event_bus=event_bus, event_queue=queue)
bridge.start() # Subscribe to domain events
# ... plan execution ...
bridge.stop() # Unsubscribe
```
Domain events mapped to `TaskStatusUpdateEvent`:
`PLAN_CREATED`, `PLAN_PHASE_CHANGED`, `PLAN_STATE_CHANGED`, `PLAN_APPLIED`,
`PLAN_CANCELLED`, `PLAN_ERRORED`.
Domain events mapped to `TaskArtifactUpdateEvent`:
`CHECKPOINT_RESTORED`.
---
## Standard A2A Operations
In addition to the `_cleveragents/` extension methods, CleverAgents supports the
standard A2A operations defined by the external specification:
| Operation | Direction | Purpose |
|-----------|-----------|---------|
| `message/send` | Client → Server | Send a message to the agent; receive a Task |
| `message/stream` | Client → Server | Send a message with SSE streaming of task updates |
| `tasks/get` | Client → Server | Retrieve current state of a task |
| `tasks/list` | Client → Server | List tasks with optional filtering |
| `tasks/cancel` | Client → Server | Cancel a running task |
| `tasks/subscribe` | Client → Server | Subscribe to task updates via SSE |
| `pushNotificationConfig/*` | Client → Server | Manage push notification webhooks |
| `getExtendedAgentCard` | Client → Server | Fetch authenticated Agent Card |
### Task Lifecycle States
| State | Description |
|-------|-------------|
| `submitted` | Task received, not yet started |
| `working` | Task actively executing |
| `input-required` | Task paused, awaiting human input (automation profile gate) |
| `completed` | Task finished successfully |
| `failed` | Task terminated with an error |
| `canceled` | Task was explicitly cancelled |
| `rejected` | Task was rejected before starting |
### Multi-Turn Interactions
When a plan's automation profile requires human approval, the Task enters
`input-required` state. The client resumes execution by sending a
`message/send` with the task's context ID:
```json
{
"jsonrpc": "2.0",
"id": 2,
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{ "text": "Approved — proceed with the apply phase." }],
"contextId": "task_01HXRCF1..."
}
}
}
```
---
## Agent Card Discovery
In server mode, the Agent Card is served at `/.well-known/agent.json` and
declares CleverAgents capabilities:
```json
{
"name": "CleverAgents",
"description": "Autonomous software engineering agent",
"skills": ["plan-lifecycle", "registry-crud", "context-management"],
"extensions": [
{
"uri": "urn:cleveragents:extensions:v1",
"methods": ["_cleveragents/plan/*", "_cleveragents/registry/*"]
}
],
"securitySchemes": {
"bearerAuth": { "type": "http", "scheme": "bearer" }
}
}
```
An authenticated extended Agent Card with additional detail is available via
`getExtendedAgentCard`.
---
## CLI Commands
Every CLI command maps to an A2A operation. The CLI is a thin rendering layer
over A2A.
```bash
# Session management
agents a2a session create --actor openai/gpt-4o
agents a2a session list
agents a2a session close <SESSION_ID>
# Plan lifecycle
agents plan use <ACTION_NAME> [--automation-profile supervised]
agents plan execute <PLAN_ID>
agents plan status <PLAN_ID>
agents plan diff <PLAN_ID>
agents plan apply <PLAN_ID>
agents plan cancel <PLAN_ID>
agents plan tree <PLAN_ID>
agents plan correct <PLAN_ID> --guidance "Use dependency injection"
agents plan rollback <PLAN_ID>
agents plan list
# Registry
agents tool list [--namespace <NS>]
agents resource list [--type <TYPE>]
agents actor list
agents skill list
# Health
agents health check
agents diagnostics run
```
---
## Versioning
| Aspect | Mechanism |
|--------|-----------|
| JSON-RPC version | Always `"2.0"` in the `jsonrpc` field |
| A2A protocol version | `A2A-Version` HTTP header (server mode) |
| Extension version | `urn:cleveragents:extensions:v1` in Agent Card |
| Backward compatibility | Servers support current + one prior minor version |
| Breaking changes | Require major version bump of the extension namespace |
---
## Error Types
| Exception | Description |
|-----------|-------------|
| `A2aError` | Base A2A exception |
| `A2aNotAvailableError` | Operation not available in current mode |
| `A2aOperationNotFoundError` | Unknown operation name |
| `A2aVersionMismatchError` | Incompatible protocol versions |
```python
from cleveragents.a2a.errors import A2aOperationNotFoundError
try:
response = facade.dispatch(request)
except A2aOperationNotFoundError as exc:
print(f"Unknown method: {exc.operation}")
```
---
## See Also
- [`cleveragents.a2a`](a2a.md) — Python API reference for A2A classes
- [ADR-026](../adr/ADR-026-agent-client-protocol.md) — A2A protocol design rationale
- [ADR-047](../adr/ADR-047-acp-standard-adoption.md) — A2A standard adoption
- [Guards](guards.md) — Guard enforcement during plan execution
- [Automation Profiles](automation-profiles.md) — Profile resolution and autonomy control
+359
View File
@@ -0,0 +1,359 @@
# Automation Profiles
> **Version:** v3.2.0+
> **ADR:** [ADR-017](../adr/ADR-017-automation-profiles.md)
**Automation profiles** are named configuration bundles that control the
autonomy level of plan execution. They define confidence thresholds for each
phase transition and safety constraints that govern sandboxing, checkpointing,
and human approval gates.
Eight built-in profiles span the spectrum from fully manual to fully
autonomous. Custom profiles can be created and registered via YAML
configuration.
---
## What Are Automation Profiles?
Different tasks require different levels of autonomy:
- A trusted refactoring in a well-tested codebase can run **fully automatically**
- A production infrastructure change requires **human approval at every step**
Automation profiles provide a single, configurable mechanism to control this
spectrum without code changes. The profile is resolved once at `plan use` time
and **locked to the plan** — it cannot change during execution.
---
## Profile Fields
Each profile consists of **confidence thresholds** (0.01.0) and a composed
**safety profile** sub-model.
### Confidence Thresholds
A threshold of **0.0** means "always proceed automatically."
A threshold of **1.0** means "always require human approval."
Intermediate values (e.g., 0.7) mean "auto-proceed only when the system's
confidence score meets or exceeds the threshold; otherwise prompt the user."
| Field | Description |
|-------|-------------|
| `decompose_task` | Auto-enter Strategize after `plan use` |
| `create_tool` | Auto-proceed from Strategize to Execute |
| `select_tool` | Auto-proceed from Execute to Apply |
| `edit_code` | Auto-make decisions during Strategize |
| `execute_command` | Auto-make decisions during Execute |
| `create_file` | Auto-attempt to fix validation failures |
| `delete_content` | Auto-revise strategy when Execute hits constraints |
| `access_network` | Auto-revert from Apply (`constrained`) to Strategize |
| `install_dependency` | Auto-spawn and execute child plans |
| `modify_config` | Auto-retry on transient failures |
| `approve_plan` | Auto-restore from checkpoint on failure |
### Safety Profile (`safety`)
The `safety` field is a `SafetyProfile` sub-model with hard safety constraints:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `require_sandbox` | `bool` | `true` | Require sandbox isolation |
| `require_checkpoints` | `bool` | `true` | Require checkpointing |
| `allow_unsafe_tools` | `bool` | `false` | Allow tools marked as unsafe |
| `require_human_approval` | `bool` | `false` | Require approval before each action step |
| `allowed_skill_categories` | `list[str]` | `[]` | Permitted skill categories (empty = all) |
| `max_cost_per_plan` | `float \| None` | `None` | Maximum cost (USD) per plan execution |
| `max_total_cost` | `float \| None` | `None` | Maximum total cost (USD) across all plans |
| `max_retries_per_step` | `int` | `3` | Maximum retry attempts per action step |
---
## Eight Built-In Profiles
| Profile | Intent | Key Settings |
|---------|--------|-------------|
| `manual` | Human controls everything | All thresholds 1.0; sandbox + checkpoints required |
| `review` | Auto-strategize, human reviews before execute and apply | `decompose_task: 0.0`; execute/apply thresholds high |
| `supervised` | Auto through strategize/execute, human approves apply | `select_tool: 1.0`; others low |
| `cautious` | Mostly auto with conservative thresholds | Moderate thresholds (~0.7); sandbox required |
| `trusted` | Fully auto for well-tested domains | Low thresholds (~0.3); sandbox required |
| `auto` | Fully automatic with safety nets | All thresholds 0.0; sandbox + checkpoints required |
| `ci` | CI/CD pipeline automation | All thresholds 0.0; sandbox + checkpoints required |
| `full-auto` | Maximum autonomy, minimal guardrails | All thresholds 0.0; no sandbox; unsafe tools allowed |
!!! warning "full-auto profile"
The `full-auto` profile removes key safety nets (`require_sandbox: false`,
`allow_unsafe_tools: true`). Use only in fully trusted, isolated environments.
---
## Profile Resolution Precedence
The effective profile for a plan is resolved at `plan use` time using a
four-level precedence chain:
```
plan (--automation-profile flag)
> action (automation_profile field on the action template)
> project (core.automation-profile project config)
> global (core.automation-profile config or CLEVERAGENTS_AUTOMATION_PROFILE env var)
```
Once resolved, the profile is **locked to the plan**. Changing upstream
configuration after plan creation has no effect on the locked plan.
### Resolution Examples
**Example 1 — Plan flag wins:**
```
Plan flag: --automation-profile supervised ← WINS (highest precedence)
Action: automation_profile: cautious
Project: core.automation-profile: trusted
Global: CLEVERAGENTS_AUTOMATION_PROFILE=auto
```
Result: `supervised` profile is used.
**Example 2 — Action wins when no plan flag:**
```
Plan flag: (not set)
Action: automation_profile: cautious ← WINS
Project: core.automation-profile: trusted
Global: CLEVERAGENTS_AUTOMATION_PROFILE=auto
```
Result: `cautious` profile is used.
**Example 3 — Global env var as fallback:**
```
Plan flag: (not set)
Action: (not set)
Project: (not set)
Global: CLEVERAGENTS_AUTOMATION_PROFILE=auto ← WINS (lowest precedence)
```
Result: `auto` profile is used.
If no profile is set at any level, the default is `manual`.
---
## Python API
### `AutomationProfileService`
```python
from cleveragents.application.services.automation_profile_service import (
AutomationProfileService,
)
service = AutomationProfileService(
repo=profile_repository, # Optional; enables custom profiles
global_default="supervised", # Optional; overrides env var / "manual"
)
```
### Resolving the Effective Profile
```python
profile = service.resolve_profile(
plan_profile="supervised", # From --automation-profile flag
action_profile="cautious", # From action template
project_profile="trusted", # From project config
)
# Returns the AutomationProfile for "supervised"
```
### Getting a Profile by Name
```python
profile = service.get_profile("auto")
print(profile.decompose_task) # 0.0
print(profile.safety.require_sandbox) # True
```
### Listing All Profiles
```python
profiles = service.list_profiles()
for p in profiles:
print(f"{p.name}: decompose_task={p.decompose_task}")
```
### Creating a Custom Profile
```python
profile = service.create_profile({
"name": "my-custom-profile",
"decompose_task": 0.0,
"create_tool": 0.5,
"select_tool": 0.8,
"edit_code": 0.6,
"execute_command": 0.9,
"create_file": 0.4,
"delete_content": 0.9,
"access_network": 0.7,
"install_dependency": 0.5,
"modify_config": 0.8,
"approve_plan": 0.3,
"safety": {
"require_sandbox": True,
"require_checkpoints": True,
"allow_unsafe_tools": False,
"max_cost_per_plan": 10.00,
"max_retries_per_step": 3,
},
})
```
### Loading from YAML
```python
profile = AutomationProfileService.from_yaml("/path/to/profile.yaml")
```
### Evaluating a Guard
```python
from cleveragents.domain.models.core.automation_guard import GuardScope
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"Blocked: {result.reason}")
```
---
## `AutomationProfile` Domain Model
```python
from cleveragents.domain.models.core.automation_profile import (
AutomationProfile,
BUILTIN_PROFILES,
)
# Access built-in profiles directly
auto_profile = BUILTIN_PROFILES["auto"]
manual_profile = BUILTIN_PROFILES["manual"]
# Check a guard
from cleveragents.domain.models.core.automation_guard import GuardScope
result = auto_profile.check_guard(
tool_name="shell_exec",
is_write=False,
cost_so_far=1.00,
calls_so_far=3,
scope=GuardScope.PLAN,
)
```
---
## YAML Configuration
Custom profiles are defined in YAML and registered via:
```bash
agents automation-profile add --config my-profile.yaml
```
Example YAML:
```yaml
name: strict-ci
decompose_task: 0.0
create_tool: 0.0
select_tool: 0.0
edit_code: 0.0
execute_command: 0.0
create_file: 0.0
delete_content: 0.5
access_network: 0.8
install_dependency: 0.3
modify_config: 0.7
approve_plan: 0.0
safety:
require_sandbox: true
require_checkpoints: true
allow_unsafe_tools: false
require_human_approval: false
max_cost_per_plan: 2.00
max_retries_per_step: 2
```
---
## CLI Commands
```bash
# List all profiles (built-in + custom)
agents automation-profile list
# Show a specific profile
agents automation-profile show supervised
# Add a custom profile from YAML
agents automation-profile add --config my-profile.yaml
# Update a custom profile
agents automation-profile update my-profile --config updated.yaml
# Delete a custom profile
agents automation-profile delete my-profile
# Use a specific profile when creating a plan
agents plan use <ACTION_NAME> --automation-profile supervised
# Show the effective profile for a plan
agents plan profile show <PLAN_ID>
```
---
## Environment Variable
The global default profile can be set via environment variable:
```bash
export CLEVERAGENTS_AUTOMATION_PROFILE=supervised
agents plan use my-action # Uses "supervised" profile
```
Precedence: `--automation-profile` flag > action template > project config >
`CLEVERAGENTS_AUTOMATION_PROFILE` env var > `"manual"` (hardcoded default).
---
## Constraints
- Threshold values must be in the range `[0.0, 1.0]`.
- `safety.require_sandbox: false` is only valid if the sandbox strategy is `none`.
- `safety.require_checkpoints: false` is only valid if `sandbox.checkpoint.enabled` is `false`.
- Built-in profile names (`manual`, `review`, `supervised`, `cautious`, `trusted`,
`auto`, `ci`, `full-auto`) are reserved and cannot be overridden by custom profiles.
- The effective profile is resolved once at `plan use` time and locked to the plan.
To change the profile, create a new plan.
---
## See Also
- [Guards](guards.md) — Runtime guard enforcement hooks
- [A2A Protocol](a2a-protocol.md) — Protocol layer for plan execution
- [ADR-017](../adr/ADR-017-automation-profiles.md) — Automation profiles design rationale
- [ADR-041](../adr/ADR-041-safety-profile-extraction.md) — Safety profile extraction
+418
View File
@@ -0,0 +1,418 @@
# Guards — Autonomy Guardrail Enforcement
> **Version:** v3.5.0 (autonomy hardening milestone)
> **ADR:** [ADR-017](../adr/ADR-017-automation-profiles.md)
**Guards** are runtime enforcement hooks that gate tool invocations and plan
execution steps beyond the phase-transition thresholds defined by an
[Automation Profile](automation-profiles.md). 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.
```python
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:
```python
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.
```python
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).
```python
guard = AutomationGuard(
max_total_cost=5.00, # Stop after $5.00 of tool costs
)
```
When the budget is exceeded:
```python
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.
```python
guard = AutomationGuard(
max_tool_calls_per_step=10, # Max 10 tool calls per step
)
```
When the limit is reached:
```python
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.).
```python
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.
```python
guard = AutomationGuard(
require_approval_for_apply=True,
)
```
---
## `AutomationGuard` Model
```python
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`
```python
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:
```python
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).
```python
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-level**`AutonomyGuardrails.tool_budget` (checked first)
2. **Session-level**`CostBudgetService` session budget
3. **Org-level**`CostBudgetService` org budget
```python
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`:
```python
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.
```python
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:
```python
# 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
```bash
# 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:
```yaml
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](automation-profiles.md) — Profile resolution and autonomy control
- [A2A Protocol](a2a-protocol.md) — Protocol layer for plan execution
- [ADR-017](../adr/ADR-017-automation-profiles.md) — Automation profiles design
- [ADR-041](../adr/ADR-041-safety-profile-extraction.md) — Safety profile extraction
+3
View File
@@ -15,6 +15,9 @@ nav:
- Overview: api/index.md
- Core Utilities: api/core.md
- A2A Protocol: api/a2a.md
- A2A Protocol Guide: api/a2a-protocol.md
- Guards: api/guards.md
- Automation Profiles: api/automation-profiles.md
- Actor System: api/actor.md
- Skills: api/skills.md
- Tools: api/tool.md