Compare commits

..
Author SHA1 Message Date
967bff6cda fix(auth): expose OAuth source grants in whoami (takeover of #3279)
Rebase of #3279 onto current master: the whoami oauth shape gains
source_id (AuthInfo.sourceId, null when absent) and federated_read
(AuthInfo.allowedSources, [] when absent) — read-only self-introspection
that widens no grant. Re-applied against the post-#3091 description
string (stdio transport shape preserved) and merged the grant tests
into the current whoami.test.ts alongside the stdio cases.

Co-authored-by: boundless-forest <boundless-forest@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:38:40 -07:00
6 changed files with 73 additions and 28 deletions
File diff suppressed because one or more lines are too long
+5 -1
View File
@@ -3823,7 +3823,7 @@ const whoami: Operation = {
name: 'whoami',
description:
'Introspect the calling identity. Returns one of three transport shapes: ' +
'{transport: "oauth", client_id, client_name, scopes, expires_at}, ' +
'{transport: "oauth", client_id, client_name, scopes, expires_at, source_id, federated_read}, ' +
'{transport: "legacy", token_name, scopes, expires_at: null}, or ' +
'{transport: "local", scopes: []}, or {transport: "stdio", scopes: []} ' +
'for the auth-less stdio MCP pipe. Throws unknown_transport when the ' +
@@ -3865,6 +3865,10 @@ const whoami: Operation = {
client_name: ctx.auth.clientName ?? ctx.auth.clientId,
scopes: ctx.auth.scopes,
expires_at: ctx.auth.expiresAt ?? null,
// Read-only self-introspection of the token's source grants —
// widens nothing; absent grants serialize fail-closed (null / []).
source_id: ctx.auth.sourceId ?? null,
federated_read: ctx.auth.allowedSources ?? [],
};
}
return {
+2 -2
View File
@@ -4835,11 +4835,11 @@ export class PGLiteEngine implements BrainEngine {
const { rows } = await this.db.query(
`SELECT t.id AS take_id, t.page_id, p.slug AS page_slug, t.row_num,
t.claim, t.kind, t.holder, t.weight,
word_similarity($1, t.claim)::real AS score
similarity(t.claim, $1)::real AS score
FROM takes t
JOIN pages p ON p.id = t.page_id
WHERE t.active
AND $1 <% t.claim
AND t.claim % $1
AND ($2::text[] IS NULL OR t.holder = ANY($2::text[]))
AND ($4::text[] IS NULL OR p.source_id = ANY($4::text[]))
AND ($5::text IS NULL OR p.source_id = $5::text)
+2 -2
View File
@@ -4962,11 +4962,11 @@ export class PostgresEngine implements BrainEngine {
const rows = await sql`
SELECT t.id AS take_id, t.page_id, p.slug AS page_slug, t.row_num,
t.claim, t.kind, t.holder, t.weight,
word_similarity(${query}, t.claim)::real AS score
similarity(t.claim, ${query})::real AS score
FROM takes t
JOIN pages p ON p.id = t.page_id
WHERE t.active
AND ${query} <% t.claim
AND t.claim % ${query}
AND (
${opts.takesHoldersAllowList ?? null}::text[] IS NULL
OR t.holder = ANY(${opts.takesHoldersAllowList ?? null}::text[])
-16
View File
@@ -100,22 +100,6 @@ describe('searchTakes', () => {
const worldHits = await engine.searchTakes('founder', { takesHoldersAllowList: ['world'] });
expect(worldHits.every(h => h.holder === 'world')).toBe(true);
});
// #3267: whole-string trigram % structurally can't match a short keyword
// against a long claim (similarity between the full strings stays under the
// 0.3 threshold). word_similarity (<%) matches the keyword against the
// best-matching word span instead.
test('single-word keyword matches a long claim containing it (#3267)', async () => {
await engine.addTakesBatch([
{
page_id: acmePageId, row_num: 50,
claim: 'Acme will consolidate the mid-market vertical SaaS landscape through disciplined acquisitions and a shared billing platform over the next five years',
kind: 'bet', holder: 'garry', weight: 0.6,
},
]);
const hits = await engine.searchTakes('consolidate');
expect(hits.some(h => h.claim.includes('consolidate the mid-market'))).toBe(true);
});
});
describe('updateTake', () => {
+63 -6
View File
@@ -55,23 +55,75 @@ describe('whoami op contract', () => {
expect(result.scopes).toEqual([]);
});
test('oauth transport returns full client identity', async () => {
test('oauth transport returns client identity and exact source grants', async () => {
const auth: AuthInfo = {
token: 'gbrain_at_xxx',
clientId: 'gbrain_cl_abc',
clientName: 'gstack-test',
scopes: ['read', 'sources_admin'],
expiresAt: 1234567890,
sourceId: 'hot-memory',
allowedSources: ['hot-memory', 'canonical-brain'],
};
const result = (await whoami.handler(
ctxWith({ remote: true, sourceId: 'transport-fallback', auth }),
{},
)) as any;
expect(result).toEqual({
transport: 'oauth',
client_id: 'gbrain_cl_abc',
client_name: 'gstack-test',
scopes: ['read', 'sources_admin'],
expires_at: 1234567890,
source_id: 'hot-memory',
federated_read: ['hot-memory', 'canonical-brain'],
});
});
test('oauth transport uses fail-closed empty values when source grants are absent', async () => {
const auth: AuthInfo = {
token: 'gbrain_at_pre_migration',
clientId: 'gbrain_cl_pre_migration',
scopes: ['read'],
};
const result = (await whoami.handler(
ctxWith({ remote: true, sourceId: 'transport-fallback', auth }),
{},
)) as any;
expect(result.source_id).toBeNull();
expect(result.federated_read).toEqual([]);
});
test('oauth transport preserves an explicit empty federated grant', async () => {
const auth: AuthInfo = {
token: 'gbrain_at_empty',
clientId: 'gbrain_cl_empty',
scopes: ['read', 'write'],
sourceId: 'hot-memory',
allowedSources: [],
};
const result = (await whoami.handler(
ctxWith({ remote: true, auth }),
{},
)) as any;
expect(result.transport).toBe('oauth');
expect(result.client_id).toBe('gbrain_cl_abc');
expect(result.client_name).toBe('gstack-test');
expect(result.scopes).toEqual(['read', 'sources_admin']);
expect(result.expires_at).toBe(1234567890);
expect(result.source_id).toBe('hot-memory');
expect(result.federated_read).toEqual([]);
});
test('oauth transport does not widen federated_read with the write source', async () => {
const auth: AuthInfo = {
token: 'gbrain_at_narrow',
clientId: 'gbrain_cl_narrow',
scopes: ['read', 'write'],
sourceId: 'hot-memory',
allowedSources: ['canonical-brain'],
};
const result = (await whoami.handler(
ctxWith({ remote: true, auth }),
{},
)) as any;
expect(result.source_id).toBe('hot-memory');
expect(result.federated_read).toEqual(['canonical-brain']);
});
test('legacy transport (token name as clientId, no gbrain_cl_ prefix)', async () => {
@@ -150,6 +202,11 @@ describe('whoami op contract', () => {
});
describe('whoami op metadata', () => {
test('description documents OAuth source grant fields', () => {
expect(whoami.description).toContain('source_id');
expect(whoami.description).toContain('federated_read');
});
test('scope is read (any authenticated caller can introspect itself)', () => {
expect(whoami.scope).toBe('read');
});