@fiscalmindset/blindfold 0.4.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.
@@ -0,0 +1,81 @@
1
+ // src/wrap.ts
2
+ import http from "node:http";
3
+
4
+ // src/constants.ts
5
+ var SENTINEL = "__BLINDFOLD__";
6
+ var DEFAULT_PORT = 8787;
7
+
8
+ // src/wrap.ts
9
+ var PROXY_TOKEN_HEADER = "x-blindfold-token";
10
+ function socketFetch(socketPath, token) {
11
+ return async (input, init = {}) => {
12
+ const reqObj = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
13
+ const rawUrl = reqObj ? reqObj.url : String(input);
14
+ const url = new URL(rawUrl);
15
+ const method = init?.method ?? reqObj?.method ?? "GET";
16
+ const headers = new Headers(init?.headers ?? reqObj?.headers);
17
+ if (token) headers.set(PROXY_TOKEN_HEADER, token);
18
+ const outHeaders = {};
19
+ headers.forEach((v, k) => {
20
+ outHeaders[k] = v;
21
+ });
22
+ return await new Promise((resolve, reject) => {
23
+ const req = http.request(
24
+ { socketPath, path: url.pathname + url.search, method, headers: outHeaders },
25
+ (res) => {
26
+ const chunks = [];
27
+ res.on("data", (c) => chunks.push(c));
28
+ res.on("end", () => {
29
+ const respHeaders = new Headers();
30
+ for (const [k, v] of Object.entries(res.headers)) {
31
+ if (typeof v === "string") respHeaders.set(k, v);
32
+ else if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
33
+ }
34
+ resolve(new Response(Buffer.concat(chunks), {
35
+ status: res.statusCode ?? 502,
36
+ statusText: res.statusMessage ?? "",
37
+ headers: respHeaders
38
+ }));
39
+ });
40
+ }
41
+ );
42
+ req.on("error", reject);
43
+ const body = init?.body;
44
+ if (body != null) {
45
+ if (typeof body === "string" || body instanceof Buffer || body instanceof Uint8Array) req.write(body);
46
+ else if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) req.write(body.toString());
47
+ else if (ArrayBuffer.isView(body)) {
48
+ const v = body;
49
+ req.write(Buffer.from(v.buffer, v.byteOffset, v.byteLength));
50
+ } else if (body instanceof ArrayBuffer) req.write(Buffer.from(body));
51
+ else {
52
+ req.destroy();
53
+ reject(new Error("socketFetch: unsupported body type (use string/Buffer/typed-array/URLSearchParams)"));
54
+ return;
55
+ }
56
+ }
57
+ req.end();
58
+ });
59
+ };
60
+ }
61
+ function wrap(client, opts = {}) {
62
+ const baseUrl = opts.baseUrl ?? `http://127.0.0.1:${DEFAULT_PORT}/v1`;
63
+ client.baseURL = baseUrl;
64
+ client.apiKey = SENTINEL;
65
+ const token = (opts.token ?? process.env.BLINDFOLD_PROXY_TOKEN ?? "").trim() || void 0;
66
+ const socket = (opts.socket ?? process.env.BLINDFOLD_PROXY_SOCKET ?? "").trim() || void 0;
67
+ if (socket) {
68
+ client.fetch = socketFetch(socket, token);
69
+ } else if (token) {
70
+ const base = client.fetch ?? fetch;
71
+ client.fetch = (input, init = {}) => {
72
+ const headers = new Headers(init?.headers);
73
+ headers.set(PROXY_TOKEN_HEADER, token);
74
+ return base(input, { ...init, headers });
75
+ };
76
+ }
77
+ return client;
78
+ }
79
+ export {
80
+ wrap
81
+ };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@fiscalmindset/blindfold",
3
+ "version": "0.4.0",
4
+ "type": "module",
5
+ "description": "Make any AI agent's API keys un-leakable, by sealing them inside a Terminal 3 TDX enclave.",
6
+ "license": "MIT",
7
+ "author": "FiscalMindset",
8
+ "homepage": "https://github.com/FiscalMindset/Blindfold#readme",
9
+ "repository": { "type": "git", "url": "git+https://github.com/FiscalMindset/Blindfold.git", "directory": "packages/blindfold" },
10
+ "bugs": { "url": "https://github.com/FiscalMindset/Blindfold/issues" },
11
+ "keywords": ["ai", "agent", "api-key", "secrets", "security", "prompt-injection", "tee", "tdx", "confidential-computing", "terminal3", "enclave"],
12
+ "publishConfig": { "access": "public" },
13
+ "main": "./dist/lib/index.mjs",
14
+ "types": "./src/index.ts",
15
+ "exports": {
16
+ ".": { "types": "./src/index.ts", "import": "./dist/lib/index.mjs" },
17
+ "./proxy": { "types": "./src/proxy.ts", "import": "./dist/lib/proxy.mjs" },
18
+ "./register": { "types": "./src/register.ts", "import": "./dist/lib/register.mjs" },
19
+ "./wrap": { "types": "./src/wrap.ts", "import": "./dist/lib/wrap.mjs" }
20
+ },
21
+ "bin": {
22
+ "blindfold": "dist/cli.mjs"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "bin",
27
+ "src",
28
+ "assets",
29
+ "postinstall.mjs",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "scripts": {
34
+ "start": "tsx bin/blindfold.ts",
35
+ "build": "npm run build:cli && npm run build:lib && node copy-assets.mjs",
36
+ "build:cli": "esbuild bin/blindfold.ts --bundle --platform=node --format=esm --target=node18 --outfile=dist/cli.mjs --external:@terminal3/t3n-sdk",
37
+ "build:lib": "esbuild src/index.ts src/proxy.ts src/register.ts src/wrap.ts --bundle --platform=node --format=esm --target=node18 --outdir=dist/lib --out-extension:.js=.mjs --external:@terminal3/t3n-sdk",
38
+ "prepare": "npm run build",
39
+ "postinstall": "node postinstall.mjs"
40
+ },
41
+ "dependencies": {},
42
+ "devDependencies": {
43
+ "esbuild": "^0.24.0"
44
+ },
45
+ "optionalDependencies": {
46
+ "@terminal3/t3n-sdk": "^3.12.3"
47
+ }
48
+ }
@@ -0,0 +1,21 @@
1
+ // Convenience: on a Windows GLOBAL install, npm drops the `blindfold` shim in
2
+ // %APPDATA%\npm, but that folder isn't always on PATH โ€” so `blindfold` comes up
3
+ // "not recognized". This adds it to the user PATH (idempotent, append-only).
4
+ // Windows-only, global-only, and never allowed to fail the install.
5
+ import { spawnSync } from "node:child_process";
6
+
7
+ if (process.platform === "win32" && process.env.npm_config_global === "true") {
8
+ try {
9
+ const ps =
10
+ "$np=$env:APPDATA+'\\npm';" +
11
+ "$p=[Environment]::GetEnvironmentVariable('PATH','User'); if($p -eq $null){$p=''};" +
12
+ "if($p -notlike ('*'+$np+'*')){[Environment]::SetEnvironmentVariable('PATH',($p.TrimEnd(';')+';'+$np),'User');[Console]::Out.Write('ADDED')}else{[Console]::Out.Write('PRESENT')}";
13
+ const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "-"],
14
+ { input: ps, encoding: "utf8" });
15
+ if (typeof r.stdout === "string" && r.stdout.includes("ADDED")) {
16
+ console.log("blindfold: added %APPDATA%\\npm to your user PATH. Open a NEW terminal, then run `blindfold login`.");
17
+ }
18
+ } catch {
19
+ /* a PATH convenience must never break the install */
20
+ }
21
+ }
package/src/attest.ts ADDED
@@ -0,0 +1,235 @@
1
+ /**
2
+ * attest() โ€” client-side remote attestation of the Terminal 3 enclave cluster.
3
+ *
4
+ * Trust upgrade: instead of *trusting* Terminal 3 to run a genuine TDX enclave,
5
+ * we cryptographically VERIFY it before trusting it with a secret. The T3 node
6
+ * publishes a DKG attestation bundle at `${nodeUrl}/status`:
7
+ * - one raw TDX v4 quote per cluster node (base64),
8
+ * - an attestation message binding the quotes to the cluster's ML-KEM key.
9
+ *
10
+ * The SDK's `verifyDkgAttestation()` does the full cryptographic check in WASM:
11
+ * 1. the attestation message starts with the ML-KEM encaps key (the node
12
+ * can't swap the key it's attesting to),
13
+ * 2. every quote's ECDSA-P256 signature chains to Intel's SGX root CA,
14
+ * 3. every quote's report_data == keccak512(attestation message),
15
+ * 4. (optional) RTMR3 pinning โ€” the measurement of the code/config running
16
+ * inside the enclave matches an expected value you pin.
17
+ *
18
+ * If (1)โ€“(3) pass, the enclave is genuine Intel TDX silicon. If you also pin
19
+ * RTMR3, you additionally prove it's running the exact code you expect โ€”
20
+ * "verify the hardware", not "trust the operator".
21
+ *
22
+ * This does not need the tenant key; it reads the node's public /status.
23
+ */
24
+ import fs from "node:fs";
25
+ import path from "node:path";
26
+ import { loadBlindfoldEnv, configPath, withFileLockSync } from "./env.ts";
27
+ import type { BlindfoldEnv } from "./types.ts";
28
+
29
+ const PIN_FIELD = "expectedRtmr3";
30
+
31
+ /** The RTMR3 measurement pinned in config.json, if any. */
32
+ export function readPinnedRtmr3(): string | undefined {
33
+ try {
34
+ const obj = JSON.parse(fs.readFileSync(configPath(), "utf8")) as Record<string, unknown>;
35
+ const v = obj[PIN_FIELD];
36
+ return typeof v === "string" && v ? v : undefined;
37
+ } catch {
38
+ return undefined;
39
+ }
40
+ }
41
+
42
+ /** Persist a pinned RTMR3 in config.json (preserving other fields, 0600).
43
+ * Locked read-modify-write so a concurrent `login` can't clobber it (M4). */
44
+ export function writePinnedRtmr3(value: string): void {
45
+ const cfg = configPath();
46
+ fs.mkdirSync(path.dirname(cfg), { recursive: true });
47
+ withFileLockSync(cfg, () => {
48
+ let obj: Record<string, unknown> = {};
49
+ try { obj = JSON.parse(fs.readFileSync(cfg, "utf8")) as Record<string, unknown>; } catch { /* new file */ }
50
+ obj[PIN_FIELD] = value;
51
+ fs.writeFileSync(cfg, JSON.stringify(obj, null, 2), { mode: 0o600 });
52
+ try { fs.chmodSync(cfg, 0o600); } catch { /* best effort */ }
53
+ });
54
+ }
55
+
56
+ /** Subset of the @terminal3/t3n-sdk attestation surface we rely on. */
57
+ interface AttestSdk {
58
+ setEnvironment: (env: "testnet" | "production") => void;
59
+ setNodeUrl: (url: string | null) => void;
60
+ getNodeUrl: (baseUrl?: string) => string;
61
+ fetchDkgAttestation: (baseUrl?: string) => Promise<
62
+ { peer_ids: string[]; quotes: Record<string, string>; attestation_msg: string } | undefined
63
+ >;
64
+ fetchMlKemPublicKey: (baseUrl?: string) => Promise<string>;
65
+ verifyDkgAttestation: (
66
+ encapsKeyB64: string,
67
+ attestationMsgB64: string,
68
+ peerIds: string[],
69
+ quotes: Record<string, string>,
70
+ expectedRtmr3B64?: string,
71
+ ) => Promise<{
72
+ valid: boolean;
73
+ valid_count: number;
74
+ expected_count: number;
75
+ error?: string;
76
+ results: { peer_id: string; valid: boolean; error?: string; rtmr3?: string }[];
77
+ }>;
78
+ }
79
+
80
+ export interface AttestResult {
81
+ /** False when the node publishes no attestation (mock signer / bootstrapping). */
82
+ available: boolean;
83
+ nodeUrl: string;
84
+ valid: boolean;
85
+ validCount: number;
86
+ expectedCount: number;
87
+ /** Distinct RTMR3 measurements observed across the cluster's quotes. */
88
+ rtmr3s: string[];
89
+ /** True only when an expected RTMR3 was supplied and every quote matched it. */
90
+ pinned: boolean;
91
+ error?: string;
92
+ }
93
+
94
+ export interface AttestOpts {
95
+ env?: BlindfoldEnv;
96
+ /** Base64 48-byte RTMR3 to pin (fail unless every quote matches it). */
97
+ expectRtmr3?: string;
98
+ /** Skip the short-lived result cache and force a fresh fetch+verify. */
99
+ noCache?: boolean;
100
+ }
101
+
102
+ // Quotes/RTMR3 are stable between enclave restarts, so a fresh fetch + full
103
+ // ECDSA WASM verify on every gate call is wasteful (S3). Cache per
104
+ // env+node+pin for a short TTL; the CLI `attest` command bypasses it (noCache).
105
+ const ATTEST_TTL_MS = Number(process.env.BLINDFOLD_ATTEST_TTL_MS) || 300_000;
106
+ const attestCache = new Map<string, { at: number; result: AttestResult }>();
107
+
108
+ async function loadAttestSdk(): Promise<AttestSdk> {
109
+ try {
110
+ return (await import("@terminal3/t3n-sdk")) as unknown as AttestSdk;
111
+ } catch {
112
+ throw new Error(
113
+ "@terminal3/t3n-sdk not installed โ€” attestation needs the real SDK. Run `npm install @terminal3/t3n-sdk`.",
114
+ );
115
+ }
116
+ }
117
+
118
+ export async function attest(opts: AttestOpts = {}): Promise<AttestResult> {
119
+ const env = opts.env ?? loadBlindfoldEnv();
120
+ if (env.mock) {
121
+ throw new Error("attestation is unavailable in mock mode (BLINDFOLD_MOCK=1) โ€” it needs a real T3 node.");
122
+ }
123
+
124
+ const cacheKey = `${env.t3Env}|${env.t3BaseUrl}|${opts.expectRtmr3 ?? ""}`;
125
+ if (!opts.noCache) {
126
+ const hit = attestCache.get(cacheKey);
127
+ if (hit && Date.now() - hit.at < ATTEST_TTL_MS) return hit.result;
128
+ }
129
+
130
+ const sdk = await loadAttestSdk();
131
+ sdk.setEnvironment(env.t3Env);
132
+ if (env.t3BaseUrl) sdk.setNodeUrl(env.t3BaseUrl);
133
+ const nodeUrl = sdk.getNodeUrl(env.t3BaseUrl || undefined);
134
+
135
+ const bundle = await sdk.fetchDkgAttestation(nodeUrl);
136
+ if (!bundle) {
137
+ const unavailable: AttestResult = { available: false, nodeUrl, valid: false, validCount: 0, expectedCount: 0, rtmr3s: [], pinned: false };
138
+ attestCache.set(cacheKey, { at: Date.now(), result: unavailable });
139
+ return unavailable;
140
+ }
141
+
142
+ const encapsKey = await sdk.fetchMlKemPublicKey(nodeUrl);
143
+ const r = await sdk.verifyDkgAttestation(
144
+ encapsKey,
145
+ bundle.attestation_msg,
146
+ bundle.peer_ids,
147
+ bundle.quotes,
148
+ opts.expectRtmr3,
149
+ );
150
+
151
+ const rtmr3s = [...new Set(r.results.map((p) => p.rtmr3).filter((x): x is string => Boolean(x)))];
152
+ const result: AttestResult = {
153
+ available: true,
154
+ nodeUrl,
155
+ valid: r.valid,
156
+ validCount: r.valid_count,
157
+ expectedCount: r.expected_count,
158
+ rtmr3s,
159
+ pinned: Boolean(opts.expectRtmr3) && r.valid,
160
+ error: r.error,
161
+ };
162
+ attestCache.set(cacheKey, { at: Date.now(), result });
163
+ return result;
164
+ }
165
+
166
+ export interface GateResult {
167
+ /** True when attestation was actually required (a pin exists or was forced). */
168
+ enforced: boolean;
169
+ /** True when not enforced, or enforced and verified. */
170
+ ok: boolean;
171
+ message?: string;
172
+ /** Non-fatal caveat the caller should print loudly (e.g. bypass / no-pin). */
173
+ warning?: string;
174
+ }
175
+
176
+ /**
177
+ * Attestation gate for sensitive operations (seal, proxy). It is a no-op unless
178
+ * the user has pinned an RTMR3 (`attest --pin`) or set `BLINDFOLD_REQUIRE_ATTEST=1`
179
+ * โ€” so it's opt-in and back-compat. When enforced, it verifies the live enclave
180
+ * (and RTMR3 pin) and returns ok=false with a reason if it doesn't check out.
181
+ * Skipped entirely in mock mode or when `skip` is set (`--no-attest`).
182
+ */
183
+ export async function attestationGate(
184
+ opts: { env?: BlindfoldEnv; skip?: boolean; requirePin?: boolean } = {},
185
+ ): Promise<GateResult> {
186
+ const env = opts.env ?? loadBlindfoldEnv();
187
+ const pinned = readPinnedRtmr3();
188
+ const required = Boolean(pinned) || process.env.BLINDFOLD_REQUIRE_ATTEST === "1";
189
+
190
+ // --no-attest: honor it, but make the bypass LOUD when a gate was in force (H6).
191
+ if (opts.skip) {
192
+ return {
193
+ enforced: false,
194
+ ok: true,
195
+ warning: required ? "attestation gate BYPASSED via --no-attest โ€” the enclave was NOT verified" : undefined,
196
+ };
197
+ }
198
+ // Mock mode: refuse when attestation is required, rather than silently no-op (M5).
199
+ if (env.mock) {
200
+ if (required) {
201
+ return { enforced: true, ok: false, message: "refusing to run in mock mode while attestation is required (a pin or BLINDFOLD_REQUIRE_ATTEST is set) โ€” mock mode never touches a real enclave" };
202
+ }
203
+ return { enforced: false, ok: true };
204
+ }
205
+ if (!required) return { enforced: false, ok: true };
206
+
207
+ let r: AttestResult;
208
+ try {
209
+ r = await attest({ env, expectRtmr3: pinned });
210
+ } catch (e) {
211
+ return { enforced: true, ok: false, message: (e as Error).message };
212
+ }
213
+ if (!r.available) return { enforced: true, ok: false, message: `node published no attestation (${r.nodeUrl})` };
214
+
215
+ // A pin proves "runs MY code"; valid-without-pin only proves "genuine TDX
216
+ // silicon" โ€” insufficient to seal a NEW secret to (H4).
217
+ if (opts.requirePin && !pinned) {
218
+ return {
219
+ enforced: true,
220
+ ok: false,
221
+ message: "refusing: this operation requires a pinned RTMR3 (run `blindfold attest --pin`). Unpinned attestation only proves it's a TDX enclave, not that it runs your expected code.",
222
+ };
223
+ }
224
+
225
+ const ok = r.valid && (!pinned || r.pinned);
226
+ const warning = ok && !pinned
227
+ ? "attestation valid but NO RTMR3 pinned โ€” proves genuine TDX silicon, not that the enclave runs your expected code. Pin it: `blindfold attest --pin`."
228
+ : undefined;
229
+ return {
230
+ enforced: true,
231
+ ok,
232
+ warning,
233
+ message: ok ? undefined : `enclave attestation failed${pinned ? " (RTMR3 pin mismatch)" : ""} at ${r.nodeUrl}`,
234
+ };
235
+ }
package/src/color.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Tiny, dependency-free ANSI color helper. Colors are enabled only when the
3
+ * output is a TTY and NO_COLOR isn't set (so piped/redirected output and CI logs
4
+ * stay clean plain-text). Honors FORCE_COLOR=1 to force on.
5
+ */
6
+ const on =
7
+ process.env.FORCE_COLOR === "1" ||
8
+ (!process.env.NO_COLOR && process.env.TERM !== "dumb" && Boolean(process.stdout.isTTY));
9
+
10
+ const wrap = (code: string) => (s: string): string => (on ? `\x1b[${code}m${s}\x1b[0m` : s);
11
+
12
+ export const c = {
13
+ bold: wrap("1"),
14
+ dim: wrap("2"),
15
+ red: wrap("31"),
16
+ green: wrap("32"),
17
+ yellow: wrap("33"),
18
+ blue: wrap("34"),
19
+ magenta: wrap("35"),
20
+ cyan: wrap("36"),
21
+ gray: wrap("90"),
22
+ };
23
+
24
+ /** Whether color output is active (TTY + not disabled). */
25
+ export const colorOn = on;
26
+
27
+ /** Semantic helpers used across CLI output. */
28
+ export const ok = (s: string): string => c.green(s);
29
+ export const bad = (s: string): string => c.red(s);
30
+ export const warn = (s: string): string => c.yellow(s);
31
+ export const head = (s: string): string => c.bold(c.cyan(s));
package/src/compat.ts ADDED
@@ -0,0 +1,217 @@
1
+ /**
2
+ * blindfold compat โ€” detects local agent tooling and prints the exact
3
+ * env-var swap that routes each one through Blindfold.
4
+ *
5
+ * Honesty principle: where a tool's auth model is OAuth/session (not a
6
+ * user-supplied API key), Blindfold cannot protect it. Those tools are
7
+ * reported as "not applicable" with a clear explanation.
8
+ */
9
+ import { spawn } from "node:child_process";
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
15
+ const REPO_ROOT = path.resolve(HERE, "..", "..", "..");
16
+
17
+ const colour = process.stdout.isTTY ? (c: string, s: string) => `\x1b[${c}m${s}\x1b[0m` : (_: string, s: string) => s;
18
+ const bold = (s: string) => colour("1", s);
19
+ const dim = (s: string) => colour("2", s);
20
+ const green = (s: string) => colour("32", s);
21
+ const yellow = (s: string) => colour("33", s);
22
+ const red = (s: string) => colour("31", s);
23
+ const cyan = (s: string) => colour("36", s);
24
+
25
+ interface Tool {
26
+ name: string;
27
+ // How we detect it
28
+ detect: () => Promise<DetectionResult>;
29
+ /** "applies" โ†’ Blindfold can protect it; "oauth" โ†’ it uses session/OAuth, not a user key; "needs-base-url" โ†’ has hard-coded URL */
30
+ applies: "applies" | "oauth-only" | "depends" | "needs-base-url";
31
+ /** Exact change a user makes to wire it. */
32
+ recipe: { env?: Record<string, string>; note?: string };
33
+ // Optional explanation
34
+ explanation?: string;
35
+ }
36
+
37
+ interface DetectionResult {
38
+ detected: boolean;
39
+ via: string; // "PATH", "node_modules", "config file"
40
+ detail?: string;
41
+ }
42
+
43
+ async function detectBinary(name: string): Promise<DetectionResult> {
44
+ return new Promise((resolve) => {
45
+ const child = spawn("which", [name], { stdio: ["ignore", "pipe", "ignore"] });
46
+ const chunks: Buffer[] = [];
47
+ child.stdout.on("data", (c) => chunks.push(c));
48
+ child.on("close", (code) => {
49
+ const out = Buffer.concat(chunks).toString("utf8").trim();
50
+ resolve({ detected: code === 0 && !!out, via: "PATH", detail: out || undefined });
51
+ });
52
+ child.on("error", () => resolve({ detected: false, via: "PATH" }));
53
+ });
54
+ }
55
+
56
+ async function detectPackage(pkg: string): Promise<DetectionResult> {
57
+ const candidates = [
58
+ path.join(REPO_ROOT, "node_modules", pkg),
59
+ path.join(REPO_ROOT, "node_modules", pkg, "package.json"),
60
+ ];
61
+ for (const c of candidates) {
62
+ if (fs.existsSync(c)) return { detected: true, via: "node_modules", detail: c };
63
+ }
64
+ return { detected: false, via: "node_modules" };
65
+ }
66
+
67
+ const TOOLS: Tool[] = [
68
+ {
69
+ name: "Claude Code (claude)",
70
+ detect: () => detectBinary("claude"),
71
+ applies: "depends",
72
+ recipe: {
73
+ env: { ANTHROPIC_BASE_URL: "http://127.0.0.1:8787/anthropic", ANTHROPIC_API_KEY: "__BLINDFOLD__" },
74
+ note: "Only applies if you authenticate Claude Code with an Anthropic API key (enterprise/proxy mode). Default Claude Code uses claude.ai OAuth โ€” there is no exposed key to protect.",
75
+ },
76
+ explanation:
77
+ "Claude Code respects ANTHROPIC_BASE_URL when ANTHROPIC_API_KEY is set; in the default subscription/OAuth flow it does not, and Blindfold has nothing to protect.",
78
+ },
79
+ {
80
+ name: "OpenCode (sst.dev/opencode)",
81
+ detect: () => detectBinary("opencode"),
82
+ applies: "applies",
83
+ recipe: {
84
+ env: { OPENAI_BASE_URL: "http://127.0.0.1:8787/v1", OPENAI_API_KEY: "__BLINDFOLD__" },
85
+ note: "Configure your provider in ~/.config/opencode/config.json to use the proxy URL.",
86
+ },
87
+ },
88
+ {
89
+ name: "Aider",
90
+ detect: () => detectBinary("aider"),
91
+ applies: "applies",
92
+ recipe: {
93
+ env: { OPENAI_API_BASE: "http://127.0.0.1:8787/v1", OPENAI_API_KEY: "__BLINDFOLD__" },
94
+ note: "Aider reads OPENAI_API_BASE (the older name). Same idea.",
95
+ },
96
+ },
97
+ {
98
+ name: "Continue.dev (continue)",
99
+ detect: () => detectBinary("continue"),
100
+ applies: "applies",
101
+ recipe: {
102
+ note: "Edit ~/.continue/config.json โ€” set apiBase to http://127.0.0.1:8787/v1 and apiKey to __BLINDFOLD__ on each OpenAI-flavoured model.",
103
+ },
104
+ },
105
+ {
106
+ name: "Cline (VS Code extension)",
107
+ detect: () => detectBinary("code"),
108
+ applies: "applies",
109
+ recipe: {
110
+ note: "In Cline settings โ†’ API Provider โ†’ set baseURL to http://127.0.0.1:8787/v1 and api key to __BLINDFOLD__. (Detection here just checks for VS Code; Cline itself is a VS Code extension.)",
111
+ },
112
+ },
113
+ {
114
+ name: "OpenAI Codex CLI (codex)",
115
+ detect: () => detectBinary("codex"),
116
+ applies: "applies",
117
+ recipe: { env: { OPENAI_BASE_URL: "http://127.0.0.1:8787/v1", OPENAI_API_KEY: "__BLINDFOLD__" } },
118
+ },
119
+ {
120
+ name: "Cursor (desktop app)",
121
+ detect: () => detectBinary("cursor"),
122
+ applies: "needs-base-url",
123
+ recipe: {
124
+ note: "Cursor does not expose a per-request base-URL setting in current builds. If you self-host an Anthropic/OpenAI-compatible gateway, point Cursor at that, then at Blindfold from there.",
125
+ },
126
+ },
127
+ {
128
+ name: "openai (Node SDK)",
129
+ detect: () => detectPackage("openai"),
130
+ applies: "applies",
131
+ recipe: { env: { OPENAI_BASE_URL: "http://127.0.0.1:8787/v1", OPENAI_API_KEY: "__BLINDFOLD__" } },
132
+ },
133
+ {
134
+ name: "@anthropic-ai/sdk (Node SDK)",
135
+ detect: () => detectPackage("@anthropic-ai/sdk"),
136
+ applies: "applies",
137
+ recipe: { env: { ANTHROPIC_BASE_URL: "http://127.0.0.1:8787/anthropic", ANTHROPIC_API_KEY: "__BLINDFOLD__" } },
138
+ },
139
+ {
140
+ name: "@langchain/openai (LangChain JS)",
141
+ detect: () => detectPackage("@langchain/openai"),
142
+ applies: "applies",
143
+ recipe: { note: "Use `new ChatOpenAI({ apiKey: '__BLINDFOLD__', configuration: { baseURL: 'http://127.0.0.1:8787/v1' } })`." },
144
+ },
145
+ {
146
+ name: "ollama (local model runner)",
147
+ detect: () => detectBinary("ollama"),
148
+ applies: "oauth-only",
149
+ recipe: { note: "Ollama runs models locally with no external API key โ€” there's no secret for Blindfold to protect. Skip." },
150
+ },
151
+ ];
152
+
153
+ export async function runCompat(opts: { json?: boolean } = {}): Promise<void> {
154
+ const results = await Promise.all(
155
+ TOOLS.map(async (t) => ({ tool: t, detection: await t.detect() })),
156
+ );
157
+
158
+ if (opts.json) {
159
+ process.stdout.write(JSON.stringify(results.map((r) => ({
160
+ tool: r.tool.name,
161
+ detected: r.detection.detected,
162
+ detected_at: r.detection.detail,
163
+ applies: r.tool.applies,
164
+ env: r.tool.recipe.env,
165
+ note: r.tool.recipe.note,
166
+ explanation: r.tool.explanation,
167
+ })), null, 2));
168
+ return;
169
+ }
170
+
171
+ process.stdout.write(`\n${bold("๐Ÿ›ก๏ธ Blindfold โ€” compatibility scan")}\n${dim("Probing local box for agent tools and SDKs Blindfold can protect.")}\n\n`);
172
+
173
+ const detected = results.filter((r) => r.detection.detected);
174
+ const notFound = results.filter((r) => !r.detection.detected);
175
+
176
+ process.stdout.write(`${bold(`Detected (${detected.length}):`)}\n`);
177
+ if (detected.length === 0) process.stdout.write(` ${dim("(none โ€” install one of the tools below, or just use the OpenAI/Anthropic SDK in your own code)")}\n`);
178
+ for (const r of detected) {
179
+ renderTool(r.tool, r.detection, true);
180
+ }
181
+
182
+ process.stdout.write(`\n${bold(`Not found on this machine (${notFound.length}):`)}\n`);
183
+ for (const r of notFound) {
184
+ renderTool(r.tool, r.detection, false);
185
+ }
186
+
187
+ process.stdout.write(`\n${dim("For a longer-form compatibility writeup see docs/05-compatibility.md.")}\n`);
188
+ }
189
+
190
+ function renderTool(tool: Tool, detection: DetectionResult, detected: boolean): void {
191
+ const mark = !detected
192
+ ? dim("ยท")
193
+ : tool.applies === "applies"
194
+ ? green("โœ“")
195
+ : tool.applies === "depends"
196
+ ? yellow("?")
197
+ : tool.applies === "needs-base-url"
198
+ ? yellow("!")
199
+ : red("โœ–");
200
+ const status = !detected
201
+ ? dim("(not installed)")
202
+ : tool.applies === "applies"
203
+ ? green("Blindfold protects this")
204
+ : tool.applies === "depends"
205
+ ? yellow("Depends on how you authenticate")
206
+ : tool.applies === "needs-base-url"
207
+ ? yellow("No base-URL hook โ€” needs upstream support")
208
+ : red("Doesn't apply (no user-supplied key)");
209
+ process.stdout.write(` ${mark} ${bold(tool.name)} ${dim("ยท")} ${status}\n`);
210
+ if (detected && detection.detail) process.stdout.write(` ${dim("at " + detection.detail)}\n`);
211
+ if (detected && tool.recipe.env) {
212
+ const envLine = Object.entries(tool.recipe.env).map(([k, v]) => `${k}=${v}`).join(" ");
213
+ process.stdout.write(` ${cyan(envLine)}\n`);
214
+ }
215
+ if (detected && tool.recipe.note) process.stdout.write(` ${dim(tool.recipe.note)}\n`);
216
+ if (detected && tool.explanation) process.stdout.write(` ${dim(tool.explanation)}\n`);
217
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The sentinel string that stands in for a secret as it flows through
3
+ * the agent process and the Blindfold proxy. Inside the T3 contract,
4
+ * every occurrence of this string in any header value is replaced with
5
+ * the real secret from the enclave's KV map.
6
+ *
7
+ * KEEP IN SYNC with `contract/src/forward.rs::SENTINEL`.
8
+ */
9
+ export const SENTINEL = "__BLINDFOLD__";
10
+
11
+ /** Default port for the local OpenAI-shaped proxy. */
12
+ export const DEFAULT_PORT = 8787;
13
+
14
+ /** The contract tail we publish under in the developer's tenant. */
15
+ export const CONTRACT_TAIL = "blindfold-proxy";
16
+
17
+ /** Default contract version (semver, used at register time). Bump on every change to contract/. */
18
+ export const CONTRACT_VERSION = "0.5.6";
19
+
20
+ /** Marker the proxy uses to surface "registered but mock" status in /health. */
21
+ export const HEALTH_BANNER = "blindfold/0.1.0";
22
+
23
+ /** Default port for the dashboard server (separate from the proxy port). */
24
+ export const DEFAULT_DASHBOARD_PORT = 8799;