diff --git a/src/core/skill-catalog.ts b/src/core/skill-catalog.ts index 31d6fe9e0..fa3c2432c 100644 --- a/src/core/skill-catalog.ts +++ b/src/core/skill-catalog.ts @@ -295,6 +295,7 @@ export function confineManifestPath(skillsDir: string, entry: ManifestEntry): st function opCallableByCaller(op: Operation, ctx: OperationContext): boolean { if (ctx.remote === false) return true; // local CLI — OS is the trust boundary if (op.localOnly) return false; // not reachable over a remote transport + if (ctx.transport === 'stdio') return true; // auth-less local pipe — dispatch enforces no scopes return hasScope(ctx.auth?.scopes ?? [], op.scope ?? 'read'); } diff --git a/test/skill-catalog-transports.test.ts b/test/skill-catalog-transports.test.ts index a09542f39..ba621db0a 100644 --- a/test/skill-catalog-transports.test.ts +++ b/test/skill-catalog-transports.test.ts @@ -54,7 +54,7 @@ function unpack(res: { content: { text: string }[]; isError?: boolean }): { async function call( name: string, params: Record, - opts: { remote: boolean; auth?: AuthInfo }, + opts: { remote: boolean; auth?: AuthInfo; transport?: 'stdio' }, ) { return unpack(await dispatchToolCall(engine, name, params, { sourceId: 'default', ...opts })); } @@ -139,3 +139,32 @@ describe('get_skill over dispatch', () => { }); }); }); + +// --------------------------------------------------------------------------- +// #3635: stdio transport must see usable_tools, not empty +// --------------------------------------------------------------------------- + +describe('stdio transport — catalog advertises tools as usable (#3635)', () => { + test('list_skills over stdio reports brain-ops tools in usable_tools', async () => { + await withEnv({ GBRAIN_HOME: home }, async () => { + await engine.setConfig('mcp.publish_skills', 'true'); + const r = await call('list_skills', {}, { remote: true, transport: 'stdio' }); + expect(r.isError).toBe(false); + const bo = r.body.skills.find((s: any) => s.name === 'brain-ops'); + expect(bo.usable_tools).toContain('search'); + expect(bo.usable_tools).toContain('put_page'); + expect(bo.unavailable_tools).not.toContain('search'); + expect(bo.unavailable_tools).not.toContain('put_page'); + }); + }); + + test('localOnly ops remain unavailable on stdio', async () => { + await withEnv({ GBRAIN_HOME: home }, async () => { + await engine.setConfig('mcp.publish_skills', 'true'); + const r = await call('list_skills', {}, { remote: true, transport: 'stdio' }); + expect(r.isError).toBe(false); + const allUsable = r.body.skills.flatMap((s: any) => s.usable_tools); + expect(allUsable).not.toContain('purge_deleted_pages'); + }); + }); +});