feat(tools): enforce skill capabilities and secret scopes

This commit is contained in:
William Valentin
2026-02-15 10:16:51 -08:00
parent 9900f41057
commit 3451df41b9
11 changed files with 483 additions and 4 deletions
+102
View File
@@ -160,4 +160,106 @@ describe('ToolExecutor', () => {
const result = await resultPromise;
expect(result.success).toBe(true);
});
it('enforces skill filesystem write allowlist', async () => {
const registry = new ToolRegistry();
registry.register({
name: 'file.write',
description: 'write',
inputSchema: { type: 'object', properties: {} },
execute: async () => ({ success: true, output: 'ok' }),
});
const hooks = new HookEngine({ confirm: [], log: [], silent: [] });
const executor = new ToolExecutor(registry, hooks);
const allowed = await executor.execute(
'file.write',
{ path: '/tmp/flynn-skill-ok.txt', content: 'hello' },
{
skillName: 'test-skill',
skillPermissions: {
execution_environment: 'host',
fs: { write: ['/tmp/**'] },
},
executionEnvironment: 'host',
autonomyLevel: 'autonomous',
},
);
expect(allowed.success).toBe(true);
const denied = await executor.execute(
'file.write',
{ path: '/etc/passwd', content: 'nope' },
{
skillName: 'test-skill',
skillPermissions: {
execution_environment: 'host',
fs: { write: ['/tmp/**'] },
},
executionEnvironment: 'host',
autonomyLevel: 'autonomous',
},
);
expect(denied.success).toBe(false);
expect(denied.error).toContain('path not allowed');
});
it('enforces tool secret scopes for skill contexts', async () => {
const registry = new ToolRegistry();
registry.register({
name: 'gmail.list',
description: 'gmail',
requiredSecretScopes: ['gmail'],
inputSchema: { type: 'object', properties: {} },
execute: async () => ({ success: true, output: 'ok' }),
});
const hooks = new HookEngine({ confirm: [], log: [], silent: [] });
const executor = new ToolExecutor(registry, hooks);
const result = await executor.execute('gmail.list', {}, {
skillName: 'no-secrets-skill',
skillPermissions: { secrets: [] },
executionEnvironment: 'host',
});
expect(result.success).toBe(false);
expect(result.error).toContain('missing secret scopes');
});
it('blocks high-risk tool calls with injection markers when untrusted content is present', async () => {
const registry = new ToolRegistry();
registry.register({
name: 'shell.exec',
description: 'shell',
inputSchema: { type: 'object', properties: {} },
execute: async () => ({ success: true, output: 'ok' }),
});
const hooks = new HookEngine({ confirm: [], log: [], silent: [] });
const executor = new ToolExecutor(registry, hooks);
const result = await executor.execute('shell.exec', { command: 'rm -rf /' }, {
untrustedContent: true,
executionEnvironment: 'host',
});
expect(result.success).toBe(false);
expect(result.error).toContain('blocked');
});
it('blocks passing secret-like args to network tools when untrusted content is present', async () => {
const registry = new ToolRegistry();
registry.register({
name: 'web.fetch',
description: 'fetch',
inputSchema: { type: 'object', properties: {} },
execute: async () => ({ success: true, output: 'ok' }),
});
const hooks = new HookEngine({ confirm: [], log: [], silent: [] });
const executor = new ToolExecutor(registry, hooks);
const result = await executor.execute('web.fetch', { url: 'https://example.com', authorization: 'Bearer abcdef' }, {
untrustedContent: true,
executionEnvironment: 'host',
});
expect(result.success).toBe(false);
expect(result.error).toContain('refusing to pass');
});
});