Libraries/acttrace
acttraceAudit

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.

$ pip install cendor-acttraceTry tampering with a log ↓
loan_triage.pyauto-audited via the event bus
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
How it audits

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.

Event on the bus
Entry type
What's recorded
Every LLM call
llm_call
provider · model · token usage · cost · latency · replayed? — not the prompt text
Every tool call
tool_call
tool name · arguments (redacted)
contextkit's receipt
context_assembly
budget · tokens used · per-block kept / shrunk / dropped decisions
Your explicit events
decision · human_oversight · policy_flag
the unit of work + its input (if you pass it) · reviewer sign-off (Art. 14-style) · refusals — a blocked input is auditable, not silent

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.

Detect & decide

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.

secret

API keys (sk-…, sk-ant-…, sk-proj-…), AWS & Google keys, GitHub & Slack tokens, PEM private keys, JWTs, bearer tokens

financial

credit cards, IBANs, US routing numbers, SWIFT/BIC

pii

email, phone, IPv4 / IPv6, MAC address

gov_id

US SSN — plus UK NINO & India Aadhaar via a locale pack

credential

free-text passwords (“the password is …”)

special_category

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:

default — secrets & email redact, else flaggdpr — special-category & credentials block, other personal data redactpci — payment/financial block, secrets & PII redactstrict — high-severity groups block, the rest redact
detect.pypure · offline · no side effects
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.

Enforce, don't just observe

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.

block — record policy_flag(action="blocked") and raise PolicyViolation; the call never runs.
redact-before-send — scrub the outbound messages (via core's Reroute(messages=…)) so the provider receives cleaned content, then proceed.
flag — record and proceed untouched. Recorder / enforcer split intact: core is what stops the call.
guard.pyenforce + record
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.

Try it

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

Where it saves

Three places. All of them yours.

1

The live log

AuditLog(path="audit/2026-07.jsonl")

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.

2

The evidence pack

audit.export("evidence.jsonl", framework="eu_ai_act")

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.

3

The head hash — out-of-band

audit.head  → your ticket system, a different store

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.

No server, no account, no network. Storage is wherever you point path= — a mounted volume, a committed folder, an S3 upload you own. Verification needs only the file (and the key, for signed logs): acttrace verify evidence.jsonl --key … --expect-head … — exit 0 or fail.
Running for days? A long-lived agent can emit millions of events, and log.entries grows with every one. Pass AuditLog(max_entries=N, path="…") to cap the in-memory ring — the oldest entry is evicted so memory stays flat, while the file keeps the complete, verifiable chain. Eviction never touches the chain (it lives in log.head + the file): verify(path) re-walks the full on-disk chain and export() stays complete. log.evicted_from_memory counts what left memory — never silent. Bounding memory never bounds durability, so always pair max_entries with path=.
Privacy by design

Committable without leaking.

Prompt text is not auto-stored. "What the agent saw" is captured as context decisions (kept/dropped blocks) plus any input= you explicitly pass to decision().
Detection is on by default — the offline engine scans every auto-captured payload and Policy.default() redacts secrets & email before anything is chained. Swap in policy=Policy.gdpr() / pci() / strict() to change the posture.
The redaction is itself audited. When the scrubber removes PII, a policy_flag lands in the chain — "we removed data" is on the record, not silent.
Bring your own scrubber via AuditLog(redactor=…) for domain PII (account numbers, national IDs).
Evidence packs

Speak your auditor's language.

export(framework=…) annotates each entry with control IDs your compliance team recognizes:

EU AI Act — Art. 10 · 12 · 13 · 14 · 19 · 26(5) · 72ISO/IEC 42001GDPRNIST AI RMF

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.

Honest limits

Where the edges are — by design.

The chain alone proves internal consistency.

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.

Truncation needs a reference point.

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.

In-process, per process.

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.

Redaction is a safety net, not a guarantee.

Pattern-based scrubbing catches common secrets; keep real secrets out of prompts, and extend the redactor for your domain's PII.

Get started

Auditable in four lines.

$ pip install cendor-acttrace

acttrace docs → · EU AI Act evidence-pack cookbook → · works alone, composes with the Cendor stack