Summary
The #1336 legacy-token source grant (access_tokens.permissions.source_id) is honored by src/mcp/http-transport.ts (parseLegacyTokenScope), but not by the OAuth transport that gbrain serve --http actually uses. GBrainOAuthProvider's legacy access_tokens fallback branch (src/core/oauth-provider.ts, ~line 635) only SELECT name, never reads permissions, and hardcodes sourceId: 'default' with no allowedSources.
Effect
On a multi-source brain, every remote caller authenticating with a legacy bearer token through serve --http is silently pinned to the default source:
This contradicts the CLAUDE.md invariant: "Source isolation. Every read-side op routes through sourceScopeOpts(ctx); precedence is federated array (ctx.auth.allowedSources) > scalar (ctx.sourceId) > nothing." — the OAuth transport never populates the federated array for legacy tokens, while http-transport.ts does.
Repro
- Multi-source brain (Postgres engine),
gbrain serve --http.
UPDATE access_tokens SET permissions = jsonb_set(permissions, '{source_id}', '["default","src-a","src-b"]') WHERE name='<token>';
- Remote MCP
query with source_id: "__all__" → results only from default; tools/call whoami shows transport "legacy".
- Same grant via the bearer-only http-transport honors the array.
Suggested fix
In the legacy fallback of GBrainOAuthProvider, select permissions and reuse parseLegacyTokenScope (array → allowedSources + first element as scalar write floor; string → scalar; absent → 'default'), mirroring http-transport.ts. The serve's tool dispatch already threads auth: authInfo into the op context, so populating allowedSources on the AuthInfo is sufficient. Happy to PR if useful.
Summary
The #1336 legacy-token source grant (
access_tokens.permissions.source_id) is honored bysrc/mcp/http-transport.ts(parseLegacyTokenScope), but not by the OAuth transport thatgbrain serve --httpactually uses.GBrainOAuthProvider's legacyaccess_tokensfallback branch (src/core/oauth-provider.ts, ~line 635) onlySELECT name, never readspermissions, and hardcodessourceId: 'default'with noallowedSources.Effect
On a multi-source brain, every remote caller authenticating with a legacy bearer token through
serve --httpis silently pinned to thedefaultsource:query/get_pagewithsource_id: "__all__"collapses todefaultonly (resolveRequestedScope → sourceScopeOpts → scalar ctx.sourceId), regardless of any array grant stored inpermissions.source_id.[]with no error.This contradicts the CLAUDE.md invariant: "Source isolation. Every read-side op routes through sourceScopeOpts(ctx); precedence is federated array (ctx.auth.allowedSources) > scalar (ctx.sourceId) > nothing." — the OAuth transport never populates the federated array for legacy tokens, while http-transport.ts does.
Repro
gbrain serve --http.UPDATE access_tokens SET permissions = jsonb_set(permissions, '{source_id}', '["default","src-a","src-b"]') WHERE name='<token>';querywithsource_id: "__all__"→ results only fromdefault;tools/call whoamishows transport "legacy".Suggested fix
In the legacy fallback of
GBrainOAuthProvider, selectpermissionsand reuseparseLegacyTokenScope(array →allowedSources+ first element as scalar write floor; string → scalar; absent → 'default'), mirroring http-transport.ts. The serve's tool dispatch already threadsauth: authInfointo the op context, so populatingallowedSourceson the AuthInfo is sufficient. Happy to PR if useful.