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, REASK, or KILL. It operates on the hot path between the proxy and the LLM provider, so every millisecond matters.

Design goals:

  • Evaluation is in-process with no model call. Cost is measured per payload size in packages/proxy/benches — there is no single figure, because it is dominated by request size.
  • Fail-closed by default for the proxy's connectivity to the control plane — if the model-request policy check can't complete, block the request (see Proxy-side fail mode). This is a different guarantee from what happens inside a completed check: the hook gate's own per-check evaluation (DLP, SSO group policy, image provenance, and so on) fails open on an internal error — each check is independently wrapped so one broken detector degrades to "allowed," not "denied." Do not conflate the two.
  • 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

Two different paths evaluate a request, and it is worth keeping them apart.

Model requests go through the proxy, which calls POST /api/v1/policy/check on the control plane (routes/evaluate.ts) — the budget and loop-governance gates below.

Tool calls are evaluated at the hook gate, a separate endpoint with its own order of checks — this is also where PCAS SSO-group privilege resolution runs (see §3 below), not the model-request path. Loop detection is different again: it is an anomaly detector running in-process in the proxy, not a control-plane call.

The proxy-side model-request sequence:

Model request arrives at proxy (:4000)


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

     ┌────────────────┐
     │ 2. Loop budget │ ◀── loop_run status must be ACTIVE
     │  governance    │     (LoopGovernanceService)
     └────────┬───────┘
              │ pass

          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

Cost: A single Valkey GET. No model call, no Postgres round trip.

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); routes/evaluate.ts in the control plane, which is not open source


2. Loop breaker

Detects when an agent is stuck calling the same tool over and over.

Algorithm: consecutive-run counting over the session's tool sequence

  1. Walk the tool sequence, counting the current run of identical consecutive calls
  2. A run of 5 or more raises a finding, with confidence scaled by how far past the threshold the run has gone
  3. An intervening different tool resets the run — a repeated tool that is making progress between calls does not trip it
rust
// packages/proxy/src/plugins/anomaly/detectors.rs
const REPETITION_THRESHOLD: usize = 5;

The verdict is REASK, not KILL. Five-in-a-row is a real signal, but the number five is a chosen threshold with no measured false-positive rate behind it, so it does not qualify to block under the promotion rule. A genuinely stuck agent is told it has repeated itself and can change approach; one doing repetitive but productive work says so and continues.

Graceful degradation: the detector reads the sequence already in the request context, so it has no cache dependency to degrade.


3. PCAS policy resolution

The most complex gate — resolves effective permissions for the user+agent pair by walking the organization policy hierarchy. This runs at the hook gate as part of tool-call evaluation (hookEvents.ts's SSO group policy check calls pcasService.resolveSsoGroupPrivilege) — it is not part of the model-request /policy/check path described above, which never calls into PCAS.

Resolution cascade:

StepBackendLatencyWhat happens on failure
1Valkey cachein-memory lookupContinue to step 2
2Postgres CTE resolutionsingle query, on cache miss onlyContinue 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 is a single Valkey GET; on a miss it is one Postgres query.

→ Source: pcasService.ts in the control plane, which is not open source


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
SSL enforcementScheduling, structural and logical checks against the session's SOP graphEvery gated tool call — in shadow: findings are recorded to detector_findings and the call proceeds
SSL compliance reportingReports which SSL graph steps a session followedOn demand, POST /api/v1/sessions/:id/ssl-audit
DLP ScannerRegex-based secret/PII detection in promptsEvery request (proxy-side, pre-forwarding)
Tool-poisoning redactionStrips hidden/adversarial instructions from third-party MCP tool descriptions before the agent ever sees them ([REDACTED_TOOL_POISON]) — the same call still makes sense to the agent, it just loses the injected instructionEvery request, unconditionally (proxy-side, pre-forwarding)
SnipCompactorToken compression — collapse repetitions, truncate JSONEvery request (proxy-side, pre-forwarding)

The DLP scanner, tool-poisoning redaction, and SnipCompactor run in the proxy (Rust, on the developer's machine) — they never hit the control plane. SSL enforcement runs in the control plane, at the hook gate.

Measured, not assumed: the detector behind tool-poisoning redaction was run against 10,753 real tool and parameter descriptions (14 BFCL v3 splits, 2,711 tool + 8,042 parameter descriptions) and produced zero false positives. It is deliberately narrow — 7 patterns tuned specifically for hidden instructions in tool descriptions, disjoint from the conversational jailbreak patterns the request-body scan looks for — so a legitimate tool with an unusual description doesn't get flagged for describing itself. Source: packages/proxy/src/tool_poison.rs, corpus at packages/proxy/tests/corpus/tooldesc/tooldesc.jsonl.

SSL enforcement records; it does not block

It ships shadowed on purpose. The promotion rule requires advisory telemetry from real traffic showing a false-positive rate in the 0.1–1% band before a control that can stop a tool call is allowed to. SSL enforcement had never executed at all until it was wired here, so it has no such measurement yet. Findings are visible through GET /api/v1/findings and can be adjudicated; the rate is reported by GET /api/v1/findings/stats, grouped so shadowed findings are counted separately.


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:pcas:sso_group:{workspace_id}Cached SSO group policy, read by the gate5 min
intutic:loop:{loop_run_id}Loop governance state7 days

Continuous self-verification

The circuit breaker's own controls are checked, not just trusted:

  • Guard-liveness probes re-run every guard against a violating and a benign context every 15 minutes, so a control that stopped firing is caught by the platform, not discovered in a postmortem. Results are queryable at GET /intutic/probes. → Source: probes.rs
  • Silent-gate detection flags a gate that has gone quiet — the absence of expected activity, not a failed check — as its own finding, run hourly alongside the rest of the scheduled governance sweep. A gate that silently stops gating is an enforcement outage, and this is what catches it without waiting for someone to notice nothing was blocked.

The circuit breaker for AI agents