Metadata-Version: 2.4
Name: auditant
Version: 0.4.0
Summary: Compliance system of record for AI agents: tamper evident capture with auditant.init() (LangChain and LangGraph, CrewAI, LlamaIndex, OpenAI, Anthropic, Google GenAI, OpenAI Agents SDK), OTLP, and real time policy checks
License: Apache-2.0
Project-URL: Homepage, https://auditant.co
Project-URL: Documentation, https://dev.auditant.co/docs
Keywords: ai-agents,audit-trail,tamper-evident,compliance,ai-governance,opentelemetry,eu-ai-act
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security
Classifier: Topic :: System :: Logging
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.27
Provides-Extra: gateway
Requires-Dist: litellm>=1.96; extra == "gateway"
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.25; extra == "otel"
Requires-Dist: opentelemetry-sdk>=1.25; extra == "otel"
Provides-Extra: openai-agents
Requires-Dist: openai-agents>=0.1; extra == "openai-agents"
Provides-Extra: openinference
Requires-Dist: opentelemetry-api>=1.25; extra == "openinference"
Requires-Dist: opentelemetry-sdk>=1.25; extra == "openinference"
Requires-Dist: openinference-instrumentation-langchain>=0.1; extra == "openinference"
Requires-Dist: openinference-instrumentation-crewai>=0.1; extra == "openinference"
Requires-Dist: openinference-instrumentation-llama-index>=4.0; extra == "openinference"
Requires-Dist: openinference-instrumentation-openai>=0.1; extra == "openinference"
Requires-Dist: openinference-instrumentation-anthropic>=0.1; extra == "openinference"
Requires-Dist: openinference-instrumentation-google-genai>=0.1; extra == "openinference"
Provides-Extra: langchain
Requires-Dist: opentelemetry-api>=1.25; extra == "langchain"
Requires-Dist: opentelemetry-sdk>=1.25; extra == "langchain"
Requires-Dist: openinference-instrumentation-langchain>=0.1; extra == "langchain"
Provides-Extra: crewai
Requires-Dist: opentelemetry-api>=1.25; extra == "crewai"
Requires-Dist: opentelemetry-sdk>=1.25; extra == "crewai"
Requires-Dist: openinference-instrumentation-crewai>=0.1; extra == "crewai"
Provides-Extra: llama-index
Requires-Dist: opentelemetry-api>=1.25; extra == "llama-index"
Requires-Dist: opentelemetry-sdk>=1.25; extra == "llama-index"
Requires-Dist: openinference-instrumentation-llama-index>=4.0; extra == "llama-index"
Provides-Extra: google-genai
Requires-Dist: opentelemetry-api>=1.25; extra == "google-genai"
Requires-Dist: opentelemetry-sdk>=1.25; extra == "google-genai"
Requires-Dist: openinference-instrumentation-google-genai>=0.1; extra == "google-genai"
Dynamic: license-file

# 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

```python
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:

```sh
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:

```python
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.

```python
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:

```python
# 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
