Metadata-Version: 2.2
Name: pyveil
Version: 0.3.0
Summary: Local PII and secret redaction for Python LLM apps, AI agents, tool calls, and MCP
Author-email: Hyeon Sang Jeon <wingnut0310@gmail.com>
Maintainer-email: Hyeon Sang Jeon <wingnut0310@gmail.com>
License: MIT
Project-URL: Homepage, https://hyeonsangjeon.github.io/pyveil/
Project-URL: Documentation, https://hyeonsangjeon.github.io/pyveil/manual.html
Project-URL: Repository, https://github.com/hyeonsangjeon/pyveil
Project-URL: Issues, https://github.com/hyeonsangjeon/pyveil/issues
Project-URL: Changelog, https://github.com/hyeonsangjeon/pyveil/releases
Project-URL: Security, https://github.com/hyeonsangjeon/pyveil/security/policy
Project-URL: Guides, https://hyeonsangjeon.github.io/pyveil/guides/
Project-URL: Evaluation, https://hyeonsangjeon.github.io/pyveil/evaluation.html
Project-URL: Funding, https://github.com/sponsors/hyeonsangjeon
Project-URL: Support, https://github.com/hyeonsangjeon/pyveil/discussions
Keywords: agent-native,agent-security,agents,ai-agents,ai-security,anthropic,data-protection,data-masking,de-identification,guardrails,llm,llm-privacy,llm-security,logs,masking,mcp,mcp-security,memory,observability,pii,pii-redaction,privacy,prompts,prompt-redaction,redaction,runtime-redaction,openai,anonymization,security,sensitive-data,tool-calls,traces
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: azure-openai
Requires-Dist: openai<3.0.0,>=1.66.0; extra == "azure-openai"
Requires-Dist: python-dotenv>=1.0.0; extra == "azure-openai"
Requires-Dist: PyYAML>=6.0.0; extra == "azure-openai"
Provides-Extra: openai
Requires-Dist: openai<3.0.0,>=2.45.0; python_version >= "3.9" and extra == "openai"
Requires-Dist: python-dotenv>=1.0.0; extra == "openai"
Requires-Dist: PyYAML>=6.0.0; extra == "openai"
Provides-Extra: anthropic
Requires-Dist: anthropic<1.0.0,>=0.116.0; python_version >= "3.9" and extra == "anthropic"
Requires-Dist: python-dotenv>=1.0.0; extra == "anthropic"
Requires-Dist: PyYAML>=6.0.0; extra == "anthropic"
Provides-Extra: ollama
Requires-Dist: ollama<1.0.0,>=0.6.2; extra == "ollama"
Requires-Dist: python-dotenv>=1.0.0; extra == "ollama"
Requires-Dist: PyYAML>=6.0.0; extra == "ollama"
Provides-Extra: dev
Requires-Dist: build>=1.2.0; extra == "dev"
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: black>=23.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: twine>=5.0.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Requires-Dist: ollama<1.0.0,>=0.6.2; extra == "dev"
Requires-Dist: openai<3.0.0,>=2.45.0; python_version >= "3.9" and extra == "dev"
Requires-Dist: anthropic<1.0.0,>=0.116.0; python_version >= "3.9" and extra == "dev"
Requires-Dist: python-dotenv>=1.0.0; extra == "dev"
Requires-Dist: PyYAML>=6.0.0; extra == "dev"
Requires-Dist: types-PyYAML>=6.0.0; extra == "dev"
Provides-Extra: test
Requires-Dist: pytest>=7.0.0; extra == "test"
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
Requires-Dist: python-dotenv>=1.0.0; extra == "test"
Requires-Dist: PyYAML>=6.0.0; extra == "test"

# pyveil: PII and secret redaction for Python AI agents

<p align="center">
  <strong>Stop sensitive data before it reaches an LLM, tool, MCP resource, memory store, log, or trace.</strong>
</p>

