Metadata-Version: 2.4
Name: taintward
Version: 0.3.0
Summary: Runtime data-exposure control for AI agents: PII and secret taint tracking, redaction and policy checks on OpenTelemetry traces
Author: Divyanshu Choudhury
Maintainer: Divyanshu Choudhury
License-Expression: Apache-2.0
Project-URL: Homepage, https://taintward.vercel.app
Project-URL: Documentation, https://dcchoudhury15.github.io/Taintward/
Project-URL: Source, https://github.com/DCchoudhury15/Taintward
Project-URL: Issues, https://github.com/DCchoudhury15/Taintward/issues
Project-URL: Changelog, https://github.com/DCchoudhury15/Taintward/blob/main/CHANGELOG.md
Keywords: ai-agents,llm,security,privacy,pii,redaction,taint-tracking,data-loss-prevention,opentelemetry,mcp,langchain,compliance
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: System :: Monitoring
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: opentelemetry-api>=1.44
Requires-Dist: opentelemetry-sdk>=1.44
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.44
Requires-Dist: presidio-analyzer>=2.2
Requires-Dist: presidio-anonymizer>=2.2
Requires-Dist: spacy>=3.8
Requires-Dist: cymem<=2.0.13
Requires-Dist: preshed<=3.0.13
Requires-Dist: murmurhash<=1.0.15
Requires-Dist: srsly<=2.5.3
Requires-Dist: cryptography>=42
Requires-Dist: requests>=2.34
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Provides-Extra: llm
Requires-Dist: openai; extra == "llm"
Requires-Dist: anthropic; extra == "llm"
Provides-Extra: litellm
Requires-Dist: litellm>=1.100; extra == "litellm"
Provides-Extra: mcp
Requires-Dist: mcp>=2.3; extra == "mcp"
Requires-Dist: PyYAML>=6; extra == "mcp"
Requires-Dist: uvicorn>=0.30; extra == "mcp"
Provides-Extra: postgres
Requires-Dist: psycopg[binary]>=3.2; extra == "postgres"
Provides-Extra: redis
Requires-Dist: redis>=5; extra == "redis"
Provides-Extra: aws
Requires-Dist: boto3>=1.34; extra == "aws"
Provides-Extra: gcp
Requires-Dist: google-cloud-kms>=3; extra == "gcp"
Provides-Extra: langchain
Requires-Dist: langchain-core>=1.7; extra == "langchain"
Requires-Dist: langchain>=1.4; extra == "langchain"
Requires-Dist: langgraph>=1.2; extra == "langchain"
Provides-Extra: openai-agents
Requires-Dist: openai-agents>=0.24; extra == "openai-agents"
Provides-Extra: crewai
Requires-Dist: crewai>=1.15; extra == "crewai"
Provides-Extra: llamaindex
Requires-Dist: llama-index-core>=0.14; extra == "llamaindex"
Provides-Extra: pydantic-ai
Requires-Dist: pydantic-ai-slim>=2.60; extra == "pydantic-ai"
Provides-Extra: google-adk
Requires-Dist: google-adk>=1.14; extra == "google-adk"
Dynamic: license-file

<img alt="taintward: customer data stays with your agent, the LLM only ever sees placeholders" src="https://raw.githubusercontent.com/DCchoudhury15/Taintward/v0.3.0/.github/assets/hero-light.svg" width="100%">

<p align="center">
  <a href="https://github.com/DCchoudhury15/Taintward/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/DCchoudhury15/Taintward/actions/workflows/ci.yml/badge.svg"></a>
  <a href="https://dcchoudhury15.github.io/Taintward/"><img alt="Docs" src="https://img.shields.io/badge/docs-online-b5652a"></a>
  <a href="https://github.com/DCchoudhury15/Taintward/blob/v0.3.0/LICENSE"><img alt="Apache-2.0" src="https://img.shields.io/badge/license-Apache--2.0-8b5e3c"></a>
  <img alt="Python 3.11+" src="https://img.shields.io/badge/python-3.11%2B-8b5e3c">
  <img alt="TypeScript" src="https://img.shields.io/badge/typescript-AI%20SDK-8b5e3c">
</p>

<p align="center">
  <a href="https://dcchoudhury15.github.io/Taintward/"><b>Docs</b></a> ·
  <a href="https://github.com/DCchoudhury15/Taintward/blob/v0.3.0/docs/quickstart.md"><b>Quickstart</b></a> ·
  <a href="https://github.com/DCchoudhury15/Taintward/blob/v0.3.0/docs/threat-model.md"><b>Threat model</b></a> ·
  <a href="https://taintward.vercel.app"><b>Website</b></a>
</p>

<br>

An agent reads a customer record, asks the model about it, calls an API and
writes a log line. Each step is fine on its own. Together they send an
email address to the LLM provider, an SSN to a third party and an API key to
your log storage, and nothing in a normal agent stack notices.

Taintward notices. It finds sensitive values in every tool call and model
call, follows them through the whole run, and changes them for **where they
are going** before they leave.

<img alt="One record, four destinations: placeholders for the LLM, a reversible token for your database, a one-way mask for external APIs, logs and RAG" src="https://raw.githubusercontent.com/DCchoudhury15/Taintward/v0.3.0/.github/assets/journey-light.svg" width="100%">

## The round trip

Masks break agents: a model that reads `Remind [PII] ([PII])` cannot write
the email. Placeholders do not. The model gets `<PERSON_1>` and `<EMAIL_1>`,
does its job, and the answer comes back with the real values in it.

<img alt="Round trip: your app sends real values, the LLM sees placeholders, your app gets the real values back, the collector records the decision without raw values" src="https://raw.githubusercontent.com/DCchoudhury15/Taintward/v0.3.0/.github/assets/roundtrip-light.svg" width="100%">

