Governance
Cap what a run may spend, attribute every cent, keep tamper-evident evidence, and redact PII
before it leaves the process — each one is a single wrapper around run(). Everything on this
page is the real tokenguard / acttrace API — the
identical objects re-exported from cendor.sdk for one-import convenience. That includes guard
(since cendor-sdk 1.7.0 / @cendor/sdk 0.10.0: acttrace’s guard() return is dual-shape — the raw
interceptor and its own scope form — so cendor.sdk.guard is cendor.acttrace.guard); it all
rides
cendor-core’s seams, so it applies to any instrumented call, not just SDK runs.
Quickstart
from cendor.sdk import Agent, run, budget, guard, Policy, AuditLog
agent = Agent(name="support", model="gpt-4o", instructions="Help politely.")
log = AuditLog(system="support", risk_tier="limited", path="audit.jsonl")
with budget(usd=0.25, on_exceed="block"), guard(Policy.default(), audit=log):
result = run(agent, "Why was I charged twice?", audit=log)
import { Agent, run, withBudget, guard, Policy, AuditLog } from '@cendor/sdk';
const agent = new Agent({ name: 'support', model: 'gpt-4o', instructions: 'Help politely.' });
const audit = new AuditLog('support', { riskTier: 'limited', path: 'audit.jsonl' });
const result = await withBudget({ usd: 0.25, onExceed: 'block' }, () =>
guard({ policy: Policy.default(), audit }, () =>
run(agent, 'Why was I charged twice?', { audit })));
Core concepts
Budgets
A pre-flight budget stops an over-budget call before it runs; a post-flight one trips after. Pick by intent (the full decision guide is in tokenguard → hard cap vs runaway guard):
from cendor.sdk import budget
with budget(usd=0.25, on_exceed="block"): # pre-flight: the over-budget call never runs
run(agent, "...")
with budget(usd=1.00, on_exceed="raise"): # post-flight: raises after the cap is crossed
run(agent, "...")
with budget(usd=0.50, on_exceed="downgrade", downgrade={"gpt-4o": "gpt-4o-mini"}):
run(agent, "...") # reroutes to the cheaper model
import { withBudget } from '@cendor/sdk';
await withBudget({ usd: 0.25, onExceed: 'block' }, // pre-flight: never runs over budget
() => run(agent, '...'));
await withBudget({ usd: 1.00, onExceed: 'raise' }, // post-flight: raises after the cap
() => run(agent, '...'));
await withBudget({ usd: 0.50, onExceed: 'downgrade', downgrade: { 'gpt-4o': 'gpt-4o-mini' } },
() => run(agent, '...')); // reroutes to the cheaper model
BudgetExceeded is raised on block/raise. Budgets stack — the innermost cap is enforced first.
Agent(max_usd=...) caps that agent’s spend pre-flight (block semantics) on every run — a
plain run(agent, ...) as well as a single agent’s segment in a multi-agent team
(Multi-agent → per-agent budgets).
Attribution
Tag ambient spend by feature, user, session — anything — and read it back grouped, or assert it in a test:
from cendor.sdk import track, report
with track(feature="support", user_id="alice"):
run(agent, "...")
report(group_by=["feature"]).assert_under(usd=0.05, feature="support")
import { track, report } from '@cendor/sdk';
await track({ feature: 'support', userId: 'alice' }, () => run(agent, '...'));
report(['feature']).assertUnder(0.05, { feature: 'support' });
Audit + redaction
from cendor.sdk import AuditLog, guard, Policy, verify
log = AuditLog(system="support", risk_tier="high", path="audit.jsonl")
with guard(Policy.gdpr(), audit=log): # redacts PII *before* the provider sees it
run(agent, "email me at alice@example.com", audit=log)
ok, detail = verify("audit.jsonl") # tamper-evident hash chain, checked offline
assert ok
import { AuditLog, guard, Policy, verify } from '@cendor/sdk';
const audit = new AuditLog('support', { riskTier: 'high', path: 'audit.jsonl' });
await guard({ policy: Policy.gdpr(), audit }, () => // redacts PII *before* the provider sees it
run(agent, 'email me at alice@example.com', { audit }));
const [ok, detail] = verify('audit.jsonl'); // tamper-evident hash chain, checked offline
AuditLogauto-subscribes to the bus and records everyllm_call/tool_call/context_assemblywith zero wiring. Editing any past entry breaks every entry after it — that’s the hash chain, andverify()re-walks it offline.guard()installs a pre-call interceptor that redacts, blocks, or flags per thePolicy.Policy.default(),Policy.gdpr(),Policy.pci(),Policy.strict()are built in; the detector catalogue and custom policies live in acttrace.- Human approvals can join the same chain — see Interop → human-in-the-loop.
Testing — record once, replay forever
from cendor import cassette
with cassette.using("tests/fixtures/run.json"): # records on first run, replays after
result = run(agent, "What's the weather in Paris?")
import { using } from '@cendor/cassette';
const result = await using('tests/fixtures/run.json', () => // records once, replays after
run(agent, "What's the weather in Paris?"));
Record/replay is the cassette library — the SDK’s eval harness drives
it, but cassette is not re-exported by the SDK: import it from the umbrella as
from cendor import cassette (Python) / @cendor/cassette (TypeScript), as the snippet above does.
Replay is deterministic and offline — the same trajectory every time, no network, no keys — and
replayed calls re-emit their recorded usage, so cost and tokens are real on replay. That’s what
makes spend a testable property; the eval harness builds directly on it.
Plugs into the stack
The SDK adds no governance machinery of its own. Each wrapper attaches to a
cendor-core seam, which is why the same line works under the SDK loop, under a
bare instrumented client, and in both languages:
| Wrapper | Seam | Moment |
|---|---|---|
budget(on_exceed="block"/"downgrade") | pre-flight interceptor | before the call runs |
budget(on_exceed="raise") | bus subscriber | after usage lands |
track / report | bus subscriber | after usage lands |
guard(Policy...) | pre-flight interceptor | before the request leaves |
AuditLog | bus subscriber | every event, appended to the chain |
Agent(guardrails=[…]) | in-loop gate (the cendor-guardrails Gate) | four stages: input / tool_call / tool_output / output |
cassette | subscriber (record) + interceptor (replay) | around the call |
Honest limits
on_exceed="raise"overshoots by one call — it’s post-flight. For a true ceiling use"block"(details in tokenguard → honest limits).- Unpriced models record
$0, so a USD cap can’t bind on them. Three remedies, and the third is the one to reach for when a silent$0is unacceptable: register a rate, use atokens=cap (tokens are counted whether or not the model is priced), or fail closed withconfigure(on_unpriced="raise")— thenon_exceed="block"rejects an unpriced call pre-flight instead of letting it through at$0. The default is"warn"(oneUnpricedModelWarningper model, the call proceeds), so a budget that must never be bypassed has to opt in (Providers → pricing unpriced models). guardredacts what its detectors find. Regex/pattern detectors plus (Python-only) Presidio NER — see acttrace for coverage and the parity matrix for the language split.guard/interceptors are process-global. They register on the single in-process event bus, not per-call or per-agent, so in a concurrent server they scope around your whole app rather than one request — install policy once at startup, don’t toggle it per request. For per-agent, per-run gating (keyword / regex / URL / length / schema, at four stages) useAgent(guardrails=[…])instead — the cendor-guardrails Gate, scoped to the agent and overridable per run, recording to the same audit chain.- Evidence, not compliance. The audit chain supports a compliance case; it doesn’t make one.