Skip to main content

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) or AUDITANT_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.

Docs: https://dev.auditant.co/docs

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)

Source distribution for auditant 0.4.0
File Size Uploaded Provenance
auditant-0.4.0.tar.gz 70.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for auditant 0.4.0
File Interpreter ABI Platform
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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page