Skip to content

Commit 9750322

Browse files
committed
feat: 添加工具黑名单支持并优化会话处理
refactor(agent): 重构工具注册逻辑以支持黑白名单 feat(cli): 为headless和print命令添加会话恢复功能 test: 添加工具过滤和会话处理的单元测试
1 parent 04e0532 commit 9750322

11 files changed

Lines changed: 639 additions & 116 deletions

File tree

packages/cli/src/agent/Agent.ts

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import * as os from 'os';
1414
import * as path from 'path';
15-
import { getCwd } from '../utils/cwd.js';
1615
import {
1716
type BladeConfig,
1817
ConfigManager,
@@ -34,7 +33,6 @@ import {
3433
type IChatService,
3534
type Message,
3635
} from '../services/ChatServiceInterface.js';
37-
3836
import { discoverSkills } from '../skills/index.js';
3937
import { SpecManager } from '../spec/SpecManager.js';
4038
import {
@@ -51,6 +49,7 @@ import { getBuiltinTools } from '../tools/builtin/index.js';
5149
import { ExecutionPipeline } from '../tools/execution/ExecutionPipeline.js';
5250
import { ToolRegistry } from '../tools/registry/ToolRegistry.js';
5351
import type { Tool } from '../tools/types/index.js';
52+
import { getCwd } from '../utils/cwd.js';
5453
import { isThinkingModel } from '../utils/modelDetection.js';
5554
import { ExecutionEngine } from './ExecutionEngine.js';
5655
import { executeLoopGenerator } from './loop/index.js';
@@ -132,6 +131,8 @@ export class Agent {
132131
permissionConfig: permissions,
133132
permissionMode,
134133
maxHistorySize: 1000,
134+
toolWhitelist: this.runtimeOptions.toolWhitelist,
135+
toolBlacklist: this.runtimeOptions.toolBlacklist,
135136
});
136137
}
137138

