auditant
Compliance system of record for AI agents. Every action your agents take is hash chained, policy checked and externally countersigned, and anyone who does not trust you can verify it offline.
Two lines
import auditant
auditant.init(api_key="ak_…", log_id="t_…/prod", agent_id="underwriter")
With OpenTelemetry present, init() registers a span processor and
activates whichever supported OpenInference instrumentors are installed:
LangChain (which is what sees LangGraph), CrewAI, LlamaIndex, OpenAI,
Anthropic and Google GenAI. LLM calls and tool executions then become
chained audit events automatically, as hashes and never payloads. When the
OpenAI Agents SDK is importable, init() registers on its trace processor
bus as well. Install the extras for what you run:
pip install "auditant[openinference]" # every supported instrumentor
pip install "auditant[langchain]" # or one at a time: [crewai], [llama-index],
# [google-genai], [openai-agents], [gateway]
What each event carries:
- model calls: the model that answered (not the alias asked for), the provider, input, output, cache read, cache write and reasoning tokens. The counts are disjoint: input is fresh input only and output is visible output only, with cache reads, cache writes and reasoning in their own counts, so pricing each count once never bills a token twice. A price is kept only when the span reports one; token counts alone are marked unpriced, never sent as $0, and priced by the control plane. With the Agents SDK and the OpenAI instrumentor both on, one request is one model call: the instrumentor's event is kept, since it always names the model.
- tool calls: the tool name, whether it failed, the error's type, and
SHA-256 hashes of the arguments and the result. Error and status text
often quote the input and the chain is append only, so the text itself is
sent only when you opt in with
init(error_text=True)orAUDITANT_ERROR_TEXT=1. - the session the app named on the span (
session.id) when there is one, so each conversation in a long lived server stays its own session.
handle.activated lists what actually turned on. An instrumentor installed
for a framework that is not present counts as not activated: the SDK
never reports coverage of code it cannot see.
Without OpenTelemetry, init() still gives you a session on the chain
(lifecycle and heartbeats, so a quiet agent is distinguishable from a dead
emitter), the manual record() API, and the synchronous policy check:
handle = auditant.init(api_key="ak_…", log_id="t_…/prod", agent_id="underwriter")
verdict = handle.decide({"action": "wire_transfer", "amount": 50_000,
"input": {"to": "acct 7"}}) # hashed into the record, never sent
# {'effect': 'pending_approval', ...} → a human approves in the dashboard
# or from Slack, and the same call then returns 'allow'.
# async hosts: await handle.adecide({...})
Every decide() chains a policy_decision event before anything happens;
that is what the approval queue is built from. The policy service failing
means a recorded enforcement gap, never a silent allow.
Delivery
Evidence writes never block your agent. Events queue, batch and send in the
background. A dropped connection or a 5xx retries with backoff; a 429 waits
the server's own retryAfterSeconds. A batch the server calls malformed is
split until the bad event is alone, so one event cannot take the rest of its
batch with it. Anything that still cannot be delivered (an outage, a refused
key, a log the key cannot write) is spilled to a file and logged once with
what to check, never dropped quietly. The spill directory is private to the
user running the agent: AUDITANT_SPILL_DIR when set, else
~/.cache/auditant, else a directory named for your user id under the
system temp folder. A directory another user owns is refused, and replay
reads only a file this user owns that nobody else can write.
handle.flush() # scripts, jobs, serverless: before the process exits
handle.stats() # {'sent': 412, 'spilled': 0, 'queueFull': 0, 'rejected': {}, 'pending': 0}
handle.replay_spill() # after fixing the cause; returns how many it queued
Replay is manual on purpose: a batch that timed out may have landed anyway,
so a replay can write an event twice. A duplicate keeps its original
eventId and is explainable; a gap is not.
Several processes can spill to the same directory and replay from it at once (POSIX; Windows has no advisory locks). A replay claims the spill file under the same lock every write takes, so a write either lands before the claim or starts a new spill file, and is never deleted unsent. A file left behind by a replay that stopped part way (a crash, a killed job) is adopted by the next replay rather than stranded.
Block, approve, resume, in your framework
Capture is automatic; enforcement is one more line, because something has
to stand in front of the tool. Both shims ask decide() before a guarded
tool runs, chain the answer, and express a hold in the framework's own
pause:
# LangGraph: interrupt() plus a checkpointer
from auditant.langgraph import guard
tools = guard(handle, [wire_transfer, lookup_balance], amount="amount")
out = graph.invoke(state, config) # held ⇒ out["__interrupt__"]
# …a named human approves in the dashboard or from Slack…
out = graph.invoke(Command(resume=True), config) # re-asks, walks through
# OpenAI Agents SDK: needs_approval
from auditant.openai_agents import guard, resolve
agent = Agent(name="underwriter", tools=guard(handle, [wire_transfer], amount="amount"))
result = await Runner.run(agent, "wire $50,000 to acct 7")
while result.interruptions:
answer = await resolve(handle, result) # answered from the chain
if not answer.ready: await asyncio.sleep(30); continue
result = await Runner.run(agent, answer.state)
The resume value is never the approval: what lets the action through is
the human's answer on the chain, matched by session and action server side.
A refusal reaches the model in the policy's words rather than as a crash,
and is chained as blocked. Monitor mode rules and enforce=False record
everything and stop nothing.
The examiner's side
Every exported bundle is self verifying: it embeds a dependency free
verifier (node verify.mjs bundle.json) that recomputes the chain and
validates every countersignature. No account, no network, nothing from us.
Metadata
Release files for auditant 0.4.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 | |
|---|---|---|---|---|
| auditant-0.4.0.tar.gz | 70.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| auditant-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 117.8 kB
Release files / auditant-0.4.0.tar.gz
| Download URL | auditant-0.4.0.tar.gz |
|---|---|
| Size | 70.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1ef7672318fdc56a6f39b812246e657d2b2b3c88b01a19eddb9867d07011d8d1
|
|
BLAKE2b-256 checksum How to use checksums |
268a8fc30bfaafe7033eb56478e205dab65e84b27b1b15c9424987dd5bc406bd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.1
|
Release files / auditant-0.4.0-py3-none-any.whl
| Download URL | auditant-0.4.0-py3-none-any.whl |
|---|---|
| Size | 47.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
247b4ac88a9e6f9f6b3f3d04224cfa9c09bb3bf01e1bd810f078d21e0a5d59ac
|
|
BLAKE2b-256 checksum How to use checksums |
5e1aa6908582a4843c256d4f7c4591d20e6dcf17391e1de42cfd1211e91f386a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.1
|
Security for auditant 0.4.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.