checkrd

Error handling

How the Checkrd SDK signals a denied request, and the three ways to handle it without crashing your agent.

Error handling

When a policy denies a request, the Checkrd SDK raises an exception (Python) or throws (JavaScript) from inside the HTTP transport, before the bytes leave your machine. That's a fail-closed contract by design: if your code forgets to handle the deny, you don't silently send a request you said you'd block.

The trade-off is that a 1-line install plus an active enforce policy can crash the agent process on the first deny unless your code is prepared. This page explains what's raised, how to catch it, and the three ways to soften the contract when you want to.

What gets raised

checkrd.exceptions.CheckrdPolicyDenied: Request <uuid> denied: denied by rule 'block-mutations'
  Request: https://hooks.zapier.com/hooks/catch/123/abc
  Fix: Blocked by rule 'block-mutations'. Edit the rule in your policy file or dashboard to allow this request.
  Dashboard: https://app.checkrd.io/agents/<agent-id>/events/<event-id>
  Docs: https://checkrd.io/errors/policy_denied

The exception carries every piece of context a caller might want:

AttributeMeaning
rule_nameWhich rule fired. None if it was the default-deny branch.
reasonHuman-readable deny reason from the WASM engine.
request_idUUID; correlates with the dashboard event and your telemetry.
urlThe request URL that was denied.
dashboard_urlDeep link to the event in the Checkrd dashboard.
suggestionActionable remediation hint.
codeOne of policy_denied, rate_limit_exceeded, default_policy_denied, kill_switch_active.

JavaScript: same shape, exception class CheckrdPolicyDenied from checkrd.

Three ways to handle denies

Pick the one that matches your agent's existing error model.

The most idiomatic pattern. Treat a deny like any other transport-level exception: log it, optionally fall back, and move on. The agent's code stays in control of what happens next.

python
from checkrd.exceptions import CheckrdPolicyDenied

try:
    r = http.post("https://api.example.com/data", json=payload)
except CheckrdPolicyDenied as exc:
    log.warning(
        "checkrd denied request",
        rule=exc.rule_name,
        request_id=exc.request_id,
        dashboard=exc.dashboard_url,
    )
    # Decide: skip, return a default, raise to caller, etc.
javascript
import { Checkrd, CheckrdPolicyDenied } from "checkrd";

const checkrd = new Checkrd({
  agentId: process.env.CHECKRD_AGENT_ID,
  apiKey: process.env.CHECKRD_API_KEY,
});
const fetch = checkrd.wrap(globalThis.fetch);
const payload = { task: "summarize", contents: "..." };

try {
  const r = await fetch("https://api.example.com/data", {
    method: "POST",
    body: JSON.stringify(payload),
  });
} catch (err) {
  if (err instanceof CheckrdPolicyDenied) {
    console.warn("checkrd denied request", {
      rule: err.ruleName,
      requestId: err.requestId,
      dashboard: err.dashboardUrl,
    });
    // Decide: skip, return a default, rethrow, etc.
  } else {
    throw err;
  }
}

2. Run the SDK in observe-only mode

If your goal is to wire Checkrd into an existing agent without changing any of its error-handling code, pass enforce=False at init time. The SDK still evaluates the policy and emits telemetry, the dashboard still shows what would have been denied, but the request always completes and no exception is raised.

python
from checkrd import Checkrd

client = Checkrd(
    api_key="ck_live_...",
    agent_id="...",
    enforce=False,   # ← shadow mode at the client level
)
javascript
import { Checkrd } from "checkrd";

const client = new Checkrd({
  apiKey: "ck_live_...",
  agentId: "...",
  enforce: false,
});

Use this when you want all the observability without any of the blocking. It's the same posture you get from mode: dry_run in the policy YAML, but applied at the SDK layer instead of the policy layer.

3. The on_deny callback

For agents that want a single chokepoint for every deny (centralised logging, paging, custom fallback), register an on_deny hook. The callback receives the same context as the exception. If the hook returns normally the request is still blocked; if you want the request to proceed anyway, combine the hook with enforce=False.

python
def on_deny(event):
    metrics.incr("checkrd_deny", tags=[f"rule:{event.rule_name}"])
    pager.notify_if_critical(event)

client = Checkrd(
    api_key="ck_live_...",
    agent_id="...",
    on_deny=on_deny,
)

The right pattern for shipping a new policy is the same one Envoy and OPA use for ext-authz: deploy in shadow mode first, validate the decisions against real traffic in the Events table, then flip the toggle to enforce.

yaml
# Step 1: ship this. Decisions appear in the dashboard with a "dry-run"
# badge; no request is blocked. Validate for a few hours.
mode: dry_run
default: deny
rules:
  - name: allow-reads
    allow:
      method: [GET, HEAD, OPTIONS]
      url: "**"
  - name: block-mutations
    deny:
      method: [POST, PUT, PATCH, DELETE]
      url: "**"
yaml
# Step 2: flip the mode and re-activate the policy. Same rules,
# now enforced. Re-publish from the dashboard or `checkrd policies
# publish` after the diff is what you expect.
mode: enforce
default: deny
rules:
  - name: allow-reads
    allow:
      method: [GET, HEAD, OPTIONS]
      url: "**"
  - name: block-mutations
    deny:
      method: [POST, PUT, PATCH, DELETE]
      url: "**"

The dashboard's "Create policy" dialog and every built-in policy template scaffold the YAML with mode: dry_run for exactly this reason. Custom YAML you author by hand inherits the schema default of dry_run; set mode: enforce explicitly when you're ready to block.

When the SDK does NOT raise

The SDK only raises CheckrdPolicyDenied when all four of these are true:

  1. The policy on the agent has mode: enforce.
  2. The SDK was initialised with enforce=True (or enforce="auto", the default, which honours the policy mode).
  3. A rule actually matched and resolved to a deny decision.
  4. The deny applies to the active request (not a rate-limit shadow or a kill-switch-overridden allow).

A mode: dry_run policy, an enforce=False client, or a default-allow path all complete normally with no exception and no behavioural change to your agent code.

See also