@@ -231,14 +232,28 @@ export class Agent {
231232
const configManager = ConfigManager.getInstance();
232233
configManager.validateConfig(config);
233234

235+
// 3.5. 从 RuntimeConfig 继承 CLI 工具约束(allowedTools / disallowedTools)
236+
const mergedOptions = { ...options };
237+
if (!mergedOptions.toolWhitelist && config.allowedTools?.length) {
238+
mergedOptions.toolWhitelist = config.allowedTools;
239+
}
240+
if (!mergedOptions.toolBlacklist && config.disallowedTools?.length) {
241+
mergedOptions.toolBlacklist = config.disallowedTools;
242+
}
243+
234244
// 4. 创建并初始化 Agent
235245
// 将 options 作为运行时参数传递
236-
const agent = new Agent(config, options);
246+
const agent = new Agent(config, mergedOptions);
237247
await agent.initialize();
238248

239249
// 5. 应用工具白名单(如果指定)
240-
if (options.toolWhitelist && options.toolWhitelist.length > 0) {
241-
agent.applyToolWhitelist(options.toolWhitelist);
250+
if (mergedOptions.toolWhitelist && mergedOptions.toolWhitelist.length > 0) {
251+
agent.applyToolWhitelist(mergedOptions.toolWhitelist);
252+
}
253+
254+
// 6. 应用工具黑名单(如果指定)
255+
if (mergedOptions.toolBlacklist && mergedOptions.toolBlacklist.length > 0) {
256+
agent.applyToolBlacklist(mergedOptions.toolBlacklist);
242257
}
243258

244259
return agent;
@@ -248,10 +263,19 @@ export class Agent {
248263
runtime: SessionRuntime,
249264
options: AgentOptions = {}
250265
): Promise<Agent> {
266+
const storeConfig = getConfig();
267+
const mergedOptions = { ...options };
268+
if (!mergedOptions.toolWhitelist && storeConfig?.allowedTools?.length) {
269+
mergedOptions.toolWhitelist = storeConfig.allowedTools;
270+
}
271+
if (!mergedOptions.toolBlacklist && storeConfig?.disallowedTools?.length) {
272+
mergedOptions.toolBlacklist = storeConfig.disallowedTools;
273+
}
274+
251275
const agent = new Agent(
252276
runtime.getConfig(),
253-
options,
254-
runtime.createExecutionPipeline(options),
277+
mergedOptions,
278+
runtime.createExecutionPipeline(mergedOptions),
255279
runtime
256280
);
257281
await agent.initialize();
@@ -706,7 +730,6 @@ export class Agent {
706730
const registry = this.executionPipeline.getRegistry();
707731
const allTools = registry.getAll();
708732

709-
// 过滤掉不在白名单中的工具
710733
const toolsToRemove = allTools.filter((tool) => !whitelist.includes(tool.name));
711734

712735
for (const tool of toolsToRemove) {
@@ -718,6 +741,19 @@ export class Agent {
718741
);
719742
}
720743

744+
public applyToolBlacklist(blacklist: string[]): void {
745+
const registry = this.executionPipeline.getRegistry();
746+
const blacklistSet = new Set(blacklist);
747+
748+
for (const tool of registry.getAll()) {
749+
if (blacklistSet.has(tool.name)) {
750+
registry.unregister(tool.name);
751+
}
752+
}
753+
754+
logger.debug(`Applied tool blacklist: ${blacklist.join(', ')}`);
755+
}
756+
721757
/**
722758
* 获取工具统计信息
723759
*/

packages/cli/src/agent/runtime/SessionRuntime.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,16 @@ export class SessionRuntime {
155155
createExecutionPipeline(options: AgentOptions = {}): ExecutionPipeline {
156156
const registry = new ToolRegistry();
157157
const allowed = options.toolWhitelist ? new Set(options.toolWhitelist) : null;
158+
const blocked = options.toolBlacklist ? new Set(options.toolBlacklist) : null;
158159

159160
for (const tool of this.baseRegistry.getBuiltinTools()) {
161+
if (blocked?.has(tool.name)) continue;
160162
if (!allowed || allowed.has(tool.name)) {
161163
registry.register(tool);
162164
}
163165
}
164166
for (const tool of this.baseRegistry.getMcpTools()) {
167+
if (blocked?.has(tool.name)) continue;
165168
if (!allowed || allowed.has(tool.name)) {
166169
registry.registerMcpTool(tool);
167170
}
@@ -179,6 +182,8 @@ export class SessionRuntime {
179182
permissionMode,
180183
approvalStore: this.approvalStore,
181184
maxHistorySize: 1000,
185+
toolWhitelist: options.toolWhitelist,
186+
toolBlacklist: options.toolBlacklist,
182187
});
183188
}
184189

packages/cli/src/agent/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export interface AgentOptions {
5858
permissionMode?: PermissionMode;
5959
maxTurns?: number; // 最大对话轮次 (-1=无限制, 0=禁用对话, N>0=限制轮次)
6060
toolWhitelist?: string[]; // 工具白名单(仅允许指定工具)
61+
toolBlacklist?: string[]; // 工具黑名单(禁止指定工具)
6162
modelId?: string;
6263

6364
// MCP 配置

packages/cli/src/commands/headless.ts

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,43 @@
66
* while the exported JSONL contract remains snake_case and versioned.
77
*/
88
import type { Argv } from 'yargs';
9+
import yargs from 'yargs';
10+
import { hideBin } from 'yargs/helpers';
911
import { z } from 'zod';
1012
import { Agent } from '../agent/Agent.js';
1113
import { drainLoop } from '../agent/loop/index.js';
1214
import type { LoopEvent } from '../agent/loop/types.js';
15+
import { SessionRuntime } from '../agent/runtime/SessionRuntime.js';
1316
import type { ChatContext } from '../agent/types.js';
17+
import { globalOptions } from '../cli/config.js';
18+
import {
19+
loadConfiguration,
20+
validateOutput,
21+
validatePermissions,
22+
} from '../cli/middleware.js';
1423
import { PermissionMode } from '../config/types.js';
1524
import type { Message } from '../services/ChatServiceInterface.js';
1625
import type { TodoItem } from '../tools/builtin/todo/types.js';
17-
import { getCwd } from '../utils/cwd.js';
1826
import type {
1927
ConfirmationDetails,
2028
ConfirmationResponse,
2129
} from '../tools/types/ExecutionTypes.js';
2230
import {
23-
initializeCliPlugins,
24-
normalizeCliInput,
25-
readCliInput,
26-
} from './shared/commandInput.js';
31+
formatToolCallSummary,
32+
formatToolDisplay,
33+
} from '../ui/utils/toolFormatters.js';
34+
import { getCwd } from '../utils/cwd.js';
2735
import {
36+
createHeadlessJsonlEvent,
2837
type HeadlessJsonlEventPayload,
2938
type HeadlessJsonlEventType,
30-
createHeadlessJsonlEvent,
3139
} from './headlessEvents.js';
3240
import {
33-
formatToolCallSummary,
34-
formatToolDisplay,
35-
} from '../ui/utils/toolFormatters.js';
41+
initializeCliPlugins,
42+
normalizeCliInput,
43+
readCliInput,
44+
} from './shared/commandInput.js';
45+
import { resolveNonInteractiveSession } from './shared/sessionContext.js';
3646

3747
/** Minimal writable stream contract used by headless output sinks. */
3848
interface WritableLike {
@@ -67,6 +77,10 @@ export const HeadlessOptionsSchema = z.object({
6777
mcpConfig: z.array(z.string()).optional(),
6878
strictMcpConfig: z.boolean().optional(),
6979
sessionId: z.string().optional(),
80+
allowedTools: z.array(z.string()).optional(),
81+
disallowedTools: z.array(z.string()).optional(),
82+
continue: z.boolean().optional(),
83+
resume: z.union([z.string(), z.boolean()]).optional(),
7084
outputFormat: HeadlessOutputFormatSchema.optional(),
7185
});
7286

@@ -93,6 +107,14 @@ export interface HeadlessOptions {
93107
strictMcpConfig?: boolean;
94108
/** Session identifier used in the chat context. */
95109
sessionId?: string;
110+
/** Tool whitelist for this run. */
111+
allowedTools?: string[];
112+
/** Tool blacklist for this run. */
113+
disallowedTools?: string[];
114+
/** Continue the most recent conversation. */
115+
continue?: boolean;
116+
/** Resume a specific conversation. */
117+
resume?: string | boolean;
96118
/** Terminal output format. */
97119
outputFormat?: string;
98120
}
@@ -565,6 +587,7 @@ export async function runHeadless(
565587
let eventWriter = createEventWriter(io, outputFormat);
566588
const streamState = new HeadlessStreamState();
567589
const phaseState: HeadlessPhaseState = { targetLocked: false };
590+
let runtime: SessionRuntime | undefined;
568591

569592
try {
570593
const validatedOptions = validateHeadlessOptions(options);
@@ -585,22 +608,38 @@ export async function runHeadless(
585608
const permissionMode =
586609
(validatedOptions.permissionMode as PermissionMode | undefined) ??
587610
PermissionMode.YOLO;
588-
const contextMessages: Message[] = [];
611+
const { sessionId, messages } = await resolveNonInteractiveSession({
612+
sessionId: validatedOptions.sessionId,
613+
continue: validatedOptions.continue,
614+
resume: validatedOptions.resume,
615+
fallbackSessionPrefix: 'headless',
616+
});
617+
const contextMessages: Message[] = [...messages];
589618
const chatContext: ChatContext = {
590619
messages: contextMessages,
591620
userId: 'cli-user',
592-
sessionId: validatedOptions.sessionId ?? `headless-${Date.now()}`,
621+
sessionId,
593622
workspaceRoot: getCwd(),
594623
permissionMode,
595624
confirmationHandler: createConfirmationHandler(),
596625
};
597626

598-
const agent = await Agent.create({
627+
runtime = await SessionRuntime.create({
628+
sessionId,
629+
modelId: validatedOptions.model,
630+
mcpConfig: validatedOptions.mcpConfig,
631+
strictMcpConfig: validatedOptions.strictMcpConfig,
632+
});
633+
634+
const agent = await Agent.createWithRuntime(runtime, {
635+
sessionId,
599636
systemPrompt: validatedOptions.systemPrompt,
600637
appendSystemPrompt: validatedOptions.appendSystemPrompt,
601638
maxTurns: validatedOptions.maxTurns,
602639
modelId: validatedOptions.model,
603640
permissionMode,
641+
toolWhitelist: validatedOptions.allowedTools,
642+
toolBlacklist: validatedOptions.disallowedTools,
604643
mcpConfig: validatedOptions.mcpConfig,
605644
strictMcpConfig: validatedOptions.strictMcpConfig,
606645
});
@@ -765,6 +804,8 @@ export async function runHeadless(
765804
}
766805
eventWriter.error(`Error: ${extractHeadlessErrorMessage(error)}`);
767806
return 1;
807+
} finally {
808+
await runtime?.dispose();
768809
}
769810
}
770811

@@ -775,19 +816,19 @@ export async function handleHeadlessMode(): Promise<boolean> {
775816
return false;
776817
}
777818

778-
const yargs = (await import('yargs')).default;
779-
const { hideBin } = await import('yargs/helpers');
780-
const { globalOptions } = await import('../cli/config.js');
781819
const {
782-
loadConfiguration,
783-
validateOutput,
784-
validatePermissions,
785-
} = await import('../cli/middleware.js');
820+
headless: _h,
821+
'output-format': _of,
822+
'system-prompt': _sp,
823+
'append-system-prompt': _asp,
824+
'max-turns': _mt,
825+
...cliOptions
826+
} = globalOptions;
786827

787828
const cli = yargs(hideBin(process.argv))
788829
.scriptName('blade')
789830
.strict(false)
790-
.options(globalOptions)
831+
.options(cliOptions)
791832
.middleware([validatePermissions, loadConfiguration, validateOutput]);
792833

793834
headlessCommand(cli);

0 commit comments

Comments
 (0)