# Tailrace documentation (full) Generated for agents. Prefer https://tailrace.dev/llms.txt as a router, then fetch individual .md URLs. # Introduction URL: https://tailrace.dev/docs > How Tailrace governs agent data flows in-process - detect, policy, apply, audit, and restore at egress. Tailrace is agent data governance for TypeScript. It sits **inside your process** at model, tool, and MCP boundaries - detecting secrets and PII, applying policy, and restoring tokenized values only where you trust. No proxy. No sidecar. No required network call on the request hot path. *(Interactive detect → policy → apply pipeline demo - open the HTML page to try it.)* ## The pipeline [#the-pipeline] Every `check` follows the same path: 1. **Input**: A string or JSON object crosses a boundary (model prompt, tool args, MCP payload). 2. **Detect**: Tier 0 recognizers find entity spans with JSON-pointer paths. 3. **Policy resolve**: For each span: `entity × boundary × identity → action`. 4. **Apply**: `block` throws · `tokenize` writes the vault · `mask` replaces inline · `allow` passes through. 5. **Audit**: Async emit with rule path and content hash; never raw values. ```mermaid flowchart TB In["Input
string or JSON at a boundary"] --> Detect["Detect
Tier 0 recognizers to spans"] Detect --> Resolve["Policy resolve
entity x boundary x identity to action"] Resolve --> Apply["Apply
block · tokenize · mask · allow"] Apply --> Out["Output + decisions"] Apply -. "async" .-> Audit["Audit
rule path + content hash"] Out -. "later, egress only" .-> Restore["restore()
detokenize at an egress sink"] ``` **Restore** is separate: `tailrace.restore()` runs only at `{ kind: "egress", sink }` boundaries. Calling restore at a model or tool boundary throws - by design. ## Default policy [#default-policy] `createTailrace()` with no arguments: | Entity | Action | | -------------------------------------------- | -------------- | | Secrets (`api_key`, `jwt`, `private_key`, …) | **block** | | Email, phone, credit card, IBAN, SSN | **tokenize** | | IP address | allow | | Egress sinks (`egress:*`) | **detokenize** | Secrets cannot be overridden to `allow` without `dangerouslyAllowSecrets: true`. ## Where Tailrace sits [#where-tailrace-sits] ```text Your app ──► wrapModel / wrapTools / MCP wrapper ──► tailrace.check ──► provider or tool │ └──► audit (async) ``` For the Vercel AI SDK, one line wraps the model: ```ts const model = tailrace.model(openai("gpt-4o")); ``` ## Next steps [#next-steps] | Goal | Link | | -------------------------------- | --------------------------------------------------------------- | | Block a fake key in five minutes | [Quickstart](/docs/get-started/quickstart) | | Connect Cursor / Claude to docs | [Use with AI tools](/docs/get-started/use-with-ai-tools) | | Models, tools, streaming, egress | [Protect PII in the AI SDK](/docs/guides/protect-pii-in-ai-sdk) | | MCP transports | [Govern MCP tool calls](/docs/guides/govern-mcp-tool-calls) | | Hono OpenAI passthrough | [Hono integration](/docs/integrations/hono) | | Concepts | [Boundaries](/docs/concepts/boundaries) | --- # Boundaries URL: https://tailrace.dev/docs/concepts/boundaries > Where Tailrace enforces policy - model, tool, MCP, telemetry, and egress. Why does the same email get different treatment when it hits a model call versus when it hits your UI? Because Tailrace does not scan "the app." It scans **at boundaries** - typed places where data crosses a trust line. Every `check` / `restore` call names one. ## The five kinds [#the-five-kinds] | Kind | Meaning | Typical call site | | ----------- | --------------------------------------------- | ------------------------------------------------- | | `model` | Prompt or completion to/from a provider | `wrapModel` / `tailrace.model` | | `tool` | Args leaving the agent or results coming back | `wrapTools`, Claude Code PreToolUse / PostToolUse | | `mcp` | MCP `tools/call` / resource reads | `wrapTransport` / `withMcp` | | `telemetry` | Logs, traces, analytics sinks | Explicit `check` before emit | | `egress` | Trusted restore points (UI, email, webhook) | `tailrace.restore` | ```mermaid flowchart LR App[Your agent / route] Model[Model provider] Tool[Tool / MCP] UI[UI / webhook] App -->|"model"| Model Model -->|"model"| App App -->|"tool out"| Tool Tool -->|"tool in"| App App -->|"egress"| UI ``` ## Direction matters [#direction-matters] `tool` and `mcp` carry `direction: "out" | "in"`: * **out** - data leaving the agent (tool args, MCP call params) * **in** - data returning (tool result, MCP response) Policy keys encode direction (`tool:crm:out`), so you can tokenize outbound CRM writes while allowing inbound reads, or the reverse. ## Provider and name encoding [#provider-and-name-encoding] Boundary matching is **kind-scoped**: * Model: bare provider globs - `openai/*`, `anthropic/claude-sonnet-4` * Tool: `tool:{name}:{direction}` * MCP: `mcp:{server}/{tool}` * Egress: `egress:{sink}` * Telemetry: `telemetry` A model glob never matches a tool key. Exact keys beat longer globs; longer globs beat shorter ones. Matching is scoped by `kind` first, so keys from different kinds never collide - `openai/*` (model) can't accidentally match `tool:crm:out`. Within a kind, the most specific matching key wins, scored by specificity: | Boundary | Candidate keys, most specific first | | ------------------------------------------------- | ---------------------------------------- | | `{ kind: "model", provider: "openai/gpt-4o" }` | `openai/gpt-4o` → `openai/*` → `*` | | `{ kind: "tool", name: "crm", direction: "out" }` | `tool:crm:out` → `tool:crm:*` → `tool:*` | | `{ kind: "mcp", server: "sf", tool: "read" }` | `mcp:sf/read` → `mcp:sf/*` → `mcp:*` | An exact key always beats a glob; a longer glob beats a shorter one. This runs inside policy resolution, so it is precompiled and pure - see [Policy resolution](/docs/concepts/policy-resolution). ## Egress is special [#egress-is-special] Detokenization is allowed **only** at `egress` sinks whose policy says `detokenize`. Calling `restore` at `model`, `tool`, `mcp`, or `telemetry` throws `InvariantViolationError` even if a policy document asks for it. Tokens stay opaque everywhere except the sinks you name. ## Identity rides along [#identity-rides-along] Every check also carries an `Identity` (`agent` string, optional claims). Policy can override per agent. Integrations set this - they do not invent policy rules. ## See it in practice [#see-it-in-practice] * [Protect PII in the AI SDK](/docs/guides/protect-pii-in-ai-sdk) - model + tool boundaries * [Govern MCP tool calls](/docs/guides/govern-mcp-tool-calls) - MCP out / in * [Block secrets in Claude Code](/docs/guides/block-secrets-in-claude-code) - tool out / in via hooks * [Policy resolution](/docs/concepts/policy-resolution) - how boundary + entity + identity pick an action --- # Detection tiers URL: https://tailrace.dev/docs/concepts/detection-tiers > What Tier 0 and Tier 1 find - and what detection deliberately does not decide. What does Tailrace actually find in a string or JSON object? Detection produces **spans** (entity, offsets, confidence). It never chooses `block` or `tokenize` - that is policy's job. Engines are pluggable behind one `Recognizer` interface. ## Tier 0 (ships in `@tailrace/core`) [#tier-0-ships-in-tailracecore] Synchronous, zero runtime dependencies, WebCrypto only. Pattern + validator where applicable. **Secrets:** `api_key` (known prefixes), `jwt`, `private_key`, `high_entropy_secret` (entropy + keyword context), `connection_string` **Structured PII:** `email`, `phone`, `credit_card` (Luhn), `iban`, `ssn`, `ip_address`, `url_credentials` Confidence is `1.0` when a validator confirms, `0.8` for pattern-only. Spans below the configured threshold (default `0.6`) are dropped. Honest expectation: Tier 0 is strong on structured shapes and known key prefixes. It is not a free-text NER. It will miss "John lives in Austin" and can false-positive without context gates (the high-entropy recognizer requires a nearby keyword for that reason). ## Tier 1 (optional `@tailrace/recognizer-ner`) [#tier-1-optional-tailracerecognizer-ner] Quantized GLiNER-class ONNX model for `person`, `location`, `organization`. Node/Fluid only in v0.1 - not edge. Lazy-loaded; if the model file is missing, Tailrace logs a warning and continues with Tier 0 (fail open for availability). Default policy does **not** enforce NER entities - they fall through to `allow` unless you set them. ## Custom recognizers [#custom-recognizers] `defineRecognizer({ id, entities, tier, scan })` - pattern-based or arbitrary. Register via `createTailrace({ recognizers: [...] })`. ## Objects, not stringified blobs [#objects-not-stringified-blobs] `check` walks string leaves of JSON (depth-limited, cycle-safe). Spans carry an RFC 6901 JSON Pointer path. Keys are scanned too. Never serialize an object to one string for scanning - offsets and rewrites would break. ## Span merging [#span-merging] 1. Drop low-confidence spans 2. Same entity, overlapping → union 3. Different entities, overlapping → keep both; policy picks most restrictive action 4. Sort by start; apply rewrites right-to-left ```mermaid flowchart LR Raw["Raw spans"] --> Drop["Drop below
confidence threshold"] Drop --> Union["Same entity + overlap
union into one span"] Union --> Keep["Different entity + overlap
keep both"] Keep --> Rewrite["Sort by start;
rewrite right-to-left"] Rewrite --> Final["Final spans"] ``` Every rewrite (`tokenize`, `mask`) changes the length of the string. If you rewrote left-to-right, the first replacement would shift every later span's offsets and you'd have to recompute them. Applying rewrites from the highest start offset down means each edit only touches text after the spans you have not processed yet, so their offsets stay valid until you reach them. Same idea per string leaf when scanning objects: each leaf owns its offsets, and the JSON Pointer path (e.g. `/messages/2/content`) tells `apply` exactly which leaf to rewrite - which is why objects are never flattened to one string. ## Perf gate [#perf-gate] Tier 0 on a 4KB mixed fixture: p50 \< 5ms in CI. Detection is a commodity; policy and vault get the product effort. ## See it in practice [#see-it-in-practice] * [Playground](/docs/playground) - paste text, see Tier 0 spans client-side * [Block secrets in Claude Code](/docs/guides/block-secrets-in-claude-code) * [Threat model](/docs/concepts/threat-model) - what detection does not cover --- # Policy resolution URL: https://tailrace.dev/docs/concepts/policy-resolution > How Tailrace picks allow, mask, tokenize, or block for a span at a boundary. Given a detected span, a boundary, and an agent identity - which action wins? That question is the whole product. Detection only finds entities. Integrations only call `check`. The policy engine decides. ## Actions [#actions] | Action | Effect | | ------------ | ------------------------------------------------------------- | | `allow` | Pass through; still audited | | `mask` | Irreversible label like `[EMAIL]` | | `tokenize` | Reversible vault token | | `block` | Throw `PolicyViolationError` (integrations translate) | | `detokenize` | Egress-only restore | | `review` | Typed in v0.1; throws `NotImplementedError` at policy compile | ## Zero-config default [#zero-config-default] `createTailrace()` with no args: | Entities | Action | | --------------------------------------------------- | ------------------------------------------- | | Secret classes (`api_key`, `jwt`, `private_key`, …) | `block` | | `email`, `phone`, `credit_card`, `iban`, `ssn` | `tokenize` | | `ip_address` | `allow` | | `url_credentials` | `block` | | NER (`person`, `location`, `organization`) | fall through to `defaults.action` (`allow`) | | `egress:*` | `detokenize` | ## Precedence (most specific first) [#precedence-most-specific-first] For entity `E`, boundary `B`, identity `I`: 1. `identities[I.agent].boundaries[B].entities[E]` 2. `identities[I.agent].entities[E]` 3. `boundaries[B].entities[E]` 4. `entities[E].action` 5. `defaults.action` First hit wins - with one hard exception. ```mermaid flowchart TB Start(["span + boundary + identity"]) --> L1{"identity + boundary
rule for entity?"} L1 -- yes --> Hit["candidate action"] L1 -- no --> L2{"identity rule
for entity?"} L2 -- yes --> Hit L2 -- no --> L3{"boundary rule
for entity?"} L3 -- yes --> Hit L3 -- no --> L4{"entity default?"} L4 -- yes --> Hit L4 -- no --> L5["defaults.action"] --> Hit Hit --> Guard{"would allow a
secret-class entity?"} Guard -- "yes, no dangerouslyAllowSecrets" --> Forced["forced block"] Guard -- no --> Final(["final action"]) ``` ## Secrets cannot be allowed by accident [#secrets-cannot-be-allowed-by-accident] If any rule would `block` a **secret-class** entity, a more specific rule cannot override that to `allow` unless it sets `dangerouslyAllowSecrets: true`. Deliberate friction so a support-agent override cannot quietly ship API keys to a model. ## Overlapping spans [#overlapping-spans] When spans overlap after merge, the most restrictive action wins: `block` > `review` > `tokenize` > `mask` > `allow` ## Worked example [#worked-example] A Stripe test key in a tool argument for agent `support-bot`: 1. Detect → `api_key` 2. Boundary → `{ kind: "tool", name: "Bash", direction: "out" }` 3. Identity → `{ agent: "support-bot" }` 4. Resolve → default secret `block` (unless a `dangerouslyAllowSecrets` rule exists) 5. Integration → Claude Code deny JSON / AI SDK tool error string / thrown `PolicyViolationError` An `example.com` email on the same path resolves to `tokenize` under the default policy - rewrite in place, vault write, audit decision with `contentHash` (never the raw value). ## Resolution is pure and sync [#resolution-is-pure-and-sync] `resolve(policy, entity, boundary, identity)` does map lookups only (precompiled at `createTailrace`). Target: under 1µs per span. No I/O in the resolve path. `createTailrace` compiles your policy document once, up front. Each precedence level becomes a flat lookup keyed by the strings it matches on: * `identities[agent].boundaries[key].entities[entity]` * `identities[agent].entities[entity]` * `boundaries[key].entities[entity]` * `entities[entity]` At request time `resolve` walks those maps in order and returns the first hit. There is no regex, no allocation, and no I/O on this path - just object property reads - which is what keeps it under 1µs per span. The secret guard is precomputed too: the set of secret-class entities is fixed at compile time, so the "would this allow a secret?" check is a single set membership test that only runs when the winning action is `allow`. That is why the guard can be a hard invariant rather than a policy option. ## See it in practice [#see-it-in-practice] * [Protect PII in the AI SDK](/docs/guides/protect-pii-in-ai-sdk) * [Boundaries](/docs/concepts/boundaries) * Spec: policy engine (repo `docs/policy-engine.md`) --- # Threat model URL: https://tailrace.dev/docs/concepts/threat-model > What Tailrace protects against - and what it deliberately does not. What does in-process data governance actually buy you - and where should you not rely on it? ## What Tailrace is for [#what-tailrace-is-for] Tailrace sits **inside** your process at model, tool, MCP, and egress boundaries. It: * Blocks or redacts **secrets and structured PII** before they leave trusted code * Keeps reversible tokens stable across a workflow so agents can keep working * Emits audit decisions with hashes and rule paths - never raw values * Fails closed on `block` policies; fails open if optional Tier 1 is missing Deployment model: no proxy, no sidecar, no required network hop on the hot path. ## What it does not protect against [#what-it-does-not-protect-against] | Out of scope (v0.1) | Why | | --------------------------------------------- | --------------------------------------------------------------------- | | Prompt injection / jailbreaks | Different problem class; not detection of sensitive *data* | | Toxicity / content safety | Same | | Compromised host or malicious dependency | Once an attacker runs in-process, they can read memory and vault keys | | Insider with vault master key + KV access | Encrypted-at-rest helps against KV leaks, not key holders | | Secrets the model already knows from training | Cannot unlearn provider weights | | Channels you never wrap | Unwrapped `fetch`, raw SDK calls, and side channels bypass boundaries | | Perfect NER on free text | Tier 0 is structured; Tier 1 is optional and imperfect | ## Trust boundaries you still own [#trust-boundaries-you-still-own] * **Master vault key** - treat like any encryption key (`TAILRACE_VAULT_KEY` / config) * **Which sinks are `egress`** - only those may detokenize * **Which agents get which policies** - identity is a string you set * **Integrations must wrap the path** - Tailrace cannot see data that never calls `check` ## Fail-closed vs fail-open [#fail-closed-vs-fail-open] * Policy says `block` and detection throws → still block (closed) * Optional Tier 1 model missing → Tier 0 only + warning (open for availability) * Claude Code hook process error (bad JSON / missing config) → exit 1, not a silent allow framed as deny ## See it in practice [#see-it-in-practice] * [Boundaries](/docs/concepts/boundaries) - wrap every path that matters * [Detection tiers](/docs/concepts/detection-tiers) - honest accuracy expectations * [Block secrets in Claude Code](/docs/guides/block-secrets-in-claude-code) --- # Tokenization and the vault URL: https://tailrace.dev/docs/concepts/tokenization-and-the-vault > Why the same email becomes the same token in one workflow - and only restores at egress. Why does the same email become the same token at steps 1, 17, and 42 of an agent run? Because tokens are **workflow-scoped and deterministic**: HMAC over `(workflowId, entityClass, normalizedValue)`, not random IDs. ## Token shape [#token-shape] ``` tokenId = encode(HMAC-SHA256(workflowKey, entityClass || value))[0..8] token = ``` * `workflowKey` derives from your master key + `workflowId` * Alphabet is lowercase alphanumeric `[a-z0-9]` (shared by mint and restore) * No `workflowId` ⇒ `"default"` (fine for demos; use a real session id in production) **Deriving the token.** Two HMAC steps, both WebCrypto (no Node `crypto`, so it runs on edge runtimes): ``` workflowKey = HMAC-SHA256(masterKey, workflowId) tokenId = encode(HMAC-SHA256(workflowKey, entityClass || 0x00 || value))[0..8] ``` `value` is normalized first so trivial variants collapse to the same token: trim everything, lowercase `email`, digits-only for `phone` and `credit_card`. That is why `Alice@Example.com ` and `alice@example.com` mint one token. The `masterKey` comes from `createTailrace({ vault: { key } })` or `TAILRACE_VAULT_KEY`; with no key, a random per-process key is generated (tokens then don't survive a restart - fine for dev, logged once). **Encryption at rest.** The vault stores ciphertext, never the raw value: * Storage key: `tailrace:v1:{workflowId}:{token}` * Value: `AES-256-GCM(dataKey, rawValue)` with a random IV per entry * `dataKey = HMAC(masterKey, "storage")` - separate from the token key A leaked KV namespace without the master key yields no plaintext PII, only opaque ciphertext. ## Vault adapters [#vault-adapters] | Adapter | Use | | ---------------- | ------------------------------------------- | | `memoryVault()` | Default; process-local Map + TTL | | `kvVault(store)` | Cloudflare KV-shaped `{ get, put, delete }` | Values at rest are **AES-256-GCM** encrypted (key derived from the master key). Default TTL is 24h. A leaked KV namespace without the key does not yield plaintext PII. Claude Code hooks are separate processes - they use a file-backed `kvVault` under `.tailrace/vault/` with a stable `TAILRACE_VAULT_KEY` / config key so tokens survive across tool calls in one session. ## Format-preserving mode [#format-preserving-mode] Optional per entity (`format: "preserve"`): * Credit cards → Luhn-valid test-range digits (shape kept) * Email → `{tokenId}@redacted.example` * Phone → `+1555…` fictional range Everything else stays ``. ## Restore only at egress [#restore-only-at-egress] `tailrace.restore` scans for token patterns and looks them up in the vault. It runs only when: 1. Boundary kind is `egress` 2. Policy for that sink says `detokenize` Unknown or expired tokens stay as-is (`restore_miss` audit) - old transcripts should not crash the host. Calling `restore` at any other boundary throws `InvariantViolationError`. ```mermaid flowchart LR In[Raw email] -->|"check tokenize"| Tok["EMAIL token"] Tok --> Model[Model / tools] Model --> Tok2[Same token] Tok2 -->|"restore at egress:ui"| Out[Raw email] ``` ## Streaming [#streaming] Model output arrives in chunks. Entities and tokens can split across chunk boundaries. Integrations keep a hold-back buffer and never emit a bisected span - see AI SDK streaming modes in the guide. ## See it in practice [#see-it-in-practice] * [Protect PII in the AI SDK](/docs/guides/protect-pii-in-ai-sdk) - tokenize + restore at `egress:ui` * Demo 3 in `examples/nextjs-ai-sdk` - identical token across 50 steps * [Policy resolution](/docs/concepts/policy-resolution) --- # Quickstart URL: https://tailrace.dev/docs/get-started/quickstart > Block a fake API key and tokenize an email in an AI SDK route in under five minutes. Build an AI SDK route that **blocks secrets before they reach a model** and **tokenizes email**, then restores it at egress for your UI. ## What you will build [#what-you-will-build] A server route that: 1. Rejects prompts containing a fake Stripe test key with `PolicyViolationError`. 2. Tokenizes `customer@example.com` in the outbound prompt. 3. Restores the email in the HTTP response at a trusted egress boundary. ## Install [#install] ```bash pnpm add @tailrace/core @tailrace/ai-sdk ai @ai-sdk/openai ``` `ai@^5` must be installed alongside `@tailrace/ai-sdk`. ## Create Tailrace [#create-tailrace] Zero configuration blocks secrets and tokenizes common PII. ```ts import { createTailrace } from "@tailrace/core"; import { withAiSdk } from "@tailrace/ai-sdk"; export const tailrace = withAiSdk(createTailrace()); ``` ## Wrap the model [#wrap-the-model] Use the wrapped model with any AI SDK API (`generateText`, `streamText`, agents). ```ts import { openai } from "@ai-sdk/openai"; import { generateText } from "ai"; const workflowId = crypto.randomUUID(); const model = tailrace.model(openai("gpt-4o"), { agent: "demo", workflowId, }); const { text } = await generateText({ model, prompt: userPrompt, }); ``` Tailrace scans the prompt in `transformParams` **before** the provider is called. ## Trigger a block [#trigger-a-block] Send a prompt containing a fake test key (never use real secrets in examples): ```ts const prompt = "Use key sk_test_51FakeKeyForTailraceTests000FAKE to call the API."; ``` **Expected:** `PolicyViolationError` - the call aborts and the provider is never contacted. ```text policy blocked entity "api_key" via rule "entities.api_key" ``` Catch it in your route and return 422: ```ts import { PolicyViolationError } from "@tailrace/core"; try { await generateText({ model, prompt }); } catch (err) { if (err instanceof PolicyViolationError) { return Response.json( { error: { type: "policy_violation", entity: err.decisions[0]?.entity } }, { status: 422 }, ); } throw err; } ``` Seeing the block is the payoff. The secret never leaves your process. ## Tokenize, then restore [#tokenize-then-restore] Send a prompt with only an email: ```ts const prompt = "Please email customer@example.com about the invoice."; const { text } = await generateText({ model, prompt }); // text contains - what the model saw ``` Restore at egress before returning to the client: ```ts const restored = await tailrace.restore(text, { boundary: { kind: "egress", sink: "ui" }, identity: { agent: "demo" }, workflowId, }); return Response.json({ text: restored.output }); // "Please email customer@example.com about the invoice." ``` Detokenization **only** runs at egress. Calling `restore` at a model boundary throws. ## Runnable example [#runnable-example] From the Tailrace monorepo: ```bash pnpm --filter example-nextjs-ai-sdk dev ``` Open [http://localhost:3000](http://localhost:3000) and try **Run A** and **Run B**. ## Where next [#where-next] * [Protect PII in the AI SDK](/docs/guides/protect-pii-in-ai-sdk) - tools, streaming, workflow IDs * [Boundaries](/docs/concepts/boundaries) - model, tool, egress mental model * [AI SDK reference](/docs/reference/ai-sdk) - `wrapModel`, options, streaming modes --- # Use with AI tools URL: https://tailrace.dev/docs/get-started/use-with-ai-tools > Connect Cursor, Claude Code, Codex, or VS Code to Tailrace docs via MCP, llms.txt, or raw markdown URLs. Tailrace docs are published for both humans and agents. You can connect an AI tool three ways - pick one. ## 1. Docs MCP server (recommended) [#1-docs-mcp-server-recommended] The published site exposes a read-only [Model Context Protocol](https://modelcontextprotocol.io) server at: ```text https://tailrace.dev/mcp ``` It offers retrieval tools only (`search_docs`, `get_page`, `list_sections`) - no generation, no summarization. Opening that URL in a browser may return an error; use it from an MCP client. ### Cursor [#cursor] Add to `.cursor/mcp.json`: ```json { "mcpServers": { "tailrace-docs": { "url": "https://tailrace.dev/mcp" } } } ``` ### Claude Code [#claude-code] ```bash claude mcp add --transport http tailrace-docs https://tailrace.dev/mcp ``` ### Codex [#codex] ```bash codex mcp add tailrace-docs https://tailrace.dev/mcp ``` Or in `config.toml`: ```toml [mcp_servers.tailrace-docs] url = "https://tailrace.dev/mcp" ``` ### VS Code (Copilot) [#vs-code-copilot] ```json { "servers": { "tailrace-docs": { "type": "http", "url": "https://tailrace.dev/mcp" } } } ``` ### Claude (web / Desktop) [#claude-web--desktop] Settings → Connectors → Add custom connector → paste `https://tailrace.dev/mcp`. ### Try it [#try-it] After connecting, ask: * `Using the tailrace-docs MCP server, how do I block secrets in Claude Code?` * `Search Tailrace docs for tokenization and summarize the vault rules.` * `List the tools on tailrace-docs, then fetch the quickstart page.` Every docs page also has **Copy MCP URL** in the page actions bar. ## 2. llms.txt (zero install) [#2-llmstxt-zero-install] Fetch the curated index, then follow absolute `.md` links: ```bash curl -sL https://tailrace.dev/llms.txt curl -sL https://tailrace.dev/llms-full.txt ``` ## 3. Per-page markdown [#3-per-page-markdown] Append `.md` to any docs URL: ```bash curl -sL https://tailrace.dev/docs/get-started/quickstart.md ``` Agents that send `Accept: text/markdown` also receive markdown for `/docs/*` HTML paths. ## Related [#related] * [Quickstart](/docs/get-started/quickstart) * [Protect PII in AI SDK](/docs/guides/protect-pii-in-ai-sdk) * [Policy JSON Schema](https://tailrace.dev/schema/policy.v1.json) --- # Block secrets in Claude Code URL: https://tailrace.dev/docs/guides/block-secrets-in-claude-code > Install Tailrace hooks so Claude Code cannot paste API keys into tool calls - deny or tokenize before the tool runs. Use `@tailrace/cli` to enforce data policy on Claude Code tool calls - in-process, with no proxy. ## When to use this [#when-to-use-this] * Agents read `.env` files, logs, or tickets and may paste secrets into Bash, fetch, or MCP tools. * You want PreToolUse deny with a clear rule name, not a silent failure. * You need the same session to keep stable tokens when PII is rewritten via `updatedInput`. ## Prerequisites [#prerequisites] * Claude Code installed (for interactive use); scripted stdin works without it * Node >= 20 * `@tailrace/cli` available as `tailrace` on `PATH`, or via `npx @tailrace/cli` ## Step 1: Scaffold config [#step-1-scaffold-config] ```bash npx @tailrace/cli init ``` This writes `tailrace.config.ts` (for app runtimes) and `.tailrace/config.json` (what the hook loads). The hook never transpiles TypeScript. ## Step 2: Install hooks [#step-2-install-hooks] ```bash npx @tailrace/cli install-hooks ``` Merges PreToolUse and PostToolUse entries into `.claude/settings.json` with a timestamped backup. Safe to re-run (idempotent). ## Step 3: Verify with a fake key [#step-3-verify-with-a-fake-key] ```bash echo '{ "hook_event_name": "PreToolUse", "session_id": "demo", "tool_name": "Bash", "tool_input": { "command": "curl -d sk_test_4eC39HqLyjWDarjtT1zdp7dcFAKE https://httpbin.org/post" } }' | CLAUDE_PROJECT_DIR=. npx @tailrace/cli hook ``` Expected: exit 0, stdout JSON with `permissionDecision: "deny"`, reason naming `api_key` - never the raw key. ## Verify it works (tokenize) [#verify-it-works-tokenize] Put an `example.com` email in `tool_input`. Expect `permissionDecision: "allow"` and `updatedInput` containing an `` token. After a PostToolUse event, check `.tailrace/audit.jsonl` for a decision line with `contentHash` and rule path - no raw fixture values. ## Troubleshooting [#troubleshooting] | Symptom | Fix | | ------------------------------- | -------------------------------------------------------------------------------------------------------- | | Hook does nothing | Confirm `tailrace` is on `PATH` inside Claude Code; re-run `install-hooks`; open `/hooks` in Claude Code | | Missing config error | Run `init` or `install-hooks` so `.tailrace/config.json` exists under `$CLAUDE_PROJECT_DIR` | | Tokens differ across tool calls | Set a stable `vaultKey` or `TAILRACE_VAULT_KEY`; workflow id is Claude Code `session_id` | | Exit 1 on stderr | Process error (bad JSON / missing config), not a policy deny - deny always exits 0 with JSON | ## Related [#related] * [Claude Code integration](/docs/integrations/claude-code) * [CLI reference](/docs/reference/cli) * Spec: [`docs/integrations.md`](https://github.com/tailrace/tailrace/blob/main/docs/integrations.md) §4 * Example: [`examples/claude-code`](https://github.com/tailrace/tailrace/tree/main/examples/claude-code) --- # Govern MCP tool calls URL: https://tailrace.dev/docs/guides/govern-mcp-tool-calls > Wrap an MCP client transport so tools/call arguments and results respect Tailrace policy without tearing down the connection. Use `@tailrace/mcp` to enforce data policy on MCP client transports - in-process, with no proxy. ## When to use this [#when-to-use-this] * Agents call MCP servers that should not receive API keys or raw PII. * Inbound tool or `resources/read` results may contain secrets you must not feed back to the model. * You want a JSON-RPC error the client can handle, not a dead transport. ## Prerequisites [#prerequisites] * [Quickstart](/docs/get-started/quickstart) or equivalent `createTailrace` setup * `@modelcontextprotocol/sdk` `>=1` ## Step 1: Wrap the transport [#step-1-wrap-the-transport] **Fluent** (recommended): ```ts import { createTailrace } from "@tailrace/core"; import { withMcp } from "@tailrace/mcp"; const tailrace = withMcp(createTailrace()); const transport = tailrace.transport(sseTransport, { server: "salesforce" }); // Pass `transport` into the MCP Client. ``` **Standalone:** ```ts import { wrapTransport } from "@tailrace/mcp"; const transport = wrapTransport(tailrace, sseTransport, { server: "salesforce" }); ``` `server` becomes the policy boundary key prefix: `mcp:salesforce/{tool}`. ## Step 2: Set agent and workflow ID [#step-2-set-agent-and-workflow-id] ```ts const transport = tailrace.transport(sseTransport, { server: "salesforce", agent: "support-bot", workflowId: sessionId, }); ``` Defaults are `"default"` when omitted. ## What gets scanned [#what-gets-scanned] | Direction | Method | Boundary | | --------- | ----------------------- | -------------------------------------------------------- | | Out | `tools/call` arguments | `{ kind: "mcp", server, tool: name, direction: "out" }` | | In | `tools/call` result | same with `direction: "in"` | | In | `resources/read` result | `{ kind: "mcp", server, tool: "read", direction: "in" }` | All other JSON-RPC messages pass through unchanged. For `resources/read`, `tool` is always the literal `"read"` so policy keys stay stable. ## Verify it works [#verify-it-works] Outbound `tools/call` with a synthetic Stripe key (`sk_test_…FAKE`) should never reach the server. The client receives a JSON-RPC error: ```json { "jsonrpc": "2.0", "id": "", "error": { "code": -32001, "message": "Blocked by data policy: api_key may not be sent to mcp (rule: …)", "data": { "type": "policy_violation", "entity": "api_key", "rule": "…" } } } ``` Tokenize actions rewrite `arguments` / results in place and forward. ## Troubleshooting [#troubleshooting] | Symptom | Fix | | ------------------------- | -------------------------------------------------------------------------------- | | Nothing is scanned | Confirm you wrapped the transport the client actually uses | | Policy key mismatch | Match `server` and tool name to `mcp:{server}/{tool}` in `definePolicy` | | Transport closed on block | Should not happen - upgrade `@tailrace/mcp`; block synthesizes an error response | ## Related [#related] * [MCP integration](/docs/integrations/mcp) * [MCP reference](/docs/reference/mcp) * [Boundaries](/docs/concepts/boundaries) * Spec: [`docs/integrations.md`](https://github.com/tailrace/tailrace/blob/main/docs/integrations.md) §2 --- # Protect PII in the AI SDK URL: https://tailrace.dev/docs/guides/protect-pii-in-ai-sdk > Wrap models and tools, configure streaming block behavior, and restore tokenized values at egress. Use `@tailrace/ai-sdk` to enforce data policy on Vercel AI SDK model calls and tool executions - in-process, with no proxy. ## When to use this [#when-to-use-this] * You send user or ticket data to OpenAI, Anthropic, or other providers through the AI SDK. * Agent tools read CRM records, databases, or files that may contain PII or secrets. * You need the same email to become the same token across a multi-step agent run. ## Prerequisites [#prerequisites] * [Quickstart](/docs/get-started/quickstart) completed, or equivalent `createTailrace` + `wrapModel` setup * `ai@^5` and a provider package (`@ai-sdk/openai`, etc.) ## Step 1: Choose an API shape [#step-1-choose-an-api-shape] Two equivalent forms ship in v0.1: **Fluent** (recommended for app code): ```ts import { createTailrace } from "@tailrace/core"; import { withAiSdk } from "@tailrace/ai-sdk"; const tailrace = withAiSdk(createTailrace()); const model = tailrace.model(openai("gpt-4o")); const tools = tailrace.tools({ search: searchTool }); ``` **Standalone** (explicit imports): ```ts import { wrapModel, wrapTools } from "@tailrace/ai-sdk"; const model = wrapModel(tailrace, openai("gpt-4o")); const tools = wrapTools(tailrace, { search: searchTool }); ``` ## Step 2: Set agent and workflow ID [#step-2-set-agent-and-workflow-id] ```ts const model = tailrace.model(openai("gpt-4o"), { agent: "support-bot", workflowId: sessionId, }); ``` | Option | Purpose | | ------------ | -------------------------------------------------------------------------------- | | `agent` | Selects `identities` overrides in your policy document. Defaults to `"default"`. | | `workflowId` | Vault scope for tokens. Same ID + same value → same token every time. | Pass the same `workflowId` to `model`, `tools`, and `restore` within one conversation. ## Step 3: Wrap tools [#step-3-wrap-tools] ```ts import { tool } from "ai"; import { z } from "zod"; const tools = tailrace.tools( { fetchOrder: tool({ description: "Fetch order by id", inputSchema: z.object({ orderId: z.string() }), execute: async ({ orderId }) => db.orders.get(orderId), }), }, { agent: "support-bot", workflowId: sessionId }, ); ``` Tailrace checks: * Tool **arguments** at `{ kind: "tool", name, direction: "out" }` * Tool **results** at `{ kind: "tool", name, direction: "in" }` On block, the model receives a readable error (not a stack trace): ```text Blocked by data policy: api_key may not be sent to tool:fetchOrder:out (rule: entities.api_key) ``` Tools without `execute` are not wrapped. ## Step 4: Restore at egress [#step-4-restore-at-egress] The model wrapper tokenizes PII in prompts and completions. Your HTTP handler (or trusted UI sink) restores tokens: ```ts const result = await generateText({ model, prompt }); const { output } = await tailrace.restore(result.text, { boundary: { kind: "egress", sink: "ui" }, identity: { agent: "support-bot" }, workflowId: sessionId, }); return Response.json({ text: output }); ``` Pick a sink name that matches your policy (`egress:ui`, `egress:*`, etc.). ## Step 5: Configure streaming (optional) [#step-5-configure-streaming-optional] For `streamText`, choose how policy **`block`** on model **output** is handled: ```ts const model = tailrace.model(openai("gpt-4o"), { streamBlockBehavior: "abort", // default - recommended }); ``` | Mode | Behavior | | -------- | ----------------------------------------------------------------------------- | | `abort` | Hold-back scan; throw `PolicyViolationError`; fail-closed | | `buffer` | Buffer full response; check at end; throw on block | | `redact` | Replace blocked spans with `[ENTITY]`; stream continues - **not fail-closed** | Input blocking always aborts before the provider runs. `redact` applies only to streaming output. Non-streaming `generateText` always throws on output `block`. ## Verify it works [#verify-it-works] ### Block [#block] ```bash curl -s -X POST http://localhost:3000/api/chat \ -H 'content-type: application/json' \ -d '{"prompt":"Use sk_test_51FakeKeyForTailraceTests000FAKE"}' | jq . ``` Expect `422` and `"entity": "api_key"`. ### Tokenize + restore [#tokenize--restore] ```bash curl -s -X POST http://localhost:3000/api/chat \ -H 'content-type: application/json' \ -H 'x-workflow-id: test-session' \ -d '{"prompt":"Email customer@example.com"}' | jq . ``` Expect `200`, `modelSaw` containing ``, and `text` containing the real email. ## Troubleshooting [#troubleshooting] **Tokens change between requests** You are not passing the same `workflowId` to `model` and `restore`. Thread the session ID through both. **`restore` throws INVARIANT** You called `restore` at a model or tool boundary. Use `{ kind: "egress", sink: "…" }` only. **Stream aborts on benign output** A secret-class pattern matched in model output. Default policy blocks echoed secrets. Adjust policy only with care - secrets cannot be overridden to `allow` without `dangerouslyAllowSecrets: true`. ## Related [#related] * [Next.js integration](/docs/integrations/nextjs) - runnable Demo 1 app * [wrapModel reference](/docs/reference/ai-sdk/wrap-model) * [streamBlockBehavior reference](/docs/reference/ai-sdk/options#streamblockbehavior) --- # Claude Code URL: https://tailrace.dev/docs/integrations/claude-code > Wire Tailrace as Claude Code PreToolUse / PostToolUse hooks - block secrets, tokenize PII, audit tool I/O. Enforce data policy on every Claude Code tool call. PreToolUse denies secrets and rewrites tokenized PII before the tool runs. PostToolUse records audit lines without rewriting results in v0.1. ## Quickstart [#quickstart] ```bash pnpm add -D @tailrace/cli npx @tailrace/cli init npx @tailrace/cli install-hooks ``` Ensure `tailrace` is on `PATH` inside Claude Code sessions (workspace bin, global install, or wrap the installed command with `npx @tailrace/cli hook` if needed). ## Boundaries covered [#boundaries-covered] | Claude Code event | Tailrace boundary | Direction | v0.1 behavior | | --------------------------- | ------------------------------------------ | --------- | ---------------------------------- | | PreToolUse `tool_input` | `{ kind: "tool", name, direction: "out" }` | Outbound | deny / tokenize via `updatedInput` | | PostToolUse `tool_response` | `{ kind: "tool", name, direction: "in" }` | Inbound | audit only | Identity agent defaults to `"claude-code"`. Workflow id is Claude Code `session_id`. ## Settings shape [#settings-shape] `install-hooks` merges (non-destructively): ```json { "hooks": { "PreToolUse": [ { "matcher": "*", "hooks": [{ "type": "command", "command": "tailrace hook" }] } ], "PostToolUse": [ { "matcher": "*", "hooks": [{ "type": "command", "command": "tailrace hook" }] } ] } } ``` ## Runnable example [#runnable-example] ```bash git clone cd tailrace pnpm install pnpm --filter example-claude-code demo:2 ``` Scripted stdin fixtures exercise deny + tokenize + audit without a live Claude Code binary. Interactive walkthrough: [`examples/claude-code`](https://github.com/tailrace/tailrace/tree/main/examples/claude-code). ## Guides [#guides] * [Block secrets in Claude Code](/docs/guides/block-secrets-in-claude-code) * [CLI reference](/docs/reference/cli) --- # Hono URL: https://tailrace.dev/docs/integrations/hono > OpenAI-compatible Hono middleware - scan chat bodies and SSE streams, return 422 on policy block. Enforce data policy on OpenAI-compatible chat routes in Hono. Scan request messages before upstream, scan JSON and SSE responses on the way back. ## Quickstart [#quickstart] ```ts import { createTailrace } from "@tailrace/core"; import { tailraceHono } from "@tailrace/hono"; import { Hono } from "hono"; const app = new Hono(); const tailrace = createTailrace(); app.use( "/v1/*", tailraceHono(tailrace, { agent: (c) => c.req.header("x-agent-id") ?? "default", workflowId: (c) => c.req.header("x-workflow-id") ?? "default", }), ); ``` Peer: `hono` `>=4`. Only `mode: "openai-compatible"` in v0.1. ## Boundaries covered [#boundaries-covered] | Location | Tailrace boundary | Direction | | ----------------------- | --------------------------------------------------- | -------------------- | | Chat request messages | `{ kind: "model", provider }` (`model` field as-is) | Outbound to upstream | | JSON chat completion | same model boundary | Inbound | | SSE `text/event-stream` | same; carry-buffer across chunks | Inbound | Block → **422** `{ error: { type: "policy_violation", entity, rule } }`. SSE: cancel upstream, emit one error `data:` event, close (abort-only; no `streamBlockBehavior` in v0.1). ## Guides [#guides] * [Protect PII in the AI SDK](/docs/guides/protect-pii-in-ai-sdk) - related model-boundary patterns * [Boundaries](/docs/concepts/boundaries) ## Reference [#reference] * [Hono package](/docs/reference/hono) --- # MCP URL: https://tailrace.dev/docs/integrations/mcp > Wrap MCP client transports with Tailrace - JSON-RPC error on block, rewrite on tokenize. Enforce data policy on MCP `tools/call` and `resources/read` without tearing down the transport. ## Quickstart [#quickstart] ```ts import { createTailrace } from "@tailrace/core"; import { withMcp } from "@tailrace/mcp"; const tailrace = withMcp(createTailrace()); const transport = tailrace.transport(sseTransport, { server: "salesforce" }); ``` Peer: `@modelcontextprotocol/sdk` `>=1`. ## Boundaries covered [#boundaries-covered] | Location | Tailrace boundary | Direction | | ----------------------- | -------------------------------------------------------- | --------- | | `tools/call` arguments | `{ kind: "mcp", server, tool, direction: "out" }` | Outbound | | `tools/call` result | same with `direction: "in"` | Inbound | | `resources/read` result | `{ kind: "mcp", server, tool: "read", direction: "in" }` | Inbound | Block → JSON-RPC error `code: -32001` with `{ type: "policy_violation", entity, rule }` (never the raw value). ## Guides [#guides] * [Govern MCP tool calls](/docs/guides/govern-mcp-tool-calls) * [Boundaries](/docs/concepts/boundaries) ## Reference [#reference] * [MCP package](/docs/reference/mcp) --- # Next.js URL: https://tailrace.dev/docs/integrations/nextjs > Run Tailrace with the Vercel AI SDK in a Next.js App Router route - block secrets, tokenize PII, restore at egress. Enforce data policy on AI SDK model calls in Next.js. Scan prompts before they reach a provider, scan completions on the way back, and restore tokenized PII at egress for your UI. ## Quickstart [#quickstart] ```ts // app/api/chat/route.ts import { createTailrace, PolicyViolationError } from "@tailrace/core"; import { withAiSdk } from "@tailrace/ai-sdk"; import { openai } from "@ai-sdk/openai"; import { generateText } from "ai"; const tailrace = withAiSdk(createTailrace()); export async function POST(req: Request) { const { prompt } = await req.json(); const workflowId = req.headers.get("x-workflow-id") ?? crypto.randomUUID(); const model = tailrace.model(openai("gpt-4o"), { workflowId, agent: "chat" }); try { const result = await generateText({ model, prompt }); const restored = await tailrace.restore(result.text, { boundary: { kind: "egress", sink: "ui" }, identity: { agent: "chat" }, workflowId, }); return Response.json({ text: restored.output }); } catch (err) { if (err instanceof PolicyViolationError) { return Response.json( { error: { type: "policy_violation", message: err.message } }, { status: 422 }, ); } throw err; } } ``` ## Boundaries covered [#boundaries-covered] | Location | Tailrace boundary | Direction | | ----------------------- | ---------------------------------------------- | --------------------------------- | | Request prompt → model | `{ kind: "model", provider: "openai/gpt-4o" }` | Outbound to provider | | Model completion → app | Same model boundary | Inbound from provider | | HTTP response → browser | `{ kind: "egress", sink: "ui" }` | Restore (explicit `restore` call) | | Tool args / results | `{ kind: "tool", name, direction }` | Out / in (with `wrapTools`) | Provider strings use `${providerId}/${modelId}`. Policy globs like `openai/*` match accordingly. ## Runnable example [#runnable-example] ```bash git clone cd tailrace pnpm install pnpm --filter example-nextjs-ai-sdk dev ``` Open [http://localhost:3000](http://localhost:3000). | Button | Expected outcome | | -------------------------- | ---------------------------------------------------- | | Run A - block secret | 422, `entity: api_key`, secret never hits mock model | | Run B - tokenize + restore | 200, UI shows real email, `modelSaw` shows token | Source: [`examples/nextjs-ai-sdk`](https://github.com/tailrace/tailrace/tree/main/examples/nextjs-ai-sdk) in the monorepo. ## Edge vs Node [#edge-vs-node] The example sets `export const runtime = "nodejs"`. `@tailrace/core` and `@tailrace/ai-sdk` also run on Edge when your provider and AI SDK usage are Edge-compatible. Keep vault keys in environment variables (`TAILRACE_VAULT_KEY` or `createTailrace({ vault: { key } })`). ## Guides [#guides] * [Protect PII in the AI SDK](/docs/guides/protect-pii-in-ai-sdk) * [Quickstart](/docs/get-started/quickstart) ## Reference [#reference] * [AI SDK package](/docs/reference/ai-sdk) --- # Playground URL: https://tailrace.dev/docs/playground > Paste text, see Tier 0 spans, and toggle policy actions - entirely in your browser. Paste text and watch Tier 0 detection plus policy resolution run in-process. Toggle actions to see how the same spans re-render under a different policy. Scanning is 100% client-side after this page loads. Pasted content is never uploaded, and the page does not ship analytics of what you paste. Once loaded, the playground works offline. ## See it in practice [#see-it-in-practice] * [Detection tiers](/docs/concepts/detection-tiers) - what Tier 0 covers and accuracy expectations * [Policy resolution](/docs/concepts/policy-resolution) - precedence and secrets-cannot-be-allowed * [Quickstart](/docs/get-started/quickstart) - the same engine in your app --- # AI SDK URL: https://tailrace.dev/docs/reference/ai-sdk > Reference for @tailrace/ai-sdk - wrap models and tools, fluent helpers, and streaming options. `@tailrace/ai-sdk` integrates Tailrace with the [Vercel AI SDK](https://sdk.vercel.ai) (`ai@^5`). ## Install [#install] ```bash pnpm add @tailrace/core @tailrace/ai-sdk ai ``` Peer dependency: `ai@^5`. ## Exports [#exports] | Export | Description | | --------------------------------------------------------------------------- | ------------------------------------------------------ | | [`withAiSdk`](/docs/reference/ai-sdk/with-ai-sdk) | Attach fluent `model` / `tools` to a Tailrace instance | | [`wrapModel`](/docs/reference/ai-sdk/wrap-model) | Middleware wrapper for `LanguageModelV2` | | [`wrapTools`](/docs/reference/ai-sdk/wrap-tools) | Type-preserving wrapper for a `ToolSet` | | [`encodeModelProvider`](/docs/reference/ai-sdk/encode-model-provider) | Provider string encoding for policy keys | | [`AiSdkWrapOptions`](/docs/reference/ai-sdk/options) | Shared options type | | [`StreamBlockBehavior`](/docs/reference/ai-sdk/options#streamblockbehavior) | Streaming output block modes | Types: `TailraceWithAiSdk`, `AiSdkWrapOptions`, `StreamBlockBehavior`. ## Default behavior [#default-behavior] With `createTailrace()` and no custom policy: * Secret-class entities → **block** * Email, phone, credit card, IBAN, SSN → **tokenize** * Model input block → throw before provider call * Model output block (stream default) → throw after hold-back scan See [`@tailrace/core` default policy](/docs/concepts/policy-resolution) for the full table. ## See also [#see-also] * [Quickstart](/docs/get-started/quickstart) * [Protect PII guide](/docs/guides/protect-pii-in-ai-sdk) * [Normative spec](https://github.com/tailrace/tailrace/blob/main/docs/integrations.md) --- # encodeModelProvider URL: https://tailrace.dev/docs/reference/ai-sdk/encode-model-provider > Encode a LanguageModelV2 as a policy boundary provider string. Convert an AI SDK model's `provider` and `modelId` into the string used in `{ kind: "model", provider }` boundaries and policy glob keys. ## Import [#import] ```ts import { encodeModelProvider } from "@tailrace/ai-sdk"; ``` ## Signature [#signature] ```ts function encodeModelProvider(model: Pick): string; ``` ## Returns [#returns] | Input | Output | | ------------------------------------------ | ------------------------- | | `provider: "openai"`, `modelId: "gpt-4o"` | `"openai/gpt-4o"` | | `modelId: "openai/gpt-4o"` (gateway-style) | `"openai/gpt-4o"` | | `modelId: ""` | `provider` or `"unknown"` | ## Use in policy [#use-in-policy] ```ts definePolicy({ boundaries: { "openai/*": { entities: { email: "tokenize" } }, "openai/gpt-4o-mini": { entities: { email: "allow" } }, }, }); ``` Exact keys beat globs. Model globs do not match tool or MCP boundary keys. ## Example [#example] ```ts encodeModelProvider({ provider: "openai", modelId: "gpt-4o" }); // "openai/gpt-4o" ``` ## Related [#related] * [wrapModel](/docs/reference/ai-sdk/wrap-model) --- # Options URL: https://tailrace.dev/docs/reference/ai-sdk/options > AiSdkWrapOptions and StreamBlockBehavior for model and tool wrappers. Shared configuration for [`wrapModel`](/docs/reference/ai-sdk/wrap-model), [`wrapTools`](/docs/reference/ai-sdk/wrap-tools), and [`withAiSdk`](/docs/reference/ai-sdk/with-ai-sdk). ## AiSdkWrapOptions [#aisdkwrapoptions] ```ts interface AiSdkWrapOptions { agent?: string; workflowId?: string | (() => string); streamBlockBehavior?: StreamBlockBehavior; onDecision?: (decisions: Decision[]) => void; } ``` ### `agent` [#agent] **Type:** `string`\ **Default:** `"default"` Identity agent id for policy resolution. Matches keys under `identities` in your policy document. ### `workflowId` [#workflowid] **Type:** `string | (() => string)`\ **Default:** `"default"` Vault scope for tokenization. The same workflow ID and detected value produce the same token on every check. Use a chat session ID, thread ID, or per-run UUID. Pass the same value to `restore` at egress. ### `streamBlockBehavior` [#streamblockbehavior] **Type:** `"abort" | "buffer" | "redact"`\ **Default:** `"abort"`\ **Applies to:** `wrapStream` only (model output streaming) Controls how a policy **`block`** on model output is translated. Input blocking always throws before the provider runs. #### `abort` [#abort] Hold-back scan with a 128-character carry buffer. Safe prefix text is emitted incrementally. On block: cancel the stream and throw `PolicyViolationError`. **Fail-closed.** Recommended default. #### `buffer` [#buffer] Accumulate the entire stream, run one `check` at end, throw on block. No incremental output until the stream completes. **Fail-closed.** Use for very long single-line secrets (large JWTs, PEM keys) that exceed the carry window. #### `redact` [#redact] Hold-back scan. On block, apply **`mask`** (`[ENTITY]` labels) instead of throwing. Stream continues. **Not fail-closed.** Opt-in only. Audit decisions record `action: "block"` with `appliedAs: "mask"`. Non-streaming `wrapGenerate` always throws on block regardless of this option. ### `onDecision` [#ondecision] **Type:** `(decisions: Decision[]) => void` Called after each successful `check` with audit decisions. Decisions include `entity`, `rule`, `contentHash`, and span offsets - **never raw values**. Also forwarded to Tailrace audit sinks configured on the instance. ## StreamBlockBehavior [#streamblockbehavior-1] ```ts type StreamBlockBehavior = "abort" | "buffer" | "redact"; ``` ## Example [#example] ```ts tailrace.model(openai("gpt-4o"), { agent: "billing-bot", workflowId: () => getSessionId(), streamBlockBehavior: "abort", onDecision: (decisions) => { metrics.count("tailrace.decisions", decisions.length); }, }); ``` ## Related [#related] * [wrapModel](/docs/reference/ai-sdk/wrap-model) * [Protect PII guide - streaming](/docs/guides/protect-pii-in-ai-sdk#step-5--configure-streaming-optional) --- # withAiSdk URL: https://tailrace.dev/docs/reference/ai-sdk/with-ai-sdk > Attach fluent model and tools helpers to an existing Tailrace instance. Add `model()` and `tools()` methods to a Tailrace instance without importing AI SDK types into `@tailrace/core`. ## Import [#import] ```ts import { withAiSdk } from "@tailrace/ai-sdk"; ``` ## Signature [#signature] ```ts function withAiSdk(tailrace: Tailrace): TailraceWithAiSdk; ``` ## Parameters [#parameters] ### `tailrace` [#tailrace] A Tailrace instance from `createTailrace()`. ## Returns [#returns] The same instance, extended with: ```ts interface TailraceWithAiSdk extends Tailrace { model(model: LanguageModelV2, opts?: AiSdkWrapOptions): LanguageModelV2; tools(tools: T, opts?: AiSdkWrapOptions): T; } ``` Implementations delegate to [`wrapModel`](/docs/reference/ai-sdk/wrap-model) and [`wrapTools`](/docs/reference/ai-sdk/wrap-tools). ## Example [#example] ```ts import { createTailrace } from "@tailrace/core"; import { withAiSdk } from "@tailrace/ai-sdk"; import { openai } from "@ai-sdk/openai"; const tailrace = withAiSdk(createTailrace()); const model = tailrace.model(openai("gpt-4o")); const tools = tailrace.tools({/* ... */}); ``` `tailrace.restore` and `tailrace.check` remain available on the same object. ## Related [#related] * [wrapModel](/docs/reference/ai-sdk/wrap-model) - standalone form * [Quickstart](/docs/get-started/quickstart) --- # wrapModel URL: https://tailrace.dev/docs/reference/ai-sdk/wrap-model > Wrap a LanguageModelV2 with Tailrace middleware - scan prompts, completions, and streams. Wrap a language model so prompts and generated text pass through Tailrace policy at the **model** boundary. ## Import [#import] ```ts import { wrapModel } from "@tailrace/ai-sdk"; ``` ## Signature [#signature] ```ts function wrapModel( tailrace: Tailrace, model: LanguageModelV2, opts?: AiSdkWrapOptions, ): LanguageModelV2; ``` ## Parameters [#parameters] ### `tailrace` [#tailrace] A Tailrace instance from `createTailrace()`. ### `model` [#model] Any AI SDK `LanguageModelV2` (for example from `@ai-sdk/openai`). ### `opts` [#opts] Optional. See [AiSdkWrapOptions](/docs/reference/ai-sdk/options). ## Returns [#returns] A new `LanguageModelV2` that delegates to the original model through Tailrace middleware. Pass it to `generateText`, `streamText`, or other AI SDK APIs. ## Middleware hooks [#middleware-hooks] | Hook | When | On `block` | | ----------------- | ------------------------------ | -------------------------------------------------- | | `transformParams` | Before provider call | Throws `PolicyViolationError`; provider not called | | `wrapGenerate` | After non-streaming completion | Throws `PolicyViolationError` | | `wrapStream` | On each stream chunk | Depends on `streamBlockBehavior` (default: throw) | Scanned content: **text** parts in prompt messages. Non-text parts pass through in v0.1. ## Provider encoding [#provider-encoding] The model boundary uses `encodeModelProvider(model)`: * `openai` + `gpt-4o` → `openai/gpt-4o` * Gateway id `openai/gpt-4o` → `openai/gpt-4o` (no double prefix) Policy keys such as `openai/*` match this string. ## Example [#example] ```ts import { createTailrace } from "@tailrace/core"; import { wrapModel } from "@tailrace/ai-sdk"; import { openai } from "@ai-sdk/openai"; import { generateText } from "ai"; const tailrace = createTailrace(); const model = wrapModel(tailrace, openai("gpt-4o"), { agent: "support", workflowId: "sess-1", }); await generateText({ model, prompt: "Hello" }); ``` ## Throws [#throws] * **`PolicyViolationError`**: policy resolved to `block` on input or non-streaming output, or on streaming output when `streamBlockBehavior` is `abort` or `buffer`. ## Related [#related] * [wrapTools](/docs/reference/ai-sdk/wrap-tools) * [streamBlockBehavior](/docs/reference/ai-sdk/options#streamblockbehavior) --- # wrapTools URL: https://tailrace.dev/docs/reference/ai-sdk/wrap-tools > Wrap a ToolSet so tool arguments and return values pass through Tailrace policy. Wrap each tool's `execute` function so arguments and results are checked at the **tool** boundary. ## Import [#import] ```ts import { wrapTools } from "@tailrace/ai-sdk"; ``` ## Signature [#signature] ```ts function wrapTools(tailrace: Tailrace, tools: T, opts?: AiSdkWrapOptions): T; ``` ## Parameters [#parameters] ### `tailrace` [#tailrace] A Tailrace instance from `createTailrace()`. ### `tools` [#tools] An AI SDK `ToolSet` object (for example the `tools` argument to `generateText` or an agent). ### `opts` [#opts] Optional. See [AiSdkWrapOptions](/docs/reference/ai-sdk/options). `streamBlockBehavior` has no effect on tools. ## Returns [#returns] A tool set with the **same type** `T`. Type inference must match the input; degraded inference is a bug. Tools **without** `execute` are copied unchanged. ## Check boundaries [#check-boundaries] | Phase | Boundary | | ---------------- | ------------------------------------------ | | Before `execute` | `{ kind: "tool", name, direction: "out" }` | | After `execute` | `{ kind: "tool", name, direction: "in" }` | ## Throws [#throws] On policy `block`, throws a standard `Error` (not `PolicyViolationError`) with message: ```text Blocked by data policy: {entity} may not be sent to {boundary} (rule: {rule}) ``` The AI SDK surfaces this to the model as a tool error. ## Example [#example] ```ts import { tool } from "ai"; import { z } from "zod"; import { createTailrace } from "@tailrace/core"; import { wrapTools } from "@tailrace/ai-sdk"; const tailrace = createTailrace(); const tools = wrapTools( tailrace, { lookup: tool({ description: "Look up a record", inputSchema: z.object({ id: z.string() }), execute: async ({ id }) => ({ id, status: "ok" }), }), }, { workflowId: "run-1" }, ); ``` ## Related [#related] * [wrapModel](/docs/reference/ai-sdk/wrap-model) * [Protect PII guide](/docs/guides/protect-pii-in-ai-sdk) --- # CLI URL: https://tailrace.dev/docs/reference/cli > Reference for the tailrace binary - init, scan, install-hooks, and the Claude Code hook handler. `@tailrace/cli` ships the `tailrace` binary. Runtime: Node >= 20. Depends on `@tailrace/core` only. ## Install [#install] ```bash pnpm add -D @tailrace/cli ``` ## Commands [#commands] | Command | Description | | ------------------------------------------------ | ------------------------------------------------------------------ | | `tailrace init [--force]` | Detect stack; write `tailrace.config.ts` + `.tailrace/config.json` | | `tailrace scan [--json]` | Tier 0 scan; exit 1 on any `block`-class hit | | `tailrace install-hooks [--scope project\|user]` | Merge hooks into Claude Code settings (backup first) | | `tailrace hook` | Claude Code PreToolUse / PostToolUse handler (stdin → stdout) | ## `init` [#init] Detects `next` → `ai` → `hono` → generic Node from nearest `package.json`. Refuses to overwrite `tailrace.config.ts` unless `--force`. Prints a short integration snippet for the detected stack. ## `scan` [#scan] Walks files (skips `node_modules`, `.git`, build dirs, binaries) or reads stdin when path is `-`. Exit `1` if any span resolves to `block`. Human output: path + entity + rule (never raw values). `--json` emits a machine-readable array of hits. ## `install-hooks` [#install-hooks] | Flag | Default | Effect | | ----------------- | ------- | ------------------------------------------------------ | | `--scope project` | yes | `$CLAUDE_PROJECT_DIR/.claude/settings.json` (or `cwd`) | | `--scope user` | | `~/.claude/settings.json` | Appends matcher `"*"` PreToolUse + PostToolUse command hooks with `command: "tailrace hook"` only if not already present. Ensures `.tailrace/config.json` exists. ## `hook` [#hook] JSON path exclusively for policy decisions (always exit 0). Process errors exit 1. | Case | Stdout | | ---------------- | ----------------------------------------------------- | | Clean PreToolUse | empty | | Tokenize / mask | `permissionDecision: "allow"` + full `updatedInput` | | Block | `permissionDecision: "deny"` + reason (entity + rule) | | PostToolUse | empty (audit-only in v0.1) | Config: `.tailrace/config.json` only (no TS transpile on the hot path). Identity agent default `"claude-code"`; `workflowId` = Claude Code `session_id`. Perf budget: spawn-to-exit p50 \< 150ms (CI gate). ## Compiled config [#compiled-config] ```json { "version": 1, "agent": "claude-code", "vaultKey": "", "policy": {} } ``` Omit `policy` for the default (secrets → block, common PII → tokenize). Prefer `TAILRACE_VAULT_KEY` over committing `vaultKey` in prod. ## Guides [#guides] * [Block secrets in Claude Code](/docs/guides/block-secrets-in-claude-code) * [Claude Code integration](/docs/integrations/claude-code) --- # Errors URL: https://tailrace.dev/docs/reference/errors > Stable Tailrace error codes - what each means and where to fix the call site. Every Tailrace error extends `TailraceError` with a stable `code`. Messages never include detected values - only entity classes, rule paths, and hashes. Each thrown message ends with a docs URL for that code. | Code | Class | When | | ------------------------------------------------------------- | ------------------------- | --------------------------------------------- | | [`POLICY_VIOLATION`](/docs/reference/errors/POLICY_VIOLATION) | `PolicyViolationError` | A `block` rule matched | | [`POLICY_INVALID`](/docs/reference/errors/POLICY_INVALID) | `PolicyValidationError` | Policy document failed validation | | [`INVARIANT`](/docs/reference/errors/INVARIANT) | `InvariantViolationError` | Contract breach (e.g. restore at non-egress) | | [`VAULT`](/docs/reference/errors/VAULT) | `VaultError` | Vault storage / crypto / lookup failure | | [`RECOGNIZER`](/docs/reference/errors/RECOGNIZER) | `RecognizerError` | Recognizer threw while scanning | | [`NOT_IMPLEMENTED`](/docs/reference/errors/NOT_IMPLEMENTED) | `NotImplementedError` | Specified but not shipped yet (e.g. `review`) | --- # INVARIANT URL: https://tailrace.dev/docs/reference/errors/INVARIANT > An internal contract was breached - for example restore outside egress. ## What it means [#what-it-means] `InvariantViolationError` (`code: INVARIANT`). A hard product invariant was violated regardless of policy. ## Message shape [#message-shape] ```text → https://tailrace.dev/docs/reference/errors/INVARIANT ``` ## Common causes [#common-causes] 1. Calling `tailrace.restore` at a `model`, `tool`, `mcp`, or `telemetry` boundary. 2. Asking the vault/actions layer to do something only egress may do. ## Fixes [#fixes] 1. Restore only at `{ kind: "egress", sink: "…" }`. 2. Keep model/tool paths on `check` (tokenize/mask/block), not `restore`. ## Safe to catch? [#safe-to-catch] Treat as a programmer error. Fix the call site; do not swallow and continue. --- # NOT_IMPLEMENTED URL: https://tailrace.dev/docs/reference/errors/NOT_IMPLEMENTED > A specified surface is not implemented in this release. ## What it means [#what-it-means] `NotImplementedError` (`code: NOT_IMPLEMENTED`). The API is typed or documented but not shipped yet. ## Message shape [#message-shape] ```text → https://tailrace.dev/docs/reference/errors/NOT_IMPLEMENTED ``` ## Common causes [#common-causes] 1. Policy action `review` (human-in-the-loop) - v0.2. 2. Other milestone-gated surfaces still throwing stubs. ## Fixes [#fixes] 1. Use `block`, `tokenize`, `mask`, or `allow` instead of `review`. 2. Check the package changelog / milestone docs for when the surface lands. ## Safe to catch? [#safe-to-catch] At config validation time - treat as a config error and refuse to start with that policy. --- # POLICY_INVALID URL: https://tailrace.dev/docs/reference/errors/POLICY_INVALID > The policy document failed schema or semantic validation. ## What it means [#what-it-means] `PolicyValidationError` (`code: POLICY_INVALID`). `definePolicy` / compile rejected the document. `path` points at the offending key. ## Message shape [#message-shape] ```text → https://tailrace.dev/docs/reference/errors/POLICY_INVALID ``` ## Common causes [#common-causes] 1. Unknown action string. 2. `detokenize` under a non-egress boundary key. 3. Invalid `format` on an entity rule. ## Fixes [#fixes] 1. Validate against [Policy schema](/docs/reference/policy-schema) / `https://tailrace.dev/schema/policy.v1.json`. 2. Put `detokenize` only under `egress:*` boundary keys. 3. Use `allow` | `mask` | `tokenize` | `block` (and `review` only when you accept `NOT_IMPLEMENTED`). ## Safe to catch? [#safe-to-catch] Usually at startup / config load. Fail closed - do not run with an invalid policy. --- # POLICY_VIOLATION URL: https://tailrace.dev/docs/reference/errors/POLICY_VIOLATION > A block policy matched - the sensitive value must not cross the boundary. ## What it means [#what-it-means] `PolicyViolationError` (`code: POLICY_VIOLATION`). Policy resolved to `block` for one or more spans. Integrations translate this into their host failure mode (throw, JSON-RPC error, HTTP 422, Claude Code deny). ## Message shape [#message-shape] ```text policy blocked entity "" via rule "" → https://tailrace.dev/docs/reference/errors/POLICY_VIOLATION ``` `decisions` on the error lists each blocked span (entity, boundary, rule, contentHash) - never the raw value. ## Common causes [#common-causes] 1. Default policy (or yours) blocks secret classes (`api_key`, `jwt`, …) at this boundary. 2. Prompt, tool args, or MCP payload still contains a live secret. 3. Streaming `abort` mode hit a complete secret span mid-stream. ## Fixes [#fixes] 1. Remove the secret from the payload, or tokenize upstream and send the token. 2. If you intentionally need the value at a trusted egress, use `tailrace.restore` only at `{ kind: "egress", sink }` - never at model/tool/mcp. 3. Relaxing a secret to `allow` requires `dangerouslyAllowSecrets: true` on the rule (and is almost never correct). ## Safe to catch? [#safe-to-catch] Yes at integration boundaries - map to the host error. Do not log `decisions` values that might have been attached elsewhere; hashes and entity classes only. --- # RECOGNIZER URL: https://tailrace.dev/docs/reference/errors/RECOGNIZER > A recognizer threw while scanning input. ## What it means [#what-it-means] `RecognizerError` (`code: RECOGNIZER`). A detection engine threw during `scan`. ## Message shape [#message-shape] ```text → https://tailrace.dev/docs/reference/errors/RECOGNIZER ``` ## Common causes [#common-causes] 1. Custom recognizer bug. 2. Optional Tier 1 engine misconfiguration (should degrade to Tier 0 with a warning - if you see this, the recognizer threw instead of failing open). ## Fixes [#fixes] 1. Fix or remove the custom recognizer. 2. Ensure Tier 1 optional models fail open per product rules - never crash the host for a missing model. ## Safe to catch? [#safe-to-catch] For `block`-configured entity classes, fail closed if detection cannot run. For availability of optional Tier 1, prefer degrade-to-Tier-0. --- # VAULT URL: https://tailrace.dev/docs/reference/errors/VAULT > Vault storage, encryption, or lookup failed. ## What it means [#what-it-means] `VaultError` (`code: VAULT`). The vault adapter failed (corrupt record, collision, missing key material, KV errors). ## Message shape [#message-shape] ```text → https://tailrace.dev/docs/reference/errors/VAULT ``` ## Common causes [#common-causes] 1. Corrupt or truncated ciphertext in storage. 2. Token collision for the same workflow key. 3. Misconfigured vault key / KV binding. ## Fixes [#fixes] 1. Ensure a stable vault key (`TAILRACE_VAULT_KEY` or config) across the workflow lifetime. 2. Purge or rotate workflow vault entries if records are corrupt. 3. Check KV permissions and key encoding (hex / string as documented). ## Safe to catch? [#safe-to-catch] At egress you may surface a user-visible failure. Prefer fail closed over returning a tokenized value as if it were restored. --- # Hono URL: https://tailrace.dev/docs/reference/hono > Reference for @tailrace/hono - tailraceHono middleware and TailraceHonoOptions. `@tailrace/hono` provides OpenAI-compatible passthrough middleware for Hono. It scans chat request bodies and JSON / SSE responses at the model boundary. ## Install [#install] ```bash pnpm add @tailrace/core @tailrace/hono hono ``` Peer dependency: `hono` `>=4` (bound against `4.12.x` `MiddlewareHandler` / `Context`). ## Exports [#exports] | Export | Description | | --------------------- | ---------------------------------- | | `tailraceHono` | Returns a Hono `MiddlewareHandler` | | `TailraceHonoOptions` | Options type | ## Options [#options] | Option | Default | Notes | | ------------ | --------------------- | --------------------------- | | `mode` | `"openai-compatible"` | Only mode in v0.1 | | `agent` | `"default"` | `(c) => string` | | `workflowId` | `"default"` | `string` or `(c) => string` | | `onDecision` | - | Audit callback | Boundary provider is the request body's `model` string as-is (no `${providerId}/${modelId}` rewrite). ## Block shape [#block-shape] **422** JSON `{ error: { type: "policy_violation", entity, rule } }`. SSE: one `data:` error event then close (abort-equivalent only). ## Guides [#guides] * [Hono integration](/docs/integrations/hono) * [Protect PII in the AI SDK](/docs/guides/protect-pii-in-ai-sdk) --- # MCP URL: https://tailrace.dev/docs/reference/mcp > Reference for @tailrace/mcp - wrapTransport, withMcp, and McpWrapOptions. `@tailrace/mcp` wraps an MCP client `Transport` so outbound `tools/call` arguments and inbound tool / `resources/read` results pass through Tailrace. ## Install [#install] ```bash pnpm add @tailrace/core @tailrace/mcp @modelcontextprotocol/sdk ``` Peer dependency: `@modelcontextprotocol/sdk` `>=1` (bound against `1.29.0` `Transport`). ## Exports [#exports] | Export | Description | | ----------------- | ----------------------------------------------------- | | `wrapTransport` | Standalone transport wrapper | | `withMcp` | Attach fluent `transport(...)` to a Tailrace instance | | `McpWrapOptions` | Options type | | `TailraceWithMcp` | Fluent instance type | ## Options [#options] | Option | Required | Notes | | ------------ | -------- | --------------------------------------- | | `server` | yes | Policy key prefix `mcp:{server}/{tool}` | | `agent` | no | Default `"default"` | | `workflowId` | no | Default `"default"` | | `onDecision` | no | Audit callback | ## Block shape [#block-shape] JSON-RPC 2.0 error on the pending request id: `code: -32001`, message names entity + rule, `data: { type: "policy_violation", entity, rule }`. Transport stays open. ## Guides [#guides] * [Govern MCP tool calls](/docs/guides/govern-mcp-tool-calls) * [MCP integration](/docs/integrations/mcp) --- # Policy schema URL: https://tailrace.dev/docs/reference/policy-schema > Shape of a Tailrace policy document - defaults, entities, boundaries, identities, and the published JSON Schema. A policy document is what you pass to `definePolicy` / `createTailrace({ policy })`. Validate JSON configs against the published schema: ```text https://tailrace.dev/schema/policy.v1.json ``` ## Top-level fields [#top-level-fields] | Field | Type | Notes | | ------------------------- | ------------------------------------ | --------------------------------------------------- | | `defaults.action` | action | Fallback when no entity rule matches | | `entities` | map of entity → rule | Default actions per entity class | | `boundaries` | map of boundary key → `{ entities }` | Glob keys like `openai/*`, `mcp:salesforce/*` | | `identities` | map of agent id → scope | Per-agent overrides (same shape as top-level scope) | | `dangerouslyAllowSecrets` | boolean | Required to relax secret classes away from `block` | ## Actions [#actions] `allow` · `mask` · `tokenize` · `block` · `review` (throws `NOT_IMPLEMENTED` in v0.1) · `detokenize` (egress keys only) A rule is either a bare action string or: ```json { "action": "tokenize", "format": "preserve", "dangerouslyAllowSecrets": false } ``` ## Minimal example [#minimal-example] ```json { "$schema": "https://tailrace.dev/schema/policy.v1.json", "entities": { "api_key": "block", "email": "tokenize" }, "boundaries": { "mcp:salesforce/*": { "entities": { "email": "tokenize" } } } } ``` ## Related [#related] * [Policy resolution](/docs/concepts/policy-resolution) * [Use with AI tools](/docs/get-started/use-with-ai-tools) ---