fix(skills): advertise usable tools on stdio transport (#3725)

Wave-assembled from PR #3725 by @gregario.

Co-Authored-By: Greg Jackson <gregj64@gmail.com>
This commit is contained in:
Garry Tan
2026-08-12 14:38:36 -07:00
committed by Sina Matian
co-authored by Greg Jackson
parent 8d5bdfe125
commit 9b6db85d39
2 changed files with 31 additions and 1 deletions
+1
View File
@@ -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');
}
+30 -1
View File
@@ -54,7 +54,7 @@ function unpack(res: { content: { text: string }[]; isError?: boolean }): {
async function call(
name: string,
params: Record<string, unknown>,
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');
});
});
});