<p align="center">
  <a href="https://pypi.org/project/pyveil/"><img alt="PyPI version" src="https://img.shields.io/pypi/v/pyveil?style=flat-square"></a>
  <a href="https://github.com/hyeonsangjeon/pyveil/actions/workflows/tests.yml"><img alt="Tests" src="https://github.com/hyeonsangjeon/pyveil/actions/workflows/tests.yml/badge.svg"></a>
  <a href="https://www.python.org/"><img alt="Python 3.8 to 3.14" src="https://img.shields.io/badge/python-3.8%20to%203.14-3776AB?style=flat-square"></a>
  <img alt="Zero core dependencies" src="https://img.shields.io/badge/core%20dependencies-zero-111827?style=flat-square">
  <img alt="Typed package" src="https://img.shields.io/badge/typed-py.typed-7C3AED?style=flat-square">
  <a href="https://hyeonsangjeon.github.io/pyveil/evaluation.html"><img alt="Synthetic evaluation: 39 cases passing" src="https://img.shields.io/badge/synthetic%20evaluation-39%20cases%20passing-4ade80?style=flat-square"></a>
  <a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/badge/license-MIT-green?style=flat-square"></a>
</p>

<p align="center">
  <a href="https://hyeonsangjeon.github.io/pyveil/manual.html">Documentation</a> &middot;
  <a href="https://hyeonsangjeon.github.io/pyveil/guides/">Guides</a> &middot;
  <a href="https://github.com/hyeonsangjeon/pyveil/blob/main/docs/privacy-replay.md">Privacy Replay</a> &middot;
  <a href="https://hyeonsangjeon.github.io/pyveil/evaluation.html">Evaluation</a> &middot;
  <a href="https://pypi.org/project/pyveil/">PyPI</a> &middot;
  <a href="https://github.com/hyeonsangjeon/pyveil/blob/main/docs/cookbook.md">Cookbook</a> &middot;
  <a href="https://github.com/hyeonsangjeon/pyveil/blob/main/docs/redaction-reference.md">Detection reference</a> &middot;
  <a href="https://github.com/hyeonsangjeon/pyveil/discussions">Support</a> &middot;
  <a href="https://github.com/hyeonsangjeon/pyveil/security">Security</a>
</p>

`pyveil` is local, dependency-free redaction middleware for LLM applications and
AI agents. It replaces high-confidence PII and credentials with deterministic,
scoped HMAC placeholders before data crosses an application boundary.

| Raw application context | Context sent to the model |
| --- | --- |
| `Email alice@example.com` | `Email [EMAIL:a13f7c91b0d2]` |
| `api_key: sk-proj-...` | `api_key: [API_KEY:38ded98a17e7]` |
| `Authorization: Bearer ...` | `[AUTH_HEADER:4fe2926b7d20]` |

No network calls in the redactor. No reversible vault. No raw values in findings by default.

## Five-Minute File Run

