diff --git a/CLAUDE.md b/CLAUDE.md index 83e052785..502a7e753 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,7 +22,7 @@ strict behavior when unset. ## Key files -- `src/core/operations.ts` — Contract-first operation definitions (the foundation). Also exports upload validators: `validateUploadPath`, `validatePageSlug`, `validateFilename`. `OperationContext.remote` flags untrusted callers. +- `src/core/operations.ts` — Contract-first operation definitions (the foundation). Also exports upload validators: `validateUploadPath`, `validatePageSlug`, `validateFilename`. `OperationContext.remote` flags untrusted callers. As of v1.0.0, every `Operation` carries `scope?: 'read' | 'write' | 'admin'` + `localOnly?: boolean`. All 30 ops are annotated; `sync_brain`, `file_upload`, `file_list`, and `file_url` are `admin + localOnly` (rejected over HTTP). `OperationContext.auth?: AuthInfo` is threaded through HTTP dispatch for scope enforcement in `serve-http.ts` before the op runs. - `src/core/engine.ts` — Pluggable engine interface (BrainEngine). `clampSearchLimit(limit, default, cap)` takes an explicit cap so per-operation caps can be tighter than `MAX_SEARCH_LIMIT`. Exports `LinkBatchInput` / `TimelineBatchInput` for the v0.12.1 bulk-insert API (`addLinksBatch` / `addTimelineEntriesBatch`). As of v0.13.1, `BrainEngine` has a `readonly kind: 'postgres' | 'pglite'` discriminator so migrations (`src/core/migrate.ts`) and other consumers can branch on engine without `instanceof` + dynamic imports. - `src/core/engine-factory.ts` — Engine factory with dynamic imports (`'pglite'` | `'postgres'`) - `src/core/pglite-engine.ts` — PGLite (embedded Postgres 17.5 via WASM) implementation, all 40 BrainEngine methods. `addLinksBatch` / `addTimelineEntriesBatch` use multi-row `unnest()` with manual `$N` placeholders. As of v0.13.1, `connect()` wraps `PGlite.create()` in a try/catch that emits an actionable error naming the macOS 26.3 WASM bug (#223) and pointing at `gbrain doctor`; the lock is released on failure so the next process can retry cleanly. @@ -77,7 +77,10 @@ strict behavior when unset. - `src/commands/features.ts` — `gbrain features --json --auto-fix`: usage scan + feature adoption salesman - `src/commands/autopilot.ts` — `gbrain autopilot --install`: self-maintaining brain daemon (sync+extract+embed) - `src/mcp/server.ts` — MCP stdio server (generated from operations) -- `src/commands/auth.ts` — Standalone token management (create/list/revoke/test) +- `src/commands/serve-http.ts` (v1.0.0) — Express 5 HTTP MCP server with OAuth 2.1, admin dashboard, and SSE live activity feed. Started via `gbrain serve --http [--port N] [--token-ttl N] [--enable-dcr]`. Combines MCP SDK's `mcpAuthRouter` (authorize / token / register / revoke endpoints), a custom `client_credentials` handler (SDK's token endpoint throws `UnsupportedGrantTypeError` for CC; the custom handler runs BEFORE the router and falls through for `auth_code` / `refresh_token`), `requireBearerAuth` middleware for `/mcp`, scope enforcement before op dispatch, `localOnly` rejection, and `express-rate-limit` at 50 req / 15 min on `/token`. Serves the built admin SPA from `admin/dist/` with SPA fallback. `/admin/events` SSE endpoint broadcasts every MCP request to connected admin browsers. CORS: `/mcp` + `/token` open, `/admin` same-origin. Startup logging prints port, engine, registered-client count, DCR status, issuer URL, and the admin bootstrap token. +- `src/core/oauth-provider.ts` (v1.0.0) — `GBrainOAuthProvider` implementing the MCP SDK's `OAuthServerProvider` + `OAuthRegisteredClientsStore` interfaces. Backed by raw SQL (works on both PGLite and Postgres — OAuth is infrastructure, not a BrainEngine concern). Full OAuth 2.1 spec: `authorize` + `exchangeAuthorizationCode` with PKCE (for ChatGPT), `client_credentials` (for Perplexity / Claude), `refresh_token` with rotation, `revokeToken`, `registerClient`. All tokens + client secrets SHA-256 hashed before storage (`hashToken` / `generateToken` extracted to `src/core/utils.ts`). Auth codes single-use with 10-minute TTL. Legacy `access_tokens` fallback in `verifyAccessToken` grandfathers pre-v1.0 bearer tokens as `read+write+admin`. `sweepExpiredTokens()` runs on startup wrapped in try/catch (server boots even if sweep fails). +- `admin/` (v1.0.0) — React 19 + Vite + TypeScript admin SPA embedded in the binary via `admin/dist/` served by `serve-http.ts`. 7 screens: Login (bootstrap token → session cookie), Dashboard (metrics + SSE feed + token health), Agents (sortable table + sparklines + Register button), Register (modal with scope checkboxes + grant type selector), Credentials reveal (full-screen modal with Copy + Download JSON + yellow one-time-only warning), Request Log (filterable paginated), Agent Detail drawer (Details / Activity / Config Export tabs + Revoke). Design tokens: `#0a0a0f` bg, Inter for UI, JetBrains Mono for data, 4-32px spacing scale, rounded pill badges. HTTP-only SameSite=Strict cookie auth. 65KB gzip. Build: `cd admin && bun install && bun run build`; output at `admin/dist/` is committed for self-contained binaries. +- `src/commands/auth.ts` — Standalone token management (create/list/revoke/test) — legacy bearer tokens. Not wired through `gbrain` as a subcommand; invoke via `bun run src/commands/auth.ts`. As of v1.0.0, legacy tokens grandfather to `read+write+admin` scopes on the OAuth HTTP server, so pre-v1.0 deployments keep working with no migration. - `src/commands/upgrade.ts` — Self-update CLI. `runPostUpgrade()` enumerates migrations from the TS registry (src/commands/migrations/index.ts) and tail-calls `runApplyMigrations(['--yes', '--non-interactive'])` so the mechanical side of every outstanding migration runs unconditionally. - `src/commands/migrations/` — TS migration registry (compiled into the binary; no filesystem walk of `skills/migrations/*.md` needed at runtime). `index.ts` lists migrations in semver order. `v0_11_0.ts` = Minions adoption orchestrator (8 phases). `v0_12_0.ts` = Knowledge Graph auto-wire orchestrator (5 phases: schema → config check → backfill links → backfill timeline → verify). `phaseASchema` has a 600s timeout (bumped from 60s in v0.12.1 for duplicate-heavy brains). `v0_12_2.ts` = JSONB double-encode repair orchestrator (4 phases: schema → repair-jsonb → verify → record). `v0_14_0.ts` = shell-jobs + autopilot cooperative (2 phases: schema ALTER minion_jobs.max_stalled SET DEFAULT 3 — superseded by v0.14.3's schema-level DEFAULT 5 + UPDATE backfill; pending-host-work ping for skills/migrations/v0.14.0.md). All orchestrators are idempotent and resumable from `partial` status. As of v0.14.2 (Bug 3), the RUNNER owns all ledger writes — orchestrators return `OrchestratorResult` and `apply-migrations.ts` persists a canonical `{version, status, phases}` shape after return. Orchestrators no longer call `appendCompletedMigration` directly. `statusForVersion` prefers `complete` over `partial` (never regresses). 3 consecutive partials → wedged → `--force-retry ` writes a `'retry'` reset marker. v0.14.3 (fix wave) ships schema-only migrations v14 (`pages_updated_at_index`) + v15 (`minion_jobs_max_stalled_default_5` with UPDATE backfill) via the `MIGRATIONS` array in `src/core/migrate.ts` — no orchestrator phases needed. - `src/commands/repair-jsonb.ts` — `gbrain repair-jsonb [--dry-run] [--json]`: rewrites `jsonb_typeof='string'` rows in place across 5 affected columns (pages.frontmatter, raw_data.data, ingest_log.pages_updated, files.metadata, page_versions.frontmatter). Fixes v0.12.0 double-encode bug on Postgres; PGLite no-ops. Idempotent. @@ -176,6 +179,11 @@ Key commands added in v0.14.2: - `GBRAIN_POOL_SIZE` env var — honored by both the singleton pool (`src/core/db.ts`) and the parallel-import worker pool (`src/commands/import.ts`). Default is 10; lower to 2 for Supabase transaction pooler to avoid MaxClients crashes during `gbrain upgrade` subprocess spawns. Read at call time via `resolvePoolSize()`. - `gbrain doctor` gains two new checks: `sync_failures` (surfaces unacknowledged parse failures with exact paths + fix hints) and `brain_score` (renders the 5-component breakdown when score < 100: embed coverage / 35, link density / 25, timeline coverage / 15, orphans / 15, dead links / 10 — sum equals total). +Key commands added in v1.0.0 (OAuth 2.1 + HTTP server + admin dashboard): +- `gbrain serve --http [--port 3131] [--token-ttl 3600] [--enable-dcr]` — HTTP MCP server with OAuth 2.1, admin dashboard at `/admin`, SSE activity feed at `/admin/events`, health check at `/health`. Prints admin bootstrap token on first start. Alongside (not replacing) stdio `gbrain serve`. +- **OAuth client registration** — via the `/admin` dashboard (Register client modal → credential reveal with Copy + Download JSON) or programmatically via `oauthProvider.registerClientManual(name, grantTypes, scopes, redirectUris)`. `--enable-dcr` on `serve --http` opens the `/register` endpoint for RFC 7591 self-service registration (off by default). There is no `gbrain auth register-client` CLI subcommand in v1.0.0 — registration goes through the dashboard or the SDK. +- `bun run src/commands/auth.ts create|list|revoke|test` — legacy bearer tokens still work and grandfather to `read+write+admin` scopes on the OAuth server. No migration required to keep pre-v1.0 clients working. + Key commands added in v0.14.3 (fix wave): - `gbrain doctor --index-audit` — opt-in Postgres-only check reporting zero-scan indexes from `pg_stat_user_indexes`. Informational only; never auto-drops. - `gbrain doctor` schema_version check fails loudly when `version=0` — catches `bun install -g github:...` postinstall failures (#218) and routes users to `gbrain apply-migrations --yes`. @@ -236,7 +244,8 @@ parity), `test/cli.test.ts` (CLI structure), `test/config.test.ts` (config redac `test/sync.test.ts` (sync logic + v0.12.3 regression guard asserting top-level `engine.transaction` is not called), `test/doctor.test.ts` (doctor command + v0.12.3 assertions that `jsonb_integrity` scans the four v0.12.0 write sites and `markdown_body_completeness` is present), `test/utils.test.ts` (shared SQL utilities + `tryParseEmbedding` null-return and single-warn semantics), -`test/build-llms.test.ts` (llms.txt/llms-full.txt generator: path resolution, idempotence, spec shape, regen-drift guard, content contract, AGENTS.md install-path mirror, size-budget enforcement — 7 cases). +`test/build-llms.test.ts` (llms.txt/llms-full.txt generator: path resolution, idempotence, spec shape, regen-drift guard, content contract, AGENTS.md install-path mirror, size-budget enforcement — 7 cases), +`test/oauth.test.ts` (v1.0.0 OAuth 2.1 provider — 27 cases: register, getClient, `client_credentials` grant exchange, `authorization_code` flow with PKCE challenge / verifier, refresh token rotation, `verifyAccessToken` with both OAuth + legacy `access_tokens` fallback, `revokeToken`, `sweepExpiredTokens`, and a contract test asserting `scope` + `localOnly` annotations are set correctly on all 30 operations). E2E tests (`test/e2e/`): Run against real Postgres+pgvector. Require `DATABASE_URL`. - `bun run test:e2e` runs Tier 1 (mechanical, all operations, no API keys). Includes 9 dedicated cases for the postgres-engine `addLinksBatch` / `addTimelineEntriesBatch` bind path — postgres-js's `unnest()` binding is structurally different from PGLite's and gets its own coverage. diff --git a/README.md b/README.md index dfd4b0231..574e32d00 100644 --- a/README.md +++ b/README.md @@ -77,15 +77,32 @@ GBrain exposes 30+ MCP tools via stdio: Add to `~/.claude/server.json` (Claude Code), Settings > MCP Servers (Cursor), or your client's MCP config. -### Remote MCP (Claude Desktop, Cowork, Perplexity) +### Remote MCP with OAuth 2.1 (ChatGPT, Claude Desktop, Cowork, Perplexity) + +`gbrain serve --http` starts a production-grade OAuth 2.1 server with an embedded admin dashboard. Zero external infrastructure. Every major AI client connects, every request is scoped, every action is logged. ```bash -ngrok http 8787 --url your-brain.ngrok.app -bun run src/commands/auth.ts create "claude-desktop" -claude mcp add gbrain -t http https://your-brain.ngrok.app/mcp -H "Authorization: Bearer TOKEN" +# Start the HTTP server (prints admin bootstrap token on first start) +gbrain serve --http --port 3131 + +# Open the admin dashboard, paste the bootstrap token, register a client +open http://localhost:3131/admin + +# Expose publicly (for remote clients) +ngrok http 3131 --url your-brain.ngrok.app ``` -Per-client guides: [`docs/mcp/`](docs/mcp/DEPLOY.md). ChatGPT requires OAuth 2.1 (not yet implemented). +Register OAuth clients from the `/admin` dashboard — click **Register client**, +pick scopes, save the credentials shown once in the reveal modal. Programmatic +registration via `oauthProvider.registerClientManual(...)` is also available +for host repos that wrap the server. + +- **OAuth 2.1 via the MCP SDK** — client credentials (machine-to-machine: Perplexity, Claude), authorization code + PKCE (browser-based: ChatGPT), refresh token rotation, revocation, protected resource metadata. Optional Dynamic Client Registration behind `--enable-dcr`. +- **Scoped operations** — 30 operations tagged `read | write | admin`. `sync_brain` and `file_upload` are `localOnly`, rejected over HTTP. +- **React admin dashboard** — 7 screens baked into the binary (~65KB gzip). Live SSE activity feed, agents table, credential reveal, filterable request log, per-client config export. +- **Legacy bearer tokens still work** — pre-v1.0 `gbrain auth create` tokens continue to authenticate as `read+write+admin`. + +Per-client guides: [`docs/mcp/`](docs/mcp/DEPLOY.md). ## The 26 Skills @@ -566,6 +583,11 @@ ADMIN gbrain doctor --fix [--dry-run] Auto-fix DRY violations (delegate inlined rules to conventions) gbrain stats Brain statistics gbrain serve MCP server (stdio) + gbrain serve --http [--port 3131] HTTP MCP server with OAuth 2.1 + admin dashboard + [--token-ttl 3600] [--enable-dcr] + bun run src/commands/auth.ts Legacy bearer token management (create/list/revoke/test) + # OAuth 2.1 clients: register from the /admin dashboard or via + # oauthProvider.registerClientManual() for host-repo wrappers. gbrain integrations Integration recipe dashboard gbrain check-backlinks check|fix Back-link enforcement gbrain lint [--fix] LLM artifact detection diff --git a/docs/mcp/CHATGPT.md b/docs/mcp/CHATGPT.md new file mode 100644 index 000000000..0757e46de --- /dev/null +++ b/docs/mcp/CHATGPT.md @@ -0,0 +1,103 @@ +# Connect GBrain to ChatGPT + +**Status (v1.0.0):** Unblocked. GBrain's `gbrain serve --http` ships OAuth 2.1 +with PKCE, which is the ChatGPT MCP connector's hard requirement. Before v1.0, +this was a P0 TODO — the only major AI client that could not connect. + +ChatGPT does not support bearer-token MCP servers. You must use the OAuth 2.1 +HTTP server. + +## Setup + +### 1. Start the HTTP server + +```bash +gbrain serve --http --port 3131 +``` + +Save the admin bootstrap token printed on stderr. Open +`http://localhost:3131/admin` and paste it to access the dashboard. + +### 2. Register a ChatGPT client + +ChatGPT uses the authorization code flow with PKCE (browser-based OAuth). +Register from the `/admin` dashboard: + +1. Click **Register client**. +2. Name: `chatgpt`. +3. Grant type: `authorization_code`. +4. Scopes: `read`, `write` (leave `admin` unchecked for ChatGPT). +5. Redirect URI: ChatGPT's OAuth redirect (copy it from the ChatGPT + connector setup screen — something like + `https://chat.openai.com/connector_platform_oauth_redirect`). +6. Hit **Register**. The credential-reveal modal shows the `client_id` once + with Copy and Download JSON buttons. There is no client secret for + PKCE-based public clients. + +Host-repo wrappers can register programmatically: + +```ts +await oauthProvider.registerClientManual( + 'chatgpt', + ['authorization_code'], + 'read write', + ['https://chat.openai.com/connector_platform_oauth_redirect'], +); +``` + +### 3. Expose the server publicly + +```bash +brew install ngrok +ngrok http 3131 --url your-brain.ngrok.app +``` + +Your OAuth issuer URL becomes `https://your-brain.ngrok.app`. ChatGPT's +connector auto-discovers the spec-compliant endpoint at +`/.well-known/oauth-authorization-server`. + +### 4. Add the connector in ChatGPT + +1. Open ChatGPT > Settings > Connectors. +2. Click **Add connector**. +3. MCP server URL: `https://your-brain.ngrok.app/mcp`. +4. Client ID: the `client_id` you saved in step 2. +5. Click **Connect**. ChatGPT opens the OAuth consent page, you approve, and + the connector is live. + +Start a new conversation and ask ChatGPT to search your brain. The MCP tool +calls show up in the admin dashboard's live SSE feed in real time. + +## Scopes + +ChatGPT clients can request any combination of `read`, `write`, `admin`. The +scopes granted at consent time are enforced on every tool call. Four +operations are `localOnly` and rejected over HTTP regardless of scope: +`sync_brain`, `file_upload`, `file_list`, `file_url`. The HTTP server fails +closed for any attempt to reach local filesystem surface area. + +Recommended ChatGPT scope: `read write`. Leave `admin` for your local CLI +and the admin dashboard. + +## Troubleshooting + +**"Invalid redirect_uri" during the ChatGPT connector OAuth handshake** +The registered `redirect-uri` must match ChatGPT's exactly. If ChatGPT +rejects your server, check the admin dashboard's **Agents** table for the +client, confirm the redirect URI matches what the error page shows, and +re-register with the correct URI. + +**ChatGPT shows an MCP connection error after approval** +Open `/admin`, watch the SSE feed, and try again. If no request arrives, the +connector isn't reaching your ngrok URL. If a request arrives but fails, +the Request Log tab shows the exact error. + +**"Unsupported grant_type" on the token endpoint** +ChatGPT uses `authorization_code`, which the MCP SDK supports natively. +If you see this error, verify the client was registered with +`--grant-types authorization_code` and not `client_credentials`. + +## See also + +- [DEPLOY.md](DEPLOY.md) — full OAuth 2.1 setup reference +- [ALTERNATIVES.md](ALTERNATIVES.md) — tunnel options (ngrok, Tailscale, Fly) diff --git a/docs/mcp/DEPLOY.md b/docs/mcp/DEPLOY.md index 7370b34f3..d51f41a6b 100644 --- a/docs/mcp/DEPLOY.md +++ b/docs/mcp/DEPLOY.md @@ -1,12 +1,12 @@ # Deploy GBrain Remote MCP Server -Access your brain from any device, any AI client. GBrain's MCP server runs locally -via `gbrain serve` (stdio). For remote access, wrap it in an HTTP server behind a -public tunnel. +Access your brain from any device, any AI client. GBrain ships two transports: +`gbrain serve` (stdio) for local agents, and `gbrain serve --http` (v1.0.0) for +remote clients over OAuth 2.1. -## Two Paths +## Three Paths -### Local (zero setup) +### Local stdio (zero setup) ```bash gbrain serve @@ -15,7 +15,27 @@ gbrain serve Works with Claude Code, Cursor, Windsurf, and any MCP client that supports stdio. No server, no tunnel, no token needed. -### Remote (any device, any AI client) +### Remote over OAuth 2.1 (recommended, v1.0.0+) + +```bash +gbrain serve --http --port 3131 +ngrok http 3131 --url your-brain.ngrok.app +``` + +Built-in HTTP transport with OAuth 2.1, scoped operations, an admin dashboard +at `/admin`, and a live SSE activity feed. Zero external dependencies. This is +the only path that works with ChatGPT (OAuth 2.1 + PKCE is required by the +ChatGPT MCP connector). + +Supported clients: +- **ChatGPT** — requires OAuth 2.1 + PKCE. Works natively with `--http`. +- **Claude Desktop / Cowork** — OAuth 2.1 or legacy bearer tokens. +- **Perplexity** — OAuth 2.1 client credentials grant. +- **Claude Code, Cursor, Windsurf** — can use OAuth or legacy bearer. + +See the [OAuth 2.1 setup](#oauth-21-setup-v100) section below. + +### Remote with legacy bearer tokens (pre-v1.0 deployments) ``` Your AI client (Claude Desktop, Perplexity, etc.) @@ -29,7 +49,86 @@ This requires: 2. A public tunnel (ngrok, Tailscale, or cloud host) 3. Bearer token auth for security -## Remote Setup +Pre-v1.0 tokens are grandfathered as `read+write+admin` scopes when you upgrade +to the HTTP server, so no migration is required. + +## OAuth 2.1 Setup (v1.0.0+) + +### 1. Start the HTTP server + +```bash +gbrain serve --http --port 3131 +``` + +On first start, the server prints an **admin bootstrap token** to stderr: + +``` +Admin bootstrap token: 3a1f9c... +Open http://localhost:3131/admin and paste it to log in. +``` + +Save this token. Open `http://localhost:3131/admin` and paste it to access the +dashboard. The dashboard shows live activity, registered clients, request logs, +and per-client config export. + +### 2. Register OAuth clients + +Register clients from the **`/admin` dashboard**: + +1. Click **Register client**. +2. Enter a name (e.g. `perplexity`, `chatgpt`). +3. Pick scopes: `read`, `write`, `admin` (checkboxes). +4. Pick grant type: `client_credentials` for machine-to-machine (Perplexity, + Claude Desktop bearer mode) or `authorization_code` for browser-based + clients with PKCE (ChatGPT). +5. For `authorization_code` clients, paste the redirect URI. +6. Hit **Register**. The credential-reveal modal shows the `client_id` (and + `client_secret` for confidential clients) once. Copy or Download JSON + immediately — secrets are hashed on storage and never shown again. + +Host-repo wrappers can register programmatically: + +```ts +await oauthProvider.registerClientManual( + 'perplexity', + ['client_credentials'], + 'read write', + [], // redirect_uris, empty for CC +); +``` + +For self-service client registration (Dynamic Client Registration, RFC 7591), +start the server with `--enable-dcr`. DCR is off by default. + +### 3. Expose the server + +```bash +brew install ngrok +ngrok config add-authtoken YOUR_TOKEN +ngrok http 3131 --url your-brain.ngrok.app +``` + +Your OAuth issuer URL becomes `https://your-brain.ngrok.app`. The MCP SDK's +router exposes the spec-compliant discovery endpoint at +`/.well-known/oauth-authorization-server`. + +### 4. Scopes and localOnly + +Every operation is tagged `read | write | admin`. Four operations are +`localOnly` and rejected over HTTP regardless of scope: `sync_brain`, +`file_upload`, `file_list`, `file_url`. Remote agents cannot reach local +filesystem surface area. + +| Scope | What it allows | +|-------|---------------| +| `read` | `search`, `query`, `get_page`, `list_pages`, graph traversal | +| `write` | `put_page`, `delete_page`, `add_link`, `add_timeline_entry` | +| `admin` | Client management, token revocation, sweep, local-only ops | + +## Legacy Bearer Token Setup + +Keep using pre-v1.0 bearer tokens if you aren't ready to migrate. They +grandfather to `read+write+admin` scopes on the HTTP server. ### 1. Set up the tunnel @@ -60,6 +159,7 @@ if compromised. Tokens are stored SHA-256 hashed in your database. ### 3. Connect your AI client +- **ChatGPT:** [setup guide](CHATGPT.md) (OAuth 2.1 + PKCE, requires `gbrain serve --http`) - **Claude Code:** [setup guide](CLAUDE_CODE.md) - **Claude Desktop:** [setup guide](CLAUDE_DESKTOP.md) (must use GUI, not JSON config) - **Claude Cowork:** [setup guide](CLAUDE_COWORK.md) @@ -116,7 +216,8 @@ Remote servers must be added via Settings > Integrations, NOT | put_page | 100-500ms | Write + trigger search_vector update | | get_stats | < 100ms | Aggregate query | -**Note:** `gbrain serve --http` (built-in HTTP transport) is planned but not yet -implemented. Currently, remote MCP requires a custom HTTP wrapper. See the -production deployment pattern in the [voice recipe](../../recipes/twilio-voice-brain.md) -for a reference implementation. +**Note:** `gbrain serve --http` shipped in v1.0.0 with OAuth 2.1 + admin +dashboard baked into the binary. The custom HTTP wrapper pattern (see +[voice recipe](../../recipes/twilio-voice-brain.md)) is still supported for +teams that need bespoke middleware, but for most remote deployments the +built-in server is the recommended path.