mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
Wave-assembled from PR #3965 by @javieraldape. Co-Authored-By: javieraldape <6430563+javieraldape@users.noreply.github.com>
This commit is contained in:
committed by
Sina Matian
co-authored by
javieraldape
parent
d8e3772810
commit
3f595083fe
+29
-21
@@ -21,6 +21,23 @@ import { gcSessionContextState } from '../core/context/session-state.ts';
|
||||
import { makeContextPackIpcHandler } from './context-pack-handler.ts';
|
||||
import { logTurnContextDeliveryFireAndForget } from '../core/context/volunteer-events.ts';
|
||||
|
||||
export async function resolveMcpStdioSourceScope(
|
||||
engine: BrainEngine,
|
||||
cwd: string = process.cwd(),
|
||||
): Promise<{ sourceId: string; localFederatedSourceIds?: string[] }> {
|
||||
try {
|
||||
const { resolveSourceWithTier, localFederatedSourceIds } = await import('../core/source-resolver.ts');
|
||||
const resolved = await resolveSourceWithTier(engine, null, cwd);
|
||||
const federated = await localFederatedSourceIds(engine, resolved.source_id, resolved.tier);
|
||||
return {
|
||||
sourceId: resolved.source_id,
|
||||
...(federated ? { localFederatedSourceIds: federated } : {}),
|
||||
};
|
||||
} catch {
|
||||
return { sourceId: process.env.GBRAIN_SOURCE || 'default' };
|
||||
}
|
||||
}
|
||||
|
||||
export async function startMcpServer(engine: BrainEngine, opts: { surface?: McpSurface } = {}) {
|
||||
const server = new Server(
|
||||
{ name: 'gbrain', version: VERSION },
|
||||
@@ -48,20 +65,11 @@ export async function startMcpServer(engine: BrainEngine, opts: { surface?: McpS
|
||||
// shape and cast through `any` (the SDK accepts it via the ServerResult union).
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request: any): Promise<any> => {
|
||||
const { name, arguments: params } = request.params;
|
||||
// #3242: when the operator didn't pin a source via GBRAIN_SOURCE, stdio
|
||||
// reads span every `config.federated = true` source (same visibility set
|
||||
// as unqualified local CLI reads). GBRAIN_SOURCE set = explicit scope,
|
||||
// no widening. Best-effort: a resolver failure keeps the scalar scope.
|
||||
// ponytail: one tiny SELECT per tool call; cache it if it ever shows up.
|
||||
let localFederated: string[] | undefined;
|
||||
try {
|
||||
const { localFederatedSourceIds } = await import('../core/source-resolver.ts');
|
||||
localFederated = await localFederatedSourceIds(
|
||||
engine,
|
||||
process.env.GBRAIN_SOURCE || 'default',
|
||||
process.env.GBRAIN_SOURCE ? 'env' : 'seed_default',
|
||||
);
|
||||
} catch { /* scalar scope stands */ }
|
||||
// #3242 / #3906: stdio resolves its source through the same ambient chain
|
||||
// as local CLI dispatch: GBRAIN_SOURCE, then .gbrain-source, then the
|
||||
// non-explicit fallback tiers. Non-explicit tiers may widen to federated
|
||||
// local reads; explicit/env/dotfile scopes stay scalar.
|
||||
const sourceScope = await resolveMcpStdioSourceScope(engine);
|
||||
// v0.28: stdio MCP has no per-token auth (local pipe). Default the
|
||||
// takes-holder allow-list to ['world'] so agent-facing callers don't
|
||||
// see private hunches via takes_list / takes_search / query. Operators
|
||||
@@ -83,11 +91,10 @@ export async function startMcpServer(engine: BrainEngine, opts: { surface?: McpS
|
||||
transport: 'stdio',
|
||||
takesHoldersAllowList: ['world'],
|
||||
...(sessionId ? { sessionId } : {}),
|
||||
// v0.31: source defaults to 'default' for stdio (no per-token scope).
|
||||
// Operators who want a different source on stdio MCP should set
|
||||
// GBRAIN_SOURCE in the env or use --source via `gbrain call`.
|
||||
sourceId: process.env.GBRAIN_SOURCE || 'default',
|
||||
...(localFederated ? { localFederatedSourceIds: localFederated } : {}),
|
||||
sourceId: sourceScope.sourceId,
|
||||
...(sourceScope.localFederatedSourceIds
|
||||
? { localFederatedSourceIds: sourceScope.localFederatedSourceIds }
|
||||
: {}),
|
||||
// v0.31 (eD3): _meta.brain_hot_memory injection so Claude Desktop /
|
||||
// Code see the brain's relevant hot memory automatically alongside
|
||||
// every tool-call response. Best-effort; absorbs errors.
|
||||
@@ -112,7 +119,7 @@ export async function startMcpServer(engine: BrainEngine, opts: { surface?: McpS
|
||||
const cfg = loadConfig();
|
||||
if (cfg?.engine === 'pglite' && cfg.database_path) {
|
||||
resolveSocket = resolveSocketPath(cfg.database_path);
|
||||
const defaultSource = process.env.GBRAIN_SOURCE || 'default';
|
||||
const { sourceId: defaultSource } = await resolveMcpStdioSourceScope(engine);
|
||||
// [S3#6] turn_context requires the shared secret from the data dir
|
||||
// (created 0600 here if absent). If the secret can't be provisioned,
|
||||
// turn_context stays fail-closed ('unauthorized') while the secret-free
|
||||
@@ -194,8 +201,9 @@ export async function startMcpServer(engine: BrainEngine, opts: { surface?: McpS
|
||||
let startupSweep: { cancel: () => void } | null = null;
|
||||
try {
|
||||
const { armStartupSweep } = await import('../core/sweep.ts');
|
||||
const { sourceId } = await resolveMcpStdioSourceScope(engine);
|
||||
startupSweep = armStartupSweep(engine, {
|
||||
sourceId: process.env.GBRAIN_SOURCE || 'default',
|
||||
sourceId,
|
||||
});
|
||||
} catch {
|
||||
/* startup sweep is best-effort; never block serve */
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, test, expect, afterEach } from 'bun:test';
|
||||
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import type { BrainEngine } from '../src/core/engine.ts';
|
||||
import { resolveMcpStdioSourceScope } from '../src/mcp/server.ts';
|
||||
import { withEnv } from './helpers/with-env.ts';
|
||||
|
||||
function makeEngine(registeredSources: string[]): BrainEngine {
|
||||
return {
|
||||
kind: 'pglite',
|
||||
executeRaw: async <T>(sql: string, params?: unknown[]): Promise<T[]> => {
|
||||
if (sql.includes('SELECT id FROM sources WHERE id = $1')) {
|
||||
const id = params?.[0];
|
||||
return (typeof id === 'string' && registeredSources.includes(id)
|
||||
? [{ id } as T]
|
||||
: []);
|
||||
}
|
||||
if (sql.includes('SELECT id, local_path FROM sources')) return [];
|
||||
if (sql.includes('SELECT id, config, archived FROM sources')) {
|
||||
return registeredSources.map(id => ({ id, config: null, archived: false }) as T);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
getConfig: async () => null,
|
||||
} as unknown as BrainEngine;
|
||||
}
|
||||
|
||||
function writeSourceDotfile(dir: string, sourceId: string): void {
|
||||
const path = join(dir, '.gbrain-source');
|
||||
writeFileSync(path, `${sourceId}\n`);
|
||||
chmodSync(path, 0o600);
|
||||
}
|
||||
|
||||
describe('stdio MCP source resolution', () => {
|
||||
const scratchDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of scratchDirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('honors .gbrain-source when GBRAIN_SOURCE is absent', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'gbrain-mcp-source-'));
|
||||
scratchDirs.push(dir);
|
||||
writeSourceDotfile(dir, 'team-alpha');
|
||||
|
||||
await withEnv({ GBRAIN_SOURCE: undefined }, async () => {
|
||||
const scope = await resolveMcpStdioSourceScope(
|
||||
makeEngine(['default', 'team-alpha']),
|
||||
dir,
|
||||
);
|
||||
|
||||
expect(scope).toEqual({ sourceId: 'team-alpha' });
|
||||
});
|
||||
});
|
||||
|
||||
test('GBRAIN_SOURCE wins over .gbrain-source', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'gbrain-mcp-source-'));
|
||||
scratchDirs.push(dir);
|
||||
writeSourceDotfile(dir, 'team-alpha');
|
||||
|
||||
await withEnv({ GBRAIN_SOURCE: 'env-source' }, async () => {
|
||||
const scope = await resolveMcpStdioSourceScope(
|
||||
makeEngine(['default', 'team-alpha', 'env-source']),
|
||||
dir,
|
||||
);
|
||||
|
||||
expect(scope).toEqual({ sourceId: 'env-source' });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user