Start in an empty working directory with Python 3.8+ and pip available:

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install pyveil==0.3.0
python -m pyveil init --example
python -m pyveil run
```

This writes an input file, then processes it with the **real installed redactor**.
The input is explicitly synthetic; the result is computed, not mocked. There is
no model response, API account, or provider call in this path. Installation,
configuration, disk I/O, redaction, and receipt creation are included in the
five-minute check. Python installation, model downloads, and provider calls are not.

The command prints the paths to `redacted.json` and `receipt.json` under
`.pyveil-runs/<run-id>/`. Open the output: the email and authorization value are
replaced, while ticket `42` and `urgent: false` remain unchanged.

**Use your own file:** replace `init --example` with the following when
initializing a new working directory. No input is copied or overwritten.

```bash
python -m pyveil init --input /path/to/request.json --input-format json
python -m pyveil run
```

## One Config, Repeatable Output

`pyveil.json` is the execution ledger: a normal JSON file naming the input,
output directory, package version, channel, masking level, size limit, and
secret reference. Change it, then run the same command. All relative paths
are resolved from that file, not the shell's working directory.

Run `python -m pyveil run` twice. The same input, config, private key, and pinned
version produce identical output bytes in **different run directories**. Receipts
record hashes and counts, not raw input or key values. Keep `.pyveil.key` private:
it is a freshly generated local key, not a shared demo secret. Never commit it
or assume that a redacted file is automatically safe to publish.

[Configuration, Windows commands, failure cases, and migration](https://github.com/hyeonsangjeon/pyveil/blob/main/docs/file-runs.md)
&middot; [Web manual](https://hyeonsangjeon.github.io/pyveil/manual.html#file-runs)

## Replay Six Agent Boundaries

For a separate synthetic regression tour, run the installed command to exercise
prompt, tool-call, MCP, memory, log, and trace protection:

```bash
python -m pyveil replay --format markdown
# same source example with uv: uv run python examples/privacy_replay.py --format markdown
```

| Task | Protected channels | Observable gate | Start here |
| --- | --- | --- | --- |
| Stop sensitive context before execution | `prompt.input`, `tool.call.arguments` | Findings are replaced, benign fields survive | [Prompt and tool cases](docs/privacy-replay.md#what-runs) |
| Stop sensitive context before reuse | `mcp.resource.content`, `memory.write` | Nested shape survives with zero synthetic leaks | [MCP and memory cases](docs/privacy-replay.md#what-runs) |
| Stop sensitive context before export | `log.record`, `trace.span.attributes` | Logs and span attributes pass raw-value-free gates | [Log and trace cases](docs/privacy-replay.md#what-runs) |

The built-in replay uses synthetic fixtures, not your files or a live model.
It currently produces **6/6 passing cases, 10 findings, and
0 surviving synthetic markers**. It also runs a resume-safety pass that
re-crosses each boundary with already-redacted state, returning **0** markers
across **6/6** cases. Its JSON contains only case IDs, channels,
counts, booleans, and input/output hashes. Read the
[Privacy Boundary Replay guide](docs/privacy-replay.md) for the report schema,
failure behavior, clean-wheel CI proof, and limits.

## Start With Your Integration

Choose the boundary you need and begin with a runnable example. The keyless
paths use synthetic input and stop before a provider request.

| You use | Install | Start here | Protected boundary |
| --- | --- | --- | --- |
| OpenAI Agents SDK | `pip install pyveil openai-agents` | [Pre-dispatch Runner wrapper](https://github.com/hyeonsangjeon/pyveil/blob/main/examples/openai_agents_guardrail.py) | Input is redacted before `Runner.run`; Python 3.10+ for the current SDK |
| LiteLLM SDK or Proxy | `pip install pyveil litellm` or `pip install pyveil "litellm[proxy]"` | [SDK wrapper and Proxy pre-call hook](https://github.com/hyeonsangjeon/pyveil/blob/main/examples/litellm_proxy_filter.py) | Messages before `completion(...)` or proxy provider dispatch; Python 3.10+ for current LiteLLM |
| OpenAI Responses API | `pip install "pyveil[openai]"` | [Keyless contract guide](https://github.com/hyeonsangjeon/pyveil/blob/main/docs/integrations/openai.md) | Exact input before `client.responses.create(...)` |
| Anthropic / Claude | `pip install "pyveil[anthropic]"` | [Keyless contract guide](https://github.com/hyeonsangjeon/pyveil/blob/main/docs/integrations/anthropic.md) | Exact content before `client.messages.create(...)` |
| Azure OpenAI | `pip install "pyveil[azure-openai]"` | [Environment and YAML example](https://github.com/hyeonsangjeon/pyveil/blob/main/examples/azure_openai.py) | Prompt before the Azure Responses API request |
| Ollama | `pip install "pyveil[ollama]"` | [Local end-to-end guide](https://github.com/hyeonsangjeon/pyveil/blob/main/docs/integrations/ollama.md) | Prompt before the local model call |
| MCP | `pip install pyveil` | [Server wrapper](https://github.com/hyeonsangjeon/pyveil/blob/main/examples/mcp_server_wrapper.py) | Tool results and resource content before agent context |
| FastAPI, logs, or memory | `pip install pyveil` | [Cookbook](https://github.com/hyeonsangjeon/pyveil/blob/main/docs/cookbook.md) | Request payloads, log records, and memory writes |

Before production use, confirm the supported [detection shapes](https://github.com/hyeonsangjeon/pyveil/blob/main/docs/redaction-reference.md)
and read the [security contract](https://github.com/hyeonsangjeon/pyveil/blob/main/SECURITY.md). The examples demonstrate where
to place the boundary; they do not claim perfect PII recall or compliance.

### OpenAI Agents And LiteLLM, Side By Side

| | OpenAI Agents SDK | LiteLLM Python SDK | LiteLLM Proxy |
| --- | --- | --- | --- |
| Detection point | Before `Runner.run` | Before `litellm.completion` | `async_pre_call_hook` before provider dispatch |
| Input | String or structured agent input | Message list | `data["messages"]` when list-valued |
| Safe output | Same shape passed to `Runner.run` | Redacted message list passed to `completion(...)` | Payload copy with redacted `messages` |
| Bypass risk | Direct `Runner.run` calls | Direct `completion(...)` calls | Non-message payloads and fields |
| Example limit | Does not cover later tools, memory, logs, or traces | Does not cover other LiteLLM APIs | Only completion-style `messages` are handled |

See the [full comparison](https://github.com/hyeonsangjeon/pyveil/blob/main/docs/integrations/openai-agents-vs-litellm.md)
for install commands, failure behavior, expected output, detector scope, and
security limitations.

## Protect An LLM Call

Put `pyveil` immediately before the provider call. The same code works with
OpenAI, Azure OpenAI, Anthropic, Gemini, LiteLLM, or an internal gateway.

```python
from pyveil import Channel, Veil

