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 (from a control plane's evaluator), 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). The hook gate fails closed too, check by check: when one of its checks (the SOP rules, SSO group policy, the MCP server registry, image provenance, and so on) errors, the call is refused with GOVERNANCE_UNAVAILABLE, unless another check refuses it first with its own reason or the workspace is in shadow mode (see How a verdict is decided).
  • 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. A standalone proxy (no control-plane URL, the open-core default) applies its own daily spend cap and makes no policy call. With a control plane, the proxy calls POST /api/v1/policy/check on it (routes/evaluate.ts) — the budget and loop-governance gates below.

Tool calls are decided by the gates, with their own order of checks: locally from the policy snapshot, and with a control plane also at its hook gate endpoint, where SSO-group clearance runs on the server (see §3 below). Neither is part of 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, standalone:

Model request arrives at proxy (:4000)
              │
              ▼
     ┌────────────────┐
     │ Daily spend    │ ◀── maxDailyBudgetUsd in ~/.intutic/config.json
     │ cap (local)    │     (default $10) less today's spend on this machine
     └────────┬───────┘
              ├── over the cap ──▶ 429 OVERAGE_HARD_CAP_EXCEEDED
              │ pass
              ▼
     On to the proxy's own checks (DLP, rules, detectors); no policy pre-check

The cap refuses a request whose estimated cost exceeds what is left of the day's budget; see Budgets. With no control plane the workspace hard-cap check answers clear, so this is the only spend cap a standalone proxy applies.

With a control plane, the sequence is:

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
              ▼
     ┌────────────────┐
     │ 3. Plan daily  │ ◀── daily token and session limits
     │  limits        │     of the workspace's plan
     └────────┬───────┘
              │ 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", expiring at midnight UTC, when a workspace's daily spend exceeds daily_spend_cap_usd and the workspace has the hard cap on (enforcement_mode: "hard" and workspace_hard_cap_enabled: true in its settings)
  • The policy check reads this key — if present → deny immediately
  • Also checks loop-level budget caps: if a loop run (loop_run_id) has any status other than ACTIVE → 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 and refusing with 429 OVERAGE_HARD_CAP_EXCEEDED while it is set. This means budget blocks take effect with zero network round-trips. When a control plane is configured but the key cannot be read, the proxy refuses with 503 BUDGET_UNVERIFIABLE.

→ Source: store/valkey.rs (hard_block, proxy-side); routes/evaluate.ts in the control plane, which is not open source

Plan daily limits ​

The policy check then compares the workspace's token count and session count for the UTC day with its plan's daily limits, and denies the request once either is exceeded. A request from a session already counted today is not refused by the session limit. If these counters cannot be read, the check allows the request.


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.


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: Option<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

A custom WASM or Rego rule that reaches no verdict (deadline, budget, an error, or a result that is not a verdict) is outside this setting: it blocks the request with GOVERNANCE_UNAVAILABLE either way, because an agent can cause it by padding its input. See When a rule reaches no verdict.

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, these layers also evaluate traffic. SSL enforcement only records. The proxy-side layers run inline: DLP and tool-poisoning redaction rewrite the request before it is forwarded, and DLP can refuse it outright (see Data flow between nodes); SnipCompactor rewrites a non-streamed response:

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 detection, plus validated PII detectors for payment cards, IBANs and US Social Security numbers (email and phone opt-in)Every request (proxy-side, pre-forwarding), and responses
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 of tool-result text — collapse repetitions, truncate JSONNon-streamed responses (proxy-side, after the output DLP scan), so the saving lands in the next turn's prompt

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 — 8 patterns tuned specifically for hidden instructions in tool descriptions (among them an order to run a destructive command: a recursive delete of the filesystem root or the home directory, formatting or raw-writing a disk, DROP DATABASE, a download piped into a shell), 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 flag, set by the billing cronUntil midnight UTC
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 gate60 s
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