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 +21 -0
- package/README.md +98 -0
- package/extensions/pi-privacy.ts +10 -0
- package/package.json +68 -0
- package/src/attest/attestation.ts +268 -0
- package/src/attest/dispatcher.ts +94 -0
- package/src/ext/patches.ts +36 -0
- package/src/extension.ts +172 -0
- package/src/index.ts +68 -0
- package/src/posture/effective.ts +24 -0
- package/src/posture/tiers.ts +110 -0
- package/src/posture/verify.ts +73 -0
- package/src/providers/catalog.ts +128 -0
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
|
+
}
|
package/src/extension.ts
ADDED
|
@@ -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
|
+
}
|