veil = Veil.high(
    secret=b"tenant-or-run-secret",
    scope="tenant/session",
)

messages = [
    {"role": "user", "content": "Email alice@example.com about my account."},
]

safe = veil.redact_data(messages, channel=Channel.PROMPT_INPUT)
response = call_llm(safe.data)  # Your provider SDK call
```

The provider receives the same list and dictionary shape, with sensitive values
replaced before serialization or transmission.

## OpenAI And Claude: Keyless Contract-Tested Templates

Install provider-specific templates without adding either SDK to pyveil's
zero-dependency core:

```bash
pip install "pyveil[openai]"     # OpenAI Responses API
pip install "pyveil[anthropic]"  # Claude Messages API
```

Both integrations redact locally at the final SDK boundary and return the exact
provider input for inspection:

```python
from pyveil.integrations.openai import ask_openai, load_settings

settings = load_settings()
result = ask_openai(
    "Write a follow-up for alice@example.com or 010-1234-5678.",
    settings,
)

print(result.redacted_input)  # exact client.responses.create(...) input
print(result.output_text)
```

```python
from pyveil.integrations.anthropic import ask_anthropic, load_settings

settings = load_settings()
result = ask_anthropic(
    "Write a follow-up for alice@example.com or 010-1234-5678.",
    settings,
)

print(result.redacted_input)  # exact client.messages.create(...) content
print(result.output_text)
```

No API key is needed to prove either boundary:

```bash
PYVEIL_SECRET=docs-demo-secret OPENAI_MODEL=gpt-5.6-luna \
  python -m pyveil.integrations.openai --dry-run

PYVEIL_SECRET=docs-demo-secret ANTHROPIC_MODEL=claude-haiku-4-5 \
  python -m pyveil.integrations.anthropic --dry-run
```

```text
sent-to-openai:    ... [EMAIL:17c25f8a4fe3] ... [PHONE:3f6dc5a3c9f3].
sent-to-anthropic: ... [EMAIL:0b77abd1b26b] ... [PHONE:ec56e2456ba2].
provider-response: skipped (--dry-run)
```

The repository also exercises the real official SDKs through local mock HTTP
transports and asserts against the serialized `/v1/responses` and `/v1/messages`
JSON bodies. These tests use no credentials, make no network requests, and
cannot incur provider spend. A live paid API call has **not** been claimed.
Historical provider models are not a free fallback and may be retired; keep the
model ID configurable and use dry-run or mock contracts for cost-free checks.

Current OpenAI and Anthropic SDKs require Python 3.9+. The pyveil core and both
keyless dry-run paths remain compatible with Python 3.8 through 3.14. Use the
checked-in [OpenAI guide](docs/integrations/openai.md) and
[Anthropic / Claude guide](docs/integrations/anthropic.md) for configuration,
offline verification, and boundary notes.

## Ollama: Local End To End

Run a local model behind the same redaction boundary. The optional integration
uses Ollama's official Python client and defaults to `qwen3.5:4b`, a Q4_K_M
4.7B model that fits comfortably on a 16GB Apple silicon Mac with a 4K context:

```bash
pip install "pyveil[ollama]"
ollama pull qwen3.5:4b
```

```python
from pyveil.integrations.ollama import ask_ollama, load_settings

