mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
fix(agent-core): enforce swarm worker tool allowlist and propagate abort
This commit is contained in:
parent
b0b61c27ca
commit
d6a3d91c72
3 changed files with 42 additions and 8 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { mapWithConcurrency } from './concurrency';
|
||||
import { parsePlan } from './parse';
|
||||
import {
|
||||
ALLOWED_WORKER_TOOLS,
|
||||
DEFAULT_WORKER_TOOLS,
|
||||
PLANNER_SYSTEM_PROMPT,
|
||||
SYNTHESIZER_SYSTEM_PROMPT,
|
||||
|
|
@ -73,7 +74,9 @@ export class SwarmCoordinator {
|
|||
const out = await this.deps.spawnSubagent({
|
||||
profileName: `swarm:${st.role}`,
|
||||
systemPrompt: st.systemPrompt,
|
||||
tools: st.toolAllowlist ?? DEFAULT_WORKER_TOOLS,
|
||||
tools: (st.toolAllowlist ?? DEFAULT_WORKER_TOOLS).filter((t) =>
|
||||
ALLOWED_WORKER_TOOLS.includes(t),
|
||||
),
|
||||
prompt: st.prompt,
|
||||
description: st.role,
|
||||
signal: this.deps.signal,
|
||||
|
|
@ -82,6 +85,7 @@ export class SwarmCoordinator {
|
|||
st.status = 'done';
|
||||
this.progress(`✓ ${st.role}: done`);
|
||||
} catch (err) {
|
||||
if (this.deps.signal.aborted) throw err;
|
||||
st.status = 'failed';
|
||||
st.error = err instanceof Error ? err.message : String(err);
|
||||
this.progress(`✗ ${st.role}: failed (${st.error})`);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
import type { SwarmPlan } from './types';
|
||||
|
||||
/** Read-only default tool set for workers; planner may widen via toolAllowlist. */
|
||||
export const DEFAULT_WORKER_TOOLS: string[] = ['Read', 'Grep', 'Glob', 'WebSearch', 'FetchURL'];
|
||||
/** Read-only default tool set for workers; planner may widen via toolAllowlist within the allowlist. */
|
||||
export const DEFAULT_WORKER_TOOLS: readonly string[] = ['Read', 'Grep', 'Glob', 'WebSearch', 'FetchURL'];
|
||||
|
||||
/** Tool names a worker is allowed to request (excludes Agent/Swarm dispatch tools). */
|
||||
export const ALLOWED_WORKER_TOOLS: string[] = [
|
||||
/** Tool names a worker is allowed to request. Read-only for Phase 1 (no Write/Edit/Bash, no dispatch tools). */
|
||||
export const ALLOWED_WORKER_TOOLS: readonly string[] = [
|
||||
'Read',
|
||||
'Write',
|
||||
'Edit',
|
||||
'Grep',
|
||||
'Glob',
|
||||
'Bash',
|
||||
'WebSearch',
|
||||
'FetchURL',
|
||||
'ReadMediaFile',
|
||||
|
|
|
|||
|
|
@ -80,4 +80,37 @@ describe('SwarmCoordinator.run', () => {
|
|||
expect(result).toBe('SYNTH');
|
||||
expect(onProgress.mock.calls.some((c) => /failed/i.test(String(c[0])))).toBe(true);
|
||||
});
|
||||
|
||||
it('strips disallowed tools (Agent/Bash) from a planner-supplied allowlist', async () => {
|
||||
const planWithBadTools = JSON.stringify({
|
||||
subtasks: [{ role: 'X', systemPrompt: 's', prompt: 'p', toolAllowlist: ['Agent', 'Read', 'Bash'] }],
|
||||
});
|
||||
const spawn = vi.fn(async (args) => {
|
||||
if (args.profileName === 'swarm-planner') return { result: planWithBadTools };
|
||||
if (args.profileName === 'swarm-synthesizer') return { result: 'S' };
|
||||
return { result: 'w' };
|
||||
});
|
||||
const coordinator = new SwarmCoordinator({ spawnSubagent: spawn, signal: new AbortController().signal });
|
||||
await coordinator.run('x');
|
||||
const worker = (spawn as ReturnType<typeof vi.fn>).mock.calls
|
||||
.map((c) => c[0])
|
||||
.find((c) => c.profileName === 'swarm:X');
|
||||
expect(worker?.tools).toEqual(['Read']);
|
||||
});
|
||||
|
||||
it('propagates abort instead of swallowing it (no synthesis after cancel)', async () => {
|
||||
const controller = new AbortController();
|
||||
const PLAN = JSON.stringify({ subtasks: [{ role: 'A', systemPrompt: 's', prompt: 'p' }] });
|
||||
const spawn = vi.fn(async (args) => {
|
||||
if (args.profileName === 'swarm-planner') return { result: PLAN };
|
||||
controller.abort();
|
||||
const e = new Error('aborted');
|
||||
e.name = 'AbortError';
|
||||
throw e;
|
||||
});
|
||||
const coordinator = new SwarmCoordinator({ spawnSubagent: spawn, signal: controller.signal });
|
||||
await expect(coordinator.run('x')).rejects.toThrow();
|
||||
const profiles = (spawn as ReturnType<typeof vi.fn>).mock.calls.map((c) => c[0].profileName);
|
||||
expect(profiles).not.toContain('swarm-synthesizer');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue