pi-privacy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick (zahnno)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # pi-privacy
2
+
3
+ **Privacy posture + TEE attestation for [Pi](https://pi.dev) providers.** A Pi
4
+ extension that cryptographically **verifies** confidential-enclave (TEE) inference,
5
+ **enforces** zero-data-retention (ZDR) routing, detects **on-device** inference, and
6
+ grades every provider on one honest ladder — so a guarantee you can *prove* never
7
+ reads like one a vendor merely *claims*.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pi install npm:pi-privacy
13
+ ```
14
+
15
+ That's it — Pi loads the extension, which registers the privacy providers below and
16
+ starts verifying posture. Check the current model any time with:
17
+
18
+ ```
19
+ /verify
20
+ ```
21
+
22
+ ## The one rule: verified ≠ asserted
23
+
24
+ Every tier states the strength of its evidence. A green "verified TEE" badge means
25
+ remote attestation actually checked the hardware; a ZDR badge means the provider
26
+ *promises* not to retain data. Those are different things, and pi-privacy never lets
27
+ them look the same.
28
+
29
+ | Tier | Badge | Evidence | Meaning |
30
+ |---|---|---|---|
31
+ | `tee-verified` | **Verified TEE** | cryptographic | Remote attestation proved genuine enclave hardware **and** the live TLS key matched the report. |
32
+ | `local` | **On-device** | observable | Loopback endpoint — inference runs locally, nothing leaves the machine. |
33
+ | `zdr-enforced` | **ZDR (enforced)** | observable | Zero-retention routing actively pinned — requests only reach non-retaining providers. Policy, not hardware. |
34
+ | `tee-unverified` | **TEE (unconfirmed)** | none | Provider claims a TEE, but attestation was incomplete or unmatched. |
35
+ | `zdr-policy` | **ZDR (by policy)** | policy | Provider promises zero retention; unverifiable. Not hardware, not attested. |
36
+ | `standard` | **Standard** | none | No special guarantee. |
37
+
38
+ ## Providers
39
+
40
+ | Provider | Tier | How it's checked |
41
+ |---|---|---|
42
+ | `tinfoil` | Verified TEE | SEV-SNP attestation; the enclave's TLS key (SPKI) is pinned against the connection Pi actually uses |
43
+ | `nearai` | Verified TEE | Attestation report (Intel TDX + NVIDIA CC) fetched over HTTPS, bound to a fresh nonce |
44
+ | `openrouter` | ZDR (posture-aware) | `zdr-policy` until enforcement pins routing → `zdr-enforced` |
45
+ | `venice`, `fireworks` | ZDR (by policy) | Provider policy; honest limits noted (e.g. Venice is not TEE-attested) |
46
+ | `ollama`, `custom` | On-device | Detected when the endpoint is a loopback URL |
47
+
48
+ Providers with no verifiable or default privacy channel (Together, DeepSeek, MiniMax,
49
+ Qwen, …) are intentionally left `standard` with **no badge** — anything else would
50
+ overclaim.
51
+
52
+ ## How verification works
53
+
54
+ - **Tinfoil (SPKI pinning).** Pi's provider requests flow through a process-wide
55
+ `undici` dispatcher that captures the enclave's TLS public-key fingerprint on the
56
+ *actual inference connection*. That fingerprint is matched against
57
+ `report_data[0:32]` of the signed SEV-SNP attestation — so "verified" means the
58
+ channel you're using demonstrably ends inside the enclave.
59
+ - **NEAR (report body).** A fresh nonce is sent with the attestation request; the
60
+ returned report must carry a TEE signing key and hardware evidence and echo the
61
+ nonce (freshness / anti-replay).
62
+ - **ZDR (enforced).** For OpenRouter, requests carry
63
+ `provider: { zdr: true, data_collection: "deny" }`. OpenRouter filters routing to
64
+ compliant providers and returns `404 No allowed providers` when the policy can't be
65
+ met — it doesn't silently ignore the constraint, which is why `zdr-enforced` is honest.
66
+ - **Local.** A loopback endpoint (`localhost` / `127.0.0.1`) is observable, so on-device
67
+ inference is detected rather than claimed.
68
+
69
+ These are *pragmatic* checks suited to an interactive agent, not a replacement for a
70
+ full verifier ([nearai/cloud-verifier](https://github.com/nearai/cloud-verifier),
71
+ [tinfoilsh/tinfoil-cli](https://github.com/tinfoilsh/tinfoil-cli)); `/verify` prints
72
+ the raw report so you can take it to one.
73
+
74
+ ## Programmatic use
75
+
76
+ ```ts
77
+ import { makePiPrivacyExtension, verifyModelPosture, effectiveTier } from "pi-privacy";
78
+
79
+ // Configure the extension (e.g. enforce ZDR, receive posture updates):
80
+ const ext = makePiPrivacyExtension({
81
+ enforceOpenRouterZdr: true, // opt-in; a model with no ZDR endpoint will 404
82
+ onPosture: (r) => renderBadge(r), // { tier, teePosture?, attestation? }
83
+ });
84
+
85
+ // Verify a specific model on demand:
86
+ const posture = await verifyModelPosture("tinfoil", "llama3-3-70b");
87
+ // → { tier: "tee-verified", teePosture: "green", attestation: {...} }
88
+
89
+ // Or just the static/enforcement tier, no network:
90
+ effectiveTier("openrouter", { zdrEnforced: true }); // → "zdr-enforced"
91
+ ```
92
+
93
+ `makePiPrivacyExtension(options?)` — `installDispatcher`, `registerProviders`,
94
+ `enforceOpenRouterZdr`, `useDispatcherTransport`, `onPosture`.
95
+
96
+ ## Requirements
97
+
98
+ Node ≥ 22.19.0 (the Pi runtime's floor). MIT licensed.
@@ -0,0 +1,10 @@
1
+ // Pi package extension entry. Pi discovers extensions from this `extensions/`
2
+ // directory (or the `pi.extensions` manifest paths) and loads each file's default
3
+ // export as the extension factory `(pi) => void`.
4
+ //
5
+ // The default export is makePiPrivacyExtension() with default options: install the
6
+ // attestation dispatcher, register the config-only privacy providers, verify TEE
7
+ // posture, and add /verify. Consumers who want to configure it (e.g. enforce
8
+ // OpenRouter ZDR, or hook onPosture) import { makePiPrivacyExtension } from the
9
+ // package root instead.
10
+ export { default } from "../src/extension.ts";
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "pi-privacy",
3
+ "version": "0.1.0",
4
+ "description": "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 one.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Patrick (zahnno)",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/privateer-agent/pi-privacy.git"
11
+ },
12
+ "homepage": "https://github.com/privateer-agent/pi-privacy",
13
+ "keywords": [
14
+ "pi-package",
15
+ "pi",
16
+ "pi-extension",
17
+ "tee",
18
+ "attestation",
19
+ "confidential-computing",
20
+ "tinfoil",
21
+ "near",
22
+ "zdr",
23
+ "zero-data-retention",
24
+ "privacy",
25
+ "sev-snp"
26
+ ],
27
+ "pi": {
28
+ "extensions": ["./extensions"]
29
+ },
30
+ "exports": {
31
+ ".": "./src/index.ts",
32
+ "./attest": "./src/attest/dispatcher.ts",
33
+ "./attestation": "./src/attest/attestation.ts",
34
+ "./extension": "./src/extension.ts"
35
+ },
36
+ "files": [
37
+ "src",
38
+ "extensions",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "scripts": {
43
+ "typecheck": "tsc --noEmit",
44
+ "test": "for f in tests/*.test.ts; do node --import tsx --test \"$f\" || exit 1; done"
45
+ },
46
+ "engines": {
47
+ "node": ">=22.19.0"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "peerDependencies": {
53
+ "@earendil-works/pi-coding-agent": ">=0.80.0",
54
+ "@earendil-works/pi-ai": ">=0.80.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "@earendil-works/pi-coding-agent": { "optional": true },
58
+ "@earendil-works/pi-ai": { "optional": true }
59
+ },
60
+ "dependencies": {
61
+ "undici": "^7.28.0"
62
+ },
63
+ "devDependencies": {
64
+ "@types/node": "^20.14.0",
65
+ "tsx": "^4.16.0",
66
+ "typescript": "^5.5.0"
67
+ }
68
+ }
@@ -0,0 +1,268 @@
1
+ import { randomBytes, createHash, X509Certificate } from "node:crypto";
2
+ import { request as httpsRequest } from "node:https";
3
+ import type { TLSSocket } from "node:tls";
4
+ import { gunzipSync } from "node:zlib";
5
+
6
+ // Ported from privateer 0.2 (tree-cli/src/providers/attestation.ts). The pure
7
+ // attestation logic is verbatim; what changed for the public package:
8
+ // - DROPPED the privateer-server-proxy path (fetchAttestationViaServer + the
9
+ // auth/privateer import) — that's the private account channel, not public.
10
+ // - config type + base-URL constants inlined (no config/schema / registry deps).
11
+ // The SEV-SNP report_data parse, SPKI match, posture mappings are untouched.
12
+
13
+ const TIMEOUT_MS = 12_000;
14
+
15
+ export const NEARAI_BASE_URL = "https://cloud-api.near.ai/v1";
16
+ export const TINFOIL_BASE_URL = "https://inference.tinfoil.sh/v1";
17
+
18
+ // Minimal provider config the attestation calls need (replaces ProviderConfig).
19
+ export interface AttestConfig {
20
+ apiKey?: string;
21
+ baseURL?: string;
22
+ }
23
+
24
+ export type TeePosture = "green" | "yellow" | "red";
25
+
26
+ // ── NEAR AI TEE attestation ──────────────────────────────────────────────────
27
+ // Every NEAR AI Cloud model runs inside a Trusted Execution Environment (Intel TDX
28
+ // confidential VM + NVIDIA confidential-computing GPU). On request, the gateway
29
+ // returns a cryptographic attestation report proving the model is running on
30
+ // genuine TEE hardware, with a signing key that never leaves the enclave bound to
31
+ // a caller-supplied nonce (report_data = signing_address || nonce).
32
+ //
33
+ // Pragmatic check suited to a TUI: fetch a fresh report bound to our nonce and
34
+ // confirm it carries a TEE signing key plus hardware evidence. We do NOT re-validate
35
+ // the raw NVIDIA/Intel quote chains here — that's the full verifier's job
36
+ // (github.com/nearai/cloud-verifier); expose `raw` so a user can take it there.
37
+
38
+ export interface Attestation {
39
+ model: string;
40
+ nonce: string; // the 32-byte hex nonce we sent (freshness / anti-replay)
41
+ signingAddress?: string; // TEE-bound key that signs inference responses
42
+ nonceEchoed: boolean; // our nonce appears in the report → it's fresh, not replayed
43
+ hardware: string[]; // detected evidence, e.g. ["NVIDIA", "Intel TDX"]
44
+ raw: unknown; // full report, for display + external verification
45
+ }
46
+
47
+ function baseFor(cfg: AttestConfig): string {
48
+ return (cfg.baseURL ?? NEARAI_BASE_URL).replace(/\/+$/, "");
49
+ }
50
+
51
+ // A 32-byte (64 hex char) random nonce, per NEAR's attestation API guidance.
52
+ export function randomNonce(): string {
53
+ return randomBytes(32).toString("hex");
54
+ }
55
+
56
+ // Recursively find the first string value under any of `keys` (case-insensitive).
57
+ function deepFindString(obj: unknown, keys: string[]): string | undefined {
58
+ const want = new Set(keys.map((k) => k.toLowerCase()));
59
+ const stack: unknown[] = [obj];
60
+ while (stack.length) {
61
+ const cur = stack.pop();
62
+ if (Array.isArray(cur)) {
63
+ stack.push(...cur);
64
+ } else if (cur && typeof cur === "object") {
65
+ for (const [k, v] of Object.entries(cur)) {
66
+ if (typeof v === "string" && want.has(k.toLowerCase()) && v.trim()) return v.trim();
67
+ if (v && typeof v === "object") stack.push(v);
68
+ }
69
+ }
70
+ }
71
+ return undefined;
72
+ }
73
+
74
+ // Fetch and interpret the attestation report for a model. Throws a readable error
75
+ // on missing key / network / HTTP failure so a caller can show "unverified".
76
+ export async function fetchAttestation(cfg: AttestConfig, modelId: string): Promise<Attestation> {
77
+ if (!cfg.apiKey) throw new Error("no API key");
78
+ const nonce = randomNonce();
79
+ const url =
80
+ `${baseFor(cfg)}/attestation/report` +
81
+ `?model=${encodeURIComponent(modelId)}&signing_algo=ecdsa&nonce=${nonce}`;
82
+
83
+ const ac = new AbortController();
84
+ const timer = setTimeout(() => ac.abort(), TIMEOUT_MS);
85
+ let raw: unknown;
86
+ try {
87
+ const res = await fetch(url, {
88
+ headers: { authorization: `Bearer ${cfg.apiKey}` },
89
+ signal: ac.signal,
90
+ });
91
+ if (!res.ok) {
92
+ const body = await res.text().catch(() => "");
93
+ const hint = body.slice(0, 200).trim();
94
+ throw new Error(`HTTP ${res.status} ${res.statusText}${hint ? ` — ${hint}` : ""}`);
95
+ }
96
+ raw = await res.json();
97
+ } catch (err) {
98
+ if (err instanceof Error && err.name === "AbortError") {
99
+ throw new Error(`timed out after ${TIMEOUT_MS / 1000}s`);
100
+ }
101
+ throw err;
102
+ } finally {
103
+ clearTimeout(timer);
104
+ }
105
+
106
+ return interpretReport(modelId, nonce, raw);
107
+ }
108
+
109
+ // Turn a raw NEAR attestation report into our Attestation posture. Pure.
110
+ export function interpretReport(modelId: string, nonce: string, raw: unknown): Attestation {
111
+ const signingAddress = deepFindString(raw, ["signing_address", "signingAddress", "address"]);
112
+ // Hardware evidence is detected by scanning the serialized report for the quote
113
+ // markers each vendor uses — robust to the exact response shape.
114
+ const blob = JSON.stringify(raw).toLowerCase();
115
+ const hardware: string[] = [];
116
+ if (/nvidia|gpu/.test(blob)) hardware.push("NVIDIA");
117
+ if (/intel|tdx/.test(blob)) hardware.push("Intel TDX");
118
+ const nonceEchoed = blob.includes(nonce.toLowerCase());
119
+
120
+ return { model: modelId, nonce, signingAddress, nonceEchoed, hardware, raw };
121
+ }
122
+
123
+ // Map an attestation to a status color. GREEN: fresh report bound to our nonce with
124
+ // a TEE signing key and hardware evidence. YELLOW: a report came back but it's
125
+ // missing the signing key, hardware evidence, or nonce echo. RED: no material.
126
+ export function teePosture(att: Attestation): TeePosture {
127
+ if (!att.signingAddress && att.hardware.length === 0) return "red";
128
+ if (att.signingAddress && att.hardware.length > 0 && att.nonceEchoed) return "green";
129
+ return "yellow";
130
+ }
131
+
132
+ // ── Tinfoil TEE attestation ──────────────────────────────────────────────────
133
+ // Tinfoil enclaves publish an attestation document at /.well-known/tinfoil-attestation:
134
+ // { format: <predicate URL>, body: <base64 (gzipped) hardware report> }. The
135
+ // document attests the *host* — the gateway itself runs in the enclave. For the
136
+ // SEV-SNP predicate, report_data[0:32] is the SHA-256 of the enclave's TLS public
137
+ // key (SPKI DER): TLS terminates inside the enclave, so matching that hash against
138
+ // the key on the connection that served the document proves the channel ends in
139
+ // attested hardware. Pragmatic check — we do NOT re-validate the AMD signature
140
+ // chain or Sigstore measurements (that's github.com/tinfoilsh/tinfoil-cli's job).
141
+
142
+ export interface TinfoilAttestation {
143
+ host: string; // host the document came from (attestation is per-host, not per-model)
144
+ format: string; // predicate URL declaring the report type
145
+ hardware: string[]; // TEE platform(s) the predicate names, e.g. ["AMD SEV-SNP"]
146
+ attestedTlsKeyFp?: string; // report_data[0:32]: hash of the enclave's TLS key
147
+ liveTlsKeyFp?: string; // hash of the TLS key that actually served the document
148
+ tlsKeyMatched: boolean; // live key is the attested key → channel ends in the enclave
149
+ raw: unknown; // full document, for display + external verification
150
+ }
151
+
152
+ // What the transport hands back: the parsed well-known document plus the SPKI
153
+ // SHA-256 of the leaf cert on the connection that served it. Injectable so tests
154
+ // can exercise interpretation without a TLS handshake, and so the connection can be
155
+ // the real inference socket (see dispatcherTransport in ./dispatcher.ts).
156
+ export type TinfoilTransport = (host: string) => Promise<{ doc: unknown; liveTlsKeyFp?: string }>;
157
+
158
+ // Node's fetch never exposes the peer certificate, so the default transport drops
159
+ // to https.request and reads the leaf cert off the socket that delivered the body —
160
+ // the binding is only meaningful against that exact connection.
161
+ export const httpsTransport: TinfoilTransport = (host) =>
162
+ new Promise((resolve, reject) => {
163
+ const url = new URL(`https://${host}/.well-known/tinfoil-attestation`);
164
+ const req = httpsRequest(
165
+ {
166
+ hostname: url.hostname,
167
+ port: url.port ? Number(url.port) : 443,
168
+ path: url.pathname,
169
+ method: "GET",
170
+ timeout: TIMEOUT_MS,
171
+ },
172
+ (res) => {
173
+ let liveTlsKeyFp: string | undefined;
174
+ try {
175
+ const peer = (res.socket as TLSSocket).getPeerCertificate();
176
+ if (peer?.raw) {
177
+ const spki = new X509Certificate(peer.raw).publicKey.export({ type: "spki", format: "der" });
178
+ liveTlsKeyFp = createHash("sha256").update(spki).digest("hex");
179
+ }
180
+ } catch {
181
+ // cert unavailable → binding stays unconfirmed (yellow), not an error
182
+ }
183
+ let body = "";
184
+ res.on("data", (chunk) => (body += chunk));
185
+ res.on("end", () => {
186
+ if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
187
+ const hint = body.slice(0, 200).trim();
188
+ reject(new Error(`HTTP ${res.statusCode} ${res.statusMessage ?? ""}${hint ? ` — ${hint}` : ""}`));
189
+ return;
190
+ }
191
+ try {
192
+ resolve({ doc: JSON.parse(body), liveTlsKeyFp });
193
+ } catch {
194
+ reject(new Error("attestation endpoint returned non-JSON"));
195
+ }
196
+ });
197
+ res.on("error", reject);
198
+ },
199
+ );
200
+ req.on("timeout", () => req.destroy(new Error(`timed out after ${TIMEOUT_MS / 1000}s`)));
201
+ req.on("error", reject);
202
+ req.end();
203
+ });
204
+
205
+ // Fetch and interpret the attestation document for the configured Tinfoil endpoint.
206
+ // Needs no API key — the well-known endpoint is public.
207
+ export async function fetchTinfoilAttestation(
208
+ cfg: AttestConfig,
209
+ transport: TinfoilTransport = httpsTransport,
210
+ ): Promise<TinfoilAttestation> {
211
+ const host = new URL(cfg.baseURL ?? TINFOIL_BASE_URL).host;
212
+ const { doc, liveTlsKeyFp } = await transport(host);
213
+ return interpretTinfoilDoc(host, doc, liveTlsKeyFp);
214
+ }
215
+
216
+ // Turn a raw attestation document into our posture inputs. Pure and lenient: a
217
+ // malformed document degrades to "no material" (red) rather than throwing.
218
+ export function interpretTinfoilDoc(
219
+ host: string,
220
+ doc: unknown,
221
+ liveTlsKeyFp?: string,
222
+ ): TinfoilAttestation {
223
+ const d = (doc ?? {}) as { format?: unknown; body?: unknown };
224
+ const format = typeof d.format === "string" ? d.format : "";
225
+ // Hardware evidence comes from the predicate URL itself (e.g.
226
+ // ".../predicate/sev-snp-guest/v2", ".../snp-tdx-multiplatform/v1").
227
+ const fmt = format.toLowerCase();
228
+ const hardware: string[] = [];
229
+ if (/sev|snp/.test(fmt)) hardware.push("AMD SEV-SNP");
230
+ if (/tdx/.test(fmt)) hardware.push("Intel TDX");
231
+ if (/nitro/.test(fmt)) hardware.push("AWS Nitro");
232
+
233
+ // Decode the report: base64, gunzipped when the gzip magic leads.
234
+ let report: Buffer | undefined;
235
+ if (typeof d.body === "string" && d.body) {
236
+ try {
237
+ let bytes = Buffer.from(d.body, "base64");
238
+ if (bytes[0] === 0x1f && bytes[1] === 0x8b) bytes = gunzipSync(bytes);
239
+ report = bytes;
240
+ } catch {
241
+ // undecodable body → treated as absent
242
+ }
243
+ }
244
+
245
+ // The SEV-SNP report layout puts report_data (64 bytes) at 0x50; Tinfoil packs
246
+ // the TLS-key hash into its first half. Only that predicate's layout is known, so
247
+ // other formats fall back to scanning the report for the live key's bytes.
248
+ const attestedTlsKeyFp =
249
+ /sev-snp-guest/.test(fmt) && report && report.length >= 0x90
250
+ ? report.subarray(0x50, 0x70).toString("hex")
251
+ : undefined;
252
+ const tlsKeyMatched =
253
+ !!liveTlsKeyFp &&
254
+ !!report &&
255
+ (attestedTlsKeyFp === liveTlsKeyFp || report.includes(Buffer.from(liveTlsKeyFp, "hex")));
256
+
257
+ return { host, format, hardware, attestedTlsKeyFp, liveTlsKeyFp, tlsKeyMatched, raw: doc };
258
+ }
259
+
260
+ // Posture mapping for Tinfoil, mirroring teePosture. GREEN: a TEE predicate plus the
261
+ // live TLS key found inside the attested report (channel demonstrably ends in the
262
+ // enclave). YELLOW: a report came back but the binding couldn't be confirmed here.
263
+ // RED: no attestation material at all.
264
+ export function tinfoilTeePosture(att: TinfoilAttestation): TeePosture {
265
+ if (att.hardware.length === 0 && !att.attestedTlsKeyFp) return "red";
266
+ if (att.hardware.length > 0 && att.tlsKeyMatched) return "green";
267
+ return "yellow";
268
+ }
@@ -0,0 +1,94 @@
1
+ // The out-of-band attestation interposer — the moat, as a reusable module.
2
+ //
3
+ // Pi's extension hooks can't reach the TLS peer certificate (pi-ai strips the
4
+ // provider response to { status, headers }), so a process-wide undici global
5
+ // dispatcher captures the enclave's TLS SPKI hash on the `connect` hook — exactly
6
+ // what a Tinfoil attestation report pins. Spike-proven to work even when installed
7
+ // at extension-init (Pi resolves the global dispatcher at call time), so a Pi
8
+ // extension can install this without a pre-import boot shim.
9
+
10
+ import crypto from "node:crypto";
11
+ import { Agent, buildConnector, setGlobalDispatcher } from "undici";
12
+
13
+ export interface CapturedCert {
14
+ subject: string;
15
+ issuer: string;
16
+ // SHA-256 of the peer cert's SubjectPublicKeyInfo (SPKI DER) — the value a
17
+ // Tinfoil attestation report pins (the enclave TLS key fingerprint).
18
+ spkiSha256: string;
19
+ fingerprint256?: string;
20
+ error?: string;
21
+ }
22
+
23
+ // host -> captured cert. Keyed per-host because undici pools keep-alive sockets:
24
+ // the connect hook fires ONLY on a NEW connection, so a reused socket skips it.
25
+ // Do NOT pre-pool a connection to an attested host before the first read.
26
+ const captured = new Map<string, CapturedCert>();
27
+ let installed = false;
28
+
29
+ // Install the global dispatcher (idempotent). Call from an extension factory or a
30
+ // pre-Pi boot module — both work (see spike-ext-dispatcher).
31
+ export function installAttestationDispatcher(): void {
32
+ if (installed) return;
33
+ installed = true;
34
+
35
+ const baseConnect = buildConnector({});
36
+ const attestingConnector: typeof baseConnect = (opts, cb) =>
37
+ baseConnect(opts, (err, socket) => {
38
+ const host = opts.hostname;
39
+ if (
40
+ !err &&
41
+ socket &&
42
+ host &&
43
+ !captured.has(host) &&
44
+ typeof (socket as any).getPeerCertificate === "function"
45
+ ) {
46
+ try {
47
+ const cert = (socket as any).getPeerCertificate(true);
48
+ if (cert && cert.raw) {
49
+ const spkiDer = new crypto.X509Certificate(cert.raw).publicKey.export({
50
+ type: "spki",
51
+ format: "der",
52
+ });
53
+ captured.set(host, {
54
+ subject: cert.subject?.CN ?? JSON.stringify(cert.subject),
55
+ issuer: cert.issuer?.O ?? cert.issuer?.CN ?? JSON.stringify(cert.issuer),
56
+ spkiSha256: crypto.createHash("sha256").update(spkiDer).digest("hex"),
57
+ fingerprint256: cert.fingerprint256,
58
+ });
59
+ }
60
+ } catch (e) {
61
+ captured.set(host, { error: String(e) } as CapturedCert);
62
+ }
63
+ }
64
+ cb(err, socket as any);
65
+ });
66
+
67
+ setGlobalDispatcher(new Agent({ connect: attestingConnector, connectTimeout: 8000 }));
68
+ }
69
+
70
+ export function getCapturedCert(host: string): CapturedCert | undefined {
71
+ return captured.get(host);
72
+ }
73
+
74
+ export function capturedHosts(): ReadonlyMap<string, CapturedCert> {
75
+ return captured;
76
+ }
77
+
78
+ // A TinfoilTransport that binds attestation to the SPKI captured on the real
79
+ // provider connection (rather than a separate https.request). Fetches the
80
+ // well-known document via global fetch — which flows through the dispatcher, so the
81
+ // connect hook records the enclave's SPKI — then reads it back per host. Stronger
82
+ // than httpsTransport: it proves the CONNECTION Pi actually uses ends in the enclave.
83
+ import type { TinfoilTransport } from "./attestation.ts";
84
+
85
+ export const dispatcherTransport: TinfoilTransport = async (host) => {
86
+ const res = await fetch(`https://${host}/.well-known/tinfoil-attestation`);
87
+ if (!res.ok) {
88
+ const hint = (await res.text().catch(() => "")).slice(0, 200).trim();
89
+ throw new Error(`HTTP ${res.status} ${res.statusText}${hint ? ` — ${hint}` : ""}`);
90
+ }
91
+ const doc = await res.json();
92
+ const cert = getCapturedCert(host);
93
+ return { doc, liveTlsKeyFp: cert && !cert.error ? cert.spkiSha256 : undefined };
94
+ };
@@ -0,0 +1,36 @@
1
+ // Pure `before_provider_request` payload transforms. Kept separate so they're unit
2
+ // -testable without a live session; the extension wires them per current provider.
3
+
4
+ function asObject(payload: unknown): Record<string, unknown> {
5
+ return payload && typeof payload === "object" ? { ...(payload as Record<string, unknown>) } : {};
6
+ }
7
+
8
+ // Venice: disable Venice's injected system prompt by setting
9
+ // venice_parameters.include_venice_system_prompt = false on the request body.
10
+ // This is the code-blocker from the migration plan (Appendix A) — no models.json
11
+ // equivalent exists, so it's a request-body patch. Returns a NEW payload.
12
+ export function veniceRequestPatch(payload: unknown): Record<string, unknown> {
13
+ const p = asObject(payload);
14
+ const vp = p.venice_parameters && typeof p.venice_parameters === "object"
15
+ ? (p.venice_parameters as Record<string, unknown>)
16
+ : {};
17
+ return { ...p, venice_parameters: { ...vp, include_venice_system_prompt: false } };
18
+ }
19
+
20
+ // OpenRouter: pin ZERO-DATA-RETENTION routing so requests only reach endpoints that
21
+ // don't retain data. Turns openrouter's tier from zdr-policy → zdr-enforced.
22
+ //
23
+ // VERIFIED LIVE (2026-07-07): OpenRouter honors `provider.data_collection: "deny"`
24
+ // and `provider.zdr: true` — it filters routing to compliant providers and returns
25
+ // 404 "No allowed providers are available" when the policy can't be satisfied
26
+ // (i.e. it does NOT silently ignore the constraint). Mirrors pi-ai's native
27
+ // OpenRouterRouting (`compat.openRouterRouting`), so the request body is identical
28
+ // whether it comes from here or pi-ai. This is enforcement (observable routing),
29
+ // NOT attestation — the zdr-enforced tier says so.
30
+ export function openRouterZdrPatch(payload: unknown): Record<string, unknown> {
31
+ const p = asObject(payload);
32
+ const provider = p.provider && typeof p.provider === "object"
33
+ ? (p.provider as Record<string, unknown>)
34
+ : {};
35
+ return { ...p, provider: { ...provider, zdr: true, data_collection: "deny" } };
36
+ }
@@ -0,0 +1,172 @@
1
+ // The Pi extension entry — what a marketplace install (or privateer-agent) loads.
2
+ //
3
+ // Wires the package together: installs the attestation dispatcher at extension-init
4
+ // (spike-proven to intercept provider TLS from here), registers the config-only
5
+ // privacy providers, patches venice / OpenRouter requests, tracks the current model
6
+ // to compute its posture, and adds a `/verify` command. Structural Pi typing keeps
7
+ // it decoupled from Pi's exact internal types (verified against the installed
8
+ // ExtensionAPI / ProviderConfigInput in 0.80.3).
9
+
10
+ import { installAttestationDispatcher, dispatcherTransport } from "./attest/dispatcher.ts";
11
+ import { PRIVACY_PROVIDERS, type PrivacyProvider } from "./providers/catalog.ts";
12
+ import { veniceRequestPatch, openRouterZdrPatch } from "./ext/patches.ts";
13
+ import { verifyModelPosture, type PostureResult } from "./posture/verify.ts";
14
+ import { TIERS } from "./posture/tiers.ts";
15
+
16
+ // ── structural Pi surface (subset we use) ────────────────────────────────────
17
+ interface PiModel {
18
+ provider?: string;
19
+ id?: string;
20
+ }
21
+ interface PiCtx {
22
+ ui?: { notify?: (message: string, level?: string) => void };
23
+ }
24
+ interface PiExtensionApiLike {
25
+ registerProvider?(name: string, config: unknown): void;
26
+ registerCommand?(
27
+ name: string,
28
+ options: { description?: string; handler: (args: unknown, ctx: PiCtx) => unknown },
29
+ ): void;
30
+ on(event: string, handler: (event: any, ctx: PiCtx) => any): void;
31
+ }
32
+
33
+ export interface PiPrivacyOptions {
34
+ // Install the process-wide attestation dispatcher (default true). Set false if the
35
+ // host already installed one (e.g. privateer-agent's boot.ts).
36
+ installDispatcher?: boolean;
37
+ // Register the config-only privacy providers (tinfoil/nearai/venice/ollama) with
38
+ // seed models (default true). Built-in providers (openrouter/fireworks) are left
39
+ // to Pi so their model listings aren't clobbered.
40
+ registerProviders?: boolean;
41
+ // Enforce OpenRouter ZDR routing (default false — opt-in, since a model with no
42
+ // zero-retention endpoint will 404 rather than fall back). VERIFIED honest: when
43
+ // on, requests carry provider.{zdr:true,data_collection:"deny"}, which OpenRouter
44
+ // observably enforces (it 404s if unsatisfiable), so the zdr-enforced badge is earned.
45
+ enforceOpenRouterZdr?: boolean;
46
+ // Called whenever the current model's posture is (re)computed — the badge feed.
47
+ onPosture?: (result: PostureResult) => void;
48
+ // Bind Tinfoil attestation to the real provider connection via the dispatcher
49
+ // (default true when the dispatcher is installed). Falls back to httpsTransport.
50
+ useDispatcherTransport?: boolean;
51
+ }
52
+
53
+ // Config-only providers Pi doesn't ship: register these. Built-ins + custom skipped.
54
+ const BUILTIN = new Set(["openrouter", "fireworks"]);
55
+ const SEED_MODELS: Record<string, string> = {
56
+ tinfoil: "deepseek-v4-pro",
57
+ nearai: "zai-org/GLM-5.1-FP8",
58
+ venice: "qwen3-coder-480b-a35b-instruct-turbo",
59
+ ollama: "llama3.1",
60
+ };
61
+
62
+ function registerable(p: PrivacyProvider): boolean {
63
+ return !!p.baseUrl && !BUILTIN.has(p.id) && p.id !== "custom";
64
+ }
65
+
66
+ function providerConfig(p: PrivacyProvider): unknown {
67
+ const seed = SEED_MODELS[p.id];
68
+ const models = seed
69
+ ? [
70
+ {
71
+ id: seed,
72
+ name: seed,
73
+ reasoning: false,
74
+ input: ["text"] as ("text" | "image")[],
75
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
76
+ contextWindow: 128000,
77
+ maxTokens: 4096,
78
+ },
79
+ ]
80
+ : [];
81
+ const cfg: Record<string, unknown> = { name: p.label, baseUrl: p.baseUrl, api: p.api, models };
82
+ if (p.keyEnv) {
83
+ cfg.apiKey = p.keyEnv; // env template ${...}; Pi resolves it
84
+ cfg.authHeader = true;
85
+ } else if (p.local && models.length) {
86
+ // Pi requires apiKey (or oauth) whenever a provider defines models. Local
87
+ // servers (ollama) ignore the auth header, so a placeholder satisfies the
88
+ // validation without sending a meaningful credential.
89
+ cfg.apiKey = "local";
90
+ }
91
+ return cfg;
92
+ }
93
+
94
+ function nearApiKey(): string | undefined {
95
+ // Both spellings are used in the wild (see privateer redact.ts).
96
+ return process.env.NEARAI_API_KEY ?? process.env.NEAR_AI_API_KEY;
97
+ }
98
+
99
+ export function makePiPrivacyExtension(opts: PiPrivacyOptions = {}) {
100
+ const {
101
+ installDispatcher = true,
102
+ registerProviders = true,
103
+ enforceOpenRouterZdr = false,
104
+ onPosture,
105
+ useDispatcherTransport = true,
106
+ } = opts;
107
+
108
+ return function piPrivacy(pi: PiExtensionApiLike): void {
109
+ if (installDispatcher) installAttestationDispatcher();
110
+
111
+ if (registerProviders && typeof pi.registerProvider === "function") {
112
+ for (const p of PRIVACY_PROVIDERS) {
113
+ if (registerable(p)) pi.registerProvider(p.id, providerConfig(p));
114
+ }
115
+ }
116
+
117
+ let currentProviderId: string | undefined;
118
+ let currentModelId: string | undefined;
119
+
120
+ // Recompute + publish posture for the current model.
121
+ const refreshPosture = async () => {
122
+ if (!currentProviderId || !currentModelId || !onPosture) return;
123
+ const result = await verifyModelPosture(currentProviderId, currentModelId, {
124
+ apiKey: currentProviderId === "nearai" ? nearApiKey() : undefined,
125
+ zdrEnforced: currentProviderId === "openrouter" && enforceOpenRouterZdr,
126
+ transport: useDispatcherTransport && installDispatcher ? dispatcherTransport : undefined,
127
+ });
128
+ onPosture(result);
129
+ };
130
+
131
+ pi.on("model_select", (event) => {
132
+ const model = event?.model as PiModel | undefined;
133
+ currentProviderId = model?.provider;
134
+ currentModelId = model?.id;
135
+ void refreshPosture();
136
+ });
137
+
138
+ // Per-provider request patches. Scoped to the current provider so we never
139
+ // mutate a payload bound for a different endpoint.
140
+ pi.on("before_provider_request", (event) => {
141
+ if (currentProviderId === "venice") return veniceRequestPatch(event?.payload);
142
+ if (currentProviderId === "openrouter" && enforceOpenRouterZdr) {
143
+ return openRouterZdrPatch(event?.payload);
144
+ }
145
+ return undefined;
146
+ });
147
+
148
+ if (typeof pi.registerCommand === "function") {
149
+ pi.registerCommand("verify", {
150
+ description: "Verify the current model's privacy posture (TEE attestation)",
151
+ handler: async (_args, ctx) => {
152
+ if (!currentProviderId || !currentModelId) {
153
+ ctx.ui?.notify?.("No model selected.", "warning");
154
+ return;
155
+ }
156
+ const res = await verifyModelPosture(currentProviderId, currentModelId, {
157
+ apiKey: currentProviderId === "nearai" ? nearApiKey() : undefined,
158
+ zdrEnforced: currentProviderId === "openrouter" && enforceOpenRouterZdr,
159
+ transport: useDispatcherTransport && installDispatcher ? dispatcherTransport : undefined,
160
+ });
161
+ const info = TIERS[res.tier];
162
+ const detail = res.teePosture ? ` [${res.teePosture}]` : "";
163
+ const err = res.error ? ` — ${res.error}` : "";
164
+ ctx.ui?.notify?.(`${info.label}${detail}: ${info.blurb}${err}`, "info");
165
+ },
166
+ });
167
+ }
168
+ };
169
+ }
170
+
171
+ // Default export: the marketplace-installable extension with default options.
172
+ export default makePiPrivacyExtension();
package/src/index.ts ADDED
@@ -0,0 +1,68 @@
1
+ // Public API for the pi-privacy package (working name).
2
+ //
3
+ // Two things a consumer wants: (1) the honest privacy taxonomy — tiers + provider
4
+ // catalog — and (2) [coming next] the Pi extension that registers these providers,
5
+ // installs the attestation dispatcher, verifies TEE posture, and enforces/labels
6
+ // ZDR. This turn ships (1) + the catalog; the attestation engine + extension land
7
+ // next (ported from privateer 0.2 attestation.ts).
8
+
9
+ export {
10
+ type PrivacyTier,
11
+ type Verifiability,
12
+ type TierInfo,
13
+ TIERS,
14
+ tierRank,
15
+ tierFromTeePosture,
16
+ } from "./posture/tiers.ts";
17
+
18
+ export {
19
+ type ProviderApi,
20
+ type PrivacyProvider,
21
+ PRIVACY_PROVIDERS,
22
+ PROVIDER_BY_ID,
23
+ isLocalEndpoint,
24
+ } from "./providers/catalog.ts";
25
+
26
+ // Attestation engine (ported from privateer 0.2, minus the private server-proxy path).
27
+ export {
28
+ type TeePosture,
29
+ type AttestConfig,
30
+ type Attestation,
31
+ type TinfoilAttestation,
32
+ type TinfoilTransport,
33
+ NEARAI_BASE_URL,
34
+ TINFOIL_BASE_URL,
35
+ randomNonce,
36
+ fetchAttestation,
37
+ interpretReport,
38
+ teePosture,
39
+ httpsTransport,
40
+ fetchTinfoilAttestation,
41
+ interpretTinfoilDoc,
42
+ tinfoilTeePosture,
43
+ } from "./attest/attestation.ts";
44
+
45
+ export {
46
+ type CapturedCert,
47
+ installAttestationDispatcher,
48
+ getCapturedCert,
49
+ capturedHosts,
50
+ dispatcherTransport,
51
+ } from "./attest/dispatcher.ts";
52
+
53
+ export { effectiveTier } from "./posture/effective.ts";
54
+
55
+ // Posture verification (attestation-backed) + the Pi extension entry.
56
+ export {
57
+ type PostureResult,
58
+ type VerifyOptions,
59
+ verifyModelPosture,
60
+ } from "./posture/verify.ts";
61
+
62
+ export {
63
+ type PiPrivacyOptions,
64
+ makePiPrivacyExtension,
65
+ default as piPrivacyExtension,
66
+ } from "./extension.ts";
67
+
68
+ export { veniceRequestPatch, openRouterZdrPatch } from "./ext/patches.ts";
@@ -0,0 +1,24 @@
1
+ import { PROVIDER_BY_ID, isLocalEndpoint } from "../providers/catalog.ts";
2
+ import type { PrivacyTier } from "./tiers.ts";
3
+
4
+ // Resolve a provider's effective tier, accounting for on-device detection and
5
+ // posture-aware providers. `zdrEnforced` reflects whether ZDR routing is actively
6
+ // pinned this session (OpenRouter). The one honest source of truth shared by the
7
+ // badge layer, the picker sort, and posture verification.
8
+ //
9
+ // NOTE: for TEE providers this returns only the pre-attestation CEILING
10
+ // (tee-verified as an advertised capability). The real green/yellow/red verdict
11
+ // comes from verifyModelPosture() at runtime — never claim tee-verified from this
12
+ // function alone.
13
+ export function effectiveTier(
14
+ providerId: string,
15
+ opts: { baseUrl?: string; zdrEnforced?: boolean } = {},
16
+ ): PrivacyTier {
17
+ const p = PROVIDER_BY_ID[providerId];
18
+ if (!p) return "standard";
19
+ if (p.local || isLocalEndpoint(opts.baseUrl)) return "local";
20
+ if (p.postureAware && p.id === "openrouter") {
21
+ return opts.zdrEnforced ? "zdr-enforced" : "zdr-policy";
22
+ }
23
+ return p.tier;
24
+ }
@@ -0,0 +1,110 @@
1
+ // The graded privacy ladder — the honest-labeling contract, as code.
2
+ //
3
+ // The single rule this file exists to enforce: NEVER conflate a *verified*
4
+ // guarantee with an *asserted* one. A TEE tier means we cryptographically checked
5
+ // the enclave (remote attestation + a live-TLS-key match against the report). A
6
+ // ZDR tier means the provider *promises* zero retention — we can't verify it, and
7
+ // the badge must say so. Local means the bytes never left the machine (a loopback
8
+ // endpoint we can observe). Rendering these the same would overclaim; the whole
9
+ // credibility of the package rests on keeping them distinct.
10
+
11
+ // Strongest → weakest. Ordered so a picker can sort by privacy strength.
12
+ export type PrivacyTier =
13
+ | "tee-verified" // attestation ran, enclave genuine, live TLS key matched the report
14
+ | "tee-unverified" // provider claims a TEE; attestation incomplete/unconfirmed (TeePosture "yellow")
15
+ | "local" // loopback endpoint — inference on-device, nothing leaves the machine
16
+ | "zdr-enforced" // zero-retention actively pinned this session (e.g. OpenRouter ZDR routing on)
17
+ | "zdr-policy" // zero-retention by provider policy only — not verifiable
18
+ | "standard"; // no special guarantee
19
+
20
+ // How strong the EVIDENCE is behind a tier — the honest core. Only "cryptographic"
21
+ // is a real proof; "observable" is a weak local check (loopback / route pinned);
22
+ // "policy" is a promise; "none" is nothing.
23
+ export type Verifiability = "cryptographic" | "observable" | "policy" | "none";
24
+
25
+ export interface TierInfo {
26
+ tier: PrivacyTier;
27
+ // One-word status for the badge. Deliberately different words for verified vs
28
+ // asserted tiers so they never read alike.
29
+ label: string;
30
+ verifiability: Verifiability;
31
+ // Traffic-light bucket, mirroring the 0.2 TeePosture green/yellow/red so the
32
+ // status-bar shield + picker badges keep the same semantics.
33
+ posture: "green" | "yellow" | "red" | "neutral";
34
+ // Honest one-liner shown on hover / in the picker. States the LIMIT of the claim.
35
+ blurb: string;
36
+ }
37
+
38
+ export const TIERS: Record<PrivacyTier, TierInfo> = {
39
+ "tee-verified": {
40
+ tier: "tee-verified",
41
+ label: "Verified TEE",
42
+ verifiability: "cryptographic",
43
+ posture: "green",
44
+ blurb:
45
+ "Confidential-enclave inference, cryptographically verified: remote attestation " +
46
+ "proved genuine TEE hardware and the live TLS key matched the attestation report.",
47
+ },
48
+ "tee-unverified": {
49
+ tier: "tee-unverified",
50
+ label: "TEE (unconfirmed)",
51
+ verifiability: "none",
52
+ posture: "yellow",
53
+ blurb:
54
+ "Provider claims a TEE, but attestation was incomplete or the live key could not " +
55
+ "be matched here — treat as unverified until it goes green.",
56
+ },
57
+ local: {
58
+ tier: "local",
59
+ label: "On-device",
60
+ verifiability: "observable",
61
+ posture: "green",
62
+ blurb: "Runs against a loopback endpoint — inference is local; no prompt leaves the machine.",
63
+ },
64
+ "zdr-enforced": {
65
+ tier: "zdr-enforced",
66
+ label: "ZDR (enforced)",
67
+ verifiability: "observable",
68
+ posture: "green",
69
+ blurb:
70
+ "Routing is constrained to providers that contractually don't retain data — " +
71
+ "verified observable: OpenRouter refuses to serve when the policy can't be met. " +
72
+ "Enforcement of a policy, not hardware attestation.",
73
+ },
74
+ "zdr-policy": {
75
+ tier: "zdr-policy",
76
+ label: "ZDR (by policy)",
77
+ verifiability: "policy",
78
+ posture: "yellow",
79
+ blurb:
80
+ "The provider states it doesn't retain data, but this is a policy promise we can't " +
81
+ "verify — not hardware, not attested.",
82
+ },
83
+ standard: {
84
+ tier: "standard",
85
+ label: "Standard",
86
+ verifiability: "none",
87
+ posture: "neutral",
88
+ blurb: "No special privacy guarantee.",
89
+ },
90
+ };
91
+
92
+ // Sort key: strongest privacy first. Used by pickers to rank providers/models.
93
+ const ORDER: PrivacyTier[] = [
94
+ "tee-verified",
95
+ "local",
96
+ "zdr-enforced",
97
+ "tee-unverified",
98
+ "zdr-policy",
99
+ "standard",
100
+ ];
101
+ export function tierRank(t: PrivacyTier): number {
102
+ const i = ORDER.indexOf(t);
103
+ return i === -1 ? ORDER.length : i;
104
+ }
105
+
106
+ // Map the 0.2 TeePosture (green/yellow/red) onto the graded ladder, so the ported
107
+ // attestation engine's output slots into this vocabulary without a rewrite.
108
+ export function tierFromTeePosture(p: "green" | "yellow" | "red"): PrivacyTier {
109
+ return p === "green" ? "tee-verified" : p === "yellow" ? "tee-unverified" : "standard";
110
+ }
@@ -0,0 +1,73 @@
1
+ import { PROVIDER_BY_ID } from "../providers/catalog.ts";
2
+ import { effectiveTier } from "./effective.ts";
3
+ import { type PrivacyTier, tierFromTeePosture } from "./tiers.ts";
4
+ import {
5
+ fetchTinfoilAttestation,
6
+ fetchAttestation,
7
+ tinfoilTeePosture,
8
+ teePosture,
9
+ httpsTransport,
10
+ type TeePosture,
11
+ type TinfoilTransport,
12
+ } from "../attest/attestation.ts";
13
+
14
+ // The badge/queryable posture for a specific model. For TEE providers this ACTIVELY
15
+ // verifies (attestation → green/yellow/red); for everything else it resolves the
16
+ // static tier + on-device / ZDR-enforcement. The one place a `tee-verified` verdict
17
+ // can legitimately come from — never from effectiveTier() alone.
18
+
19
+ export interface PostureResult {
20
+ providerId: string;
21
+ modelId: string;
22
+ tier: PrivacyTier;
23
+ teePosture?: TeePosture; // present for TEE providers
24
+ attestation?: unknown; // raw report, for /verify display + external verification
25
+ error?: string; // attestation fetch failed → tier falls back to tee-unverified
26
+ }
27
+
28
+ export interface VerifyOptions {
29
+ apiKey?: string; // for NEAR (report is key-gated)
30
+ baseUrl?: string; // override the provider's default endpoint
31
+ zdrEnforced?: boolean; // OpenRouter: is ZDR routing actively pinned?
32
+ transport?: TinfoilTransport; // inject for tests / to bind to the real connection
33
+ }
34
+
35
+ export async function verifyModelPosture(
36
+ providerId: string,
37
+ modelId: string,
38
+ opts: VerifyOptions = {},
39
+ ): Promise<PostureResult> {
40
+ const p = PROVIDER_BY_ID[providerId];
41
+
42
+ if (p?.id === "tinfoil") {
43
+ try {
44
+ const att = await fetchTinfoilAttestation(
45
+ { baseURL: opts.baseUrl },
46
+ opts.transport ?? httpsTransport,
47
+ );
48
+ const tp = tinfoilTeePosture(att);
49
+ return { providerId, modelId, tier: tierFromTeePosture(tp), teePosture: tp, attestation: att };
50
+ } catch (e) {
51
+ // A TEE provider we couldn't verify is unverified — NOT downgraded to a
52
+ // false "standard": the honest state is "claims TEE, unconfirmed".
53
+ return { providerId, modelId, tier: "tee-unverified", error: (e as Error).message };
54
+ }
55
+ }
56
+
57
+ if (p?.id === "nearai") {
58
+ try {
59
+ const att = await fetchAttestation({ apiKey: opts.apiKey, baseURL: opts.baseUrl }, modelId);
60
+ const tp = teePosture(att);
61
+ return { providerId, modelId, tier: tierFromTeePosture(tp), teePosture: tp, attestation: att };
62
+ } catch (e) {
63
+ return { providerId, modelId, tier: "tee-unverified", error: (e as Error).message };
64
+ }
65
+ }
66
+
67
+ // Non-TEE: static tier + on-device detection + ZDR enforcement state.
68
+ return {
69
+ providerId,
70
+ modelId,
71
+ tier: effectiveTier(providerId, { baseUrl: opts.baseUrl, zdrEnforced: opts.zdrEnforced }),
72
+ };
73
+ }
@@ -0,0 +1,128 @@
1
+ import type { PrivacyTier } from "../posture/tiers.ts";
2
+
3
+ // The privacy-oriented providers this package registers with Pi, with their tier
4
+ // and the config a Pi provider entry needs (baseUrl / api / key env). The honest
5
+ // notes are ported ~verbatim from privateer 0.2 (tree-cli catalog.ts) — that copy
6
+ // is load-bearing: it states the LIMIT of each guarantee, and must not be softened.
7
+ //
8
+ // Providers deliberately NOT here (together/deepseek/minimax/qwen/…): they have no
9
+ // verifiable or default privacy channel, so claiming one would overclaim. They stay
10
+ // "standard" and get no badge — same stance as 0.2.
11
+
12
+ export type ProviderApi =
13
+ | "openai-completions"
14
+ | "openai-responses"
15
+ | "anthropic-messages";
16
+
17
+ export interface PrivacyProvider {
18
+ id: string;
19
+ label: string;
20
+ // The BEST tier this provider can offer. `postureAware` providers resolve their
21
+ // actual tier at runtime (OpenRouter is zdr-policy until ZDR routing is enforced,
22
+ // then zdr-enforced), so this is the ceiling, not a promise.
23
+ tier: PrivacyTier;
24
+ postureAware?: boolean;
25
+ // True when the provider exposes a remote-attestation endpoint we actively verify
26
+ // (the only path to tee-verified). NEAR = report-body over HTTPS; Tinfoil = SPKI
27
+ // pinned via the out-of-band dispatcher.
28
+ attestable?: boolean;
29
+ baseUrl?: string;
30
+ api: ProviderApi;
31
+ keyEnv?: string; // env template, e.g. "${TINFOIL_API_KEY}"; omit for keyless/local
32
+ local?: boolean;
33
+ // Honest note (ported): states where to get a key AND the limit of the guarantee.
34
+ note: string;
35
+ // Provider-specific request nuances Pi needs (compat), carried through to the entry.
36
+ compat?: Record<string, unknown>;
37
+ }
38
+
39
+ export const PRIVACY_PROVIDERS: PrivacyProvider[] = [
40
+ {
41
+ id: "tinfoil",
42
+ label: "Tinfoil (private TEE inference)",
43
+ tier: "tee-verified",
44
+ attestable: true,
45
+ baseUrl: "https://inference.tinfoil.sh/v1",
46
+ api: "openai-completions",
47
+ keyEnv: "${TINFOIL_API_KEY}",
48
+ note: "tinfoil.sh → dashboard → API Keys. Confidential-enclave inference; verified by attestation + live-TLS-key match.",
49
+ },
50
+ {
51
+ id: "nearai",
52
+ label: "NEAR AI (private TEE inference)",
53
+ tier: "tee-verified",
54
+ attestable: true,
55
+ baseUrl: "https://cloud-api.near.ai/v1",
56
+ api: "openai-completions",
57
+ keyEnv: "${NEARAI_API_KEY}",
58
+ note: "cloud.near.ai → API Keys. Confidential-compute TEE; attestation carried in the report body (verified over HTTPS).",
59
+ },
60
+ {
61
+ id: "venice",
62
+ label: "Venice (no-retention inference)",
63
+ tier: "zdr-policy",
64
+ baseUrl: "https://api.venice.ai/api/v1",
65
+ api: "openai-completions",
66
+ keyEnv: "${VENICE_API_KEY}",
67
+ // Honest copy: policy, not hardware. Venice injects a body param to disable its
68
+ // system prompt — handled by a before_provider_request hook, not config.
69
+ note: 'venice.ai → API Keys (no retention by policy, not TEE-attested; "anonymized" models proxy upstream).',
70
+ compat: { veniceDisableSystemPrompt: true },
71
+ },
72
+ {
73
+ id: "fireworks",
74
+ label: "Fireworks (no-retention inference)",
75
+ tier: "zdr-policy",
76
+ baseUrl: "https://api.fireworks.ai/inference/v1",
77
+ api: "openai-completions",
78
+ keyEnv: "${FIREWORKS_API_KEY}",
79
+ // Honest copy: ZDR is the default for OPEN models only; Fireworks's own f1 /
80
+ // FireFunction may log. Not TEE-attested.
81
+ note: "fireworks.ai → API Keys (open models: zero retention by default, not TEE-attested; Fireworks's own f1/FireFunction may log).",
82
+ },
83
+ {
84
+ id: "openrouter",
85
+ label: "OpenRouter",
86
+ tier: "zdr-enforced", // ceiling; posture-aware — see postureAware
87
+ postureAware: true,
88
+ baseUrl: "https://openrouter.ai/api/v1",
89
+ api: "openai-completions",
90
+ keyEnv: "${OPENROUTER_API_KEY}",
91
+ // Honest copy: ZDR is per-model/account. Yellow (zdr-policy) until enforcement
92
+ // pins requests to zero-retention endpoints, then green (zdr-enforced).
93
+ note: "openrouter.ai/keys. ZDR is per-model; not guaranteed until ZDR routing is enforced (then requests only hit zero-retention endpoints).",
94
+ compat: { openRouterRouting: { zdr: true } },
95
+ },
96
+ {
97
+ id: "ollama",
98
+ label: "Ollama (local)",
99
+ tier: "local",
100
+ local: true,
101
+ baseUrl: "http://localhost:11434/v1", // OpenAI-compat surface, not native /api
102
+ api: "openai-completions",
103
+ note: "runs locally — no key needed. Inference on-device; nothing leaves the machine.",
104
+ },
105
+ {
106
+ id: "custom",
107
+ label: "Custom (OpenAI-compatible)",
108
+ tier: "standard", // resolved to `local` at runtime when the baseUrl is loopback
109
+ api: "openai-completions",
110
+ note: "any OpenAI-compatible endpoint — LM Studio, vLLM, llama.cpp, a proxy. Detected as on-device when the URL is loopback.",
111
+ },
112
+ ];
113
+
114
+ export const PROVIDER_BY_ID: Record<string, PrivacyProvider> = Object.fromEntries(
115
+ PRIVACY_PROVIDERS.map((p) => [p.id, p]),
116
+ );
117
+
118
+ // A loopback / on-device endpoint? Used to promote `custom` (and confirm `ollama`)
119
+ // to the `local` tier — observable, not merely claimed.
120
+ export function isLocalEndpoint(baseUrl?: string): boolean {
121
+ if (!baseUrl) return false;
122
+ try {
123
+ const h = new URL(baseUrl).hostname;
124
+ return h === "localhost" || h === "127.0.0.1" || h === "::1" || h.endsWith(".local");
125
+ } catch {
126
+ return false;
127
+ }
128
+ }