settings = load_settings()  # OLLAMA_* + PYVEIL_* environment variables
result = ask_ollama(
    "Write a follow-up for alice@example.com or 010-1234-5678.",
    settings,
)

print(result.redacted_input)  # The exact prompt sent to Ollama
print(result.output_text)     # The local model response
```

Prove the boundary without loading a model:

```bash
PYVEIL_SECRET=docs-demo-secret \
  python -m pyveil.integrations.ollama --dry-run
```

```text
mode: dry-run
model: qwen3.5:4b
host: http://127.0.0.1:11434
sent-to-ollama: Write a one-sentence support follow-up for [EMAIL:71c6727a7fa2] or [PHONE:b4b889df07ce].
findings: EMAIL=1, PHONE=1
ollama-response: skipped (--dry-run)
```

For a live local call, set `PYVEIL_SECRET` and run the module. Configuration
priority is process environment, `.env`, YAML, then defaults:

```bash
PYVEIL_SECRET=a-long-random-hmac-secret \
  python -m pyveil.integrations.ollama

python -m pyveil.integrations.ollama \
  --config examples/ollama.example.yaml --env-file .env
```

The checked-in [`.env` template](examples/ollama.env.example) and
[YAML template](examples/ollama.example.yaml) expose model, host, context,
output length, temperature, timeout, and keep-alive. pyveil caps the default
context at 4096 and uses `keep_alive=0`, so one-shot calls release model memory;
set `OLLAMA_KEEP_ALIVE=5m` for faster repeated calls.

Observed on this project's M1 Mac mini with 16GB memory and Ollama 0.31.2:
`qwen3.5:4b` used about 3.2GB at a 4096-token context, a cold request took
8.1 seconds, and a warm request took 1.3 seconds. These are local measurements,
not portable performance guarantees. See the
[Ollama integration guide](docs/integrations/ollama.md) for the full setup and
memory trade-offs.

## Azure OpenAI: End To End

Install the optional Azure example dependencies, then load configuration from
environment variables, `.env`, or YAML:

```bash
pip install "pyveil[azure-openai]"
```

```python
from pyveil.integrations.azure_openai import ask_azure_openai, load_settings

settings = load_settings()  # AZURE_OPENAI_* + PYVEIL_* environment variables
result = ask_azure_openai(
    "Write a follow-up for alice@example.com or 010-1234-5678.",
    settings,
)

print(result.redacted_input)  # The exact text sent to Azure OpenAI
print(result.output_text)     # The model response
```

The integration uses Azure OpenAI's v1 endpoint and Responses API. The
deployment name is passed as `model`; pyveil redacts the prompt before
`client.responses.create(...)` runs.

Prove the boundary without an Azure request:

```bash
PYVEIL_SECRET=docs-demo-secret \
  python -m pyveil.integrations.azure_openai --dry-run
```

```text
mode: dry-run
deployment: not configured
sent-to-azure: Write a one-sentence support follow-up for [EMAIL:347ab11285a3] or [PHONE:548017338f6f].
findings: EMAIL=1, PHONE=1
azure-response: skipped (--dry-run)
```

For a live call, either export `AZURE_OPENAI_ENDPOINT`,
`AZURE_OPENAI_DEPLOYMENT`, `AZURE_OPENAI_API_KEY`, `PYVEIL_SECRET`, and
optionally `PYVEIL_SCOPE`, or use the checked-in
[`.env` template](https://github.com/hyeonsangjeon/pyveil/blob/main/examples/azure_openai.env.example)
and [YAML template](https://github.com/hyeonsangjeon/pyveil/blob/main/examples/azure_openai.example.yaml):

```bash
python -m pyveil.integrations.azure_openai --env-file .env
python -m pyveil.integrations.azure_openai \
  --config examples/azure_openai.example.yaml --env-file .env
