Metadata-Version: 2.5
Name: whiteout-ai
Version: 0.3.0
Summary: Whiteout AI Governance SDK — wrap AI API calls with policy enforcement
Project-URL: Homepage, https://whiteout.groovysec.com
Project-URL: Documentation, https://docs.whiteout.groovysec.com/sdk/python
Project-URL: Repository, https://github.com/groovysec/whiteout-ai-python
Author-email: Groovy Security <engineering@groovysec.com>
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.9
Requires-Dist: httpx>=0.25.0
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.20.0; extra == 'anthropic'
Provides-Extra: auto
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.2.0; extra == 'langchain'
Provides-Extra: openai
Requires-Dist: openai>=1.0.0; extra == 'openai'
Description-Content-Type: text/markdown

# Whiteout AI Governance SDK

Wrap AI API calls with Whiteout policy enforcement. Three lines of code to add governance to any Python AI application.

## Install

```bash
pip install whiteout-ai
```

## Custom AI Apps: `AppGuard` (0.2+)

For your own LLM applications registered under **Integrations → Custom AI Apps**.
Authenticates with the app key from the setup wizard, with no registration step, and
checks **both** the prompt and the model's reply.

```bash
pip install "whiteout-ai>=0.2"
export WHITEOUT_APP_KEY=wo_app_prod_...
export WHITEOUT_BASE_URL=https://api.whiteout.groovysec.com   # the default
```

```python
from openai import OpenAI
from whiteout_ai import AppGuard, WhiteoutBlockedError

guard = AppGuard()
client = guard.openai(OpenAI())            # or guard.anthropic(Anthropic())

try:
    reply = client.chat.completions.create(
        model="gpt-5", messages=messages,
        whiteout={"end_user": user.id},     # optional; never sent to OpenAI
    )
except WhiteoutBlockedError as e:
    print(e.stage, e.reason, e.violated_policies)   # stage: "input" or "output"
```

The wrapped client is the same object with `chat.completions.create` (or
`messages.create`) governed. Everything else passes straight through.

**What gets scanned.** By default this is the latest user message. Pass
`scan=["user", "system", "context", "tool"]` to the constructor, or per call via
`whiteout={"scan": [...]}`, to also catch sensitive data in system prompts,
retrieved documents (`context=`) and tool results.

**Checking without a wrapper:**

```python
d = guard.check_input(messages, context=retrieved_docs, model="gpt-5", provider="openai")
reply = call_your_model(messages)
guard.check_output(d.evaluation_id, reply)    # raises WhiteoutBlockedError on block
```

**Streaming.** `stream=True` works for both providers.

| `stream_mode` | Behaviour |
|---|---|
| `"buffered"` (default) | The whole response is held until the output check passes, so nothing flagged reaches the user. |
| `"passthrough"` | Chunks are yielded as they arrive, with a check every `check_every` characters (default 2000) plus a final check. It stops at the first flagged checkpoint. Text already shown cannot be retracted, so use buffered mode where you enforce. |

**Fail behaviour.** If Whiteout is unreachable, or the key is revoked (logged at
ERROR), the SDK applies your policy group's fail behaviour from
`GET /v1/guard/config`. That setting is cached for 5 minutes and revalidated with an ETag.
Pass `fail_open=True/False` to override it. With no config yet, the SDK fails open.

**Async.** `AsyncAppGuard` has the same surface (`await guard.check_input(...)`,
`guard.openai(AsyncOpenAI())`, `async for` over streams). Use it as
`async with AsyncAppGuard() as guard:` so pending background checks drain on exit.

**LangChain.**

```python
from whiteout_ai.langchain import WhiteoutCallbackHandler   # pip install "whiteout-ai[langchain]"
llm = ChatOpenAI(callbacks=[WhiteoutCallbackHandler(AppGuard())])
```

A block raises out of the chain. Streaming chains are checked when the
completion finishes.

**Other options:**
- `block_output=False` makes output checks fire-and-forget (observe the reply without blocking it).
- `send_events([...])` reports calls after the fact (observe-only, batched).
- `timeout` sets the request timeout in seconds (default 10).

## No code changes: auto-instrumentation (0.3+)