The same value always gets the same placeholder within a run, so the model
can still tell two people apart. If it rewrites one (`[EMAIL_1]`,
`EMAIL_1`), it is still restored. The map from placeholder to value never
leaves your process.

## In numbers

<img alt="About 8 ms per tool call, 0 raw values reached the model, 3 in 4 answers fully restored, about 4000 spans per second per core" src="https://raw.githubusercontent.com/DCchoudhury15/Taintward/v0.3.0/.github/assets/stats-light.svg" width="100%">

<img alt="Detector coverage: reliable for email, IP address, valid cards and IBANs, AWS keys, GitHub tokens, JWTs, private keys; partial for English names, US SSNs, US phones, street addresses; weak for names in other languages, non-US phones, national ids, plain passwords" src="https://raw.githubusercontent.com/DCchoudhury15/Taintward/v0.3.0/.github/assets/coverage-light.svg" width="100%">

Measured on one laptop, and on public datasets the rules were never tuned
on. How: [benchmark](https://github.com/DCchoudhury15/Taintward/blob/v0.3.0/docs/benchmark.md) ·
[placeholders with real models](https://github.com/DCchoudhury15/Taintward/blob/v0.3.0/docs/placeholders-eval.md).

## Where it runs

<img alt="Three ways in: SDK in your code (LangChain, CrewAI, LlamaIndex, Pydantic AI, Google ADK, LiteLLM and more), gateway in front of MCP servers, collector next to your traces; all feed a signed evidence log" src="https://raw.githubusercontent.com/DCchoudhury15/Taintward/v0.3.0/.github/assets/deploy-light.svg" width="100%">

## See it work

```bash
git clone https://github.com/DCchoudhury15/Taintward && cd Taintward
docker compose up --build
```

Open http://localhost:16686 and pick `taintward-hr-bot`. A demo HR agent
leaks a fake SSN and AWS key to an LLM, an external API and a log. All three
calls show up flagged, and the real values are nowhere in the traces.

## Add it to your agent

**Python** · LangChain, LangGraph, OpenAI Agents SDK, CrewAI, LlamaIndex,
Pydantic AI, Google ADK, LiteLLM, OpenAI, Anthropic

```bash
pip install "taintward[langchain] @ git+https://github.com/DCchoudhury15/Taintward"
python -m spacy download en_core_web_sm
```

```python
import taintward as tw

tw.init("my-agent", destinations={"search_db": "internal", "send_email": "external"})
tools = tw.wrap_langchain_tools(tools)
model = tw.protect_langchain_model(model)      # or tw.protect_openai(OpenAI())

with tw.run():
    agent.invoke(...)
```

Every framework gets the same two calls, one for its tools and one for its
model: `tw.wrap_crewai_tools(...)` and `tw.protect_crewai_llm(...)`,
`tw.wrap_adk_tools(...)` and `tw.protect_adk_model(...)`, and so on
([the full table](https://github.com/DCchoudhury15/Taintward/blob/v0.3.0/docs/integrations.md#install)).

**TypeScript** · Vercel AI SDK, `generateText` and `streamText`

```ts
import { wrapLanguageModel } from "ai";
import { taintwardMiddleware } from "taintward/ai-sdk";

const model = wrapLanguageModel({
  model: openai("gpt-4.1"),
  middleware: taintwardMiddleware(),
});
```

**MCP** · no code changes: put `taintward-gateway --config gateway.yaml` in
your MCP client config in place of the real server.

**LiteLLM proxy** · no code changes: add
`taintward.sdk.integrations.litellm.TaintwardGuardrail` as a guardrail. The
model providers get placeholders, and the apps behind the proxy still get
answers with real values.

More: [Python integrations](https://github.com/DCchoudhury15/Taintward/blob/v0.3.0/docs/integrations.md) ·
[TypeScript SDK](https://github.com/DCchoudhury15/Taintward/blob/v0.3.0/packages/taintward-js/README.md) ·
[MCP gateway](https://github.com/DCchoudhury15/Taintward/blob/v0.3.0/docs/mcp-gateway.md) ·
[Kubernetes](https://github.com/DCchoudhury15/Taintward/blob/v0.3.0/docs/deploy-kubernetes.md)

## Where it stops

- It protects the calls that go through it. A client or tool you did not
  wrap is not seen.
- It does not stop prompt injection. It limits what an injected
  instruction can get out.
- A value that is encoded, split across fields or in an unknown format can
  slip past detection.

Every known gap and a hardening checklist: [threat model](https://github.com/DCchoudhury15/Taintward/blob/v0.3.0/docs/threat-model.md).

## Background

Built on the formal model from
[AgentRaft](https://arxiv.org/abs/2603.07557) (Lin et al., 2026), which
found data over-exposure in 57% of the agent call chains it studied.
AgentRaft runs offline and asks three LLMs to judge each step. Taintward
runs live, with no LLM in the decision:
[how the parts map](https://github.com/DCchoudhury15/Taintward/blob/v0.3.0/docs/agentraft-mapping.md). It was called AgentWard
before 0.2.0.

<details>
<summary><b>Development</b></summary>

```bash
python -m pytest -q                                   # Python
cd collector/processor/taintpolicy && go test ./...   # Go collector processor
cd packages/taintward-js && npm ci && npm test        # TypeScript
```

[Contributing](https://github.com/DCchoudhury15/Taintward/blob/v0.3.0/CONTRIBUTING.md) · [Security](https://github.com/DCchoudhury15/Taintward/blob/v0.3.0/SECURITY.md) · [Changelog](https://github.com/DCchoudhury15/Taintward/blob/v0.3.0/CHANGELOG.md)

</details>

<sub>Apache-2.0</sub>