```

Process environment variables override `.env`, which overrides non-secret YAML
settings. API keys and the pyveil HMAC secret are rejected if placed directly
in YAML; YAML names the environment variables that contain them.

<p align="center">
  <img src="https://raw.githubusercontent.com/hyeonsangjeon/pyveil/main/docs/media/pyveil-awesome-demo.gif"
       alt="pyveil redacts synthetic PII and secrets before an AI agent boundary"
       width="820">
</p>

## Why pyveil

| Need | What pyveil provides |
| --- | --- |
| Keep data local | Standard-library core, zero required dependencies, zero network calls |
| Preserve references | Stable `[TYPE:12hex]` placeholders from HMAC-SHA256 |
| Isolate tenants and runs | Caller-defined `scope` changes placeholders across boundaries |
| Redact real agent payloads | Recursive dictionaries, lists, tuples, and JSON strings |
| Cover more than prompts | Policy channels for tools, MCP, memory, logs, traces, input, and output |
| Stop credentials in tool calls | Auth headers, private keys, API keys, JWTs, and tokens block by default |
| Cover app-specific data | Exact known-value and trusted custom-regex rules |
| Audit without leaking | Findings contain type, rule, path, placeholder, and fingerprint, not raw values |
| Verify supported behavior | Public 39-case synthetic regression corpus, evaluator, and CI gate |
| Replay agent boundaries | One keyless command proves prompt, tool, MCP, memory, log, and trace handling |

## Reproducible Evidence

The repository ships a public synthetic detector corpus and a standard-library
evaluator:

```bash
python evaluation/evaluate.py --check
```

For corpus v1, the regression expectation is 36 findings across 39 cases
(33 positive, 6 negative), with no corpus false positives, false negatives,
labeled-value leaks, or non-empty `Finding.raw` values.

The repository also ships a machine-readable
[protection-surface contract](compatibility/README.md) with synthetic fixtures
for every channel, a validator that fails on manifest or documentation drift,
and a privacy-safe evidence receipt:

```bash
python -m pyveil replay --format markdown          # six-boundary checkout tour
python scripts/validate_compatibility.py --check   # manifest, fixtures, and docs agree
python -m pytest tests/test_compatibility_surfaces.py
python scripts/verify_zero_dependencies.py         # 0 runtime dependencies
```

These numbers describe documented supported shapes only. They are **not** a
real-world PII recall benchmark and do not cover unknown names, addresses,
languages, documents, or images. Read the
[methodology and limits](https://hyeonsangjeon.github.io/pyveil/evaluation.html).

## Known Names And Domain IDs

Regex cannot discover arbitrary names or addresses. When your application
already knows a value is sensitive, teach that value to `pyveil` without adding
an NER model:

```python
from pyveil import CustomRule, Veil

rules = [
    CustomRule.exact("PERSON", ["Alice Kim", "Hong Gildong"]),
    CustomRule("CUSTOMER_ID", r"\bCUS-[A-Z0-9]{8}\b", rule_id="customer_id"),
]

veil = Veil.high(
    secret=b"tenant-secret",
    scope="tenant/session",
    rules=rules,
)

result = veil.redact_text("Alice Kim owns CUS-A1B2C3D4.")
print(result.text)
# [PERSON:...] owns [CUSTOMER_ID:...].
```

Custom patterns are trusted application code. Keep them narrow and test them
against realistic positive and negative samples.

## Agent Boundaries

Classic masking often stops at text input. Agents move data across several
surfaces, so channels are first-class policy inputs:

| Channel | Redact before |
| --- | --- |
| `prompt.input` | User, RAG, or application context reaches a model |
| `prompt.output` | Model output is displayed or chained |
| `tool.call.arguments` | A model-controlled tool executes |
| `tool.call.result` | Tool output returns to a model |
| `mcp.resource.content` | MCP resource content enters context |
| `memory.write` | Text is embedded or persisted |
| `trace.span.attributes` | Attributes leave through telemetry |
| `log.record` | Records reach handlers or external sinks |

```text
user / retrieval / tool / resource data
                    |
                  pyveil
                    |
