Skip to content

PII Protection

This is a core project goal, not a feature flag. Every prompt sent to a remote LLM provider has PII stripped before it leaves the host. Every outbound HTTP request carries an opaque, single-use request ID with no PII, no timestamp, and no correlation data. This behavior cannot be disabled.

When you call a remote LLM provider (Anthropic, OpenAI, Azure, OpenRouter, etc.), three categories of data leave your machine:

CategoryRiskAgentZero Defense
Prompt textMay contain user-authored PII (names, emails, SSNs, credit cards) or PII surfaced by tools (file contents, API responses, database query results)PrivacyFirstLayer — mandatory PII redaction on every prompt
HTTP headersMay contain correlatable identifiers (session IDs, User-Agent fingerprints, IP-derived trace IDs)Opaque UUID v4 X-Request-ID, generic User-Agent: agentzero
Tool call resultsMay contain data from files, databases, or APIs that include PIISame PrivacyFirstLayer — tool results are sanitized inside ConversationMessage::ToolResult before they enter the prompt window

Local providers (Candle, llama.cpp, Ollama on localhost) are exempt — data never leaves the machine, so stripping it would reduce model quality for no security benefit.

The PiiRedactionGuard detects and replaces 9 PII pattern categories:

PatternExampleReplacement
Email addressesalice@example.com[EMAIL_REDACTED]
US phone numbers555-123-4567[PHONE_REDACTED]
Social Security Numbers123-45-6789[SSN_REDACTED]
API keyssk-abcdef..., AKIA..., ghp_...[API_KEY_REDACTED]
Credit card numbers4111 1111 1111 1111[CC_REDACTED]
JWT tokenseyJhbGci...[JWT_REDACTED]
SSH private keys-----BEGIN RSA PRIVATE KEY-----[SSH_KEY_REDACTED]
Database connection stringspostgres://user:pass@host/db[DB_CONN_REDACTED]
IPv4 addresses203.0.113.42[IP_REDACTED]

Patterns are evaluated most-specific-first so that structured patterns (database URIs containing @) are replaced before less specific patterns (email addresses matching @) can partially match.

IPv4 addresses include private ranges (10.x, 127.x, 192.168.x) — the Rust regex crate does not support lookahead, so we err on the side of over-redaction. False positives are safer than false negatives.

User Prompt
→ PrivacyFirstLayer (outermost pipeline layer, always on)
→ PiiRedactionGuard.check_input() on every text field
→ System message content
→ User message content
→ Assistant message content
→ Tool result content
→ Sanitized prompt forwarded to inner layers
→ GuardrailsLayer (optional user-configured guards)
→ MetricsLayer (timing + token counting)
→ CostCapLayer (per-run budget enforcement)
→ Base Provider (makes the actual HTTP call)
→ apply_privacy_headers()
→ X-Request-ID: <uuid-v4>
→ User-Agent: agentzero
→ traceparent (process-internal span ID, no PII)
→ HTTP POST to provider API

Every outbound HTTP request to a remote provider gets a fresh UUID v4 X-Request-ID header. UUID v4 is 128 bits of cryptographic randomness — no timestamp component, no MAC address, no session affinity. Each request gets a unique ID that cannot be correlated with any other request, any user, or any session.

The User-Agent header is set to the generic string agentzero — no version number, no OS fingerprint, no platform information. This prevents provider-side fingerprinting.

These headers are applied at the transport layer (apply_privacy_headers() in transport.rs), which is the last code that touches the reqwest::RequestBuilder before .send(). All 5 HTTP send paths (Anthropic sync + 2 streaming, OpenAI sync + 2 streaming) go through this single chokepoint.

Each PII redaction event increments the Prometheus counter agentzero_pii_redactions_total. This lets operators monitor for PII in their prompt streams — if the counter is climbing, users are submitting prompts that contain PII, which may indicate a training gap or a need for upstream data masking.

The counter does not record which PII was found — only that a redaction occurred. The actual PII never appears in metrics, logs, or any observable surface.

BehaviorWhy It’s Mandatory
PII redaction on remote provider promptsCore project safety guarantee. An opt-out would create a class of deployments where PII can leak.
UUID v4 request IDs (no PII, no timestamp)Correlatable identifiers would allow providers to link requests across sessions.
Generic User-AgentPlatform fingerprinting is a passive tracking vector.
SettingDefaultDescription
[guardrails] in agentzero.tomlAudit modeAdditional user-configured guards (prompt injection detection, Unicode injection, custom patterns) run inside the mandatory PrivacyFirstLayer. These are opt-in and configurable.
[privacy] modeThe Noise protocol encrypted transport (privacy = "encrypted" or "full") adds transport-level encryption on top of PII stripping. This is complementary — Noise encrypts the entire request, while PII stripping removes PII from the content.
Local provider exemptionAutomaticLocal providers (Candle, llama.cpp, Ollama, LMStudio, etc.) are automatically exempt from PII stripping because data never leaves the machine. The exemption is checked via is_local_provider() — it cannot be manually overridden to force-strip local prompts.

To add a new detection pattern, extend PiiRedactionGuard::default() in crates/agentzero-providers/src/guardrails.rs. Each pattern is a named regex with a redaction placeholder:

PiiPattern {
name: "my_custom_pattern",
regex: regex::Regex::new(r"my-regex-here")
.expect("regex should compile"),
redaction: "[CUSTOM_REDACTED]",
},

Place more specific patterns before less specific ones in the patterns vec. The PrivacyFirstLayer will automatically pick up the new pattern — no additional wiring required.

FileRole
crates/agentzero-providers/src/privacy_layer.rsPrivacyFirstLayer — mandatory pipeline wrapper
crates/agentzero-providers/src/guardrails.rsPiiRedactionGuard — pattern definitions and redaction logic
crates/agentzero-providers/src/transport.rsapply_privacy_headers() — request ID and User-Agent injection
crates/agentzero-infra/src/runtime.rsPipeline wiring — PrivacyFirstLayer added as outermost layer