@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.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/assets/SKILL.md +205 -0
- package/assets/blindfold_proxy.wasm +0 -0
- package/bin/blindfold.ts +90 -0
- package/bin/cli-shared.ts +72 -0
- package/bin/cmd-auth.ts +193 -0
- package/bin/cmd-enclave.ts +373 -0
- package/bin/cmd-lifecycle.ts +151 -0
- package/bin/cmd-secrets.ts +136 -0
- package/bin/cmd-serve.ts +165 -0
- package/bin/cmd-tenant.ts +84 -0
- package/dist/cli.mjs +4317 -0
- package/dist/lib/index.mjs +2362 -0
- package/dist/lib/proxy.mjs +1078 -0
- package/dist/lib/register.mjs +756 -0
- package/dist/lib/wrap.mjs +81 -0
- package/package.json +48 -0
- package/postinstall.mjs +21 -0
- package/src/attest.ts +235 -0
- package/src/color.ts +31 -0
- package/src/compat.ts +217 -0
- package/src/constants.ts +24 -0
- package/src/dashboard.ts +785 -0
- package/src/env.ts +261 -0
- package/src/index.ts +11 -0
- package/src/init.ts +391 -0
- package/src/keychain.ts +155 -0
- package/src/log.ts +51 -0
- package/src/migrate.ts +135 -0
- package/src/prompt.ts +114 -0
- package/src/providers.ts +224 -0
- package/src/proxy.ts +454 -0
- package/src/register.ts +81 -0
- package/src/release.ts +82 -0
- package/src/sealed-ledger.ts +158 -0
- package/src/t3-client.ts +580 -0
- package/src/types.ts +50 -0
- package/src/usage-log.ts +149 -0
- package/src/versions.ts +64 -0
- package/src/wrap.ts +122 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FiscalMindset
|
|
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,105 @@
|
|
|
1
|
+
# @fiscalmindset/blindfold
|
|
2
|
+
|
|
3
|
+
> Your AI agent can't leak the API key it never had.
|
|
4
|
+
|
|
5
|
+
> **⚠️ Beta · testnet-backed.** `blindfold signup` provisions a **Terminal 3
|
|
6
|
+
> testnet** tenant. Secrets are sealed in a real TDX enclave (the key never
|
|
7
|
+
> leaks), but testnet has no production SLA and tenants may be reset — treat this
|
|
8
|
+
> as a beta/demo, not yet a production secret store. A production path is planned.
|
|
9
|
+
|
|
10
|
+
Blindfold seals your API keys inside a **Terminal 3 TDX enclave**. Your agent
|
|
11
|
+
only ever sees a placeholder — `__BLINDFOLD__` — while the real credential is
|
|
12
|
+
substituted *inside* the enclave, right before the outbound call. A prompt
|
|
13
|
+
injection can't exfiltrate a secret that was never in the agent's process.
|
|
14
|
+
|
|
15
|
+
- **The key never enters your agent's memory or context.** It lives in the
|
|
16
|
+
enclave; the agent talks to a local proxy that forwards through it.
|
|
17
|
+
- **One-line adoption.** Point your tool at the proxy (an env-var swap) or wrap
|
|
18
|
+
your client in-process with `wrap()`.
|
|
19
|
+
- **Self-serve.** `npm i -g @fiscalmindset/blindfold && blindfold signup` mints a
|
|
20
|
+
funded Terminal 3 testnet tenant — no manual provisioning.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm i -g @fiscalmindset/blindfold
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Get a tenant (self-serve, ~30s)
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
blindfold signup --email you@example.com
|
|
32
|
+
# → generates your tenant key locally (stored in the OS keychain, never printed),
|
|
33
|
+
# emails you a verification code, and self-admits a funded testnet tenant.
|
|
34
|
+
blindfold doctor # ✅ handshake + authenticate OK · Ready to seal & use
|
|
35
|
+
blindfold credit # see your token balance
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
One email binds to one tenant. Already have Terminal 3 credentials? Use
|
|
39
|
+
`blindfold login --did did:t3n:… ` instead.
|
|
40
|
+
|
|
41
|
+
## Seal a secret, then use it
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Seal (one-time). Prompts with no echo; the plaintext never touches disk.
|
|
45
|
+
blindfold register --name openai_api_key
|
|
46
|
+
|
|
47
|
+
# Run any tool with the sealed key injected for that command only:
|
|
48
|
+
blindfold use --name openai_api_key --as OPENAI_API_KEY -- your-cli …
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Or run the local proxy
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
blindfold proxy # http://127.0.0.1:8787, __BLINDFOLD__ sentinel
|
|
55
|
+
blindfold proxy --auth # per-session token (only the wrapped agent can use it)
|
|
56
|
+
blindfold proxy --socket # 0600 unix socket (only your OS user can connect)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Point your agent's base URL at the proxy and send `Authorization: Bearer
|
|
60
|
+
__BLINDFOLD__`; the enclave swaps in the real key.
|
|
61
|
+
|
|
62
|
+
### In-process (Node)
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import { wrap } from "@fiscalmindset/blindfold/wrap";
|
|
66
|
+
|
|
67
|
+
const fetch = wrap(); // ensures the proxy is running
|
|
68
|
+
// use `fetch` for your provider calls — send the sentinel, not your key.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Common commands
|
|
72
|
+
|
|
73
|
+
| Command | What it does |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `blindfold signup` | Self-serve: mint a funded testnet tenant. |
|
|
76
|
+
| `blindfold login` / `logout` / `whoami` | Manage stored tenant credentials (key in OS keychain). |
|
|
77
|
+
| `blindfold register --name <k>` | Seal a secret into the enclave. |
|
|
78
|
+
| `blindfold use --name <k> -- <cmd>` | Run a command with the released secret injected. |
|
|
79
|
+
| `blindfold proxy [--auth] [--socket]` | Local sentinel proxy your agent points at. |
|
|
80
|
+
| `blindfold attest [--pin]` | Verify the enclave's TDX attestation; pin the code measurement. |
|
|
81
|
+
| `blindfold credit` | Show the tenant's Terminal 3 token balance. |
|
|
82
|
+
| `blindfold doctor` | Show mode + config; live reachability check. |
|
|
83
|
+
| `blindfold update` | Update the global install. |
|
|
84
|
+
|
|
85
|
+
Run `blindfold help` for the full list.
|
|
86
|
+
|
|
87
|
+
## Security model (the short version)
|
|
88
|
+
|
|
89
|
+
- **Proxy / forward path** — the plaintext key is substituted **inside** the
|
|
90
|
+
enclave; this CLI and your agent never see it. This is the un-leakable path.
|
|
91
|
+
- **Release path** (`use`, `export`, `rotate`) — returns the plaintext to the
|
|
92
|
+
*local* process on purpose (for tools that need the raw value). Protection
|
|
93
|
+
rests on the tenant key living in the OS keychain, not a readable file.
|
|
94
|
+
- **Attestation** — `blindfold attest --pin` proves the enclave runs the exact
|
|
95
|
+
expected code (RTMR3 measurement, chained to Intel's root CA) before sealing.
|
|
96
|
+
|
|
97
|
+
## Requirements
|
|
98
|
+
|
|
99
|
+
- Node.js ≥ 18.
|
|
100
|
+
- `@terminal3/t3n-sdk` (installed automatically as an optional dependency) for
|
|
101
|
+
REAL mode. `BLINDFOLD_MOCK=1` runs a no-network mock for CI/onboarding.
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT © FiscalMindset
|
package/assets/SKILL.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: blindfold
|
|
3
|
+
description: Use Blindfold (Terminal 3 TDX enclave wrapper) to seal and use API keys safely. Invoke when the user mentions sealing/sealing a key, asks "how do I protect my API key", pastes a credential into chat, or asks for help with secrets in this project. Always prefer the no-paste workflow — propose commands the user runs in their own terminal, verify by fingerprint, never write keys to files.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Blindfold skill — the rulebook
|
|
7
|
+
|
|
8
|
+
You're working in (or with) Blindfold — a Terminal 3 TDX-enclave wrapper that keeps AI-agent API keys un-leakable. Read `current_status.md`, `process.md`, `usage_by_claude.md`, and `vicky.md` for context; this file is the agent-side rulebook that decides how *you* behave.
|
|
9
|
+
|
|
10
|
+
## When to invoke this skill
|
|
11
|
+
|
|
12
|
+
- User pastes (or is about to paste) any API key, password, token, or credential.
|
|
13
|
+
- User asks "how do I use [provider]" in a way that implies handling a key.
|
|
14
|
+
- User asks about `.env`, sealing, vaulting, or "where do I put my key".
|
|
15
|
+
- You're about to write code that would read `process.env.SOME_PROVIDER_KEY` for an outbound call.
|
|
16
|
+
- User says "use Blindfold" or "seal it" or "make this safe".
|
|
17
|
+
|
|
18
|
+
## The four rules (mirror of `usage_by_claude.md` §3)
|
|
19
|
+
|
|
20
|
+
1. **R1 — no-paste-into-chat.** If a new secret needs sealing, *propose* the command (`npm run blindfold -- register --name <KV_KEY>`) for the user to run in their own terminal. Do NOT ask them to paste the value into chat. Only use `printf 'VALUE' | ...` from chat as a fallback, and only after the user explicitly says "go ahead from here".
|
|
21
|
+
2. **R2 — verify by fingerprint, never by value.** To check what's sealed: `npm run blindfold -- sealed`. To check what's in `.env`: `npm run env:fingerprint`. To check a specific sealed key matches an expected value: `npx tsx scripts/test-v5-release.ts <secret_name>` (prints `first3…last2 (N bytes)`, never plaintext). Ask the user to paste the *output* of those commands — that's safe.
|
|
22
|
+
3. **R3 — code uses release-broker pattern.** Any code you write that needs a provider key must fetch it from T3 just-in-time via `tenant.contracts.execute("blindfold-proxy", { version: CONTRACT_VERSION, functionName: "release-to-tenant", input: { secret_key: "<name>" } })`, use it inside a `try { … } finally { /* dropped */ }`, and never reference `process.env.<provider>_API_KEY`. Reference templates: `examples/grok-via-blindfold.ts` (HTTPS) and `scripts/smtp-with-blindfold.ts` (non-HTTP).
|
|
23
|
+
4. **R4 — propose `.env` cleanup after every successful seal.** The sealed copy is canonical; the `.env` copy is leak surface. *Exception:* `T3N_API_KEY` itself stays in `.env` — it's the root credential, can't be sealed (chicken-and-egg).
|
|
24
|
+
|
|
25
|
+
## Command kit (every output is safe to share)
|
|
26
|
+
|
|
27
|
+
| Command | Purpose | Safe to paste output? |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `npm run blindfold -- doctor` | mode + cred presence (yes/no) | ✅ |
|
|
30
|
+
| `npm run blindfold -- verify` | T3 round-trip status | ✅ |
|
|
31
|
+
| `npm run blindfold -- login` | store tenant creds in `~/.blindfold` (OS keychain for the key) — works from any dir | ⚠ tell user to run it (prompts for key) |
|
|
32
|
+
| `npm run blindfold -- whoami` | show config path, tenant, env, key source (never the value) | ✅ |
|
|
33
|
+
| `npm run blindfold -- logout` | remove stored creds (keychain + `~/.blindfold/config.json`) | ✅ |
|
|
34
|
+
| `npm run blindfold -- sealed` | sealed-keys ledger (metadata only, LOCAL) | ✅ |
|
|
35
|
+
| `npm run blindfold -- audit` | reconcile ledger against the ENCLAVE — what's actually usable now | ✅ |
|
|
36
|
+
| `npm run blindfold -- status` | one-glance: mode, tenant health, sealed list | ✅ |
|
|
37
|
+
| `npm run env:fingerprint` | `.env` lines as `KEY = first3…last2 (N bytes)` | ✅ |
|
|
38
|
+
| `npx tsx scripts/test-v5-release.ts <name>` | fingerprint of the released value | ✅ |
|
|
39
|
+
| `npm run dashboard` | live HTML dashboard at `http://127.0.0.1:8799` | n/a (UI) |
|
|
40
|
+
| `npm run blindfold -- register --name <K>` | interactive seal (no echo) | ⚠ tell user to run in their terminal |
|
|
41
|
+
| `printf 'V' \| npm run blindfold -- register --name <K>` | piped seal (value briefly in this process) | ⚠ only if user already pasted value |
|
|
42
|
+
|
|
43
|
+
## Seal once, use forever (no re-seal per session)
|
|
44
|
+
|
|
45
|
+
A sealed key lives in the **Terminal 3 enclave**, not in a session, terminal, or
|
|
46
|
+
machine. So the answer to *"do I need to re-seal in a new session?"* is **no** —
|
|
47
|
+
any new session can use an already-sealed key directly. Don't propose re-running
|
|
48
|
+
`register` for a key that's already sealed.
|
|
49
|
+
|
|
50
|
+
To **use** already-sealed keys, a session needs three persistent things (all
|
|
51
|
+
survive across sessions — none require re-sealing):
|
|
52
|
+
|
|
53
|
+
1. **Tenant creds** — `T3N_API_KEY` + `DID`. These authenticate to the enclave
|
|
54
|
+
and are what release/proxy require. Two ways to provide them:
|
|
55
|
+
- **Repo `.env`** (dev). Exception to R4: they stay in `.env`; see R4.
|
|
56
|
+
- **`blindfold login`** (v0.2+, product path): stores DID + settings in
|
|
57
|
+
`~/.blindfold/config.json` and the tenant key in the **OS keychain** (v0.3;
|
|
58
|
+
macOS Keychain / Linux secret-tool) — so the CLI works from any directory
|
|
59
|
+
and the key isn't a readable file. Precedence: `process.env` > repo `.env` >
|
|
60
|
+
`~/.blindfold`. State (ledger/usage/egress) also lives in `~/.blindfold`.
|
|
61
|
+
2. **Egress already granted** for the host you'll call (`blindfold grant --host
|
|
62
|
+
<host>`). The grant is per-tenant and persistent.
|
|
63
|
+
3. **The proxy running** if using the HTTP path: `npm run blindfold -- proxy`
|
|
64
|
+
(listens on `127.0.0.1:8787`). Point the tool at it with base URL
|
|
65
|
+
`http://127.0.0.1:8787/v1` and key `__BLINDFOLD__`. (Code paths use the
|
|
66
|
+
release-broker instead — see R3.) On a shared machine, run `proxy --auth`:
|
|
67
|
+
it mints a per-session token so only the wrapped agent (not any co-resident
|
|
68
|
+
process) can use the proxy — pass it via `BLINDFOLD_PROXY_TOKEN` /
|
|
69
|
+
`wrap(client, { token })`, or `curl -H "x-blindfold-token: <token>"`.
|
|
70
|
+
|
|
71
|
+
**Before assuming a key is usable, run `audit`, not `sealed`.** `sealed` only
|
|
72
|
+
reads the LOCAL ledger; `audit` reconciles it against the enclave (the source of
|
|
73
|
+
truth) and marks each key `present` (usable now) or `MISSING` (needs a re-seal).
|
|
74
|
+
A key in `sealed` but `MISSING` in `audit` is the *only* case that needs
|
|
75
|
+
re-sealing.
|
|
76
|
+
|
|
77
|
+
## What to do, by scenario
|
|
78
|
+
|
|
79
|
+
### "I want to seal my Stripe key" (or any new credential)
|
|
80
|
+
|
|
81
|
+
**Don't ask for the value.** Respond with:
|
|
82
|
+
|
|
83
|
+
> In your own terminal, run:
|
|
84
|
+
>
|
|
85
|
+
> ```bash
|
|
86
|
+
> npm run blindfold -- register --name stripe_api_key
|
|
87
|
+
> ```
|
|
88
|
+
>
|
|
89
|
+
> It'll prompt for the value with input hidden (no echo, no shell history). Paste your `sk_live_…` there, press Enter. Once done, paste me the output of `npm run blindfold -- sealed` so I can verify it landed in the right place.
|
|
90
|
+
|
|
91
|
+
### "Seal / use a webhook URL (Discord, Slack, …)"
|
|
92
|
+
|
|
93
|
+
A webhook is special: **the secret is the entire URL** (e.g.
|
|
94
|
+
`https://discord.com/api/webhooks/<id>/<token>`), POSTed with no auth header.
|
|
95
|
+
Seal it like any secret (`register --name webhook_discord_url --from-env …`),
|
|
96
|
+
then two ways to use it:
|
|
97
|
+
|
|
98
|
+
- **Release path** (works everywhere): inject into one command —
|
|
99
|
+
`blindfold use --name webhook_discord_url --as HOOK -- sh -c 'curl -X POST -H "Content-Type: application/json" -d "{\"content\":\"hi\"}" "$HOOK"'`
|
|
100
|
+
- **Proxy path** (agent never holds the URL; contract v0.5.5+): the `webhook`
|
|
101
|
+
scheme substitutes the sealed URL *in the URL* inside the enclave. There's a
|
|
102
|
+
`/discord` provider — grant egress (`grant --host discord.com`), run the proxy,
|
|
103
|
+
and POST a JSON body to `http://127.0.0.1:8787/discord`. See
|
|
104
|
+
`examples/discord-webhook/`.
|
|
105
|
+
|
|
106
|
+
### "Add support for an API that isn't a provider yet" (register a new proxy provider)
|
|
107
|
+
|
|
108
|
+
If the user wants an API Blindfold doesn't list (unlisted paths get a 404 "no
|
|
109
|
+
upstream mapping" — deny-by-default), **prefer adding a first-class proxy provider
|
|
110
|
+
over the release-broker fallback** — the proxy path never returns plaintext, so it's
|
|
111
|
+
the stronger option. This is pure code (no secret value), so **you can do it end to
|
|
112
|
+
end**; only the final `register` (plaintext) is handed back to the user.
|
|
113
|
+
|
|
114
|
+
1. Add a ~5-line entry to `packages/blindfold/src/providers.ts` — no enclave/Rust
|
|
115
|
+
change is needed as long as the API uses one of the four existing auth schemes
|
|
116
|
+
(`bearer` / `basic` / `sigv4` / `webhook`). Example for a bearer API (Notion):
|
|
117
|
+
```ts
|
|
118
|
+
{
|
|
119
|
+
id: "notion",
|
|
120
|
+
prefix: "/notion/",
|
|
121
|
+
upstream: (p) => `https://api.notion.com${stripPrefix(p, "/notion/")}`,
|
|
122
|
+
secretKey: "notion_api_key",
|
|
123
|
+
auth: () => ({ scheme: "bearer" }),
|
|
124
|
+
defaultHeaders: { "Notion-Version": "2022-06-28" }, // provider-required headers
|
|
125
|
+
},
|
|
126
|
+
```
|
|
127
|
+
2. Rebuild (`npm run build` in `packages/blindfold`) so `dist/` picks up the entry.
|
|
128
|
+
3. Hand back the two one-time human steps: seal the key —
|
|
129
|
+
`npm run blindfold -- register --name notion_api_key` (in their terminal, R1) —
|
|
130
|
+
and grant egress: `blindfold grant --host api.notion.com`.
|
|
131
|
+
4. The agent then calls `http://127.0.0.1:8787/notion/...` with
|
|
132
|
+
`Authorization: Bearer __BLINDFOLD__`; the enclave swaps in the sealed key.
|
|
133
|
+
|
|
134
|
+
Only touch the Rust contract (`contract/src/forward.rs`) if the API needs a brand-new
|
|
135
|
+
auth scheme none of the four cover — otherwise this stays a TypeScript-only change.
|
|
136
|
+
|
|
137
|
+
### "I already pasted my OpenAI key in chat — seal it"
|
|
138
|
+
|
|
139
|
+
It's in our chat context now (can't undo). Reduce future surface:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
printf 'sk-...' | npx tsx packages/blindfold/bin/blindfold.ts register --name openai_api_key
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Then propose deleting any `.env` copy. Note in your response that the chat-context exposure already happened and you can't retroactively fix it — only forward-protect.
|
|
146
|
+
|
|
147
|
+
### "Write me code that calls OpenAI"
|
|
148
|
+
|
|
149
|
+
Write the release-broker pattern (see `examples/grok-via-blindfold.ts`). Never `import OpenAI from "openai"` + `apiKey: process.env.OPENAI_API_KEY`. Always:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const { value: apiKey } = await tenant.contracts.execute("blindfold-proxy", {
|
|
153
|
+
version: CONTRACT_VERSION,
|
|
154
|
+
functionName: "release-to-tenant",
|
|
155
|
+
input: { secret_key: "openai_api_key" },
|
|
156
|
+
}) as { value: string };
|
|
157
|
+
try {
|
|
158
|
+
// use apiKey for ONE call
|
|
159
|
+
} finally { /* dropped */ }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### "Read my .env to check what's there"
|
|
163
|
+
|
|
164
|
+
**Don't `Read .env`.** Run:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npm run env:fingerprint
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Ask the user to paste that output. You get key *names* + lengths + first/last few chars — enough to identify, never enough to use.
|
|
171
|
+
|
|
172
|
+
### "It's broken — what's wrong?"
|
|
173
|
+
|
|
174
|
+
Run these in order, asking the user to paste each output:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npm run blindfold -- doctor # config sanity
|
|
178
|
+
npm run blindfold -- verify # T3 reachability
|
|
179
|
+
npm run blindfold -- sealed # is the key there?
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Cross-reference any errors with `vicky.md` Q6 (keyword-indexed error table).
|
|
183
|
+
|
|
184
|
+
## What this skill must NEVER do
|
|
185
|
+
|
|
186
|
+
- Write a real plaintext key value to any file, doc, or commit message.
|
|
187
|
+
- `console.log` / `safeLog` a value whose origin is `process.env.*_API_KEY` or an `Authorization` header.
|
|
188
|
+
- Use `Read` on `.env` when `npm run env:fingerprint` would do.
|
|
189
|
+
- Suggest the user paste a key into chat as a default — always default to "run the command in your terminal".
|
|
190
|
+
- Generate code that references `process.env.<provider>_API_KEY` for outbound provider calls.
|
|
191
|
+
|
|
192
|
+
## When the user objects to a recommendation
|
|
193
|
+
|
|
194
|
+
If the user explicitly chooses a less-defensive path ("I know, just seal it from here"), honor it but say so once: *"Going to use the piped-stdin path — value will be in this chat's transcript; if you want zero chat exposure, prefer running `register` in your own terminal."* Then proceed with their choice.
|
|
195
|
+
|
|
196
|
+
## Reference files (when in doubt)
|
|
197
|
+
|
|
198
|
+
| File | What's in it |
|
|
199
|
+
|---|---|
|
|
200
|
+
| `usage_by_claude.md` | The user-facing twin of this rulebook; refresh §5 status table when sealing |
|
|
201
|
+
| `process.md` | First-time-user walkthrough — copy command shapes from here |
|
|
202
|
+
| `vicky.md` | Plain-English Q&A — copy explanations from here when answering questions |
|
|
203
|
+
| `current_status.md` | What's working vs blocked right now — quote from here when status is asked |
|
|
204
|
+
| `examples/grok-via-blindfold.ts` | The canonical release-broker template for HTTPS APIs |
|
|
205
|
+
| `scripts/smtp-with-blindfold.ts` | The canonical release-broker template for non-HTTP protocols |
|
|
Binary file
|
package/bin/blindfold.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* blindfold CLI — thin dispatcher. Command logic lives in ./cmd-*.ts, shared
|
|
4
|
+
* helpers in ./cli-shared.ts. Every action prints what it did, never a secret.
|
|
5
|
+
*/
|
|
6
|
+
import { type Argv, parseArgv } from "./cli-shared.ts";
|
|
7
|
+
import { c, head } from "../src/color.ts";
|
|
8
|
+
import { handleAuth } from "./cmd-auth.ts";
|
|
9
|
+
import { handleSecrets } from "./cmd-secrets.ts";
|
|
10
|
+
import { handleLifecycle } from "./cmd-lifecycle.ts";
|
|
11
|
+
import { handleTenant } from "./cmd-tenant.ts";
|
|
12
|
+
import { handleServe } from "./cmd-serve.ts";
|
|
13
|
+
import { handleEnclave } from "./cmd-enclave.ts";
|
|
14
|
+
|
|
15
|
+
type Handler = (cmd: string, argv: Argv, cmdArgs: string[]) => Promise<void>;
|
|
16
|
+
|
|
17
|
+
const ROUTES: Record<string, Handler> = {
|
|
18
|
+
signup: handleAuth, login: handleAuth, logout: handleAuth, whoami: handleAuth,
|
|
19
|
+
register: handleSecrets, use: handleSecrets, export: handleSecrets,
|
|
20
|
+
rotate: handleLifecycle, rollback: handleLifecycle, versions: handleLifecycle, migrate: handleLifecycle,
|
|
21
|
+
grant: handleTenant, share: handleTenant, revoke: handleTenant,
|
|
22
|
+
proxy: handleServe, attest: handleServe, dashboard: handleServe, stats: handleServe, "stats:clear": handleServe,
|
|
23
|
+
publish: handleEnclave, init: handleEnclave, verify: handleEnclave, compat: handleEnclave,
|
|
24
|
+
sealed: handleEnclave, audit: handleEnclave, status: handleEnclave, doctor: handleEnclave, skill: handleEnclave,
|
|
25
|
+
credit: handleEnclave, balance: handleEnclave,
|
|
26
|
+
update: handleEnclave, upgrade: handleEnclave,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
async function main(): Promise<void> {
|
|
30
|
+
// Split on a bare `--` so `blindfold use --name X -- <command...>` keeps the
|
|
31
|
+
// child command intact (parseArgv would otherwise swallow it).
|
|
32
|
+
const raw = process.argv.slice(2);
|
|
33
|
+
const ddIdx = raw.indexOf("--");
|
|
34
|
+
const cmdArgs = ddIdx >= 0 ? raw.slice(ddIdx + 1) : [];
|
|
35
|
+
const argv = parseArgv(ddIdx >= 0 ? raw.slice(0, ddIdx) : raw);
|
|
36
|
+
const cmd = argv._[0] ?? "help";
|
|
37
|
+
const handler = ROUTES[cmd];
|
|
38
|
+
if (handler) await handler(cmd, argv, cmdArgs);
|
|
39
|
+
else printHelp();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function printHelp(): void {
|
|
43
|
+
console.log(`${head("🛡️ Blindfold")} ${c.gray("— protect your AI agent's API keys with Terminal 3 enclaves.")}
|
|
44
|
+
|
|
45
|
+
${c.bold("Commands:")}
|
|
46
|
+
signup [--email <you@x.com>] Self-serve: create a Terminal 3 testnet tenant from scratch. Generates a key locally, verifies your email by code, and mints welcome credits — no manual provisioning. Then run doctor + register.
|
|
47
|
+
init [--seed KV:ENV]... [--start] One-command zero-knowledge setup. Walks through .env, build, auth, publish, seed; can auto-launch the proxy.
|
|
48
|
+
verify Handshake + auth against T3 (smoke test).
|
|
49
|
+
compat [--json] Scan this machine for AI agent tools/SDKs and print the exact env-var swap for each.
|
|
50
|
+
register --name <KV_KEY> [--from-env <ENV_VAR>] Seal a secret into the enclave (one-time). With --from-env: reads process.env. Without: prompts the terminal with no echo (preferred — never touches disk/history). Also accepts piped stdin.
|
|
51
|
+
use --name <secret> [--as <ENV>] -- <cmd> USE a sealed secret: release it and run <cmd> with it injected as $ENV for that command only — never back in your env. --as is auto-detected for known tools (gh→GH_TOKEN, psql→PGPASSWORD, …). Or --url <https> for a quick auth check.
|
|
52
|
+
rotate --name <secret> [--from-env <ENV_VAR>] Replace a sealed secret's value (snapshots the old value for rollback; shows before/after fingerprints, never the value).
|
|
53
|
+
export --name <secret> [--as <ENV_VAR>] CI-only: release a sealed secret into $GITHUB_ENV for later steps (masked in logs). Used by the Blindfold GitHub Action.
|
|
54
|
+
rollback --name <secret> [--to <fp|iso-ts>] Restore a previous value snapshotted by rotate (most recent by default).
|
|
55
|
+
versions [--name <secret>] List the snapshots available to roll back to (metadata only).
|
|
56
|
+
migrate [--dry-run] [--keep] Seal EVERY secret in your .env in one shot, then remove the plaintext lines (backup kept). --dry-run previews; --keep comments lines instead of deleting. Skips T3 creds + config.
|
|
57
|
+
status One-glance overview: mode, tenant health, and the list of sealed secrets.
|
|
58
|
+
sealed List sealed keys — metadata only (name, byte-length, when, where). Never the value.
|
|
59
|
+
audit Verify the ledger's tamper-evident hash-chain AND reconcile it against the enclave (the source of truth) — flags drift/missing/tampering.
|
|
60
|
+
proxy [--port 8787] [--auth] [--socket [path]] Run the local OpenAI-shaped proxy. --auth mints a per-session token; --socket binds a 0600 unix socket (only your OS user can connect).
|
|
61
|
+
attest [--expect-rtmr3 <b64>] [--pin] [--json] Verify the enclave's TDX attestation (chains to Intel's root CA). --pin records the RTMR3 so seal/proxy auto-verify it first.
|
|
62
|
+
publish [--wasm path/to/blindfold_proxy.wasm] Publish the Rust→WASM contract (one-time).
|
|
63
|
+
grant --host <host>[,<host2>...] Authorize the contract to call these hosts (required before the proxy / in-enclave path can reach them). E.g. --host api.openai.com
|
|
64
|
+
share --to <agent-did> --host <host>[,...] Let a teammate's agent USE your sealed keys for those hosts via the enclave — they never receive the plaintext (forward only, least privilege).
|
|
65
|
+
revoke --to <agent-did> Remove a teammate's access. Immediate and complete — nobody holds a raw key copy.
|
|
66
|
+
|
|
67
|
+
skill install [--global|--cursor|--opencode|--cline|--all] Install the Blindfold agent skill so your coding agent handles secrets safely. Default: this project.
|
|
68
|
+
skill uninstall Remove all installed skill files.
|
|
69
|
+
|
|
70
|
+
dashboard [--port 8799] Live HTML dashboard of proxy usage.
|
|
71
|
+
stats CLI summary of proxy usage.
|
|
72
|
+
stats:clear Wipe the usage log.
|
|
73
|
+
doctor Show current mode + config.
|
|
74
|
+
credit [--json] Show the tenant's Terminal 3 token/credit balance (no credit cost).
|
|
75
|
+
update [--from <path>] Update the global blindfold (from npm, or a local repo with --from).
|
|
76
|
+
|
|
77
|
+
The friendliest path is just: blindfold init
|
|
78
|
+
|
|
79
|
+
Quick start:
|
|
80
|
+
1) ./scripts/build-contract.sh # build the Rust contract (REAL mode only)
|
|
81
|
+
2) blindfold publish # register the contract on T3
|
|
82
|
+
3) blindfold register --name openai_api_key --from-env OPENAI_API_KEY
|
|
83
|
+
4) blindfold proxy # then point your agent at it
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main().catch((e) => {
|
|
88
|
+
console.error("✖", (e as Error).message);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI helpers used by the command modules (bin/cmd-*.ts) and the
|
|
3
|
+
* dispatcher (bin/blindfold.ts). Kept in bin/ so the `../src/` import paths in
|
|
4
|
+
* the command modules don't change.
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { c } from "../src/color.ts";
|
|
11
|
+
|
|
12
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const REPO_ROOT = path.resolve(HERE, "..", "..", "..");
|
|
14
|
+
const PKG_ROOT = path.resolve(HERE, ".."); // packages/blindfold (or the installed package root)
|
|
15
|
+
|
|
16
|
+
/** Resolve an asset needed by skill/publish: prefer the repo source (dev), else
|
|
17
|
+
* the copy bundled in the package's assets/ (standalone/global install). */
|
|
18
|
+
export function assetPath(repoRelative: string, assetName: string): string {
|
|
19
|
+
const repoPath = path.join(REPO_ROOT, ...repoRelative.split("/"));
|
|
20
|
+
if (fs.existsSync(repoPath)) return repoPath;
|
|
21
|
+
return path.join(PKG_ROOT, "assets", assetName);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type Argv = { _: string[]; flags: Record<string, string | boolean> };
|
|
25
|
+
|
|
26
|
+
export function parseArgv(argv: string[]): Argv {
|
|
27
|
+
const out: Argv = { _: [], flags: {} };
|
|
28
|
+
for (let i = 0; i < argv.length; i++) {
|
|
29
|
+
const a = argv[i] ?? "";
|
|
30
|
+
if (a.startsWith("--")) {
|
|
31
|
+
const key = a.slice(2);
|
|
32
|
+
const next = argv[i + 1];
|
|
33
|
+
if (next && !next.startsWith("--")) {
|
|
34
|
+
out.flags[key] = next;
|
|
35
|
+
i += 1;
|
|
36
|
+
} else {
|
|
37
|
+
out.flags[key] = true;
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
out._.push(a);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Non-reversible fingerprint of a secret — for verification without exposure. */
|
|
47
|
+
export function fingerprint(s: string): string {
|
|
48
|
+
return createHash("sha256").update(s).digest("hex").slice(0, 8);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Known CLI tools → the env var they read their credential from. */
|
|
52
|
+
const TOOL_ENV: Record<string, string> = {
|
|
53
|
+
gh: "GH_TOKEN", git: "GH_TOKEN", glab: "GITLAB_TOKEN",
|
|
54
|
+
psql: "PGPASSWORD", pg_dump: "PGPASSWORD", mysql: "MYSQL_PWD",
|
|
55
|
+
aws: "AWS_SECRET_ACCESS_KEY", stripe: "STRIPE_API_KEY",
|
|
56
|
+
vercel: "VERCEL_TOKEN", npm: "NPM_TOKEN", docker: "DOCKER_PASSWORD",
|
|
57
|
+
openai: "OPENAI_API_KEY", anthropic: "ANTHROPIC_API_KEY",
|
|
58
|
+
doctl: "DIGITALOCEAN_ACCESS_TOKEN", heroku: "HEROKU_API_KEY",
|
|
59
|
+
cloudflared: "CLOUDFLARE_API_TOKEN", wrangler: "CLOUDFLARE_API_TOKEN",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Pick the env-var name for `use`: explicit --as, else infer from the tool, else NAME upper-cased. */
|
|
63
|
+
export function resolveEnvVar(asFlag: string | undefined, command: string | undefined, name: string): string {
|
|
64
|
+
if (asFlag) return asFlag;
|
|
65
|
+
if (command && TOOL_ENV[command]) return TOOL_ENV[command];
|
|
66
|
+
return name.toUpperCase();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function die(msg: string): never {
|
|
70
|
+
console.error(c.red(`✖ ${msg}`));
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|