Skip to content

Circuit Breaker Open-Core

The circuit breaker is the decision engine that evaluates every AI agent tool call and returns an enforcement action — BYPASS, ENHANCE, HIJACK, or KILL. It operates on the hot path between the proxy and the LLM provider, so every millisecond matters.

Design goals:

  • Typical evaluation in < 50ms (real benchmarks in T12)
  • Fail-closed by default — if the check can't complete, block the request
  • Graceful degradation — if a backend is unavailable, fall back to the next tier
  • Zero single points of failure — Valkey cache and Postgres each provide a degradation layer

Hot path architecture

The circuit breaker runs on every pre-request policy check. The proxy calls POST /api/v1/policy/check on the control plane, which evaluates three gates in sequence:

Tool call arrives at proxy (:4000)


     ┌────────────────┐
     │ 1. Budget Gate │ ◀── Valkey: v2:budget:hard_block:{wk_id}
     │   (< 1ms)      │     + loop governance kill check
     └────────┬───────┘
              │ pass

     ┌────────────────┐
     │ 2. Loop Breaker│ ◀── Valkey: v2:loop:{session_id}
     │   (< 2ms)      │     sliding window of prompt hashes
     └────────┬───────┘
              │ pass

      ┌────────────────┐
      │ 3. PCAS Policy │ ◀── Valkey cache (< 1ms on hit)
      │   Resolution   │     Postgres query (< 15ms on miss)
      └────────┬───────┘


          Final verdict

If any gate returns a deny/kill, evaluation short-circuits immediately — subsequent gates are skipped.


1. Budget gate

The fastest check — a single Valkey key lookup.

How it works:

  • The billing cron job sets v2:budget:hard_block:{workspace_id} = "1" when a workspace's daily spend exceeds daily_spend_cap_usd
  • The policy check reads this key — if present → KILL immediately
  • Also checks loop-level budget caps: if a loop run (loop_run_id) has status KILLED → deny

Latency: Typically < 1ms (single Valkey GET)

typescript
// services/control-plane/src/routes/evaluate.ts
const budgetBlock = await valkey.get(budgetHardBlockKey(workspace_id))
if (budgetBlock) {
  return c.json({ action: 'deny', reason: 'Workspace budget cap exceeded' })
}

The proxy also does a local budget check before even calling the control plane — checking v2:budget:hard_block:{workspace_id} directly from its own Valkey connection. This means budget blocks take effect with zero network round-trips.

→ Source: metering.rs (proxy-side), evaluate.ts (control-plane-side)


2. Loop breaker

Detects when an agent is stuck repeating the same prompt in a loop.

Algorithm: Sliding-window hash deduplication

  1. Normalize the prompt: trim → lowercase → collapse whitespace
  2. SHA-256 hash, truncated to 16 hex characters
  3. Check the hash against a sliding window of the last 20 prompts stored in Valkey
  4. If the hash appears ≥ 5 times (configurable threshold) → it's a loop → KILL
  5. Append the new hash and write back with a 1-hour TTL
typescript
// services/control-plane/src/services/loopBreakerService.ts

const WINDOW_SIZE = 20      // hashes in the sliding window
const DEFAULT_THRESHOLD = 5  // repetitions to trigger loop detection
const LOOP_STATE_TTL = 3_600 // 1 hour (matches session inactivity)

Graceful degradation: If Valkey is unavailable, returns { isLoop: false } — the loop breaker fails open so it doesn't block legitimate requests when the cache is down.

Latency: Typically < 2ms (Valkey GET + JSON parse + SET)

→ Source: loopBreakerService.ts


3. PCAS policy resolution

The most complex gate — resolves effective permissions for the user+agent pair by walking the organization policy hierarchy.

Resolution cascade:

StepBackendLatencyWhat happens on failure
1Valkey cache< 1msContinue to step 2
2Postgres CTE resolution< 15msContinue to step 3
3Synthetic empty set0msReturn fallbackMode: true → forces HIJACK
typescript
// services/control-plane/src/services/pcasService.ts

// 1. Valkey cache check
const cached = await valkey.get(pcasCacheKey(workspaceId, userId))
if (cached) return { ...JSON.parse(cached), fallbackMode: false }

// 2. Postgres graph CTE resolution
const permissions = await graphProvider.resolveEffectivePermissions(
  userId, agentId, '*'
)

// 3. Cache the result (5 min TTL)
await valkey.set(pcasCacheKey(workspaceId, userId), ..., 'EX', PCAS_CACHE_TTL)

// 4. If database fails → synthetic empty set
return { allowedTools: [], deniedTools: [], budgetRemaining: 0, fallbackMode: true }

Fallback mode: When Postgres is unavailable, the service returns fallbackMode: true with an empty permission set. The circuit breaker can then escalate to HIJACK — restricting the agent to safe operations rather than blocking entirely.

Cache TTL: 5 minutes (PCAS_CACHE_TTL). On a warm cache, this gate completes in < 1ms.

→ Source: pcasService.ts


Proxy-side fail mode

The proxy has its own circuit breaker behavior, configured via PolicyConfig:

rust
// packages/proxy/src/config.rs
pub struct PolicyConfig {
    pub control_plane_url: String,
    pub fail_closed: bool,    // default: true
    pub timeout_ms: u64,      // default: 3,000ms
}
SettingBehavior
fail_closed: true (default)If the policy check times out or fails → block the request
fail_closed: falseIf the policy check times out or fails → allow the request (fail-open)
timeout_ms: 3000Maximum time to wait for the control plane policy check response

Fail-closed is the safe default

In production, always use fail_closed: true. Fail-open mode should only be used during initial setup or development when the control plane is not yet deployed.


Additional evaluation layers

Beyond the three hot-path gates, the circuit breaker can invoke additional evaluation layers asynchronously (they don't block the request):

LayerWhat it doesRuns on
SOP Hook ExecutorV8-sandboxed hook scripts — allow/block/modify/warnPRE_TOOL, POST_TOOL, PRE_RESPONSE, POST_RESPONSE
SSL EnforcementThree-layer enforcement (scheduling → structural → logical)Tool calls matching active SSL graphs
DLP ScannerRegex-based secret/PII detection in promptsEvery request (proxy-side, pre-forwarding)
SnipCompactorToken compression — collapse repetitions, truncate JSONEvery request (proxy-side, pre-forwarding)

The DLP scanner and SnipCompactor run in the proxy (Rust, on the developer's machine) — they never hit the control plane. SOP hooks and SSL enforcement run in the control plane when triggered.


Valkey key patterns

All circuit breaker state lives in Valkey for fast access:

Key patternPurposeTTL
v2:budget:hard_block:{workspace_id}Budget cap exceeded flagSet by billing cron
v2:budget:{workspace_id}:monthly_limitMonthly spend limitPersistent
v2:budget:{workspace_id}:daily_limitDaily spend limitPersistent
v2:loop:{session_id}Sliding window of prompt hashes1 hour
v2:pcas:{workspace_id}:{user_id}Cached permission set5 min
intutic:loop:{loop_run_id}Loop governance state7 days

Source code references

FileWhat it implements
evaluate.tsPOST /api/v1/policy/check — the hot-path endpoint
loopBreakerService.tsSliding-window loop detection
pcasService.tsPCAS permission resolution cascade
metering.rsProxy-side budget gate and virtual key validation
config.rsPolicyConfig — fail-closed, timeout settings
sopHookExecutor.tsV8-sandboxed hook execution
sslEnforcementService.tsThree-layer SSL enforcement

The active firewall for AI agents