Skip to content

Commit 1ec32d5

Browse files
authored
[recipes] Brain smoke test — install verification harness
Adds a read-only-by-default Open Brain install smoke-test recipe with optional destructive core-feature checks, JSON output, cleanup handling, RLS/access-key probes, and setup documentation.
1 parent dd0e700 commit 1ec32d5

4 files changed

Lines changed: 1347 additions & 0 deletions

File tree

recipes/brain-smoke-test/README.md

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
# Brain Smoke Test
2+
3+
> One-shot harness that probes every live surface of an Open Brain install and reports which ones are healthy, skipped (optional feature not installed), or broken.
4+
5+
## What It Does
6+
7+
Runs ~30 independent checks across seven categories against your deployed Open Brain and prints a pass/skip/fail dashboard. Optional features (REST API, ob-graph, enhanced-thoughts, smart-ingest) are detected automatically and skipped with a clear reason rather than failing the run, so the same script works on stock core installs and fully-loaded instances.
8+
9+
## Why Use This
10+
11+
Open Brain is a lot of moving parts -- a database, an Edge Function, a secret access key, RLS policies, and optionally more tables and endpoints from recipes and integrations. When something is wrong it is usually one specific thing: a missing `GRANT`, a mismatched access key, a forgotten column, a function that failed to deploy. This harness catches those misconfigurations before you waste an hour wondering why Claude Desktop sees no tools or why semantic search returns nothing.
12+
13+
Run it:
14+
15+
- After initial setup to confirm every surface is wired correctly
16+
- After adding an extension or recipe that touches the schema
17+
- Before reporting a bug -- the output tells a maintainer exactly which surface is broken
18+
- In CI to guard a shared instance from regressing
19+
20+
## Categories Checked
21+
22+
1. **MCP Server** -- The `open-brain-mcp` Edge Function responds, exposes the four canonical tools (`search_thoughts`, `list_thoughts`, `thought_stats`, `capture_thought`), and completes a JSON-RPC `initialize` handshake.
23+
2. **REST API** -- If you have installed the optional `rest-api` integration or set `REST_API_BASE`, the gateway answers `/health`, `/thoughts`, `/search`, and `/stats`. The `NEXT_PUBLIC_API_URL` env var (the base URL the dashboard is pointed at) is also probed and only passes on a 2xx response. Skipped otherwise.
24+
3. **DB Schema** -- The canonical `public.thoughts` table exists with `id, content, embedding, metadata, created_at, updated_at`, the dedup fingerprint column is present, and `match_thoughts` + `upsert_thought` RPCs are callable. Optional tables (`graph_nodes`, `graph_edges`, `ingestion_jobs`) and the `search_thoughts_text` RPC are detected and skipped when absent.
25+
4. **Auth** -- `MCP_ACCESS_KEY` is enforced: requests with no key, with a wrong key, with the header (`x-brain-key`), and with the query string (`?key=`) all produce the expected outcome.
26+
5. **Core Features** -- **Destructive, opt-in via `--destructive`.** End-to-end capture + search + cleanup. Inserts a uniquely-tagged row via REST (triggers embedding + LLM metadata generation), fetches it back, calls MCP's `capture_thought` and `search_thoughts`, then deletes by tag. Skipped by default so CI can run this harness against shared/prod instances without mutating data or spending external model credits.
27+
6. **Access Key Enforcement** -- The Supabase PostgREST gateway rejects requests with no `apikey` and with an invalid `apikey`. This runs **before** RLS, so these checks alone do not prove RLS is configured -- see category 7.
28+
7. **Row-Level Security** -- Actually probes whether RLS is on and policies are restrictive. Tries an optional helper RPC (`pg_class_rls`) to read `pg_catalog.pg_class.relrowsecurity`. Also, if `SUPABASE_ANON_KEY` is set, does an anon-key read of `public.thoughts` and **fails loud** if rows come back (means RLS is off or a permissive `ALL USING (true)` policy is leaking data). Without `SUPABASE_ANON_KEY`, the anon probe is skipped with a clear note that RLS is unverified.
29+
30+
## Prerequisites
31+
32+
- Working Open Brain setup ([guide](../../docs/01-getting-started.md))
33+
- Node.js 18 or later (uses the built-in `fetch` and `AbortController`)
34+
- A local `.env.local` file (or exported environment variables) sitting next to `smoke-all.js`. The script looks for `.env.local` in its own directory first (so `node recipes/brain-smoke-test/smoke-all.js` works from any cwd) and falls back to the current working directory.
35+
36+
## Credential Tracker
37+
38+
Copy this block into a text editor and fill it in.
39+
40+
```text
41+
BRAIN SMOKE TEST -- CREDENTIAL TRACKER
42+
--------------------------------------
43+
44+
FROM YOUR OPEN BRAIN SETUP
45+
Project URL: ____________ (e.g. https://abcd1234.supabase.co)
46+
Service role key: ____________ (Supabase "Secret key")
47+
MCP access key: ____________ (from Step 5 of the getting-started guide)
48+
49+
OPTIONAL (unlocks extra checks, safe to leave blank on stock installs)
50+
REST API base URL: ____________ (e.g. https://<ref>.supabase.co/functions/v1/open-brain-rest)
51+
Dashboard REST base URL: ____________ (the NEXT_PUBLIC_API_URL the dashboard uses;
52+
same shape as REST API base URL)
53+
Anon/publishable key: ____________ (enables a real anon-key RLS probe in the
54+
Row-Level Security category)
55+
56+
--------------------------------------
57+
```
58+
59+
## Installation
60+
61+
No build step. Just drop the file in and run it.
62+
63+
1. Copy `smoke-all.js` into a local folder on your machine (any folder is fine -- it does not need to live inside your Supabase project directory).
64+
65+
2. Create `.env.local` next to it:
66+
67+
```text
68+
SUPABASE_URL=https://YOUR_PROJECT_REF.supabase.co
69+
SUPABASE_SERVICE_ROLE_KEY=your-service-role-secret-key
70+
MCP_ACCESS_KEY=your-access-key-from-step-5
71+
72+
# Optional -- unlocks the REST API category. Leave unset on stock installs.
73+
# REST_API_BASE=https://YOUR_PROJECT_REF.supabase.co/functions/v1/open-brain-rest
74+
75+
# Optional -- same base URL the dashboard uses (NEXT_PUBLIC_API_URL).
76+
# NEXT_PUBLIC_API_URL=https://YOUR_PROJECT_REF.supabase.co/functions/v1/open-brain-rest
77+
78+
# Optional -- enables a real RLS probe that reads public.thoughts with
79+
# the anon key and fails if rows come back.
80+
# SUPABASE_ANON_KEY=your-anon-publishable-key
81+
```
82+
83+
3. Run it:
84+
85+
```bash
86+
node smoke-all.js
87+
```
88+
89+
## Usage
90+
91+
```bash
92+
# Pretty-printed dashboard (default). Read-only: no data is inserted or
93+
# deleted, no LLM calls are made. Core Features is SKIPPED.
94+
node smoke-all.js
95+
96+
# Machine-readable JSON -- pipe into jq, a log aggregator, or CI assertions
97+
node smoke-all.js --json
98+
99+
# Opt in to the destructive Core Features category. Inserts a uniquely-
100+
# tagged row via the service-role key, triggers embedding + LLM metadata
101+
# generation, then deletes by tag. Cleanup runs on normal exit, on a
102+
# thrown check, and on SIGINT/SIGTERM, so ctrl-c does not leave residue.
103+
# Use this against a dev/scratch project, NOT a shared/prod instance.
104+
node smoke-all.js --destructive
105+
node smoke-all.js --write # alias
106+
107+
# Run only one category (names are case-insensitive)
108+
node smoke-all.js --category="DB Schema"
109+
node smoke-all.js --category=Auth
110+
node smoke-all.js --category="Core Features" --destructive
111+
node smoke-all.js --category="Row-Level Security"
112+
113+
# Show this usage
114+
node smoke-all.js --help
115+
```
116+
117+
The seven category names are `MCP Server`, `REST API`, `DB Schema`, `Auth`, `Core Features`, `Access Key Enforcement`, and `Row-Level Security`.
118+
119+
## Security Note -- `?key=` Access-Key Logging
120+
121+
One Auth category check (`MCP accepts correct access key (?key=)`) verifies the URL-query-string auth path that OB1 supports for clients that cannot send custom headers (documented in [docs/01-getting-started.md](../../docs/01-getting-started.md) Step 5). **This check puts `MCP_ACCESS_KEY` into the URL**, which means the key ends up in:
122+
123+
- Supabase's function-invocation logs (visible in the Studio UI to anyone with dashboard access)
124+
- any corporate HTTPS-inspection proxy that logs URLs (common on enterprise networks)
125+
- shell history if you pipe the output through `tee` or redirect stderr to a file
126+
- CI run logs if a future Node or `fetch` implementation verbose-logs request URLs
127+
128+
The header-based auth check (`x-brain-key`) does **not** have this problem and is always preferred.
129+
130+
If you run this harness from a network with HTTPS proxying, or ship the output anywhere public, **rotate `MCP_ACCESS_KEY`** afterward (Step 5 of the getting-started guide). To skip the `?key=` check entirely on sensitive networks, run `node smoke-all.js --category=MCP\ Server` and the other categories except `Auth` -- you lose a small amount of coverage but the key never touches a URL.
131+
132+
## Example Output
133+
134+
The harness prints one row per check, grouped by category, then a summary line. Row glyphs:
135+
136+
- `` **pass** -- the surface is wired correctly. Detail column shows what the check saw (`HTTP 204`, `rows=1247`, `callable`, etc.).
137+
- `` **skip** -- an optional dependency is not installed, or a prerequisite env var is unset. Detail column explains why (e.g. `REST API not installed`, `SUPABASE_ANON_KEY unset -- RLS not verified end-to-end`). Skipped checks never fail the run.
138+
- `` **fail** -- the surface exists but answered wrong. Detail column shows the error (`HTTP 500`, `content mismatch`, `RLS is OFF or a permissive ALL USING (true) policy exists`).
139+
140+
Each row ends with the elapsed milliseconds for that check so a slow endpoint is visible at a glance.
141+
142+
The summary line has the form `Summary: <N> pass, <N> skip, <N> fail (<total> total)` where `<total>` matches the number of rows printed above it, followed by `Result: OK` (exit 0) or `Result: FAIL` (exit 1).
143+
144+
A representative shape on a healthy stock install, run without `--destructive` (no REST API, no ob-graph, no smart-ingest, no enhanced-thoughts, no `SUPABASE_ANON_KEY`):
145+
146+
```text
147+
Open Brain Smoke Test -- 28 checks across 7 categories
148+
Target: https://abcd1234.supabase.co
149+
150+
MCP Server:
151+
✓ open-brain-mcp endpoint responds 124ms -- HTTP 204
152+
✓ MCP tools/list returns core tools 312ms -- tools=4 (search_thoughts, list_thoughts, thought_stats, capture_thought)
153+
✓ MCP initialize handshake 289ms -- server=open-brain
154+
155+
REST API:
156+
⚠ GET /health 92ms -- REST API not installed
157+
⚠ GET /thoughts?limit=3 88ms -- REST API not installed
158+
⚠ POST /search (text) 91ms -- REST API not installed
159+
⚠ GET /stats 87ms -- REST API not installed
160+
⚠ REST API base URL (NEXT_PUBLIC_API_URL) responds 2xx 1ms -- NEXT_PUBLIC_API_URL unset
161+
162+
DB Schema:
163+
✓ thoughts table present 178ms -- rows=1247
164+
✓ thoughts has canonical columns 142ms -- id, content, embedding, metadata, created_at, updated_at
165+
✓ content_fingerprint column (dedup) 138ms -- present
166+
✓ match_thoughts RPC 301ms -- callable
167+
✓ upsert_thought RPC 198ms -- callable
168+
✓ thoughts recently written (last 7d) 165ms -- rows_7d=84
169+
⚠ graph_nodes table (optional recipe: ob-graph) 142ms -- graph_nodes table not installed
170+
⚠ graph_edges table (optional recipe: ob-graph) 141ms -- graph_edges table not installed
171+
⚠ ingestion_jobs table (optional integration: smart-ingest) 139ms -- ingestion_jobs table (requires schemas/smart-ingest-tables, not yet on main) not installed
172+
⚠ search_thoughts_text RPC (optional schema: enhanced-thoughts) 138ms -- enhanced-thoughts not installed
173+
174+
Auth:
175+
✓ MCP rejects missing access key 96ms -- HTTP 401 (rejected)
176+
✓ MCP rejects wrong access key 98ms -- HTTP 401 (rejected)
177+
✓ MCP accepts correct access key (header) 197ms -- HTTP 200
178+
✓ MCP accepts correct access key (?key=) 201ms -- HTTP 200
179+
180+
Core Features:
181+
⚠ Core Features (destructive) 0ms -- pass --destructive to exercise capture + search + cleanup (writes rows, spends LLM credits)
182+
183+
Access Key Enforcement:
184+
✓ PostgREST rejects missing apikey 98ms -- HTTP 401 (rejected before RLS)
185+
✓ PostgREST rejects invalid apikey 102ms -- HTTP 401 (rejected before RLS)
186+
✓ Service role can read thoughts 154ms -- rows=1247
187+
188+
Row-Level Security:
189+
⚠ pg_class.relrowsecurity = true for public.thoughts 94ms -- pg_class_rls helper RPC not installed (rely on anon probe)
190+
⚠ Anon key cannot read thoughts (real RLS probe) 1ms -- SUPABASE_ANON_KEY unset -- RLS not verified end-to-end
191+
192+
Summary: 16 pass, 12 skip, 0 fail (28 total)
193+
Result: OK
194+
```
195+
196+
Row counts by category on a stock install without `--destructive`: MCP Server=3, REST API=5, DB Schema=10, Auth=4, Core Features=1 (synthetic skip), Access Key Enforcement=3, Row-Level Security=2 -- **28 total**. Pass/skip split depends on which optional recipes are installed; `16 pass + 12 skip + 0 fail` is the stock-install baseline above.
197+
198+
With `--destructive` the Core Features skip row is replaced by 5 real checks (insert, retrieve, MCP capture, MCP search, cleanup), so the total grows to 32. Installing ob-graph, smart-ingest (once its schema lands), enhanced-thoughts, REST API, and setting `SUPABASE_ANON_KEY` converts skips into passes without changing the row count.
199+
200+
## Exit Codes
201+
202+
- `0` -- all checks passed, or passed-or-skipped
203+
- `1` -- at least one check failed
204+
- `2` -- setup error (missing required env var, unknown `--category`)
205+
206+
Skipped checks never fail the run, so you can wire this into CI without it going red every time an optional recipe is absent. **Warning:** a `` on the anon-key RLS probe means RLS was not actually verified -- set `SUPABASE_ANON_KEY` before trusting the run as a safety-rail gate.
207+
208+
## Extending
209+
210+
Each category is a plain array of `{ name, fn }` entries. To add a check:
211+
212+
1. Pick the category array (`mcpChecks`, `restChecks`, `dbChecks`, `authChecks`, `coreChecks`, `accessKeyChecks`, or `rlsChecks`) inside `smoke-all.js`.
213+
2. Append an entry:
214+
215+
```js
216+
{
217+
name: "My new check",
218+
fn: async (signal) => {
219+
const res = await fetch(`${REST_BASE}/my_table?select=id&limit=1`, {
220+
headers: SVC_HEADERS, signal,
221+
});
222+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
223+
return "ok";
224+
},
225+
},
226+
```
227+
228+
3. For optional features, throw `SkipError` when the dependency is absent rather than letting the check fail:
229+
230+
```js
231+
if (res.status === 404) throw new SkipError("my-feature not installed");
232+
```
233+
234+
Each `fn` gets an `AbortSignal` that fires at 10 seconds by default. Return a short string to show in the dashboard, or throw any other error to fail the check.
235+
236+
## Troubleshooting
237+
238+
**Issue: `ERROR: missing required env var(s): SUPABASE_URL, ...`**
239+
Solution: Create `.env.local` in the current directory with `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`, and `MCP_ACCESS_KEY`, or export them into your shell. The script refuses to start without all three.
240+
241+
**Issue: `Auth: ✗ MCP accepts correct access key` fails with HTTP 401**
242+
Solution: The `MCP_ACCESS_KEY` in `.env.local` does not match what Supabase has stored. Re-run `supabase secrets set MCP_ACCESS_KEY=<your-key>` and confirm the key in your credential tracker is identical.
243+
244+
**Issue: `DB Schema: ✗ thoughts has canonical columns` fails with HTTP 400**
245+
Solution: Your `public.thoughts` table is missing one of the canonical columns (most commonly `embedding`). Re-run the SQL in [Step 2.2 of the getting-started guide](../../docs/01-getting-started.md). Additive migrations are safe -- the script only reads, it does not drop anything.
246+
247+
**Issue: `MCP search_thoughts finds test row` fails even though capture succeeded**
248+
Solution: Embedding generation is asynchronous in some setups and may not land before search runs. The check already retries once with a 1.5 s delay; if it still fails, check the Edge Function logs in the Supabase dashboard for OpenRouter errors (missing or rate-limited key).
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "Brain Smoke Test",
3+
"description": "Smoke harness verifying a fresh Open Brain install: REST API, MCP, DB schema, auth, RLS, access key, and (with --destructive) capture core-features (28 checks stock, 32 with --destructive).",
4+
"category": "recipes",
5+
"author": {
6+
"name": "Alan Shurafa",
7+
"github": "alanshurafa"
8+
},
9+
"version": "1.0.0",
10+
"requires": {
11+
"open_brain": true,
12+
"services": [],
13+
"tools": ["Node.js 18+"]
14+
},
15+
"tags": ["smoke-test", "verification", "health-check", "operational"],
16+
"difficulty": "beginner",
17+
"estimated_time": "10 minutes",
18+
"created": "2026-04-18",
19+
"updated": "2026-04-18"
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "ob1-brain-smoke-test",
3+
"version": "1.0.0",
4+
"description": "Open Brain Brain Smoke Test -- full-surface verification of an Open Brain install (read-only by default).",
5+
"private": true,
6+
"type": "module",
7+
"main": "smoke-all.js",
8+
"bin": {
9+
"ob1-brain-smoke-test": "./smoke-all.js"
10+
},
11+
"engines": {
12+
"node": ">=18"
13+
},
14+
"scripts": {
15+
"smoke": "node smoke-all.js",
16+
"smoke:json": "node smoke-all.js --json",
17+
"smoke:destructive": "node smoke-all.js --destructive"
18+
},
19+
"license": "FSL-1.1-MIT"
20+
}

0 commit comments

Comments
 (0)