auditrail
Tamper-evident audit trails and a "lethal trifecta" policy guard for AI agents.
pip install auditrail # Python 3.10 or newer; then try: auditrail demo
The package contains the library and the auditrail command. The examples, the sample policy, the documents and the
Rust verifier live in the GitHub repository and are not in the package;
links below point there. To work from a source checkout, see the Quickstart.
In plain words. An AI agent can read private data, read text from untrusted places, and send things out. When all three happen in one session (the "lethal trifecta"), a trick hidden in the text can make it leak the data. auditrail is a small library that can refuse a tool call that would complete that combination, and that writes each decision to a log in which every entry is chained to the one before it (changing an old entry breaks the chain unless every later entry is recomputed, which is why entries are also signed) and, if you run a separate recorder process, signed by it (the Quickstart's ledger is not signed). Someone who does not trust you can then check, with the recorder's public key obtained from a route the operator cannot change, whether entries were edited; noticing that entries were deleted or the log cut short needs a witness copy. The operator is whoever runs the agent and owns these files; the recorder is the process that holds the signing key and writes the log; the agent is the AI program whose tool calls are logged. A witness is meant to be a place outside the operator's control that keeps copies of the log's fingerprints (anchors), published periodically; a heartbeat is a periodic entry in which the recorder says which tools it wraps and how many calls it saw versus recorded, so that, if you pass a gap limit when verifying, silent gaps in the wrapped tools show up. It covers only the tools you wrapped, does not detect prompt injection, and is not a compliance product.
Status and what this is not. Early (v0.3), with no independent security review. The evidence format is a draft. Both verifiers (Python and Rust) were written by the same author, so they check that the two agree, not yet that the format is right. It is not a certified compliance product. What it deliberately does not do is listed in SECURITY.md; read that before deciding whether it fits.
The problem
AI agents now read private data, browse untrusted content, and act on the outside world -- often all in the same session. When those three capabilities land in one place, a prompt injected into "just some text the agent read" turns into real data exfiltration. Reported incidents of that kind (each with its source; preconditions are given where the source states them):
- EchoLeak (CVE-2025-32711) -- a crafted email, with no action by the user, got Microsoft 365 Copilot to disclose confidential data. CVSS 3.1: Microsoft scored it 9.3, NIST (NVD) scored it 7.5.
- Slack AI (PromptArmor, August 2024) -- an attacker could post hidden instructions in a public channel that only they were in, and make Slack AI answer a victim with a link carrying the victim's private-channel data (for example an API key); the data left when the victim clicked. Slack replied at the time that messages in public channels are visible to workspace members by design. (report)
- ForcedLeak (Noma Labs, September 2025; Noma rated it CVSS 9.4) -- indirect prompt injection through a Salesforce Agentforce Web-to-Lead form could send CRM data to a domain that had expired but was still on the allow-list (Noma bought it for about $5). Salesforce enforced Trusted URLs in September 2025. (report)
- GitHub MCP (Invariant Labs, May 2025) -- when a user's agent can reach both a public and a private repository, a malicious issue in the public one can make the agent write private-repository content into a public pull request. The discoverers describe this as an architectural issue at the agent layer, not a defect in the GitHub server code. (report)
Reported incidents that are not prompt-injection exfiltration (each with its source):
- Replit (July 2025) -- SaaStr founder Jason Lemkin said Replit's AI coding agent deleted his production database during a code freeze and invented data; Replit's CEO publicly called this unacceptable. The details come from Lemkin and were not independently verified. (The Register)
- GTG-1002 (Anthropic, November 2025) -- Anthropic assessed with high confidence that a Chinese state-sponsored group (which its full report designates GTG-1002) used Claude Code against roughly 30 targets; according to Anthropic the AI performed 80-90% of the tactical operations, humans approved at key decision points, and a handful of intrusions succeeded. (news page, full report)
OWASP's Top 10 for Agentic Applications 2026 (December 2025) includes Tool Misuse and Exploitation (ASI02), Identity and Privilege Abuse (ASI03) and Insecure Inter-Agent Communication (ASI07). Its own incident table files EchoLeak, ForcedLeak and the GitHub issue case, among others, under ASI01 (Agent Goal Hijack) and ASI02; the GitHub issue case is also listed under ASI07. Independently, Simon Willison named a shape he calls the "lethal trifecta" (June 2025): access to private data, exposure to untrusted content, and the ability to communicate externally. His point is that an agent that combines all three can be tricked into leaking private data.
auditrail gives an agent session a policy engine that refuses to complete that trio, and a tamper-evident log of every decision it made along the way -- so that afterwards you can check which calls to wrapped tools were allowed or refused (with the reason, for a refusal), and whether that record has been altered since (given a signing key the operator cannot reach and a witness copy it does not control). The ledger stores digests of arguments and results, not what the agent "saw".
What it actually does (v0.3)
auditrail lets someone who does not trust you check, given the recorder's public key and a witness copy, what an agent did through the tools you wrapped. It is a library plus a standalone verifier, not a governance platform; it is meant to sit next to the gateways and guardrails you already use.
- Signed, hash-chained ledger. Every record is chained to the previous one and signed (Ed25519) by a recorder key the agent never sees (when the recorder runs as its own process). Editing a record and recomputing the hashes -- which defeated v0.1 -- now fails verification when you verify with the recorder's public key (a ledger written without a signer still passes after the hashes are recomputed).
- Anchors and witnesses. The ledger head is published (signed) to a place you must arrange so that the operator cannot rewrite it alone; a verifier holding only that copy can then detect removal or editing of records up to the last anchor, provided the witness is independent of the operator.
- Heartbeats. The recorder periodically writes which tools it wraps, a hash of the policy in force, and how many calls it intercepted vs. recorded, so silent periods, tool/policy changes, and calls that ran without evidence show up.
- Independent implementation of the verifier.
verifier/is a separate Rust program, written from the public format spec and tested against the same conformance vectors as the Python code. One offline binary, no network. It was written by the same author as the Python code, so it checks that the two agree, not yet that the format is right. - Lethal-trifecta policy by workflow. A call that would complete
data-access + untrusted-input + external-comm is refused before it runs, counted
across a whole workflow (shared
trace_id, or agents linked by a signed message that carries the sender's categories), not just one session. - Per-agent signed messages with receipts. Each agent has its own key, so a
message is attributable and bound to its recipient. Both sides log it and
auditrail reconcilefinds a message only one side recorded. - Process-isolated execution (
isolate=True): allow-listed environment and a timeout. Reduces accidents; it is not a security sandbox.
What it deliberately does not do -- detect prompt injection, prove that unwrapped tools stayed quiet, protect you if the recorder key is stolen, or certify compliance -- is in SECURITY.md. Read that before deciding whether it fits your threat model.
Stricter modes and behavior changes
policy = PolicyEngine.from_yaml("policies/example_policy.yaml", strict=True) # the sample policy is in the GitHub repository, not the package
guard = Guard(policy, ledger) # a wrapped tool with no category is refused and recorded as `unclassified_tool`
replay = SqliteReplayGuard("replay.sqlite") # one replay decision shared by every process on this host
strict(orstrict_classification: truein the policy): a tool with no category is refused. Without it, a warning is printed when such a tool is wrapped. Harmless tools say so with an explicit label such asinternal.state_changeis a category for actions that change an external system without necessarily communicating outward. Withenforce_state_change_after_untrusted: trueper agent, such a tool is refused once its workflow has read untrusted input, unless it is listed instate_change_allowed_tools.SqliteReplayGuardreplaces the in-memoryReplayGuardwhen several agent processes on one host receive messages.auditrail recorderwarns at startup about a missing or empty token. Behavior change: it now also refuses to start (exit code 2) when its signing key is readable by other accounts, its ledger/anchors are writable by them, or the permission check itself could not run (an unknown is treated as unsafe). Pass--allow-loose-permissionsto get warnings (NOT CHECKEDfor the unchecked case) and start anyway. The Windows check reads the ACL for the broad groups only, so passing it is not proof the key is private.- Behavior change:
internalcannot be combined with another category on one tool. Such a policy fails to load (PolicyError, naming the tool and the categories), and so does a call-site label that contradicts the policy. Remove one of the two labels; there is no switch to allow the combination. - Behavior change: a category must be a built-in name (
data_access,untrusted_input,external_comm,state_change,internal,agent_to_agent) or be declared once under a top-levelcustom_categories:list in the policy (lower_snake_case).State_Change,internal,state-changeand typos raisePolicyErrorwith the closest known name, in every mode. Declaring your own names is the way back; there is no switch that skips the check. - Behavior change: the same tool labelled differently by two agent sections raises
PolicyErrorwhen it is wrapped; give it one label set. - Behavior change (verifiers, Python and Rust): two new statuses,
UNSUPPORTED_ALGORITHM(inconclusive, exit 1: the trusted key is registered as an algorithm the verifier cannot check; a.pubfile cannot express that, so file input getsBAD_SIGNATURE"algorithm mismatch" for any record that names an algorithm other than its Ed25519 key's) andUNSUPPORTED_FORMAT(formatis an integer other than 2). An optionalsig_algmember names the algorithm; absent means Ed25519 and Ed25519 is always written by omission, so existing ledgers are unchanged. An explicit"sig_alg":"ed25519"is rejected. See the format spec.
These make a missing label and an unordered write visible and refusable. They cannot tell that a label is wrong; see "Opt-in hardening" in SECURITY.md for what each one does not cover.
Evidence that can be checked without trusting the operator
This holds only if (1) the signing key is outside the agent's process and account (the recorder runs separately) and, for the "without trusting the operator" claim, also out of the operator's reach after the fact -- for example on a host the operator does not administer, or in hardware that signs on request (another OS account that the operator can administer does not count) -- so the operator cannot later take the key and re-sign a rewritten history (a key that signs on request from a KMS/TPM would also do, but that is not provided: you would write the signer yourself); otherwise only an independent witness limits what the operator can rewrite; (2) the checker gets the recorder's public key by a route the operator cannot alter; and (3) the witness copy is held somewhere the operator cannot rewrite. Without those, see the limits in SECURITY.md.
# 1. a recorder key, kept where the agent process cannot read it
auditrail keygen --out keys/recorder
# 2. run the recorder as its own process (ideally its own OS user or host);
# it holds the key and the ledger, and agents can only append through a socket
# (policies/example_policy.yaml is the sample policy in the GitHub repository, not in the package)
export AUDITRAIL_RECORDER_TOKEN=change-me
auditrail recorder --ledger evidence.jsonl --key keys/recorder.key \
--policy policies/example_policy.yaml --anchors anchors.jsonl \
--git-repo ../witness # anchors are committed here; you push it somewhere you don't control
from auditrail import Guard, PolicyEngine, RemoteLedger
guard = Guard(PolicyEngine.from_yaml("policies/example_policy.yaml"), RemoteLedger(port=8765))
guard.start_heartbeats() # the agent's own "still wrapped" signal
# 3. anyone, offline, with the recorder's public key (obtained by a route the operator cannot alter) and a witness copy.
# auditrail-verify is the standalone Rust binary (build it or download it, see below):
auditrail-verify evidence.jsonl --pubkey keys/recorder.pub --witness witness-copy.jsonl --max-gap 120
# the Python CLI does the same; note the flag is --witness-file there:
auditrail verify evidence.jsonl --pubkey keys/recorder.pub --witness-file witness-copy.jsonl --max-gap 120
pip install installs only the auditrail command. auditrail-verify comes from cd verifier && cargo build --locked --release
or from a release download.
The verifier prints OK only with a list of what it did not check (no key
given, no witness, no heartbeat limit, records after the last anchor).
Getting the standalone verifier
auditrail-verify is a single static file written in Rust. The PyPI package does not contain it or its source. Pre-built
files, when a release provides them, are on the project's
GitHub Releases page; if there is no file for your version or
platform, build it from source:
- Get the source:
git clone https://github.com/x91652-pixel/auditrail, then work in itsverifier/folder (on GitHub). - Linux:
sudo apt install cargo(or install Rust with rustup); Windows: install rustup and the Visual Studio C++ build tools it asks for; macOS: rustup. - Then:
cd verifier && cargo build --locked --release. - The binary is
verifier/target/release/auditrail-verify(.exeon Windows); check it withverifier/target/release/auditrail-verify --version.
A release is built by the release workflow
when a tag v* is pushed: it builds Linux x86_64, Windows x86_64 and macOS (Apple silicon and Intel) files, runs the
conformance vectors against each native build, and publishes them with a SHA256SUMS list and GitHub build-provenance
attestations. To use a downloaded file (names as listed on the release you picked; <tag> is that release's tag):
gh release download <tag> --repo x91652-pixel/auditrail --pattern 'auditrail-verify-linux-x86_64' --pattern SHA256SUMS
sha256sum --check --ignore-missing SHA256SUMS
gh attestation verify auditrail-verify-linux-x86_64 --repo x91652-pixel/auditrail # which workflow built it
chmod +x auditrail-verify-linux-x86_64 && ./auditrail-verify-linux-x86_64 --version
What this does and does not give you: the checksum and attestation say the file is the one this repository's
workflow built from a given commit. They do not say the repository itself is trustworthy, and a verifier you
downloaded from the same place as the evidence is only as independent as that place. For a high-stakes check,
build it from source at a commit you reviewed. Builds use Cargo.lock and fixed path/timestamp flags; two clean
builds on one machine were byte-identical, but identical bytes across different machines are not verified. The
Intel macOS binary is cross-compiled and is not executed in CI.
Anchoring (protocol v1)
verify only proves the ledger is internally consistent: someone who rewrites
the whole ledger, or cuts off its tail, still passes it. Anchoring periodically
records the chain head (sequence number and hash, no tool arguments or results)
and publishes it to a witness outside the ledger owner's sole control. The full
current specification, including the signed (v2) anchors, is docs/spec/evidence-format-v2.md.
# record the current head; with --push, also push the witness commit
auditrail anchor --ledger ledger.jsonl --anchors anchors.jsonl --git-repo ../witness
auditrail verify ledger.jsonl --anchors anchors.jsonl --git-repo ../witness
verify --anchors reports one of OK, TAMPERED_LEDGER, TAMPERED_ANCHOR,
UNSIGNED_ANCHOR, UNKNOWN_KEY, BAD_SIGNATURE, REWRITTEN, TRUNCATED,
SINK_MISMATCH, or SINK_UNREACHABLE, plus how many records after the last anchor
are not covered. (SINK_* exist only in the Python verifier; the Rust one reads a
witness file and has no sinks. The full status list, including the record- and
heartbeat-level ones, is in the format spec.)
Behavior change (anchored coverage): for an owner's anchors file whose lines are not in increasing seq order, the Python verifier now reports anchored_upto as the highest anchored seq (as the format and the Rust verifier do), not the seq of the last line; the number of records "not covered" can therefore be smaller than before for such a file.
Behavior change: auditrail verify --anchors FILE now stops with [ERROR] FILE: file not found and exit code 2 when FILE
does not exist (before, it reported OK as if there were no anchors yet). Leave out --anchors to verify without one. A
witness copy, or an anchors file that is a regular file but cannot be read (a lock, permissions), is SINK_UNREACHABLE; a
directory given to --anchors is an [ERROR] with exit code 2 (not a file).
What anchoring proves: records up to the last anchor were not rewritten, deleted, or reordered, provided that anchor has been stored outside the owner's control. Truncation below an anchor is detected in the same way.
What it does not prove: records after the last anchor; a ledger rewritten from
the start whose anchors were never stored externally; records that were never
written at all; a compromised host making changes in real time; or a witness and
the owner acting together. The git witness is only as strong as the place the
repository lives. A repository the owner controls can be rewritten by that owner,
so anchor hashes should also be copied to a third party periodically. Anchors made
with --key are signed (anchor_version 2, see the format spec);
Merkle proofs and RFC 3161 timestamps are not implemented yet.
A readable report
auditrail report evidence.jsonl --pubkey keys/recorder.pub --witness-file witness-copy.jsonl --max-gap 120 \
--format all --out report # writes report.md, report.html, report.json; exit 0 only if no check had a finding
The report says what the checks found, what they did not examine, and, in a section that cannot be switched
off, what no report of this kind proves. It is deterministic (same input, same bytes; add --generated-at now for a
timestamp), self-contained HTML, makes no network call, is not signed, and is a rendering of verify's result, not
new evidence. --redact replaces the names of agents, tools and recorders by codes in the fields that carry them
(never the status words or the structure); the codes are not secrets (a guessable name can be recovered), so add
--redact-salt SECRET if that matters, and note that policy versions, timestamps, counts and input hashes are not
masked, nor is anything you put in --title or --note. Those two are printed on one line, escaped (a newline or
Markdown syntax in them cannot add a section or a link) and labelled as supplied and not examined. The first screen
says what ran and what did not from the outcomes, not from the flags (an empty witness file, an empty ledger or a
ledger reconciled against itself is named as such). Failures to write are a one-line error with exit 2; an existing
--out file is refused unless --force, and an input file is never overwritten. stdout is written as UTF-8 with
LF line ends, the same bytes as --out. Exit code 0 means the main ledger and every check you supplied (including peer ledgers) found nothing; 1 means any finding or an inconclusive result, which is not necessarily tampering (a problem with a peer ledger gives 1 while the main ledger's own status stays as it is, listed under overall.secondary in the JSON); 2 means an input file or the output could not be read or written, and no result is produced (the same for verify and reconcile: 0 no finding, 1 a finding or an inconclusive result, 2 an input file could not be read, for example a malformed --pubkey file). A --redact-salt is passed on the command line, so it is visible in the process list and shell history.
To see the whole path in one go (an agent, a signature, a witness copy, one record altered, the verifier catching it, two reports), on Windows or Linux:
python -m examples.mvp_demo.run
In that demonstration the agent and the key are on one machine, so it shows the mechanism, not a deployment.
Reading a report
A report is easy to over-read. The numbered points below are what it does not say; the full, fixed list is in its last section.
- "No inconsistency found" is not "safe". It means the checks that ran, over the records they covered, found nothing; the report lists what ran and what did not.
- It covers only the tools the recorder wrapped. An action by a tool that was not wrapped leaves no record, and the report cannot see it.
- An unreadable or empty input is not a clean one. An empty ledger, an empty witness file or a v0.1 ledger is reported as not examined, never as consistent. An empty ledger still exits 0 with status
OK, and the report text says nothing was examined; an unreadable input exits 2 and no report is written. - Without a key, a consistent chain proves little. Anyone who could write the file could have rewritten it consistently; check signatures with a key you trust.
- Without an independent witness copy, removal or rewriting of records is not detectable, and records after the last anchor are not covered by it.
- A witness is only as independent as the people who hold it. If the operator controls the witness copy, it adds little.
- "Trusted" key means supplied, not verified. The tool does not check where a key file came from.
- A matched message pair shows two records agree, not that the other side is independent. The same signing key on both sides is a clue that it is not; a different key does not prove that it is.
- Exit code 1 is a finding or an inconclusive result, not necessarily tampering.
UNSUPPORTED_*can be provoked by editing a field and never means "untouched". - The report is not signed and can be edited; its fingerprint helps compare two copies, it does not vouch for the content.
--redactis masking, not anonymisation. The codes are not secrets, several fields stay visible, and the title and note are never masked.- The report was produced from the files you gave it. If the recorder's host or key was compromised, none of it holds.
- Heartbeats show only that the gap limit was checked. They do not show that the agent used no tool that was not wrapped.
- A
.mdfile read as plain text shows escaping backslashes; the rendered Markdown and the HTML do not. - The first screen is not the whole report. The complete list of limits is in section 10; do not rely on the Result section alone.
SECURITY.mdis not shipped with the package. The report records only the hash of the file its list of limits was generated from, not which commit it came from.- "Verifier's original wording" is the verifier's text, not a promise by the report. A phrase such as "all signatures valid" there describes what that check examined, under the limits above.
Quickstart
Python 3.10 or newer is required (CI tests 3.10 and 3.12). Work in a virtual environment (a folder of Python packages kept just for this project): on recent Debian-based distributions (we saw it on Ubuntu 26.04) pip can refuse a system-wide install with "externally-managed-environment" (PEP 668); this is from our own test run, not a statement about every Ubuntu version.
From PyPI
# Linux / macOS (on Ubuntu first: sudo apt install python3-venv python3-pip)
python3 -m venv .venv && . .venv/bin/activate
pip install auditrail
auditrail demo # allow, deny, a trifecta block, tamper detection; the last line names the ledger file
auditrail verify <the path printed on that last line>
auditrail keygen --out keys/recorder # a recorder key pair; see "Evidence that can be checked" above
# Windows PowerShell -- no activation needed
python -m venv .venv
.venv\Scripts\python.exe -m pip install auditrail
.venv\Scripts\auditrail.exe demo
.venv\Scripts\auditrail.exe verify <the path printed on that last line>
auditrail demo makes one real HTTP request (see below) and writes an unsigned ledger to your temporary directory. The
"Wrap a tool function" file further down also works with only the PyPI package. The commands that start with
python -m examples..., the sample policy policies/example_policy.yaml, pip install -e ".[dev]" and the tests need
the source checkout below: the package does not contain examples, policies, docs, tests or the Rust verifier.
From source (a git checkout)
# Linux / macOS (on Ubuntu first: sudo apt install git python3-venv python3-pip)
git clone https://github.com/x91652-pixel/auditrail
cd auditrail
python3 -m venv .venv && . .venv/bin/activate
pip install -e . # just to try it; use pip install -e ".[dev]" only if you want to run the tests
# Windows PowerShell -- no activation needed, so the execution policy does not matter
git clone https://github.com/x91652-pixel/auditrail
cd auditrail
python -m venv .venv
.venv\Scripts\python.exe -m pip install -e .
# then run everything below as .venv\Scripts\python.exe -m ... and .venv\Scripts\python.exe -m auditrail.cli ...
# (activating with .venv\Scripts\Activate.ps1 may be blocked by your PowerShell execution policy -- on a Windows client
# with no policy set, scripts are usually blocked; if you want to activate, run
# Set-ExecutionPolicy -Scope Process Bypass in that window only)
Below, python means the virtual environment's Python (after activating it, python works on Linux too; without
activating, use .venv/bin/python and .venv/bin/auditrail; on Windows .venv\Scripts\python.exe and
.venv\Scripts\auditrail.exe; a system python3 does not see the packages) and auditrail is the command pip install
created in it.
Two demos, which write to different places (the first needs the checkout; the second also works from the PyPI package):
# 1. the repository's example: writes examples/demo_ledger.jsonl
python -m examples.mock_demo
auditrail verify examples/demo_ledger.jsonl
# 2. the installed command: writes to your temporary directory; the last line of its output names the file
auditrail demo
auditrail verify <the path printed on that last line>
Windows without activating (same two demos):
.venv\Scripts\python.exe -m examples.mock_demo
.venv\Scripts\auditrail.exe verify examples\demo_ledger.jsonl
.venv\Scripts\auditrail.exe demo
.venv\Scripts\auditrail.exe verify <the path printed on that last line>
Both run on your machine with no API key. Each makes one real HTTP request (to https://api.github.com/zen, with
a 3 second timeout) to exercise an external-communication tool; if you are offline that step is skipped and
everything else still runs. They exercise an allowed pipeline, a trifecta block, a signed agent message (and a
rejected forged or replayed one), and tamper detection.
The demos write an unsigned ledger (*.jsonl is git-ignored, so the file exists only after you run the demo).
verify therefore reports what it did not check: signatures, anchors and heartbeats.
Wrap a tool function (run from any directory once the package is installed, from PyPI or from source; this is the whole file):
from auditrail import Guard, Ledger, PolicyEngine
# a minimal policy, inline so nothing else is needed
POLICY = {
"version": "quickstart",
"agents": {"hr-screener": {"allowed_tools": ["read_resume"],
"tool_categories": {"read_resume": ["data_access"]}}},
"default": {"allowed_tools": [], "enforce_lethal_trifecta": True},
}
guard = Guard(PolicyEngine(POLICY), Ledger("evidence.jsonl")) # no signer: hash-chained, not signed
@guard.guarded_tool("read_resume", categories=["data_access"])
def read_resume(candidate_id: str) -> dict:
return {"candidate": candidate_id, "years_experience": 7} # stand-in for your real function
session = guard.session(agent_id="hr-screener")
print(read_resume(session, "cand-001")) # policy-checked and logged
Then auditrail verify evidence.jsonl (.venv\Scripts\auditrail.exe verify evidence.jsonl on Windows without
activating). See examples/mock_demo.py for the full walkthrough
(including the trifecta-block and tamper-detection scenarios), policies/example_policy.yaml
for a policy file, and examples/real_agent_demo.py for a live example with Claude
doing real tool-use (ANTHROPIC_API_KEY required).
Architecture
See docs/architecture.md for the full data-flow diagram and the reasoning behind two specific design choices: why the policy engine reasons about tool categories rather than tool identities, and why the ledger stores argument/result digests rather than raw payloads.
Where this fits
auditrail is a runtime primitive, not a platform. The analogy is the change logs infrastructure teams keep -- a boring, verifiable record -- applied to wrapped agent tool calls (an analogy, not a compliance claim). It's deliberately narrow and meant to compose with things that already exist:
| If you need... | Consider |
|---|---|
| Prompt-injection / jailbreak detection on inputs and outputs | A prompt-injection / jailbreak detection product |
| Agent discovery, fleet-wide policy across SaaS platforms | An agent governance platform |
| A certifiable standard for an agent | AIUC-1, a standard for AI agents with third-party certification (per its own pages: accredited auditors and annual re-certification) |
| An org-wide AI risk/compliance workflow | An AI governance / risk-management software product |
| A tamper-evident record of the wrapped tool calls in one agent session, that you own and can verify yourself, in your own code, with no vendor lock-in | auditrail |
Status
v0.3 -- early, as far as we know not yet used in production, no independent security review. The format is a draft: expect changes until a second team has implemented it. Issues, and especially "this broke when I tried it against a real agent" reports, are exactly what this stage needs. See ROADMAP.md.
Development
pip install -e ".[dev]"
pytest -v
All 931 Python tests (a few run only on POSIX and several only on Windows, the others everywhere) are deterministic and require no network access or API key
(one demo scenario attempts a single real HTTP call and skips gracefully
if you're offline). The Rust parts have their own tests: cd verifier && cargo test
(runs every conformance vector through the independent verifier) and
cd dashboard && cargo test. Python tests that compare against the Rust verifier are
skipped until you run cd verifier && cargo build --locked --release.
The two verifiers are also fuzzed against each other (tests/test_fuzz.py, Hypothesis; raise the sample size with
AUDITRAIL_FUZZ_EXAMPLES=500): random ledgers, field edits, raw byte corruption, re-hash attacks, truncation and garbage
must give the same status in Python and Rust, and never a crash. tests/test_reader_agreement.py pins down the odd
inputs (non-UTF-8, -0, true as an integer, signatures with a space, ...) one by one.
To see the attack simulation (a fictional logistics company with five
agents, 11 normal operations and 19 attack scenarios: 15 defended, of which #17 and #18 hold only with the opt-in strict / state_change settings, and 4 documented gaps (#3, #11, #13, #19)):
python -m examples.logistics_sim.run
Results and the reasoning behind each scenario are in docs/simulation.md.
Contributing
Issues and PRs welcome. Please read SECURITY.md first -- if you're adding a feature, say explicitly in the PR description which row of that table it changes.
License
Metadata
Release files for auditrail 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| auditrail-0.3.0.tar.gz | 116.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| auditrail-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 215.9 kB
Release files / auditrail-0.3.0.tar.gz
| Download URL | auditrail-0.3.0.tar.gz |
|---|---|
| Size | 116.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7f5b027442dbcc84a86528d3fc84bd74eab48cae4ddcba36bc2fe093e6428677
|
|
BLAKE2b-256 checksum How to use checksums |
c65a71f5aa6efbfa440e97e971f788907f0bb424bd8a714a996490af95441f76
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Release files / auditrail-0.3.0-py3-none-any.whl
| Download URL | auditrail-0.3.0-py3-none-any.whl |
|---|---|
| Size | 99.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3832f969b20b94b07c26ca9b33ad61f2251299594da5f46e6ca8ba05111ea888
|
|
BLAKE2b-256 checksum How to use checksums |
2c8fade8fe32510e1487f534e1d2cac897965ba868bc58504993da9d7d31da25
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Security for auditrail 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.