SP/1.0: Deterministic, Reproducible Verdicts for AI-Agent Decisions
SHACKLE is a runtime circuit breaker protocol for autonomous AI agents, featuring 9 mathematical invariants, Ed25519-signed audit logs, and a Redis-backed distributed state engine for deterministic and verifiable decisions.
Uh oh!
There was an error while loading. Please reload this page.
Notifications You must be signed in to change notification settings
Fork 0
Star 10
Copy path
More file actions
More file actions
Latest commit
History
History
History
748 lines (580 loc) · 26.5 KB
Copy path
Raw
Copy raw file
Download raw file
Outline
SHACKLE Protocol Specification — SP/1.0
Runtime Circuit Breaker for Autonomous AI Agents
Version: 1.0.0
Status: Published
Date: 2026-06-25
Authors: Dante Bullock, Sovereign Logic
License: Creative Commons Attribution 4.0 International (CC-BY 4.0)
Reference Implementation: https://github.com/Fame510/SHACKLE
First Public Commit: 2026-06-17 23:12 UTC
Implementations of this specification are subject to the SHACKLE license terms. The reference implementation is dual-licensed: AGPLv3 (open source) and Commercial (proprietary). Contact: [email protected]
Abstract
SHACKLE is a runtime circuit breaker for autonomous AI agents. It answers one question:
Should this agent be allowed to execute this tool with these parameters at this moment?
The protocol defines a deterministic, verifiable decision function backed by 9 mathematical invariants, a language-agnostic message schema, Ed25519-signed append-only audit logging, and a Redis-backed distributed state engine. It operates as a sidecar daemon with gRPC/Unix socket transport, or as an in-process library for single-agent deployments.
SHACKLE is the first open-source runtime circuit breaker for AI agents with cryptographic audit chain-of-custody. This specification is the definitive reference for the SP/1.0 protocol.
- Introduction
1.1 The Problem
Autonomous AI agents execute tools — web search, file I/O, API calls, code execution — with no runtime oversight. The framework's recursion limit or token cap is the only guardrail. When an agent enters a retry loop (same tool, same error, burning tokens each time), there is no mechanism to detect, intercept, and stop it before the wallet is empty.
This is not hypothetical. Production deployments have documented:
Agent infinite loops consuming $6,000+ in API costs before the recursion limit fired
Duplicate tool calls repeating 50+ times with no variation
Spawned child processes hanging indefinitely while consuming tokens
The industry consensus — independently reached by multiple teams in June 2026 — is that generation authority is not release authority. The model generates candidates. A separate mediation layer must authorize execution.
SHACKLE is that mediation layer.
1.2 Design Principles
Principle Meaning
Deterministic core decide(state, call) → Verdict is a pure function. Same inputs always produce same outputs.
Daemon as authority The SHACKLE daemon is the sole source of truth for time, state, and verdicts. Agents are untrusted.
Append-only audit Every decision is Ed25519-signed and written to an immutable audit log. Chain-of-custody is cryptographically verifiable.
Mathematically verified 9 invariant properties hold under all inputs, proven by property-based testing (Hypothesis, 500+ examples each).
Graceful degradation Agents function in local/library mode without a daemon. Distributed state is an upgrade path.
Fail-closed Network failure, daemon crash, or timeout → DENY. No execution without explicit authorization.
1.3 Scope
This specification covers:
The decision function and its 9 mathematical invariants (§3)
Message schemas and semantics (§4)
State model (§5)
Transport bindings (§6)
Audit and security (§7)
Compliance framework (§8)
This specification does NOT cover:
Daemon persistence layer (implementation detail)
HITL console UI (presentation concern)
Pricing or commercial terms
- Architecture
2.1 Deployment Models
MODEL A — Library Mode (In-Process) ┌─────────────────────────┐ │ Agent Process │ │ ┌───────────────────┐ │ │ │ @Guard decorator │ │ │ │ Local state only │ │ │ └───────────────────┘ │ └─────────────────────────┘
MODEL B — Sidecar Daemon (Production) ┌─────────────────┐ Unix/gRPC ┌──────────────────────────┐ │ Agent Process │ ◄────────────────► │ SHACKLE Daemon │ │ ┌───────────┐ │ pre_exec │ ┌────────────────────┐ │ │ │ Thin │ │ post_exec │ │ Policy Engine │ │ │ │ Client │ │ register │ │ - Budgets │ │ │ │ Shim │ │ heartbeat │ │ - Counters │ │ │ └───────────┘ │ │ │ - Circuit Breakers │ │ └─────────────────┘ │ └────────────────────┘ │ │ ┌────────────────────┐ │ │ │ Audit Log │ │ │ │ Ed25519-signed │ │ │ │ Append-only │ │ │ │ Chain-linked │ │ │ └────────────────────┘ │ └──────────────────────────┘
MODEL C — Distributed (Enterprise) ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Agent A │ │ Agent B │ │ Agent C │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────────────┬─────────────┘ │ gRPC/TLS ┌────────┴────────┐ │ SHACKLE │ │ Daemon Cluster │ │ Redis (state) │ │ Postgres (logs)│ └─────────────────┘
2.2 Protocol Layers
┌──────────────────────────────────┐ │ Policy Language (future) │ ← DSL for guard rules ├──────────────────────────────────┤ │ Decision Function │ ← decide(state, call) → Verdict ├──────────────────────────────────┤ │ Message Protocol │ ← This specification ├──────────────────────────────────┤ │ Transport (Unix/gRPC/WS) │ ← Binding layer └──────────────────────────────────┘
- The Decision Function
3.1 Core Function
The decision function is the heart of SHACKLE. It is a pure function — no I/O, no side effects, no allocations in the hot path. It is human-auditable in under 10 minutes. It is under 200 lines of logic.
function decide( state: SessionState, call: ToolCall, config: GuardConfig, rng_float: float ) → Verdict
3.2 Decision Algorithm — 8 Stacked Layers
Layer 1: Circuit Breaker IF state.circuit_tripped: → DENY(CIRCUIT_OPEN)
Layer 2: Nonce Validation (Anti-Replay) IF call.nonce IN state.seen_nonces: → DENY(POLICY_VIOLATION)
Layer 3: Budget Guard IF config.budget_usd > 0: IF state.budget_remaining_usd state.budget_remaining: IF hitl_mode IN (ON_DENY, ALWAYS): → HITL("cost exceeds remaining budget") → DENY(BUDGET_EXHAUSTED)
Layer 4: Repeat Call Guard IF config.max_repeat_calls > 0: IF is_repeat(call, state.last_call): limit = max_repeat_calls IF error_amplification AND has_error_signal(call): limit = max(1, limit - 1) IF repeat_count >= limit: → DENY(MAX_REPEAT_EXCEEDED)
Layer 5: Time Window Guard IF config.window_max_calls > 0: IF window_count >= window_max_calls: → DENY(WINDOW_EXCEEDED)
Layer 6: Global Call Limit IF config.max_total_calls > 0 AND total_calls >= max_total_calls: → DENY(GLOBAL_LIMIT)
Layer 7: Probabilistic Denial (Adversarial Hardening) IF probabilistic_deny AND budget_ratio remaining
MAX_REPEAT_EXCEEDED Same tool + same params repeated too many times
WINDOW_EXCEEDED Too many calls in the current time window
GLOBAL_LIMIT Session-wide call limit reached
POLICY_VIOLATION Duplicate nonce (replay attack)
AUTH_FAILED Authentication failure
3.6 Error Signal Amplification
SHACKLE detects error signals in tool parameters WITHOUT regex (no ReDoS attack surface). When error_amplification is enabled, the repeat call threshold is reduced by 1 if the parameters contain known error signals:
Error signals: 401, unauthorized, 403, forbidden, 500, 502, 503, 504, timeout, connection refused, connection reset, permission denied, rate limit, quota exceeded, invalid api key, token expired, model not found, resource exhausted, deadline exceeded
This catches the "loop of death" — agent hits 401, retries, gets 401, retries — without waiting for the full repeat threshold.
3.7 Probabilistic Denial (Adversarial Hardening)
When probabilistic_deny is enabled and the agent is below 20% budget, a random factor is introduced:
probability = deny_jitter_ratio × (1.0 − budget_ratio) IF rng metadata = 7; }
message RegisterResponse { string session_id = 1; string daemon_version = 2; string negotiated_protocol = 3; GuardConfig active_config = 4; int64 daemon_time_ns = 5; }
4.3 Pre-Execution Check
message PreExecRequest { string session_id = 1; uint64 call_number = 2; // Monotonically increasing string tool_name = 3; bytes tool_params_hash = 4; // SHA-256 of canonical JSON params double estimated_cost_usd = 5; string parent_guard_id = 6; // For nested guard trees uint64 nonce = 7; // Anti-replay map tags = 8; }
message PreExecResponse { string session_id = 1; uint64 call_number = 2; Verdict verdict = 3; DenyReason deny_reason = 4; string human_readable_reason = 5; double budget_remaining_usd = 6; int32 repeat_count = 7; int64 daemon_time_ns = 8; bool probabilistic_deny = 9; }
4.4 Post-Execution Notification
Fire-and-forget. No response expected.
message PostExecNotification { string session_id = 1; uint64 call_number = 2; double actual_cost_usd = 3; bool success = 4; string error_message = 5; int64 duration_ms = 6; uint64 tokens_in = 7; uint64 tokens_out = 8; string model_used = 9; }
4.5 Heartbeat
Agents SHOULD send heartbeats every 30 seconds. 3 consecutive missed heartbeats → session marked STALE.
message Heartbeat { string session_id = 1; uint64 last_call_number = 2; double local_budget_remaining = 3; // For drift detection }
message HeartbeatAck { string session_id = 1; double daemon_budget_remaining = 2; // Authoritative view bool drift_detected = 3; int64 daemon_time_ns = 4; }
4.6 gRPC Service Definition
service ShackleDaemon { rpc Register(RegisterRequest) returns (RegisterResponse); rpc PreExec(PreExecRequest) returns (PreExecResponse); rpc PostExec(PostExecNotification) returns (google.protobuf.Empty); rpc Heartbeat(Heartbeat) returns (HeartbeatAck); rpc GetSessionState(GetSessionStateRequest) returns (SessionState); }
- State Model
5.1 Session State
message SessionState { string session_id = 1; string agent_id = 2; string organization_id = 3; SessionStatus status = 4; // ACTIVE | PAUSED | TERMINATED | STALE
// Budget double budget_initial_usd = 10; double budget_remaining_usd = 11; double budget_spent_usd = 12;
// Counters uint64 total_calls = 20; map repeat_counts = 21; // tool_name → consecutive identical calls map window_counts = 22; // tool_name → calls in current window
// Circuit bool circuit_tripped = 30; string circuit_trip_reason = 31; int64 circuit_tripped_at_ns = 32;
// Time int64 window_start_ns = 40; uint32 window_duration_s = 41; uint32 window_max_calls = 42;
// Last known string last_tool_name = 50; bytes last_tool_params_hash = 51; int64 last_activity_ns = 52;
// Metadata map metadata = 60; }
enum SessionStatus { ACTIVE = 0; PAUSED = 1; // HITL in progress TERMINATED = 2; STALE = 3; // Heartbeat timeout }
5.2 Guard Configuration
message GuardConfig { // Budget double budget_usd = 1; // 0 = disabled BudgetScope budget_scope = 2; // PER_SESSION | PER_AGENT | PER_ORG
// Repeat calls uint32 max_repeat_calls = 10; // 0 = disabled bool error_amplification = 11; // Lower threshold on error signals
// Timeout uint32 timeout_seconds = 20; // Wall-clock timeout. 0 = disabled
// Time window uint32 window_duration_s = 30; uint32 window_max_calls = 31;
// Global limits uint32 max_total_calls = 40; // 0 = disabled
// Adversarial hardening bool probabilistic_deny = 50; double deny_jitter_ratio = 51; // 0.0–1.0
// HITL HitlMode hitl_mode = 60; // NEVER | ON_DENY | ON_THRESHOLD | ALWAYS double hitl_budget_threshold = 61; // 0.0–1.0
// Hierarchy string parent_guard_id = 70; // For nested guard trees }
enum BudgetScope { PER_SESSION = 0; PER_AGENT = 1; PER_ORGANIZATION = 2; }
enum HitlMode { HITL_NEVER = 0; HITL_ON_DENY = 1; HITL_ON_BUDGET_THRESHOLD = 2; HITL_ALWAYS = 3; }
5.3 State Transitions
State is NEVER mutated by the decision function. The daemon applies state changes AFTER the verdict is returned:
After ALLOW: Increment counters, record nonce, update repeat/window counts
After DENY: Trip circuit breaker (session-wide block)
After HITL: Set session status to PAUSED, await human verdict
After PostExec: Update budget (budget_spent += actual_cost)
- Transport Bindings
6.1 Unix Domain Socke
[truncated for AI cost control]