model / tool / MCP / memory / trace / log boundary
```

Every channel above is backed by a synthetic fixture and a reproducible command
in the [compatibility contract](compatibility/README.md), and summarized in a
privacy-safe [Proof-of-Compatibility Receipt](compatibility/receipt.md) that
records redaction counts and output hashes, never raw values.

## Detection Board

Core detection is intentionally conservative and high precision:

| Type | Examples | HIGH output |
| --- | --- | --- |
| `EMAIL` | Email addresses | `[EMAIL:12hex]` |
| `PHONE` | Korean, separated international, and compact E.164 phone shapes | `[PHONE:12hex]` |
| `CREDIT_CARD` | Card numbers that pass Luhn validation | `[CREDIT_CARD:12hex]` |
| `JWT` | Compact JSON Web Tokens | `[JWT:12hex]` |
| `AUTH_HEADER` | Bearer and Basic authorization headers | `[AUTH_HEADER:12hex]` |
| `PRIVATE_KEY` | PEM private-key blocks | `[PRIVATE_KEY:12hex]` |
| `API_KEY` | OpenAI, GitHub, Slack, Google, and AWS-style keys | `[API_KEY:12hex]` |
| `URL_QUERY_SECRET` | Token, key, secret, code, and auth query values | `[URL_QUERY_SECRET:12hex]` |
| `KV_SECRET` | Password, cookie, secret, and token key-value pairs | `[KV_SECRET:12hex]` |
| Custom | Known values and application regex rules | `[YOUR_TYPE:12hex]` |

See the [full redaction reference](https://github.com/hyeonsangjeon/pyveil/blob/main/docs/redaction-reference.md)
for examples, validation rules, LOW masking output, and limitations.

## HIGH And LOW

Use `HIGH` at model, agent, tool, MCP, memory, trace, and external log
boundaries. It produces stable HMAC placeholders such as
`[EMAIL:a13f7c91b0d2]`.

Use `LOW` only for human-facing previews where preserving shape is useful:

```text
alice@example.com  -> al***@e******.com
010-1234-5678      -> 010-****-5678
4242 4242 4242 4242 -> **** **** **** 4242
```

Credential-like values remain aggressively hidden in both levels.

## Policy

The default high policy redacts supported findings and blocks credentials in
`tool.call.arguments`:

```python
from pyveil import Action, Channel, Entity, Policy, Veil

policy = Policy.default_high().override(
    Channel.PROMPT_INPUT,
    Entity.EMAIL,
    Action.PASS,
)

veil = Veil.high(secret=b"tenant-secret", policy=policy)
```

When both `policy` and `level` are supplied, the explicit policy decides channel
levels and actions. Build one `Veil` per tenant, session, or run and reuse it.

## CLI

Use stdin for shell pipelines, files for preflight checks, and JSON output for
structured automation:

```bash
export PYVEIL_SECRET="tenant-or-run-secret"
export PYVEIL_SCOPE="tenant/session"

