Canary policies
A canary family is a handful of neurons, on a specific model, whose activations were calibrated against an evaluation set for one behavior — prompt_injection, credential_exfiltration. With Trace you can make an agent's authorization depend on what those neurons read while its model processed the request: an injected instruction the agent's chain-of-thought never mentions still shows up in the measurement.
Write the rule
{
"rules": [
{ "id": "block-prompt-injection",
"when": [{ "fact": "canary", "set": "prompt_injection", "op": "gte", "value": 1 }],
"require": { "deny": true } },
{ "id": "review-credential-requests",
"when": [{ "fact": "canary", "set": "credential_exfiltration", "op": "gte", "value": 0.8, "best_effort": true }],
"require": { "approval": true } }
]
}
setnames a family;valueis a score in [0, 1] (1.0 = at least one member reached its calibrated threshold). Numeric operators only.- Families are model-specific: the same family has a different implementation per model version. A family must exist on at least one registered model, as a curated set or one of your own account sets, or the write is rejected (
400,resource_missing). Your own set for a family overrides the curated one for your readings. - Canary rules are unlimited on Growth; a Developer account gets a monthly Developer allowance of canary-policy writes (
canary_rule, then402 usage_limit_reached). - A deny from a canary rule is named
canary_tripped(an explicitreasonon the rule still wins).require.approvalcomposes into step-up as usual.
Give verify a reading
The agent presents the trace of the inference that produced the action:
# 1. the agent's model call, traced
curl $API/v1/trace/chat/completions … → { "provenance": { "trace_id": "trc_…", "canary_readings": {…} } }
# 2. bind it at authorize (the token carries trace: {id, canary_hash}) …
curl $API/v1/intent/authorize -d '{ "action_description": "…", "agent_session_id": "…", "device_id": "…", "trace_id": "trc_…" }'
# 3. … or present it (or a fresher one) at verify
curl $API/v1/intent/verify -d '{ "intent_token": "…", "action_description": "…", "trace_id": "trc_…" }'
Source order at verify: the trace_id you pass to verify (else the token's), as a traced inference no older than TRACE_READING_MAX_AGE_S (15 minutes by default) → a sidecar reading for that id → inference_input: {model, messages}, re-read now (metered trace_canary_read). The response then carries canary_readings, canary_source and trace_id; the decision ledger and the signed receipt carry trace_id + canary_hash.
Fail-closed
The policy engine treats a missing fact as "doesn't match", which would silently pass a canary rule. So unavailability is decided before evaluation:
| Situation | Outcome |
| --- | --- |
| No trace, stale trace, another account's trace, worker 502/503, unrecorded sandbox prompt | canary_unavailable (canary_set names the family) |
| …but every condition on that family is best_effort: true | the condition evaluates false; canary_source: "unavailable" |
| Same trace_id as the token, different canary_hash | canary_reading_mismatch |
A trace.canary_tripped webhook fires alongside intent.denied on every canary_tripped.
To stop the model itself on a trip, use the kill switch. It withholds the completion and can lock the agent session until someone reviews it. Canary policies still apply, and a locked session is denied session_canary_locked at verify.
Limitations
- Readings are signals with published precision and recall (
GET /v1/trace/registry/models/:id/canary-sets). The sandbox families are fixture-grade, measured on a 0.5B model. - The agent chooses which trace to present. Our framework adapters present it automatically (
gate(fn, { traceId })). Token binding detects a swapped reading (canary_reading_mismatch). A second, clean inference presented in place of the first is outside what binding checks; lock the session with the kill switch to stop that pattern. - Accounts whose policy references no canary pay nothing and see no change: the verify response and the ledger hash are byte-identical.
SDK
const r = await prov.trace.chat.completions.create({ model, messages });
const auth = await prov.intent.authorize({ actionDescription, agentSessionId, deviceId, traceId: r.provenance.traceId });
const v = await prov.intent.verify({ intentToken: auth.intentToken, actionDescription }); // → canary_tripped?