One edited byte,
and verify() says so.
Detect secrets & PII offline and block or redact them before they're sent — then keep a tamper-evident record of what the agent did, what it cost, and who approved it. Hash-chained, offline-verifiable, no infrastructure.
from cendor.acttrace import AuditLog, verify audit = AuditLog(system="loan_triage", risk_tier="high", path="audit/2026-07.jsonl", # ← lives on YOUR disk signing_key=os.environ["AUDIT_KEY"]) with audit.decision(input=application) as d: resp = client.chat.completions.create(...) # auto-logged d.human_oversight(reviewer="ops@bank", action="approved") audit.export("evidence.jsonl", framework="eu_ai_act") ok, detail = verify("evidence.jsonl", key=KEY) # offline, anywhere
You never write a log line. The bus does.
Constructing an AuditLog subscribes it to core's event bus — every call flowing through instrument() becomes a hash-chained entry, with zero per-call wiring. Budgets (tokenguard) and context decisions (contextkit) ride the same stream, so the log already knows the cost and the context — without importing either.
Each entry is sealed as hash = sha256(prev_hash + entry) from a genesis of 64 zeros. With signing_key=, every entry — and the export header — also carries an HMAC, so the log provably came from a key-holder.
Find the sensitive data. Then choose what happens to it.
Before a payload is chained — or, with guard(), before it's sent — acttrace runs an offline detection engine: ~20 categories across six groups, every loose pattern validator-gated (Luhn · IBAN mod-97 · Verhoeff · ABA · ISO-3166) to keep false positives down. Regex + local arithmetic — no model, no network, no account.
API keys (sk-…, sk-ant-…, sk-proj-…), AWS & Google keys, GitHub & Slack tokens, PEM private keys, JWTs, bearer tokens
credit cards, IBANs, US routing numbers, SWIFT/BIC
email, phone, IPv4 / IPv6, MAC address
US SSN — plus UK NINO & India Aadhaar via a locale pack
free-text passwords (“the password is …”)
GDPR Art. 9 — health / biometric / religion (best-effort)
A Policy maps each category (or whole group) to an action — allow · flag · redact · block. Four presets set the posture; scan() / redact() are pure and independent of the audit chain:
from cendor.acttrace import scan, redact, Policy scan("card 4111 1111 1111 1111 for alice@example.com") # [Finding(credit_card, financial, action='flag'), # Finding(email, pii, action='redact')] ← counts only, never the raw value cleaned, findings = redact("ping alice@example.com", Policy.gdpr()) # cleaned == "ping <redacted>" ← the raw value is gone
The registry is the single source of truth — register_detector(...) adds your own, and the built-in redactor is rebuilt from it, so the original categories scrub byte-for-byte.
One line on the seam: block, warn, or redact-before-send.
acttrace records; it doesn't gate. guard() is the batteries-included enforcer — install it on core's interceptor seam and, per outbound call, the policy resolves each detected category to an action, with every decision on the same tamper-evident chain.
from cendor.core.instrument import add_interceptor from cendor.acttrace import AuditLog, Policy, guard log = AuditLog(system="support_bot", risk_tier="high", signing_key=os.environ["AUDIT_KEY"]) add_interceptor(guard(Policy.gdpr(), audit=log)) # special-category → blocked + recorded; PII → scrubbed # before it ever reaches the model. One line, offline.
Extras are all opt-in: enable_locale_pack("uk","in"), enable_entropy_detector(), and NER names/addresses via pip install "cendor-acttrace[ner]" (Presidio, still offline). A zero-extra install detects exactly the built-ins.
Tamper with this log. Watch it fail.
This is a real hash chain rendered from five entries. Click any entry to "edit" it — every link after it breaks, and verify() tells you exactly where.
▸ click an entry to flip a byte in its payload · this is the whole security model, live
Three places. All of them yours.
The live log
Append-only JSONL on your disk — created immediately, handle kept open, every entry flushed as it happens (crash-durable). No path? It lives in memory only, on log.entries.
The evidence pack
A second, annotated file: every entry mapped to control IDs, PII redacted, plus a signed header carrying the head-hash and entry count a reviewer checks first.
The head hash — out-of-band
64 hex chars that pin the whole log. verify(path, key=…, expected_head=…) then proves nothing was edited, reordered, or truncated — even by someone who can rewrite the file.
Committable without leaking.
Speak your auditor's language.
export(framework=…) annotates each entry with control IDs your compliance team recognizes:
Evidence, not a guarantee. acttrace produces evidence to support compliance — the control mappings are starting templates for your compliance team, not legal advice. This disclaimer ships in the export header itself.
Where the edges are — by design.
Without a key, an attacker who can rewrite the whole file can re-chain it. signing_key= (HMAC) is what defends against a full rewrite — and the out-of-band head hash is what defends against key theft plus rewrite.
The export header carries a signed head-hash + entry count; for the strongest completeness claim, record audit.head somewhere the log-writer can't touch.
The log rides the in-process event bus (thread-safe appends). Multiple worker processes = one log file each; aggregate downstream, don't share a file handle.
Pattern-based scrubbing catches common secrets; keep real secrets out of prompts, and extend the redactor for your domain's PII.
Auditable in four lines.
acttrace docs → · EU AI Act evidence-pack cookbook → · works alone, composes with the Cendor stack