```bash
pip install "whiteout-ai[auto]"
export WHITEOUT_APP_KEY=wo_app_prod_...
whiteout-ai run python app.py            # or: whiteout-ai run -- gunicorn app:server
whiteout-ai check                        # key set? which clients will be governed?
```

In a container where you can't change the start command, put the bootstrap on the path instead:
`PYTHONPATH="$(whiteout-ai bootstrap-path)"`. It runs any `sitecustomize` you already have.

**What gets governed:**
- `openai`: chat completions and the Responses API, sync and async.
- `anthropic`: `messages.create`, sync and async. `messages.stream()` is checked on input, and its final text is recorded.
- boto3 `bedrock-runtime`: Converse and InvokeModel (input and output) and the streaming variants (input). This runs through botocore's event system.

A library is patched when your app imports it, so there's no startup cost for clients you don't use.

**Environment variables:**

| Variable | Values |
|---|---|
| `WHITEOUT_MODE` | `enforce` (default) or `observe`. Observe checks and records but never raises. |
| `WHITEOUT_STREAM_MODE` | Stream mode for governed calls |
| `WHITEOUT_SCAN` | Which parts of the call to scan |
| `WHITEOUT_INCLUDE` / `WHITEOUT_EXCLUDE` | `openai`, `anthropic`, `bedrock` |
| `WHITEOUT_DISABLE=1` | Turns it off completely |

**Limits:** raw HTTP calls to a model API aren't seen. If a declared model host is called without Guard events, the AI Footprint still flags it.

## Prompt injection: quarantine (0.3+)

When your org has Prompt Injection Defense on, a tool result or system prompt carrying instructions can be *quarantined*. Wrapped clients, and auto-instrumentation, replace that turn with a short notice before the model sees it. Tool calls stay paired, and your own objects are never changed.

Context documents you send as `context=` can't be rewritten inside your prompt, so drop them yourself:

```python
d = guard.check_input(messages, context=docs, scan=["user", "context"])
docs = d.clean_context(docs)             # the documents Whiteout didn't quarantine
```

Replies are checked for injected instructions too. If your org blocks such content, `check_output` blocks.

## The employee's own policies (0.3+)

If the app is set to *the employee's own policies* in the admin console, pass the signed-in employee's OIDC `id_token`. Whiteout then applies their group's policies, just as it does in ChatGPT:

```python
guard.check_input(messages, user_token=session["id_token"])
client.chat.completions.create(model=..., messages=..., whiteout={"user_token": session["id_token"]})
```

## Infrastructure workloads: `WhiteoutGuard`

`WhiteoutGuard` is unchanged. It uses an enrollment token and the `/infra-agent` routes,
and suits workloads enrolled as infrastructure resources.

## Quick Start

```python
from whiteout_ai import WhiteoutGuard

guard = WhiteoutGuard(
    token="whiteout_enroll_...",
    backend_url="https://your-org.api.whiteout.groovysec.com",
)

# Option 1: Evaluate a prompt directly
result = guard.evaluate(
    prompt="Summarize this patient record...",
    ai_provider="openai",
    model="gpt-4o",
)
# result = {"decision": "block", "reason": "Contains PHI", ...}

# Option 2: Wrap an OpenAI call
import openai
response = guard.wrap(
    openai.chat.completions.create,
    model="gpt-4o",
    messages=[{"role": "user", "content": "Hello world"}],
)

# Option 3: Catch blocked prompts
from whiteout_ai import WhiteoutBlockedError
try:
    guard.evaluate(prompt="SSN 123-45-6789")
except WhiteoutBlockedError as e:
    print(f"Blocked: {e.reason}")
    print(f"Policies: {e.violated_policies}")
```

## Configuration

| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| `token` | Yes | - | Enrollment token from Whiteout admin |
| `backend_url` | Yes | - | Your org's Whiteout API URL |
| `fail_open` | No | `True` | Allow calls when backend unreachable |
| `timeout` | No | `10.0` | Backend call timeout (seconds) |
| `mode` | No | `sdk` | Deployment mode identifier |

## Features

- Automatic provider detection (OpenAI, Anthropic, Google, Mistral)
- Fast checks cached locally (provider block, model allowlist)
- Remote compliance evaluation for content policies
- Output governance (response scanning in background)
- Fail-open by default (never breaks your app)
- Thread-safe