printf 'Email alice@example.com' | pyveil redact -
pyveil redact request.json --channel tool.call.result --format json
pyveil scan prompt.txt --format json
```

`scan` emits finding metadata without raw sensitive values. JSON-shaped input
is parsed and traversed structurally. These legacy stdout commands keep their
flag/environment interface. For repeatable file jobs use `pyveil run pyveil.json`;
it rejects malformed JSON instead of silently treating it as text.

```bash
python -m pyveil test-config pyveil.json
python -m pyveil run pyveil.json --format json
```

`test-config` validates the entire JSON schema and version pin without executing
anything. `run` also checks the input and secret, then returns output locations.
Legacy reference YAML is no longer accepted by `init` or `test-config`.

## Integration Recipes

| Stack or boundary | Copy-paste example |
| --- | --- |
| Any LLM provider | [Provider-neutral client wrapper](https://github.com/hyeonsangjeon/pyveil/blob/main/examples/llm_client_boundary.py) |
| OpenAI Agents SDK | [Pre-dispatch `Runner.run` wrapper](https://github.com/hyeonsangjeon/pyveil/blob/main/examples/openai_agents_guardrail.py) |
| OpenAI Responses API | [Installable integration](https://github.com/hyeonsangjeon/pyveil/blob/main/pyveil/integrations/openai.py) and [keyless contract guide](https://github.com/hyeonsangjeon/pyveil/blob/main/docs/integrations/openai.md) |
| Anthropic / Claude | [Installable integration](https://github.com/hyeonsangjeon/pyveil/blob/main/pyveil/integrations/anthropic.py) and [keyless contract guide](https://github.com/hyeonsangjeon/pyveil/blob/main/docs/integrations/anthropic.md) |
| Azure OpenAI | [Runnable env/YAML integration](https://github.com/hyeonsangjeon/pyveil/blob/main/pyveil/integrations/azure_openai.py) and [short example](https://github.com/hyeonsangjeon/pyveil/blob/main/examples/azure_openai.py) |
| LiteLLM | [SDK wrapper and Proxy pre-call hook](https://github.com/hyeonsangjeon/pyveil/blob/main/examples/litellm_proxy_filter.py) |
| FastAPI | [Request middleware example](https://github.com/hyeonsangjeon/pyveil/blob/main/examples/fastapi_middleware.py) |
| MCP | [Server wrapper](https://github.com/hyeonsangjeon/pyveil/blob/main/examples/mcp_server_wrapper.py) and [integration guide](https://hyeonsangjeon.github.io/pyveil/manual.html#integrations) |
| Python logging | [Logging filter example](https://github.com/hyeonsangjeon/pyveil/blob/main/examples/log_filter.py) |
| Agent memory | [Before-embedding example](https://github.com/hyeonsangjeon/pyveil/blob/main/examples/memory_write.py) |

The [cookbook](https://github.com/hyeonsangjeon/pyveil/blob/main/docs/cookbook.md)
covers prompts, tools, MCP, memory, logging, tracing, JSON, and CLI workflows.

## Pick The Right Tool

Choose `pyveil` when you want a small local boundary filter for structured PII,
credentials, known values, and domain identifiers across agent context flows.

Choose Presidio, GLiNER, or another NER-backed system when you need broad
semantic discovery of unknown people, organizations, locations, or addresses.
Choose an enterprise DLP product when you need managed policy, document/image
coverage, incident workflows, or compliance reporting.

These tools can be layered. `pyveil` does not claim perfect recall.
See the full [pyveil vs Presidio, NER, guardrails, and DLP decision guide](https://hyeonsangjeon.github.io/pyveil/guides/pyveil-vs-presidio.html).

## Safety Contract

- Raw sensitive values are not stored in `Finding` objects by default.
- Placeholders use HMAC-SHA256 with a caller-provided secret and scope.
- Credential-like values can be blocked before model-controlled tools execute.
- `max_input_chars` bounds work performed on text and structured payloads.
- The core makes no network calls and has no required third-party dependency.
- pyveil has no reversible vault or unmasking API.

`pyveil` is not a compliance guarantee, enterprise DLP system, secret-scanning
replacement, or prompt-injection firewall. Read the
[threat model](https://github.com/hyeonsangjeon/pyveil/blob/main/docs/threat-model.md),
[known limitations](https://github.com/hyeonsangjeon/pyveil/blob/main/docs/known-limitations.md),
and [security policy](https://github.com/hyeonsangjeon/pyveil/blob/main/SECURITY.md)
before production use.

## Guides

- [Complete manual](https://hyeonsangjeon.github.io/pyveil/manual.html)
- [Python LLM PII redaction guide](https://hyeonsangjeon.github.io/pyveil/guides/python-llm-pii-redaction.html)
- [MCP PII redaction guide](https://hyeonsangjeon.github.io/pyveil/guides/mcp-pii-redaction.html)
- [pyveil vs Presidio / NER / DLP](https://hyeonsangjeon.github.io/pyveil/guides/pyveil-vs-presidio.html)
- [Reproducible detector evaluation](https://hyeonsangjeon.github.io/pyveil/evaluation.html)
- [English video guide](https://github.com/hyeonsangjeon/pyveil/releases/download/v0.1.2/pyveil-usage-guide-en.mp4)
- [Korean video guide](https://github.com/hyeonsangjeon/pyveil/releases/download/v0.1.2/pyveil-usage-guide-ko.mp4)
- [AGENTS.md](https://github.com/hyeonsangjeon/pyveil/blob/main/AGENTS.md) for coding agents
- [llms.txt](https://hyeonsangjeon.github.io/pyveil/llms.txt) for LLM-readable navigation
- [Contributing](https://github.com/hyeonsangjeon/pyveil/blob/main/CONTRIBUTING.md)

## Development

```bash
uv run --extra dev ruff check .
uv run --extra dev mypy pyveil tests
uv run --extra test pytest
uv run --extra test python evaluation/evaluate.py --check
```

CI runs the test suite on Python `3.8` through `3.14`. The core remains typed,
dependency-free, and MIT licensed.
