* chore(guards): allow the public Hermes platform name in tests The banned entry targeted conflating the public NousResearch agent with private deployment names. gbrain now documents and tests against the public platform (README hero, claw-test runner, install door e2e), so the public name is legal in tests; private fork names remain banned. Drops the three now-inert allowlist entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(claw-test): hermes runner, live staging + success oracles, friction diff HermesRunner (hermes -z one-shot, HERMES_BIN > which hermes, allowlist env with HERMES_HOME + OPENROUTER_API_KEY delta). Live mode now stages the scenario before the agent turn (fresh-install: brain + routing stub + init; upgrade: seed-first) and verifies outcomes after it: doctor must parse and report healthy/warnings, scenario-declared query + files_exist oracles are enforced for every kind, and upgrades use a non-mutating schema-version probe that must reach LATEST_VERSION. Missing upgrade seed dumps fail loudly in BOTH modes (a silent skip false-greened the upgrade lane). Bare gbrain in live runs resolves through a per-run PATH shim; when gbrain itself runs under the bun runtime the harness synthesizes a launcher back into cli.ts instead of handing children the bun binary. gbrain friction diff --base/--compare: identity is (kind, phase, digit-collapsed 80-char prefix); severity compares as a per-severity distribution (integer proportion test) so redistribution and delight-to-friction flips always surface; run start/end phase markers carry agent + scenario for agent-name resolution. Hardening from the adversarial gate: every harness child runs under a wall-clock timeout with process-group kill + exit-fallback settle; scenario names and declared brief/brain/seed paths are confined to the scenario dir; child friction merges require a regular file, cap size, and keep only valid JSONL lines; crashed runs stamp a non-zero end marker; GBRAIN_* routing vars are scrubbed from child env; agent stdin closes at spawn; argv agent/scenario values are charset-guarded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(hermes): real-binary install door e2e + generic workspace compat Door e2e registers this checkout's gbrain into a hermetic Hermes home via the real CLI (single --env flag with multiple values, piped confirm, enabled:true + mcp test as the success discriminators), the direct-YAML surface, and a paid one-shot smoke turn proving MCP recall of a seeded synthetic fact with a NO-GBRAIN-TOOL negative control. Triple-gated (opt-in env + resolvable binary + non-empty anthropic key) so it can never burn tokens by accident; anthropic-only auth because a second visible provider key mis-routes hermes provider auto-detection. Helpers copy exactly ONE provider key from the operator's env file, never the whole file, and scrub all provider keys from child env. workspace-generic-compat pins the documented any-repo-with-a-workspace install flow (detection tier, scaffold additivity, resolver health) on a generic fixture; the Hermes-behavior proof lives in the door test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: label-gated hermes-door job + e2e hermeticity scrub hermes-door provisions a pinned Hermes release: installer digest verified before execution, payload tag+commit flags ASSERTED post- install via rev-parse (an installer that ignores unknown flags can never run unpinned upstream code next to secrets), secretless install step, loud-fail preconditions, zero-pass-refuses-green, evidence scrubbed three ways before upload, and unconditional credential cleanup for self-hosted-runner safety. real-agent-e2e gains the door file + opt-in env. run-e2e.sh scrubs HERMES_* alongside OPENCLAW_*; e2e-test-map narrows claw-test core changes to their e2e suite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: hermes + openclaw MCP guides, CLI pin notes, harness reference updates Per-client docs for Hermes (observed-behavior guide incl. flag-order and multi-key gotchas) and OpenClaw; HERMES-CLI-PIN records every pinned CLI behavior + the CI pin posture. README MCP table rows, INSTALL_FOR_AGENTS hermes block, TESTING/KEY_FILES current-state rewrites (two runners, oracle semantics, diff identity), TODOS closure (hermes runner done, friction diff shipped, follow-ups filed) and the llms bundle regenerated in the same commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump version and changelog (v0.45.10.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: release sync — verb-count drift, hermes link, discovery rows, harness knobs Cross-referencing the diff against every .md surfaced drift beyond this wave: the memory-verbs surface prose still said five verbs (the frozen protocol grew context_pack + delta additively), docs/INSTALL.md linked a wrong Hermes repo and missed the new HERMES/OPENCLAW per-client guides, the door-suite doc pinned a tool COUNT that tracks the op catalog, the friction protocol skill missed the diff subcommand, and the claw-test KEY_FILES entry lacked the harness env knobs. Comment counts in heavy-tests.yml corrected (three triggers; four door tests). llms bundle regenerated in the same commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: quote inner expansion in evidence-scrub path strip (shellcheck SC2295) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: re-bump to v0.45.12.0 (user-pinned past the contested 0.45.11.0 slot) Two sibling PRs already claim 0.45.11.0; pinning one slot higher avoids a second merge-race re-bump. All version locations move together: VERSION, package.json, CHANGELOG entry header, openclaw.plugin.json, bootstrap runbook stamp, regenerated template stamp, CLAUDE.md example cell, llms bundle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
14 KiB
Deploy GBrain Remote MCP Server
gbrain serve --httpships full OAuth 2.1 (client credentials, auth code + PKCE, refresh rotation, optional DCR), an embedded React admin dashboard at/admin, scoped operations, and a live SSE activity feed. Legacy bearer tokens still work —verifyAccessTokenfalls back to theaccess_tokenstable and grandfathers tokens toread+write+admin. Both the legacy fallback and the OAuth tables work on PGLite and Postgres (both engine schemas carryaccess_tokens). See SECURITY.md for env vars and tunable defaults.
Access your brain from any device, any AI client. GBrain ships two transports:
gbrain serve (stdio) for local agents, and gbrain serve --http for remote
clients over OAuth 2.1.
Three Paths
Local stdio (zero setup)
gbrain serve # full operation catalog (default)
gbrain serve --surface verbs # just the 7 memory verbs (quickstart surface)
Works with Claude Code, Cursor, Windsurf, and any MCP client that supports stdio.
No server, no tunnel, no token needed. Works on both PGLite and Postgres engines.
--surface verbs exposes exactly the seven-verb memory protocol (recall,
remember, entity, synthesize, forget, context_pack, delta —
MEMORY_VERBS v1) instead of the full catalog;
omit the flag (default full) for every operation.
Remote over OAuth 2.1 (recommended)
gbrain serve --http --port 3131
ngrok http 3131 --url your-brain.ngrok.app
gbrain serve --http --port 3131 --public-url https://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). Pass --public-url whenever the server is reachable
at anything other than http://localhost:<port> so the OAuth issuer in
discovery metadata matches what clients hit (RFC 8414 §3.3).
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 section below.
Remote with legacy bearer tokens (simplest)
Your AI client (Claude Desktop, Perplexity, etc.)
→ ngrok tunnel (https://YOUR-DOMAIN.ngrok.app)
→ gbrain serve --http (built-in transport with bearer auth)
→ Postgres or PGLite
This requires:
- A machine running
gbrain serve --http(works on both PGLite and Postgres brains) - A public tunnel (ngrok, Tailscale, or cloud host)
- A bearer token created via
gbrain auth create <name>
Existing bearer tokens are grandfathered as read+write+admin scopes on the
OAuth-capable HTTP server, so no migration is required.
OAuth 2.1 Setup
1. Start the HTTP server
gbrain serve --http --port 3131
On first start in an interactive terminal, the server prints an admin bootstrap token to stderr:
Admin bootstrap token: 3a1f9c...
Open http://localhost:3131/admin and paste it to log in.
On a non-TTY start (systemd, Docker, any piped or captured logs) the generated
token is hidden so it never lands in log storage. For headless deploys either
set GBRAIN_ADMIN_BOOTSTRAP_TOKEN to a value you control before starting, or
run gbrain serve --http --print-admin-token once on a trusted terminal to
force printing.
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.
mcp_request_log.paramsand the live SSE activity feed default to a redacted summary{redacted, kind, declared_keys, unknown_key_count, approx_bytes}. Declared param keys are kept (intersected against the operation's spec); unknown keys are counted but never named, and byte sizes round up to 1KB so size-probe attacks can't binary-search secret content. Operators on a personal laptop who want raw payloads back can passgbrain serve --http --log-full-params(loud stderr warning fires at startup). Multi-tenant deployments should leave it on the redacted default.
2. Register OAuth clients
Register clients from the /admin dashboard:
- Click Register client.
- Enter a name (e.g.
perplexity,chatgpt). - Pick scopes:
read,write,admin(checkboxes). - Pick grant type:
client_credentialsfor machine-to-machine (Perplexity, Claude Desktop bearer mode) orauthorization_codefor browser-based clients with PKCE (ChatGPT). - For
authorization_codeclients, paste the redirect URI. - Hit Register. The credential-reveal modal shows the
client_id(andclient_secretfor confidential clients) once. Copy or Download JSON immediately — secrets are hashed on storage and never shown again.
Or from the CLI — faster for scripting:
gbrain auth register-client perplexity \
--grant-types client_credentials \
--scopes "read write"
Source-scoped clients. Multi-source brains can scope a client's write
authority to one source and its read scope to a curated set with the
--source and --federated-read flags:
gbrain auth register-client dept-x-agent \
--grant-types client_credentials \
--scopes "read write" \
--source dept-x \
--federated-read dept-x,shared,parent-canon
--source controls the write authority — put_page / add_link / etc only
land in dept-x. --federated-read controls the read axis independently;
queries return rows from any of the listed sources. Omit both flags for an
unscoped super-client. Clients registered before source scoping existed are
backfilled to source_id='default' on gbrain upgrade. Within a source,
slug-level write fencing is also available: --bound-slug-prefixes p1/,p2/
rejects slug-mutating writes outside the listed prefixes (update later with
gbrain auth rescope-client <id> --bound-slug-prefixes <p1,p2|none>).
Host-repo wrappers can register programmatically:
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.
DCR requests may include an optional token_ttl_seconds field (integer,
seconds) to request a per-client access-token lifetime. The server clamps the
request into an admin-configured window — never rejects over it — persists the
effective value as the client's TTL override, and echoes it back as
token_ttl_seconds in the registration response. Subsequent /token responses
for that client carry the matching expires_in. Clients that omit the field
keep the server default (--token-ttl). The window defaults fail-closed: min
300 seconds, max bounded by your --token-ttl — a self-registering client
cannot request a longer-lived token than the server default unless you
explicitly widen the window:
gbrain config set oauth.dcr_ttl_min_seconds 600
gbrain config set oauth.dcr_ttl_max_seconds 86400
3. Expose the server
Bind explicitly. gbrain serve --http defaults to 127.0.0.1.
To accept connections from the ngrok tunnel (or any non-loopback source),
restart with --bind:
gbrain serve --http --port 3131 --bind 0.0.0.0 --public-url https://your-brain.ngrok.app
When --public-url is set without --bind, a stderr WARN fires at
startup so the misconfiguration ("the tunnel is up but my agent gets
ECONNREFUSED") is loud.
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. Operations flagged
localOnly: true in src/core/operations.ts (10 today — sync_brain and
the file_* ops among them) are rejected over HTTP regardless of scope.
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 |
Write ops can additionally be fenced per client with --bound-slug-prefixes
(see Register OAuth clients above).
Legacy Bearer Token Setup
Bearer tokens are the simple path when you don't need per-client scoping.
They grandfather to read+write+admin scopes on the HTTP server.
1. Set up the tunnel
See the ngrok-tunnel recipe for full setup. Quick version:
brew install ngrok
ngrok config add-authtoken YOUR_TOKEN
ngrok http 8787 --url your-brain.ngrok.app # Hobby tier for fixed domain
2. Create access tokens
# Create a token for each client
gbrain auth create "claude-desktop"
# List all tokens
gbrain auth list
# Revoke a token
gbrain auth revoke "claude-desktop"
Tokens are per-client. Create one for each device/app. Revoke individually if compromised. Tokens are stored SHA-256 hashed in your database.
3. Connect your AI client
- ChatGPT: setup guide (OAuth 2.1 + PKCE, requires
gbrain serve --http) - Claude Code: setup guide
- Claude Desktop: setup guide (must use GUI, not JSON config)
- Claude Cowork: setup guide
- Perplexity: setup guide
4. Verify
gbrain auth test \
https://YOUR-DOMAIN.ngrok.app/mcp \
--token YOUR_TOKEN
Operations
GBrain's full operation catalog (100+ operations in src/core/operations.ts)
is available remotely, with no timeout limits on a self-hosted server. The
only exceptions are the operations flagged localOnly: true — sync_brain
and the file_* ops among them — which are rejected over HTTP regardless of
scope (see Scopes and localOnly above).
Security note on file access: the file_* operations being localOnly is
the first line of defense; as defense-in-depth, file_upload also confines
any caller that isn't verifiably the trusted local CLI to the working
directory where gbrain serve was launched. Symlinks, .. traversal, and
absolute paths outside cwd are rejected, and page slugs and filenames are
allowlist-validated (alphanumeric + hyphens; no control chars, RTL overrides,
or backslashes). Local CLI callers (gbrain files upload ...) keep
unrestricted filesystem access since the user owns the machine.
Deployment Options
See ALTERNATIVES.md for a comparison of ngrok, Tailscale Funnel, and cloud hosts (Fly.io, Railway).
Co-located Docker workloads (self-hosted Postgres)
OAuth scopes and source scoping guard the gbrain serve --http path. They do
NOT guard raw Postgres. If the brain's Postgres runs as a container on the same
Docker host as other workloads (agent runtimes, n8n, staging fixtures), any
container sharing Docker's default bridge network can open a direct DB
session — no OAuth token required — and read every source. That silently
recreates a privileged path underneath the isolation you configured at the MCP
layer.
Network-zone the host so untrusted containers can never reach Postgres:
Docker host
├── gbrain-net ← ONLY the brain's Postgres (+ gbrain serve, if containerized)
├── agent-<id>-net ← each untrusted agent runtime, isolated
└── default bridge ← no secret-bearing databases
Operator checklist:
[ ] Postgres is on a user-defined Docker network, not the default bridge
(or nothing else runs on that bridge)
[ ] If Postgres publishes a host port at all, it binds loopback only
(`-p 127.0.0.1:5432:5432`, never `0.0.0.0`)
[ ] Untrusted agent containers have no DATABASE_URL or Postgres password
[ ] Untrusted agents reach the brain via OAuth/Bearer against serve --http only
(host loopback via host.docker.internal / host gateway — never gbrain-net)
[ ] OAuth clients are least-privilege: scoped --source / --federated-read,
pre-minted short-lived tokens preferred over long-lived client secrets
[ ] Isolation verified: a team-scoped client cannot read internal-only sources
Optional defense-in-depth: a dedicated Postgres role (or RLS) limited to the
allowed source_ids, so even a leaked connection string can't read everything.
Troubleshooting
"missing_auth" error
Include the Authorization header: Authorization: Bearer YOUR_TOKEN
"invalid_token" error
Run gbrain auth list to see active tokens.
"service_unavailable" error Database connection failed. Check your Supabase dashboard for outages.
Claude Desktop doesn't connect
Remote servers must be added via Settings > Integrations, NOT
claude_desktop_config.json. See CLAUDE_DESKTOP.md.
Expected Latencies
| Operation | Typical Latency | Notes |
|---|---|---|
| get_page | < 100ms | Single DB query |
| list_pages | < 200ms | DB query with filters |
| search (keyword) | 100-300ms | Full-text search |
| query (hybrid) | 1-3s | Embedding + vector + keyword + RRF |
| put_page | 100-500ms | Write + trigger search_vector update |
| get_stats | < 100ms | Aggregate query |
Note: gbrain serve --http has OAuth 2.1 + the admin dashboard baked
into the binary. The custom HTTP wrapper pattern (see
voice recipe) is still supported for
teams that need bespoke middleware, but for most remote deployments the
built-in server is the recommended path.