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, re-exported from cendor.sdk for one-import convenience; 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
  • AuditLog auto-subscribes to the bus and records every llm_call / tool_call / context_assembly with zero wiring. Editing any past entry breaks every entry after it — that’s the hash chain, and verify() re-walks it offline.
  • guard() installs a pre-call interceptor that redacts, blocks, or flags per the Policy. 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?"));

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.

How it works

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:

WrapperSeamMoment
budget(on_exceed="block"/"downgrade")pre-flight interceptorbefore the call runs
budget(on_exceed="raise")bus subscriberafter usage lands
track / reportbus subscriberafter usage lands
guard(Policy...)pre-flight interceptorbefore the request leaves
AuditLogbus subscriberevery event, appended to the chain
cassettesubscriber (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 — register a rate or use a token cap (Providers → pricing custom models).
  • guard redacts 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.
  • Evidence, not compliance. The audit chain supports a compliance case; it doesn’t make one.