Files
Garry TanandClaude Fable 5 7fdcd8bd2e v0.45.12.0 feat(claw-test): hermes e2e install harness — runner, real-binary door, CI job, friction diff (#4058)
* 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>
2026-08-13 18:22:08 -07:00

14 KiB

Deploy GBrain Remote MCP Server

gbrain serve --http ships 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 — verifyAccessToken falls back to the access_tokens table and grandfathers tokens to read+write+admin. Both the legacy fallback and the OAuth tables work on PGLite and Postgres (both engine schemas carry access_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, deltaMEMORY_VERBS v1) instead of the full catalog; omit the flag (default full) for every operation.

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:

  1. A machine running gbrain serve --http (works on both PGLite and Postgres brains)
  2. A public tunnel (ngrok, Tailscale, or cloud host)
  3. 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.params and 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 pass gbrain 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:

  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.

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

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: truesync_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.