@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,151 @@
1
+ /** CLI command group (auto-split from the dispatcher; bodies unchanged). */
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { randomBytes } from "node:crypto";
6
+ import { loadBlindfoldEnv, configPath, homeDir, stateDir } from "../src/env.ts";
7
+ import { keychainAvailable, keychainBackend, keychainSet, keychainDelete } from "../src/keychain.ts";
8
+ import { readSecretLine } from "../src/prompt.ts";
9
+ import { registerSecret, registerContract } from "../src/register.ts";
10
+ import { startProxy } from "../src/proxy.ts";
11
+ import { attest, attestationGate, writePinnedRtmr3 } from "../src/attest.ts";
12
+ import { startDashboard } from "../src/dashboard.ts";
13
+ import { clearUsage, defaultLogPath, readUsage } from "../src/usage-log.ts";
14
+ import { runInit, runVerify } from "../src/init.ts";
15
+ import { runCompat } from "../src/compat.ts";
16
+ import { defaultSealedLogPath, readSealed, verifyLedgerChain } from "../src/sealed-ledger.ts";
17
+ import { type Argv, die, assetPath, fingerprint, resolveEnvVar } from "./cli-shared.ts";
18
+
19
+ export async function handleLifecycle(cmd: string, argv: Argv, cmdArgs: string[]): Promise<void> {
20
+ switch (cmd) {
21
+ case "rotate": {
22
+ const name = String(argv.flags.name ?? "");
23
+ const fromEnv = argv.flags["from-env"] ? String(argv.flags["from-env"]) : undefined;
24
+ if (!name) {
25
+ die("usage: blindfold rotate --name <secret> [--from-env <ENV_VAR>]");
26
+ }
27
+ const env = loadBlindfoldEnv();
28
+ const { openT3Client } = await import("../src/t3-client.ts");
29
+ const { recordVersion, versionKeyFor } = await import("../src/versions.ts");
30
+ const client = await openT3Client(env);
31
+ try {
32
+ // Snapshot the current value into the enclave (for rollback) before overwriting.
33
+ try {
34
+ const old = await client.releaseSecret(name);
35
+ const vKey = versionKeyFor(name, Date.now());
36
+ await client.seedSecret(vKey, old);
37
+ recordVersion({ t: new Date().toISOString(), name, versionKey: vKey, length: old.length, fingerprint: fingerprint(old) });
38
+ console.log(` before: "${name}" ${old.length} B fp=${fingerprint(old)} (snapshot saved — rollback available)`);
39
+ } catch {
40
+ console.log(` before: (no existing value for "${name}" — sealing fresh)`);
41
+ }
42
+ await registerSecret({ name, fromEnv }); // overwrites the live entry + records the ledger
43
+ const now = await client.releaseSecret(name);
44
+ console.log(`✓ Rotated "${name}" → ${now.length} B fp=${fingerprint(now)} (mode=real)`);
45
+ console.log(` Every place that uses "${name}" now gets the new value — no code/config change.`);
46
+ console.log(` Made a mistake? \`blindfold rollback --name ${name}\``);
47
+ if (fromEnv) console.log(` You can now DELETE ${fromEnv} from your .env.`);
48
+ } finally {
49
+ await client.close();
50
+ }
51
+ return;
52
+ }
53
+ case "rollback": {
54
+ const name = String(argv.flags.name ?? "");
55
+ if (!name) die("usage: blindfold rollback --name <secret> [--to <iso-ts | fingerprint>]");
56
+ const { readVersions, isValidVersionKey } = await import("../src/versions.ts");
57
+ const versions = readVersions(name);
58
+ if (versions.length === 0) {
59
+ die(`no saved versions for "${name}". \`blindfold rotate\` creates them; \`blindfold versions --name ${name}\` lists them.`);
60
+ }
61
+ const want = argv.flags.to ? String(argv.flags.to) : "";
62
+ let target = versions[versions.length - 1]!; // most recent snapshot
63
+ if (want) {
64
+ const sel = versions.find((v) => v.t === want || v.fingerprint === want || v.fingerprint.startsWith(want));
65
+ if (!sel) die(`no version of "${name}" matches --to ${want} (see \`blindfold versions --name ${name}\`)`);
66
+ target = sel;
67
+ }
68
+ // Integrity guard: versions.jsonl is local and could be tampered with to
69
+ // point `versionKey` at an arbitrary enclave key. A legitimate key always
70
+ // matches __bfver__<name>__<ts>; reject anything else before releasing.
71
+ if (!isValidVersionKey(name, target.versionKey)) {
72
+ die(`refusing rollback: version key "${target.versionKey}" is not a valid snapshot key for "${name}" (tampered versions.jsonl?)`);
73
+ }
74
+ const env = loadBlindfoldEnv();
75
+ const { openT3Client } = await import("../src/t3-client.ts");
76
+ const client = await openT3Client(env);
77
+ try {
78
+ const before = await client.verifySecret(name);
79
+ const restored = await client.releaseSecret(target.versionKey);
80
+ // And verify the released value matches the fingerprint recorded when the
81
+ // snapshot was taken — catches a versionKey swapped to a different value.
82
+ if (fingerprint(restored) !== target.fingerprint) {
83
+ die(`refusing rollback: released snapshot fingerprint ${fingerprint(restored)} ≠ recorded ${target.fingerprint} (tampered versions.jsonl?)`);
84
+ }
85
+ await client.seedSecret(name, restored);
86
+ console.log(`✓ Rolled back "${name}"`);
87
+ console.log(` ${before.present ? `fp ${before.fingerprint} (${before.length} B)` : "(no current value)"} → fp ${fingerprint(restored)} (${restored.length} B)`);
88
+ console.log(` restored the snapshot from ${target.t}.`);
89
+ } finally {
90
+ await client.close();
91
+ }
92
+ return;
93
+ }
94
+ case "versions": {
95
+ const name = argv.flags.name ? String(argv.flags.name) : undefined;
96
+ const { readVersions } = await import("../src/versions.ts");
97
+ const list = readVersions(name);
98
+ if (list.length === 0) {
99
+ console.log(name ? `No saved versions for "${name}".` : "No saved versions yet. `blindfold rotate` creates them.");
100
+ return;
101
+ }
102
+ console.log(`Saved versions${name ? ` for "${name}"` : ""} (newest last):\n`);
103
+ console.log(" WHEN NAME BYTES FINGERPRINT");
104
+ console.log(" ──── ──── ───── ───────────");
105
+ for (const v of list) {
106
+ const when = v.t.replace("T", " ").slice(0, 19);
107
+ console.log(` ${when} ${v.name.padEnd(22)} ${String(v.length).padStart(5)} ${v.fingerprint}`);
108
+ }
109
+ console.log(`\n Restore the newest: blindfold rollback --name <name>`);
110
+ console.log(` Restore a specific: blindfold rollback --name <name> --to <fingerprint>`);
111
+ return;
112
+ }
113
+ case "migrate": {
114
+ const { planMigration, runMigrate } = await import("../src/migrate.ts");
115
+ const dryRun = !!argv.flags["dry-run"];
116
+ const keep = !!argv.flags.keep;
117
+ const plan = planMigration();
118
+ const toSeal = plan.filter((p) => p.action === "seal");
119
+
120
+ console.log(dryRun ? "🔍 blindfold migrate --dry-run (no changes will be made)\n" : "🚚 blindfold migrate\n");
121
+ console.log(" Plan:");
122
+ for (const p of plan) {
123
+ if (p.action === "seal") console.log(` SEAL ${p.envVar.padEnd(26)} → ${p.sealName} (${p.bytes} B)`);
124
+ else console.log(` skip ${p.envVar.padEnd(26)} — ${p.reason}`);
125
+ }
126
+ if (toSeal.length === 0) {
127
+ console.log("\n Nothing to seal (no secret-looking vars found).");
128
+ return;
129
+ }
130
+ if (dryRun) {
131
+ console.log(`\n Would seal ${toSeal.length} secret(s), then ${keep ? "comment out" : "remove"} their .env lines (a .env backup is kept either way).`);
132
+ console.log(" Re-run without --dry-run to do it.");
133
+ return;
134
+ }
135
+
136
+ console.log(`\n Sealing ${toSeal.length} secret(s) …`);
137
+ const results = await runMigrate({ keep });
138
+ console.log("");
139
+ let ok = 0, fail = 0;
140
+ for (const r of results) {
141
+ if (r.action !== "seal") continue;
142
+ if (r.sealed) { ok++; console.log(` ✓ ${r.sealName} sealed (${r.bytes} B) — .env line ${keep ? "commented" : "removed"}`); }
143
+ else { fail++; console.log(` ✖ ${r.sealName}: ${(r.error ?? "").slice(0, 100)}`); }
144
+ }
145
+ console.log(`\n Done: ${ok} sealed, ${fail} failed. (.env backup saved alongside .env)`);
146
+ if (ok > 0) console.log(` Use any of them with no code: blindfold use --name <name> -- <command>`);
147
+ if (fail > 0) { console.log(` Failed seals kept their .env line. Check \`blindfold doctor\`.`); process.exitCode = 1; }
148
+ return;
149
+ }
150
+ }
151
+ }
@@ -0,0 +1,136 @@
1
+ /** CLI command group (auto-split from the dispatcher; bodies unchanged). */
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { randomBytes } from "node:crypto";
6
+ import { loadBlindfoldEnv, configPath, homeDir, stateDir } from "../src/env.ts";
7
+ import { keychainAvailable, keychainBackend, keychainSet, keychainDelete } from "../src/keychain.ts";
8
+ import { readSecretLine } from "../src/prompt.ts";
9
+ import { registerSecret, registerContract } from "../src/register.ts";
10
+ import { startProxy } from "../src/proxy.ts";
11
+ import { attest, attestationGate, writePinnedRtmr3 } from "../src/attest.ts";
12
+ import { startDashboard } from "../src/dashboard.ts";
13
+ import { clearUsage, defaultLogPath, readUsage } from "../src/usage-log.ts";
14
+ import { runInit, runVerify } from "../src/init.ts";
15
+ import { runCompat } from "../src/compat.ts";
16
+ import { defaultSealedLogPath, readSealed, verifyLedgerChain } from "../src/sealed-ledger.ts";
17
+ import { type Argv, die, assetPath, fingerprint, resolveEnvVar } from "./cli-shared.ts";
18
+
19
+ export async function handleSecrets(cmd: string, argv: Argv, cmdArgs: string[]): Promise<void> {
20
+ switch (cmd) {
21
+ case "register": {
22
+ const name = String(argv.flags.name ?? "");
23
+ const fromEnv = argv.flags["from-env"] ? String(argv.flags["from-env"]) : undefined;
24
+ if (!name) {
25
+ die("usage: blindfold register --name <KV_KEY> [--from-env <ENV_VAR>]");
26
+ }
27
+ // Attestation gate (no-op unless a measurement is pinned): don't seal into
28
+ // an enclave that doesn't verify.
29
+ const sealGate = await attestationGate({ skip: !!argv.flags["no-attest"], requirePin: true });
30
+ if (sealGate.warning) console.error(`⚠️ ${sealGate.warning}`);
31
+ if (sealGate.enforced && !sealGate.ok) {
32
+ die(`attestation gate: ${sealGate.message}. Refusing to seal into an unverified enclave. (bypass: --no-attest, or clear the pin)`);
33
+ }
34
+ if (sealGate.enforced) console.log("✓ enclave attestation verified");
35
+ await registerSecret({ name, fromEnv });
36
+ if (fromEnv) {
37
+ console.log(`✓ Registered "${name}" (value read from ${fromEnv} once, then dropped).`);
38
+ console.log(` You can now DELETE ${fromEnv} from your .env.`);
39
+ } else {
40
+ console.log(`✓ Registered "${name}" — value lives only in the enclave.`);
41
+ }
42
+ // The other half: tell the user how to actually USE what they just sealed.
43
+ const asVar = name.toUpperCase();
44
+ console.log("");
45
+ console.log(" Use it (the plaintext never returns to your env):");
46
+ console.log(` blindfold use --name ${name} -- <your command> # injects ${asVar} into that command only`);
47
+ console.log(` blindfold use --name ${name} --as ${asVar} -- <cmd> # custom env-var name`);
48
+ console.log(` blindfold use --name ${name} --url <https url> # quick "does it auth?" check`);
49
+ console.log(` proxy/SDK: set the key to "__BLINDFOLD__" + route via \`blindfold proxy\`, or \`release("${name}")\` in code`);
50
+ return;
51
+ }
52
+ case "use": {
53
+ const name = String(argv.flags.name ?? "");
54
+ if (!name) {
55
+ die("usage: blindfold use --name <secret> [--as <ENV_VAR>] -- <command...>\n" +
56
+ " blindfold use --name <secret> --url <https url> (quick auth test)");
57
+ }
58
+ const { release } = await import("../src/release.ts");
59
+ const value = await release(name, { via: "use" }); // plaintext, kept local; never printed
60
+
61
+ // Mode 0: --check just confirms the secret is sealed + usable (no URL, no
62
+ // command, no value printed). The dashboard's "copy command" uses this.
63
+ if (argv.flags.check) {
64
+ console.log(`✓ "${name}" is sealed and usable — ${value.length} bytes (value never shown)`);
65
+ return;
66
+ }
67
+
68
+ // Mode A: quick auth test against an HTTPS endpoint with Bearer auth.
69
+ if (argv.flags.url) {
70
+ const url = String(argv.flags.url);
71
+ // The released plaintext key is sent as a Bearer token to this URL.
72
+ // Refuse a non-https target (localhost excepted) so a mistyped/hostile
73
+ // URL can't exfiltrate the key. --allow-insecure overrides for testing.
74
+ if (!argv.flags["allow-insecure"]) {
75
+ let u: URL;
76
+ try { u = new URL(url); } catch (e) { die(`invalid --url: ${(e as Error).message}`); return; }
77
+ const isLocal = ["localhost", "127.0.0.1", "::1", "[::1]"].includes(u.hostname);
78
+ if (u.protocol !== "https:" && !isLocal) {
79
+ die(`refusing to send the released key to a non-https URL (${u.protocol}//${u.hostname}). Use https, target localhost, or pass --allow-insecure to override.`);
80
+ return;
81
+ }
82
+ // M3: gate --url behind the same egress allowlist the proxy uses, so a
83
+ // hijacked agent can't POST the released key to an arbitrary https host.
84
+ if (!isLocal) {
85
+ const { loadEgressHosts } = await import("../src/t3-client.ts");
86
+ const env = loadBlindfoldEnv();
87
+ const granted = loadEgressHosts(env.did);
88
+ const allowed = granted.some((h) => h === u.hostname || u.hostname.endsWith(`.${h}`));
89
+ if (!allowed) {
90
+ die(`refusing to send the released key to ${u.hostname}: host is not in the egress allowlist. Run \`blindfold grant --host ${u.hostname}\` first, or pass --allow-insecure to override.`);
91
+ return;
92
+ }
93
+ }
94
+ }
95
+ const res = await fetch(url, {
96
+ headers: { Authorization: `Bearer ${value}`, "User-Agent": "blindfold", Accept: "*/*" },
97
+ });
98
+ console.log(`✓ released "${name}" (${value.length} B, value not shown) → ${url}`);
99
+ console.log(` HTTP ${res.status} ${res.statusText} ${res.ok ? "✅ accepted" : "✖ rejected"}`);
100
+ return;
101
+ }
102
+
103
+ // Mode B: run any command with the secret injected as an env var, for
104
+ // that subprocess only. The parent never persists the plaintext.
105
+ if (cmdArgs.length === 0) {
106
+ die("provide a command after `--` (e.g. `-- gh api user`), or pass --url <url> to test auth");
107
+ }
108
+ const asVar = resolveEnvVar(argv.flags.as ? String(argv.flags.as) : undefined, cmdArgs[0], name);
109
+ console.error(`✓ released "${name}" (${value.length} B) → injecting $${asVar} into: ${cmdArgs.join(" ")}`);
110
+ const child = spawn(cmdArgs[0]!, cmdArgs.slice(1), {
111
+ stdio: "inherit",
112
+ env: { ...process.env, [asVar]: value },
113
+ });
114
+ child.on("exit", (code) => process.exit(code ?? 0));
115
+ child.on("error", (e) => die(`failed to run "${cmdArgs[0]}": ${e.message}`));
116
+ return;
117
+ }
118
+ case "export": {
119
+ // CI-only: release a sealed secret into $GITHUB_ENV for later steps, and
120
+ // mask it in the logs. Lets a GitHub Action pull keys from the enclave
121
+ // instead of storing them as GitHub secrets.
122
+ const name = String(argv.flags.name ?? "");
123
+ if (!name) die("usage: blindfold export --name <secret> [--as <ENV_VAR>] (for GitHub Actions / CI)");
124
+ const ghEnv = process.env.GITHUB_ENV;
125
+ if (!ghEnv) die("`export` writes to $GITHUB_ENV (GitHub Actions only). Use `blindfold use` locally.");
126
+ const asVar = resolveEnvVar(argv.flags.as ? String(argv.flags.as) : undefined, undefined, name);
127
+ const { release } = await import("../src/release.ts");
128
+ const value = await release(name, { via: "export" });
129
+ // ::add-mask:: tells the runner to redact this value everywhere in the logs.
130
+ console.log(`::add-mask::${value}`);
131
+ fs.appendFileSync(ghEnv, `${asVar}=${value}\n`);
132
+ console.error(`✓ exported $${asVar} from sealed "${name}" (${value.length} B, masked in logs)`);
133
+ return;
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,165 @@
1
+ /** CLI command group (auto-split from the dispatcher; bodies unchanged). */
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { randomBytes } from "node:crypto";
6
+ import { loadBlindfoldEnv, configPath, homeDir, stateDir } from "../src/env.ts";
7
+ import { keychainAvailable, keychainBackend, keychainSet, keychainDelete } from "../src/keychain.ts";
8
+ import { readSecretLine } from "../src/prompt.ts";
9
+ import { registerSecret, registerContract } from "../src/register.ts";
10
+ import { startProxy } from "../src/proxy.ts";
11
+ import { attest, attestationGate, writePinnedRtmr3 } from "../src/attest.ts";
12
+ import { startDashboard } from "../src/dashboard.ts";
13
+ import { clearUsage, defaultLogPath, readUsage } from "../src/usage-log.ts";
14
+ import { runInit, runVerify } from "../src/init.ts";
15
+ import { runCompat } from "../src/compat.ts";
16
+ import { defaultSealedLogPath, readSealed, verifyLedgerChain } from "../src/sealed-ledger.ts";
17
+ import { type Argv, die, assetPath, fingerprint, resolveEnvVar } from "./cli-shared.ts";
18
+
19
+ export async function handleServe(cmd: string, argv: Argv, cmdArgs: string[]): Promise<void> {
20
+ switch (cmd) {
21
+ case "proxy": {
22
+ const port = argv.flags.port ? Number(argv.flags.port) : undefined;
23
+ const secret = argv.flags.secret ? String(argv.flags.secret) : undefined;
24
+ // Per-session auth: `--token <t>` supplies one; `--auth` mints a random
25
+ // one. Without either, the proxy stays open to any local process (the key
26
+ // still can't be stolen — this only gates *use* by co-resident processes).
27
+ let token = argv.flags.token ? String(argv.flags.token) : undefined;
28
+ if (!token && argv.flags.auth) token = randomBytes(24).toString("hex");
29
+ // `--socket [path]` binds a unix-domain socket (0600) instead of a TCP
30
+ // port, so only same-user processes can connect. Bare `--socket` defaults
31
+ // to <stateDir>/proxy.sock.
32
+ let socket: string | undefined;
33
+ if (argv.flags.socket !== undefined) {
34
+ socket = typeof argv.flags.socket === "string" && argv.flags.socket.length > 0
35
+ ? String(argv.flags.socket)
36
+ : path.join(stateDir(), "proxy.sock");
37
+ }
38
+ // Attestation gate (no-op unless a measurement is pinned): don't route
39
+ // secrets through an enclave that doesn't verify.
40
+ const proxyGate = await attestationGate({ skip: !!argv.flags["no-attest"] });
41
+ if (proxyGate.warning) console.error(`⚠️ ${proxyGate.warning}`);
42
+ if (proxyGate.enforced && !proxyGate.ok) {
43
+ die(`attestation gate: ${proxyGate.message}. Refusing to start the proxy against an unverified enclave. (bypass: --no-attest, or clear the pin)`);
44
+ }
45
+ if (proxyGate.enforced) console.log("✓ enclave attestation verified");
46
+ if (socket && !token) {
47
+ console.error("⚠️ --socket without --auth: access is gated only by the socket's 0600 owner permission. Add --auth for a per-process token too.");
48
+ }
49
+ const handle = await startProxy({ port, secretKey: secret, token, socket });
50
+ console.log(`✓ Blindfold proxy listening at ${handle.url}`);
51
+ if (handle.socket) {
52
+ console.log(` Unix socket (0600) — only your user's processes can connect.`);
53
+ console.log(` Call it with: curl --unix-socket ${handle.socket} http://localhost/v1/...`);
54
+ } else {
55
+ console.log(` Point your agent at: OPENAI_BASE_URL=${handle.url}/v1`);
56
+ }
57
+ if (handle.token) {
58
+ // Print auth material to STDERR (not stdout) so it doesn't land in piped
59
+ // logs, and warn that env/argv are readable by other same-user processes.
60
+ console.error(` Auth ON — every request must send header:`);
61
+ console.error(` x-blindfold-token: ${handle.token}`);
62
+ console.error(` Give it to your agent via BLINDFOLD_PROXY_TOKEN or wrap({ token }).`);
63
+ console.error(` NOTE: env vars and argv are readable by same-user processes — on a shared`);
64
+ console.error(` machine prefer --socket (OS-enforced) over the token alone.`);
65
+ if (argv.flags.token) {
66
+ console.error(` ⚠️ --token on the command line is visible in \`ps\`/proc; prefer --auth (mints one) or BLINDFOLD_PROXY_TOKEN.`);
67
+ }
68
+ } else {
69
+ console.log(` Auth OFF — add --auth to require a per-session token (recommended on shared machines).`);
70
+ }
71
+ console.log(` Health check: ${handle.url}/health`);
72
+ // long-running: don't close until SIGINT
73
+ process.on("SIGINT", async () => {
74
+ await handle.close();
75
+ process.exit(0);
76
+ });
77
+ return;
78
+ }
79
+ case "attest": {
80
+ // Verify the T3 enclave cluster's TDX attestation (chains to Intel's root
81
+ // CA). Optionally pin RTMR3 (the running code/config measurement).
82
+ const expectRtmr3 = argv.flags["expect-rtmr3"] ? String(argv.flags["expect-rtmr3"]) : undefined;
83
+ const r = await attest({ expectRtmr3, noCache: true }); // CLI check is always fresh
84
+ if (argv.flags.json) {
85
+ console.log(JSON.stringify(r, null, 2));
86
+ return;
87
+ }
88
+ if (!r.available) {
89
+ console.log(`ℹ️ ${r.nodeUrl} publishes no attestation (mock signer or still bootstrapping).`);
90
+ return;
91
+ }
92
+ console.log(`Attestation for ${r.nodeUrl}`);
93
+ console.log(` chain to Intel root: ${r.valid ? "✅ valid" : "✖ INVALID"}${r.error ? ` (${r.error})` : ""}`);
94
+ console.log(` quotes verified: ${r.validCount}/${r.expectedCount}`);
95
+ for (const m of r.rtmr3s) console.log(` RTMR3 (code measure): ${m}`);
96
+ if (expectRtmr3) {
97
+ console.log(` RTMR3 pin: ${r.pinned ? "✅ matches expected" : "✖ DID NOT MATCH"}`);
98
+ } else if (r.rtmr3s.length) {
99
+ console.log(` Tip: pin it next time → blindfold attest --expect-rtmr3 ${r.rtmr3s[0]}`);
100
+ }
101
+ // `--pin` persists the measurement so seal/proxy auto-gate on it hereafter.
102
+ if (argv.flags.pin) {
103
+ const toPin = expectRtmr3 ?? r.rtmr3s[0];
104
+ if (r.valid && toPin) {
105
+ // TOFU caveat: pinning what the node reported only helps if that value
106
+ // matches your reproducible enclave build. Warn unless the user
107
+ // supplied the expected value explicitly (--expect-rtmr3).
108
+ if (!expectRtmr3) {
109
+ console.error(` ⚠️ Pinning the measurement the node just reported (trust-on-first-use).`);
110
+ console.error(` Cross-check ${toPin} against your enclave's published/reproducible build hash;`);
111
+ console.error(` if this first contact was with a malicious node, you'd be pinning its measurement.`);
112
+ }
113
+ writePinnedRtmr3(toPin);
114
+ console.log(` ✓ pinned — \`seal\` and \`proxy\` will now verify this measurement first.`);
115
+ } else {
116
+ console.log(` ✗ not pinning: attestation must pass first.`);
117
+ }
118
+ }
119
+ if (!r.valid || (expectRtmr3 && !r.pinned)) process.exitCode = 1;
120
+ return;
121
+ }
122
+ case "dashboard": {
123
+ const port = argv.flags.port ? Number(argv.flags.port) : undefined;
124
+ const handle = await startDashboard({ port });
125
+ console.log(`✓ Blindfold dashboard at ${handle.url}`);
126
+ console.log(` Reading: ${defaultLogPath()}`);
127
+ console.log(` (open in your browser; auto-refreshes every 2s)`);
128
+ process.on("SIGINT", async () => { await handle.close(); process.exit(0); });
129
+ return;
130
+ }
131
+ case "stats": {
132
+ const events = readUsage();
133
+ if (events.length === 0) {
134
+ console.log("No usage recorded yet. Reads from " + defaultLogPath());
135
+ return;
136
+ }
137
+ const byProvider: Record<string, number> = {};
138
+ let ok = 0, bad = 0, totalLat = 0, sentinel = 0;
139
+ for (const e of events) {
140
+ byProvider[e.provider] = (byProvider[e.provider] ?? 0) + 1;
141
+ if (e.status >= 200 && e.status < 300) ok++;
142
+ else if (e.status >= 400) bad++;
143
+ totalLat += e.latency_ms;
144
+ if (e.sentinel_in_outbound) sentinel++;
145
+ }
146
+ const total = events.length;
147
+ console.log("Blindfold usage stats (source: " + defaultLogPath() + ")");
148
+ console.log(` Total requests: ${total}`);
149
+ console.log(` 2xx / 4xx+: ${ok} / ${bad}`);
150
+ console.log(` Sentinel substituted: ${sentinel}/${total} (should equal total)`);
151
+ console.log(` Avg latency: ${total ? Math.round(totalLat / total) : 0} ms`);
152
+ console.log(` By provider: ${Object.entries(byProvider).map(([k, v]) => `${k}×${v}`).join(" ")}`);
153
+ console.log(` Recent (last 5):`);
154
+ for (const e of events.slice(-5)) {
155
+ console.log(` ${e.t} ${e.method.padEnd(6)} ${e.path} → ${e.status} (${e.latency_ms}ms, ${e.provider}, ${e.mode})`);
156
+ }
157
+ return;
158
+ }
159
+ case "stats:clear": {
160
+ clearUsage();
161
+ console.log("✓ Cleared " + defaultLogPath());
162
+ return;
163
+ }
164
+ }
165
+ }
@@ -0,0 +1,84 @@
1
+ /** CLI command group (auto-split from the dispatcher; bodies unchanged). */
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { randomBytes } from "node:crypto";
6
+ import { loadBlindfoldEnv, configPath, homeDir, stateDir } from "../src/env.ts";
7
+ import { keychainAvailable, keychainBackend, keychainSet, keychainDelete } from "../src/keychain.ts";
8
+ import { readSecretLine } from "../src/prompt.ts";
9
+ import { registerSecret, registerContract } from "../src/register.ts";
10
+ import { startProxy } from "../src/proxy.ts";
11
+ import { attest, attestationGate, writePinnedRtmr3 } from "../src/attest.ts";
12
+ import { startDashboard } from "../src/dashboard.ts";
13
+ import { clearUsage, defaultLogPath, readUsage } from "../src/usage-log.ts";
14
+ import { runInit, runVerify } from "../src/init.ts";
15
+ import { runCompat } from "../src/compat.ts";
16
+ import { defaultSealedLogPath, readSealed, verifyLedgerChain } from "../src/sealed-ledger.ts";
17
+ import { type Argv, die, assetPath, fingerprint, resolveEnvVar } from "./cli-shared.ts";
18
+
19
+ export async function handleTenant(cmd: string, argv: Argv, cmdArgs: string[]): Promise<void> {
20
+ switch (cmd) {
21
+ case "grant": {
22
+ // Authorize the contract to make outbound calls to one or more hosts.
23
+ const hosts: string[] = [];
24
+ if (argv.flags.host) hosts.push(...String(argv.flags.host).split(",").map((h) => h.trim()).filter(Boolean));
25
+ if (argv.flags.hosts) hosts.push(...String(argv.flags.hosts).split(",").map((h) => h.trim()).filter(Boolean));
26
+ if (hosts.length === 0) {
27
+ die("usage: blindfold grant --host <host>[,<host2>...] (e.g. --host api.openai.com)");
28
+ }
29
+ const replace = !!argv.flags.replace;
30
+ const env = loadBlindfoldEnv();
31
+ const { openT3Client } = await import("../src/t3-client.ts");
32
+ const client = await openT3Client(env);
33
+ try {
34
+ // T3 replaces the allowlist on every update, so grant is additive by
35
+ // default (merges with previously-granted hosts). Use --replace to reset.
36
+ const authorized = await client.grantEgress(hosts, { replace });
37
+ console.log(`✓ Egress granted for: ${hosts.join(", ")}${replace ? " (replaced allowlist)" : ""}`);
38
+ console.log(` Contract is now authorized to call ALL of: ${authorized.join(", ")}`);
39
+ console.log(` Run \`blindfold publish\` first if you haven't — the grant targets the published contract.`);
40
+ } finally {
41
+ await client.close();
42
+ }
43
+ return;
44
+ }
45
+ case "share": {
46
+ // Authorize a teammate's agent to USE this tenant's sealed keys (via the
47
+ // in-enclave forward path) for specific hosts — they never receive the key.
48
+ const to = String(argv.flags.to ?? "");
49
+ const hosts: string[] = [];
50
+ if (argv.flags.host) hosts.push(...String(argv.flags.host).split(",").map((h) => h.trim()).filter(Boolean));
51
+ if (argv.flags.hosts) hosts.push(...String(argv.flags.hosts).split(",").map((h) => h.trim()).filter(Boolean));
52
+ if (!to || hosts.length === 0) {
53
+ die("usage: blindfold share --to <agent-did> --host <host>[,host2] (e.g. --to did:t3n:… --host api.openai.com)");
54
+ }
55
+ const env = loadBlindfoldEnv();
56
+ const { openT3Client } = await import("../src/t3-client.ts");
57
+ const client = await openT3Client(env);
58
+ try {
59
+ await client.setAgentGrant(to, hosts, ["forward"]); // forward only — least privilege, no plaintext extraction
60
+ console.log(`✓ Shared access with ${to}`);
61
+ console.log(` authorized: forward → ${hosts.join(", ")} (they can USE the key via the enclave; they never receive the plaintext)`);
62
+ console.log(` Revoke any time: blindfold revoke --to ${to}`);
63
+ } finally {
64
+ await client.close();
65
+ }
66
+ return;
67
+ }
68
+ case "revoke": {
69
+ const to = String(argv.flags.to ?? "");
70
+ if (!to) die("usage: blindfold revoke --to <agent-did>");
71
+ const env = loadBlindfoldEnv();
72
+ const { openT3Client } = await import("../src/t3-client.ts");
73
+ const client = await openT3Client(env);
74
+ try {
75
+ await client.setAgentGrant(to, [], []); // empty scripts → remove all authorization
76
+ console.log(`✓ Revoked all contract access for ${to}`);
77
+ console.log(` Nobody holds the raw key, so revocation is immediate and complete — there's no leaked copy to chase.`);
78
+ } finally {
79
+ await client.close();
80
+ }
81
+ return;
82
+ }
83
+ }
84
+ }