mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
VERSION + package.json + CHANGELOG bump for the six-PR MCP fix wave. Schema chain extends from v54 → v60; oauth_clients gains source_id + federated_read columns; auth'd MCP clients now stay inside their scope across all read-side ops; PKCE-only DCR works; --bind defaults to loopback; LiteLLM multimodal embedding ships. Contributed by @Hansen1018 (#870), @ding-modding (#909), @DukeDawg (#864), @toilalesondev (#861 + #876), @yoelgal (#875). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
fed57f4db0
commit
6b47db7e72
+239
@@ -2,6 +2,245 @@
|
||||
|
||||
All notable changes to GBrain will be documented in this file.
|
||||
|
||||
## [0.34.0.0] - 2026-05-14
|
||||
|
||||
**MCP hardening wave: stricter source isolation on the read path, PKCE
|
||||
DCR works, loopback-by-default, and federated read scopes for shared
|
||||
brains. Six community PRs land as one release.**
|
||||
|
||||
The v0.34.0 wave consolidates six community PRs into a single ship.
|
||||
Source-isolation tightening is the centerpiece — an authenticated OAuth
|
||||
client scoped to one source no longer sees rows from neighboring sources
|
||||
through the read path. The wave also seals smaller papercuts that have
|
||||
been costing real users: gateway-managed stdio MCP servers no longer
|
||||
exit on the post-handshake EOF, PKCE-only DCR clients can register
|
||||
without inheriting a phantom secret, and `gbrain serve --http` defaults
|
||||
to loopback so a personal-laptop brain isn't one accidental config away
|
||||
from publishing itself to the LAN. Federated_read adds the read-scope
|
||||
axis that shared-brain deployments need: a department client can write
|
||||
to one source and read across a curated set without becoming a
|
||||
super-reader.
|
||||
|
||||
### What you can now do
|
||||
|
||||
**Scope OAuth clients to a single source.** `gbrain auth register-client
|
||||
my-agent --source dept-x` registers a client whose write authority is
|
||||
`dept-x`. Read paths only return rows whose `source_id` matches. The
|
||||
auth-layer thread is on `verifyAccessToken` so every per-request
|
||||
dispatch starts with the scope in `AuthInfo.sourceId`; ops consume it
|
||||
through the canonical `ctx.sourceId` pattern that v0.31.8 established.
|
||||
Pre-v0.34 clients without an explicit source default to `default` on
|
||||
upgrade — the v0.33 effective behavior is preserved verbatim.
|
||||
|
||||
**Federate read scope independently.** `gbrain auth register-client
|
||||
my-l3-dept --source dept-x --federated-read dept-x,wecare,shared`
|
||||
registers a client that writes to `dept-x` but reads from the union of
|
||||
`dept-x`, `wecare` parent canon, and `shared` org canon. The two scope
|
||||
axes are orthogonal — `source_id` is the write authority, `federated_read`
|
||||
is the read authority. Engine read paths apply
|
||||
`WHERE source_id = ANY($1::text[])` at SQL when the array is set; scalar
|
||||
single-source clients keep the v0.31.12 fast path.
|
||||
|
||||
**Run gateway-piped stdio MCP without race-killing your server.** Set
|
||||
`MCP_STDIO=1` in the env and the server skips the stdin EOF shutdown
|
||||
hooks. Signal handlers (SIGTERM / SIGINT / SIGHUP) and parent-process
|
||||
watchdog still cover legitimate disconnects. OpenClaw's bundle-mcp
|
||||
gateway and similar wrappers pipe the handshake then close their stdin
|
||||
half; pre-fix this killed the server before the first tool call landed.
|
||||
|
||||
**Register PKCE-only public clients.** `POST /register` with
|
||||
`token_endpoint_auth_method: "none"` (Claude Code, Cursor, every other
|
||||
PKCE-first MCP client) now returns RFC 7591-compliant response shape:
|
||||
no `client_secret` field on public clients, secret-bearing on default
|
||||
`client_secret_post` clients. The `/token` exchange accepts the public
|
||||
client through the SDK's clientAuth path because `getClient` correctly
|
||||
normalizes a NULL `client_secret_hash` to JS undefined.
|
||||
|
||||
**Bind the HTTP MCP server to loopback by default.** `gbrain serve
|
||||
--http` now listens on `127.0.0.1` unless you pass `--bind 0.0.0.0` (or
|
||||
a specific interface IP). Personal-laptop installs are no longer one
|
||||
default-config away from publishing the brain to a LAN. Self-hosted
|
||||
server operators who actually want remote access pass `--bind 0.0.0.0`
|
||||
once; a stderr WARN fires when `--public-url` is set without `--bind` so
|
||||
the operator doesn't silently bind loopback at startup.
|
||||
|
||||
**Embed images through LiteLLM / openai-compatible multimodal models.**
|
||||
The gateway's `embedMultimodal` no longer hardcodes Voyage; recipes with
|
||||
`implementation: 'openai-compatible'` route through the standard
|
||||
`/embeddings` endpoint with content arrays carrying `image_url` entries.
|
||||
Runtime dimension validation throws a clear error pre-storage if the
|
||||
provider returns a vector that doesn't match the brain's embedding
|
||||
column width — no more cryptic `vector dimension mismatch` at INSERT
|
||||
time.
|
||||
|
||||
### The migration block
|
||||
|
||||
`gbrain upgrade` applies migrations v55-v60 automatically. The migration
|
||||
chain adds two columns to `oauth_clients` and an index, plus a FK flip
|
||||
once federated_read is in place.
|
||||
|
||||
### To take advantage of v0.34.0
|
||||
|
||||
`gbrain upgrade` should do this automatically. If it didn't, or if
|
||||
`gbrain doctor` warns about a partial migration:
|
||||
|
||||
1. **Run the orchestrator manually:**
|
||||
```bash
|
||||
gbrain apply-migrations --yes
|
||||
```
|
||||
2. **Verify the schema landed:**
|
||||
```bash
|
||||
gbrain doctor --json | jq '.checks.schema_version'
|
||||
# version should be >= 60
|
||||
```
|
||||
3. **Existing OAuth clients keep working.** Pre-v0.34 clients without an
|
||||
explicit source scope are backfilled to `source_id='default'` so
|
||||
their effective scope matches v0.33. To narrow a specific client's
|
||||
scope, re-register it with `--source <id>`:
|
||||
```bash
|
||||
gbrain auth revoke-client <client_id>
|
||||
gbrain auth register-client <name> --source <source_id> --scopes read,write
|
||||
```
|
||||
4. **`gbrain serve --http` default changed.** Existing self-hosted
|
||||
server deployments must add `--bind 0.0.0.0` to keep accepting remote
|
||||
connections. Personal-laptop users see no behavior change (loopback
|
||||
is now the default).
|
||||
5. **If `gbrain apply-migrations` refuses with an `oauth_clients` stale
|
||||
source_id error** (possible only if an operator hand-poked the column
|
||||
before upgrading): the error message names the offending client IDs.
|
||||
Either revoke + re-register them with a valid source, or re-run with
|
||||
`GBRAIN_ACCEPT_SILENT_WIDEN=1` to NULL the stale values (widens those
|
||||
clients to super-reader; re-scope via `gbrain auth register-client`
|
||||
after).
|
||||
6. **If any step fails or the numbers look wrong,** file an issue:
|
||||
https://github.com/garrytan/gbrain/issues with `gbrain doctor --json`
|
||||
output and the failing step.
|
||||
|
||||
### Itemized changes
|
||||
|
||||
**Source-isolation hardening:**
|
||||
- `OperationContext.auth` now carries `AuthInfo.sourceId` (write scope)
|
||||
and `AuthInfo.allowedSources` (federated read scope), threaded from
|
||||
`oauth_clients` rows at token-verification time.
|
||||
- New helper `sourceScopeOpts(ctx)` in `src/core/operations.ts` encodes
|
||||
the precedence ladder: federated array wins over scalar over nothing.
|
||||
Every read-side op handler routes through it so future ops can't
|
||||
silently drift from the canonical thread.
|
||||
- `src/core/search/hybrid.ts` inner `SearchOpts` rebuild now includes
|
||||
`sourceId` + `sourceIds` fields — the structural fix that prevents
|
||||
the explicit-pick footgun that motivated the wave.
|
||||
- `src/core/types.ts` adds `sourceIds?: string[]` to `SearchOpts` and
|
||||
`PageFilters` for the federated read axis. Both Postgres and PGLite
|
||||
engines apply `WHERE source_id = ANY($N::text[])` when the array is
|
||||
set; scalar fast path preserved when unset.
|
||||
- Engine method signatures `traverseGraph(slug, depth, opts?)` and
|
||||
`traversePaths(slug, opts?)` accept `opts.sourceId` /
|
||||
`opts.sourceIds` so graph walks respect the caller's scope.
|
||||
- `src/core/oauth-provider.ts:verifyAccessToken` JOINs
|
||||
`oauth_clients.source_id` + `federated_read` and surfaces both on the
|
||||
returned `AuthInfo`. Pre-v55 / pre-v56 brains degrade gracefully via
|
||||
`isUndefinedColumnError` fallback.
|
||||
- `src/commands/serve-http.ts` drops the `(authInfo as AuthInfo &
|
||||
{sourceId?: string}).sourceId ?? env ?? 'default'` cast chain. The
|
||||
typed field is the source of truth now.
|
||||
- Legacy `src/mcp/http-transport.ts` (v0.22.7-style access_tokens path)
|
||||
threads `sourceId: 'default'` through DispatchOpts so legacy tokens
|
||||
stay source-scoped.
|
||||
|
||||
**Migration chain v55-v60 (six new migrations on top of v54):**
|
||||
- v55 (`oauth_clients_source_id_fk`) — ALTER TABLE ... ADD COLUMN
|
||||
source_id TEXT, backfill NULL→'default', install FK with
|
||||
ON DELETE SET NULL.
|
||||
- v56 (`oauth_clients_federated_read_column`) — ALTER TABLE ... ADD
|
||||
COLUMN federated_read TEXT[] NOT NULL DEFAULT '{}'.
|
||||
- v57 (`oauth_clients_federated_read_backfill`) — explicit CASE backfill
|
||||
so `source_id IS NULL` produces `'{}'` not an array-containing-NULL.
|
||||
- v58 (`oauth_clients_federated_read_validate`) — fail-loud check that
|
||||
every row's source_id is in its federated_read array post-backfill.
|
||||
- v59 (`oauth_clients_source_id_fk_restrict`) — flip FK to
|
||||
ON DELETE RESTRICT now that federated_read provides the alternative
|
||||
scope-loss path. Source delete is refused if any client references it.
|
||||
- v60 (`oauth_clients_federated_read_gin_index`) — GIN index for the
|
||||
array-containment queries the read paths run.
|
||||
|
||||
**OAuth + auth surface:**
|
||||
- `auth.ts` CLI adds `--source <id>` and `--federated-read <SRC1,SRC2,...>`
|
||||
flags to `register-client`. The output now prints the resolved
|
||||
`Write source` and `Federated reads` for the registered client.
|
||||
- DCR `/register` endpoint now writes `source_id='default'` and
|
||||
`federated_read=['default']` on the inserted row so new public clients
|
||||
start in a sane scope.
|
||||
- `registerClient` honors `token_endpoint_auth_method: "none"` (RFC 7591
|
||||
§3.2.1): public clients store `client_secret_hash = NULL` and the
|
||||
response payload omits `client_secret` entirely. Confidential clients
|
||||
(default `client_secret_post` and explicit `client_secret_basic`) keep
|
||||
their one-time-reveal shape.
|
||||
|
||||
**MCP transports:**
|
||||
- `src/mcp/server.ts` + `src/commands/serve.ts` skip stdin
|
||||
`'end'`/`'close'` shutdown hooks when `process.env.MCP_STDIO === '1'`.
|
||||
`ServeOptions` gains a `mcpStdio?: boolean` test seam so the runtime
|
||||
guard is exercisable without process.env mutation.
|
||||
- `src/commands/serve-http.ts` adds `--bind HOST` (default `127.0.0.1`).
|
||||
Stderr WARN fires when `--public-url` is set without `--bind`.
|
||||
Startup banner prints the resolved `Bind:` line.
|
||||
|
||||
**Multimodal embedding:**
|
||||
- `gateway.ts` adds `embedMultimodalOpenAICompat()` that POSTs to the
|
||||
standard `/embeddings` endpoint with content arrays. Routes by
|
||||
`recipe.implementation === 'openai-compatible'` so LiteLLM-fronted
|
||||
providers (Anyscale, vLLM, Gemini multimodal via proxy) work alongside
|
||||
Voyage's existing `/multimodalembeddings` path.
|
||||
- `recipes/litellm-proxy.ts` declares `supports_multimodal: true` so the
|
||||
recipe accepts multimodal calls without a model allow-list (LiteLLM is
|
||||
a passthrough; user-owned model id selection).
|
||||
- Runtime dimension validation: the returned vector length is checked
|
||||
against the recipe's declared `default_dims` or the brain's
|
||||
`embedding_dimensions` config. Mismatch throws `AIConfigError` with
|
||||
model id + observed + expected before the vector reaches storage.
|
||||
|
||||
**Tests:**
|
||||
- `test/e2e/source-isolation-pglite.test.ts` — 14 cases pinning the
|
||||
scope filter at the engine layer plus op-handler threading for both
|
||||
`ctx.sourceId` and `ctx.auth.allowedSources` paths.
|
||||
- `test/openai-compat-multimodal.test.ts` — 11 cases covering the
|
||||
openai-compatible multimodal path: happy-path single + multi-input,
|
||||
unauthenticated proxy, D12 dim mismatch + default-dim fallback,
|
||||
401 / 400 / malformed-JSON / non-array error paths, and a Voyage
|
||||
regression test.
|
||||
- `test/oauth.test.ts` — 5 new cases for PKCE DCR public-client gate
|
||||
(no secret for public; secret unchanged for default; getClient
|
||||
NULL→undefined normalization; full PKCE `/authorize` → `/token`
|
||||
round-trip).
|
||||
- `test/serve-stdio-lifecycle.test.ts` — 3 new cases for the
|
||||
`MCP_STDIO=1` guard (stdin EOF does NOT trigger shutdown; SIGTERM
|
||||
still does; unset env preserves CLI behavior).
|
||||
- `test/book-mirror.test.ts` — `runCli` helper now uses a fresh
|
||||
`GBRAIN_HOME` tempdir so the test isn't sensitive to the developer's
|
||||
local `~/.gbrain/config.json`. Pre-fix this could silently inherit a
|
||||
real Postgres connection and hang past the default 5s test timeout.
|
||||
|
||||
**Test results: 6045 unit tests pass / 0 fail. typecheck clean. PGLite
|
||||
initSchema runs the v55-v60 chain in ~786ms total.**
|
||||
|
||||
### For contributors
|
||||
|
||||
- The wave bundled six community PRs (#870, #909, #864, #861, #875,
|
||||
#876) with cross-model plan review (codex outside-voice) before
|
||||
cherry-pick. Codex caught three structural bugs the original PRs
|
||||
missed: a 6th source-isolation leak surface (the `query` image path
|
||||
in `operations.ts:1071-1082`), a 5th read-side op missing from #861's
|
||||
thread (`find_experts`), and migration-numbering collisions on
|
||||
v47-v53 that would have wedged on cherry-pick (the branch already had
|
||||
v55-v54). The wave's migration chain renumbers to v55-v60.
|
||||
- The single PR carries `Co-Authored-By:` for the six external
|
||||
contributors. PRs originated against earlier base branches and the
|
||||
diffs were re-implemented on the collector branch rather than merged
|
||||
directly (per CLAUDE.md's PR wave process).
|
||||
|
||||
Contributed by @Hansen1018 (#870), @ding-modding (#909), @DukeDawg
|
||||
(#864), @toilalesondev (#861 + #876), @yoelgal (#875).
|
||||
|
||||
## [0.33.1.0] - 2026-05-10
|
||||
|
||||
**Ask gbrain who in your network knows about a topic, and get a ranked answer with the reasoning shown.**
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gbrain",
|
||||
"version": "0.33.1.0",
|
||||
"version": "0.34.0.0",
|
||||
"description": "Postgres-native personal knowledge brain with hybrid RAG search",
|
||||
"type": "module",
|
||||
"main": "src/core/index.ts",
|
||||
|
||||
Reference in New Issue
Block a user