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 verdictIf 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 exceedsdaily_spend_cap_usd - The policy check reads this key — if present →
KILLimmediately - Also checks loop-level budget caps: if a loop run (
loop_run_id) has statusKILLED→ deny
Cost: A single Valkey GET. No model call, no Postgres round trip.
// 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
- Walk the tool sequence, counting the current run of identical consecutive calls
- A run of 5 or more raises a finding, with confidence scaled by how far past the threshold the run has gone
- An intervening different tool resets the run — a repeated tool that is making progress between calls does not trip it
// 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:
| Step | Backend | Latency | What happens on failure |
|---|---|---|---|
| 1 | Valkey cache | in-memory lookup | Continue to step 2 |
| 2 | Postgres CTE resolution | single query, on cache miss only | Continue to step 3 |
| 3 | Synthetic empty set | 0ms | Return fallbackMode: true → forces HIJACK |
// 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:
// 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
}| Setting | Behavior |
|---|---|
fail_closed: true (default) | If the policy check times out or fails → block the request |
fail_closed: false | If the policy check times out or fails → allow the request (fail-open) |
timeout_ms: 3000 | Maximum 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):
| Layer | What it does | Runs on |
|---|---|---|
| SSL enforcement | Scheduling, structural and logical checks against the session's SOP graph | Every gated tool call — in shadow: findings are recorded to detector_findings and the call proceeds |
| SSL compliance reporting | Reports which SSL graph steps a session followed | On demand, POST /api/v1/sessions/:id/ssl-audit |
| DLP Scanner | Regex-based secret/PII detection in prompts | Every request (proxy-side, pre-forwarding) |
| Tool-poisoning redaction | Strips 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 instruction | Every request, unconditionally (proxy-side, pre-forwarding) |
| SnipCompactor | Token compression — collapse repetitions, truncate JSON | Every 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 pattern | Purpose | TTL |
|---|---|---|
v2:budget:hard_block:{workspace_id} | Budget cap exceeded flag | Set by billing cron |
v2:budget:{workspace_id}:monthly_limit | Monthly spend limit | Persistent |
v2:budget:{workspace_id}:daily_limit | Daily spend limit | Persistent |
v2:pcas:sso_group:{workspace_id} | Cached SSO group policy, read by the gate | 5 min |
intutic:loop:{loop_run_id} | Loop governance state | 7 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.
Related
- Enforcement Actions — BYPASS/ENHANCE/HIJACK/REASK/KILL verdicts
- Harnesses — How the proxy and sync daemon connect
- Standard Operating Procedures — SOP definitions and policy evaluation rules
- Custom Filters (WASM) — Custom tool-call filtering and policy hooks