@steerable/agent-shell 0.6.51 → 0.6.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brand.d.ts +5 -0
- package/dist/goal-store.d.ts +43 -0
- package/dist/goal-store.js +155 -0
- package/dist/host/runtime.js +15 -3
- package/dist/host-tools.d.ts +1 -1
- package/dist/host-tools.js +6 -0
- package/dist/local-backend/portable-bundle.d.ts +16 -2
- package/dist/local-backend/portable-bundle.js +21 -9
- package/dist/local-backend/portable-service.js +25 -11
- package/dist/local-backend/router.js +71 -27
- package/dist/local-backend/subagent-profiles.js +4 -0
- package/dist/local-backend/task-service.d.ts +6 -0
- package/dist/local-backend/task-service.js +47 -2
- package/dist/local-executor.d.ts +36 -2
- package/dist/local-executor.js +171 -19
- package/dist/mcp-executor.d.ts +42 -10
- package/dist/mcp-executor.js +248 -41
- package/dist/mcp-server-registry.d.ts +56 -8
- package/dist/mcp-server-registry.js +191 -26
- package/dist/product-config.d.ts +14 -0
- package/dist/project-home.d.ts +1 -1
- package/dist/project-home.js +1 -1
- package/dist/project-registry.d.ts +4 -4
- package/dist/project-registry.js +2 -2
- package/dist/shell-exec.d.ts +62 -0
- package/dist/shell-exec.js +147 -0
- package/dist/shell-process.d.ts +3 -0
- package/dist/shell-process.js +137 -0
- package/dist/sidecar/boot.d.ts +5 -2
- package/dist/sidecar/boot.js +5 -0
- package/dist/sidecar/reverse-approval.d.ts +2 -0
- package/dist/sidecar/reverse-approval.js +2 -0
- package/dist/sidecar/reverse-tools.d.ts +6 -2
- package/dist/sidecar/reverse-tools.js +9 -1
- package/dist/sidecar/types.d.ts +10 -1
- package/dist/storage/write-lease-error.js +4 -1
- package/dist/tool-presentation.d.ts +23 -0
- package/dist/tool-presentation.js +266 -0
- package/dist/tool-router.d.ts +26 -8
- package/dist/tool-router.js +275 -24
- package/dist/workspace-search.d.ts +35 -0
- package/dist/workspace-search.js +186 -0
- package/package.json +5 -5
package/dist/tool-router.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { WorktreeService } from './local-backend/worktree-service.js';
|
|
|
7
7
|
import type { ToolContribution, ToolMode, ToolExposure } from './scenario/pack.js';
|
|
8
8
|
export type { ToolMode, ToolExposure } from './scenario/pack.js';
|
|
9
9
|
import { type AgentToolPolicy } from './local-backend/agent-capability.js';
|
|
10
|
+
import { GoalStore } from './goal-store.js';
|
|
10
11
|
/** 已注册 MCP 服务的动态工具名前缀:`mcp__<serverKey>__<toolName>`。 */
|
|
11
12
|
export declare const MCP_DYNAMIC_TOOL_PREFIX = "mcp__";
|
|
12
13
|
/**
|
|
@@ -27,17 +28,23 @@ export interface ToolCall {
|
|
|
27
28
|
* 单次工具执行的调用上下文(按 chat 解析,随每次 execute 传入)。
|
|
28
29
|
*
|
|
29
30
|
* `projectRoot` 是项目模式的硬沙箱根:非空时
|
|
30
|
-
* -
|
|
31
|
-
* -
|
|
31
|
+
* - 读路径必须落在根内,或落在 additionalReadRoots 内(含各自子目录)
|
|
32
|
+
* - 写路径必须落在根内,或落在 additionalWriteRoots 内(含各自子目录)
|
|
33
|
+
* - local_exec_shell 的 cwd 默认改为项目根;显式 cwd 必须落在上述可写根内
|
|
32
34
|
* 无项目对话(null/缺省)行为与之前完全一致。
|
|
33
35
|
*/
|
|
34
36
|
export interface ToolExecContext {
|
|
35
37
|
projectRoot?: string | null;
|
|
36
38
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
+
* 额外只读根(会话附件目录、源文件夹)。只放宽 local_read_file。
|
|
40
|
+
* 每个根包含其全部子目录。
|
|
39
41
|
*/
|
|
40
42
|
additionalReadRoots?: string[] | null;
|
|
43
|
+
/**
|
|
44
|
+
* 额外可写根(项目源文件夹)。放宽写入、编辑和命令 cwd。
|
|
45
|
+
* 每个根包含其全部子目录。附件目录不在这里。
|
|
46
|
+
*/
|
|
47
|
+
additionalWriteRoots?: string[] | null;
|
|
41
48
|
/**
|
|
42
49
|
* 4.6a:调用发生的 chat。task_run / task_status / task_result 需要它把
|
|
43
50
|
* 任务绑定到来源对话(任务表按 chatId 归组);reverse-tools 从
|
|
@@ -141,6 +148,10 @@ export declare class ToolRouter {
|
|
|
141
148
|
taskService: TaskService;
|
|
142
149
|
worktreeService: WorktreeService;
|
|
143
150
|
} | null): void;
|
|
151
|
+
/** 会话目标的持久化。缺省写到用户数据目录;测试注入临时文件。 */
|
|
152
|
+
private goalStore;
|
|
153
|
+
setGoalStore(store: GoalStore | null): void;
|
|
154
|
+
private goals;
|
|
144
155
|
setWebTools(delegate: WebToolDelegate | null, names?: readonly string[]): void;
|
|
145
156
|
setHostedWebSearch(handler: ((args: Record<string, unknown>) => Promise<unknown>) | null): void;
|
|
146
157
|
/**
|
|
@@ -168,6 +179,10 @@ export declare class ToolRouter {
|
|
|
168
179
|
* destructive,走宿主审批链。
|
|
169
180
|
*/
|
|
170
181
|
private listPluginToolSchemas;
|
|
182
|
+
/**
|
|
183
|
+
* 内容搜索、PowerShell、会话目标。后台任务的 job_* 跟 task 服务一起出场。
|
|
184
|
+
*/
|
|
185
|
+
private listParityToolSchemas;
|
|
171
186
|
/**
|
|
172
187
|
* 4.6a/4.6b 任务与 worktree 工具的 schema。task_run 立即返回
|
|
173
188
|
* (任务在后台独立流里跨 turn 跑完);worktree_* 操作绑定项目的
|
|
@@ -204,6 +219,8 @@ export declare class ToolRouter {
|
|
|
204
219
|
* 项目围栏按 chat 解析)。schema 出场 ⇒ 服务已接线,这里只缺 chatId 时
|
|
205
220
|
* 抛错(reverse-tools 总是透传;缺了就是接线 bug,响亮失败)。
|
|
206
221
|
*/
|
|
222
|
+
private executeGoalTool;
|
|
223
|
+
private executeJobTool;
|
|
207
224
|
private requireTaskContext;
|
|
208
225
|
/**
|
|
209
226
|
* Agent 写出 `…/skills/<name>/SKILL.md` 且该目录还不在已知 skill root 上时,
|
|
@@ -211,12 +228,13 @@ export declare class ToolRouter {
|
|
|
211
228
|
*/
|
|
212
229
|
private autoInstallSkillOnSuccess;
|
|
213
230
|
/**
|
|
214
|
-
* 项目模式硬沙箱(cwd
|
|
215
|
-
* 模型显式给的 cwd
|
|
216
|
-
*
|
|
231
|
+
* 项目模式硬沙箱(cwd 维度):有可写根时,cwd 默认收到项目家目录;
|
|
232
|
+
* 模型显式给的 cwd 必须落在家目录或额外可写根(源文件夹)内,含各自子目录。
|
|
233
|
+
* 相对 cwd 按家目录解析(而不是进程 cwd)。
|
|
217
234
|
*
|
|
218
235
|
* 注意边界:shell 命令文本里内嵌的绝对路径无法可靠解析,围栏只覆盖
|
|
219
|
-
* cwd
|
|
236
|
+
* cwd 与文件读写工具。命令实际写盘还受 exec 沙箱的 writableRoots 约束,
|
|
237
|
+
* 那些根同样包含子目录。
|
|
220
238
|
*
|
|
221
239
|
* local_exec_shell 与 local_run_snippet 共用本 helper。
|
|
222
240
|
*/
|
package/dist/tool-router.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { buildProjectRootViolation, isPathWithinRoot, } from './local-executor.js';
|
|
3
|
+
import { buildProjectRootViolation, expandFencePath, isPathWithinRoot, writableFenceRoots, writableFenceViolation, } from './local-executor.js';
|
|
4
4
|
import { mcpExecutor } from './mcp-executor.js';
|
|
5
5
|
import { rankTools, resolveMaxResults, TOOL_SEARCH_DEFAULT_MAX_RESULTS, } from './tool-search-rank.js';
|
|
6
6
|
import { maybeAutoInstallWrittenSkill } from './local-backend/skill-install.js';
|
|
@@ -9,6 +9,8 @@ import { processViewImage } from './image-attachment.js';
|
|
|
9
9
|
import { PRESENT_FILES_SCHEMA, PRESENT_FILES_TOOL_NAME, executePresentFiles, } from './present-files.js';
|
|
10
10
|
import { isHostToolCapabilityEnabled } from './host-tools.js';
|
|
11
11
|
import { getResolvedHostTools } from './host-tools-runtime.js';
|
|
12
|
+
import { GoalStore, GOAL_ACTIONS } from './goal-store.js';
|
|
13
|
+
import { searchGlob, searchGrep } from './workspace-search.js';
|
|
12
14
|
/** 已注册 MCP 服务的动态工具名前缀:`mcp__<serverKey>__<toolName>`。 */
|
|
13
15
|
export const MCP_DYNAMIC_TOOL_PREFIX = 'mcp__';
|
|
14
16
|
/**
|
|
@@ -97,6 +99,16 @@ export class ToolRouter {
|
|
|
97
99
|
this.taskService = services?.taskService ?? null;
|
|
98
100
|
this.worktreeService = services?.worktreeService ?? null;
|
|
99
101
|
}
|
|
102
|
+
/** 会话目标的持久化。缺省写到用户数据目录;测试注入临时文件。 */
|
|
103
|
+
goalStore = null;
|
|
104
|
+
setGoalStore(store) {
|
|
105
|
+
this.goalStore = store;
|
|
106
|
+
}
|
|
107
|
+
goals() {
|
|
108
|
+
if (!this.goalStore)
|
|
109
|
+
this.goalStore = GoalStore.default();
|
|
110
|
+
return this.goalStore;
|
|
111
|
+
}
|
|
100
112
|
setWebTools(delegate, names = []) {
|
|
101
113
|
this.webToolDelegate = delegate;
|
|
102
114
|
this.webToolNames = new Set(names);
|
|
@@ -160,7 +172,8 @@ export class ToolRouter {
|
|
|
160
172
|
return [
|
|
161
173
|
{
|
|
162
174
|
name: 'local_exec_shell',
|
|
163
|
-
description: 'Execute a
|
|
175
|
+
description: 'Execute a shell command in a pseudo-terminal. Set pty to false for a plain pipe. ' +
|
|
176
|
+
'When this host is configured with a remote endpoint, the command runs on that host instead of this machine.',
|
|
164
177
|
mode: 'destructive',
|
|
165
178
|
inputSchema: {
|
|
166
179
|
type: 'object',
|
|
@@ -168,6 +181,7 @@ export class ToolRouter {
|
|
|
168
181
|
command: { type: 'string' },
|
|
169
182
|
cwd: { type: 'string' },
|
|
170
183
|
timeout: { type: 'number', description: 'Timeout in milliseconds (e.g. 30000). If a small number like 15 or 30 is provided, it is assumed to be in seconds and automatically multiplied by 1000.' },
|
|
184
|
+
pty: { type: 'boolean', description: 'Run in a pseudo-terminal. Defaults to true.' },
|
|
171
185
|
},
|
|
172
186
|
required: ['command'],
|
|
173
187
|
},
|
|
@@ -365,32 +379,35 @@ export class ToolRouter {
|
|
|
365
379
|
},
|
|
366
380
|
{
|
|
367
381
|
name: 'mcp_list_tools',
|
|
368
|
-
description: 'List tools from one MCP server',
|
|
382
|
+
description: 'List tools from one MCP server. Pass command for stdio or url for unauthenticated Streamable HTTP.',
|
|
369
383
|
mode: 'external',
|
|
370
384
|
inputSchema: {
|
|
371
385
|
type: 'object',
|
|
372
386
|
properties: {
|
|
373
387
|
command: { type: 'string' },
|
|
388
|
+
url: { type: 'string' },
|
|
374
389
|
args: { type: 'array', items: { type: 'string' } },
|
|
375
390
|
cwd: { type: 'string' },
|
|
376
391
|
},
|
|
377
|
-
|
|
392
|
+
additionalProperties: false,
|
|
378
393
|
},
|
|
379
394
|
},
|
|
380
395
|
{
|
|
381
396
|
name: 'mcp_tool_exec',
|
|
382
|
-
description: 'Execute one MCP tool',
|
|
397
|
+
description: 'Execute one MCP tool. Pass command for stdio or url for unauthenticated Streamable HTTP.',
|
|
383
398
|
mode: 'external',
|
|
384
399
|
inputSchema: {
|
|
385
400
|
type: 'object',
|
|
386
401
|
properties: {
|
|
387
402
|
command: { type: 'string' },
|
|
403
|
+
url: { type: 'string' },
|
|
388
404
|
args: { type: 'array', items: { type: 'string' } },
|
|
389
405
|
cwd: { type: 'string' },
|
|
390
406
|
toolName: { type: 'string' },
|
|
391
407
|
toolArgs: { type: 'object' },
|
|
392
408
|
},
|
|
393
|
-
required: ['
|
|
409
|
+
required: ['toolName'],
|
|
410
|
+
additionalProperties: false,
|
|
394
411
|
},
|
|
395
412
|
},
|
|
396
413
|
// deferred 层的发现缝(Wave 2 工具分层):MCP 动态工具不占每轮
|
|
@@ -415,6 +432,7 @@ export class ToolRouter {
|
|
|
415
432
|
additionalProperties: false,
|
|
416
433
|
},
|
|
417
434
|
},
|
|
435
|
+
...this.listParityToolSchemas(),
|
|
418
436
|
// ─── 4.6a/4.6b 跨 turn 后台任务 + git worktree 隔离 ─────────────
|
|
419
437
|
// 服务未接线(CLI/test)时一个 schema 都不出——模型看不到不可用的工具。
|
|
420
438
|
...this.listTaskAndWorktreeSchemas(),
|
|
@@ -482,6 +500,105 @@ export class ToolRouter {
|
|
|
482
500
|
},
|
|
483
501
|
];
|
|
484
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* 内容搜索、PowerShell、会话目标。后台任务的 job_* 跟 task 服务一起出场。
|
|
505
|
+
*/
|
|
506
|
+
listParityToolSchemas() {
|
|
507
|
+
return [
|
|
508
|
+
{
|
|
509
|
+
name: 'grep',
|
|
510
|
+
description: 'Search file contents under the project (or an optional subdirectory). '
|
|
511
|
+
+ 'pattern is a JavaScript regular expression. Optional glob limits which relative paths are scanned. '
|
|
512
|
+
+ 'Skips dependency and VCS directories. Returns {path, line, text} hits. Prefer this over shell rg/grep.',
|
|
513
|
+
mode: 'read',
|
|
514
|
+
inputSchema: {
|
|
515
|
+
type: 'object',
|
|
516
|
+
properties: {
|
|
517
|
+
pattern: { type: 'string', description: 'JavaScript regular expression matched per line.' },
|
|
518
|
+
path: { type: 'string', description: 'Directory to search. Defaults to the project root.' },
|
|
519
|
+
glob: { type: 'string', description: 'Optional fnmatch, e.g. "*.ts" or "src/**/*.py".' },
|
|
520
|
+
},
|
|
521
|
+
required: ['pattern'],
|
|
522
|
+
additionalProperties: false,
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
name: 'glob',
|
|
527
|
+
description: 'List project files whose relative path matches a glob. A pattern without "/" matches any directory. '
|
|
528
|
+
+ 'Prefer this over shell find or ls.',
|
|
529
|
+
mode: 'read',
|
|
530
|
+
inputSchema: {
|
|
531
|
+
type: 'object',
|
|
532
|
+
properties: {
|
|
533
|
+
pattern: { type: 'string', description: 'Glob, e.g. "*.ts" or "src/**/*.py".' },
|
|
534
|
+
path: { type: 'string', description: 'Directory to search. Defaults to the project root.' },
|
|
535
|
+
},
|
|
536
|
+
required: ['pattern'],
|
|
537
|
+
additionalProperties: false,
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: 'pwsh',
|
|
542
|
+
description: 'Run a PowerShell command. On Windows this is powershell.exe; elsewhere it runs pwsh '
|
|
543
|
+
+ '(PowerShell 7) and fails clearly when that binary is not installed. '
|
|
544
|
+
+ 'Use local_exec_shell for the platform default shell.',
|
|
545
|
+
mode: 'destructive',
|
|
546
|
+
inputSchema: {
|
|
547
|
+
type: 'object',
|
|
548
|
+
properties: {
|
|
549
|
+
command: { type: 'string' },
|
|
550
|
+
cwd: { type: 'string' },
|
|
551
|
+
timeout: {
|
|
552
|
+
type: 'number',
|
|
553
|
+
description: 'Timeout in milliseconds. A small number like 30 is treated as seconds.',
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
required: ['command'],
|
|
557
|
+
additionalProperties: false,
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: 'get_goal',
|
|
562
|
+
description: 'Read this chat\'s persisted goal, including the id and revision that update_goal requires. '
|
|
563
|
+
+ 'goal is null when none exists.',
|
|
564
|
+
mode: 'read',
|
|
565
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
name: 'create_goal',
|
|
569
|
+
description: 'Create a persisted goal for this chat when the user\'s request is a long-running objective. '
|
|
570
|
+
+ 'Not for single-turn work. Fails if an unfinished goal already exists — update that one instead.',
|
|
571
|
+
mode: 'safe_write',
|
|
572
|
+
inputSchema: {
|
|
573
|
+
type: 'object',
|
|
574
|
+
properties: {
|
|
575
|
+
objective: { type: 'string', description: 'What done looks like, as a result rather than an activity.' },
|
|
576
|
+
},
|
|
577
|
+
required: ['objective'],
|
|
578
|
+
additionalProperties: false,
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
name: 'update_goal',
|
|
583
|
+
description: 'Change the current goal. Pass the id and revision from get_goal. '
|
|
584
|
+
+ 'action is edit (needs objective), pause, resume, complete, or blocked (needs reason). '
|
|
585
|
+
+ 'A stale revision is rejected; re-read and retry.',
|
|
586
|
+
mode: 'safe_write',
|
|
587
|
+
inputSchema: {
|
|
588
|
+
type: 'object',
|
|
589
|
+
properties: {
|
|
590
|
+
id: { type: 'string' },
|
|
591
|
+
revision: { type: 'integer' },
|
|
592
|
+
action: { type: 'string', enum: [...GOAL_ACTIONS] },
|
|
593
|
+
objective: { type: 'string', description: 'Required for action=edit.' },
|
|
594
|
+
reason: { type: 'string', description: 'Required for action=blocked.' },
|
|
595
|
+
},
|
|
596
|
+
required: ['id', 'revision', 'action'],
|
|
597
|
+
additionalProperties: false,
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
];
|
|
601
|
+
}
|
|
485
602
|
/**
|
|
486
603
|
* 4.6a/4.6b 任务与 worktree 工具的 schema。task_run 立即返回
|
|
487
604
|
* (任务在后台独立流里跨 turn 跑完);worktree_* 操作绑定项目的
|
|
@@ -594,6 +711,46 @@ export class ToolRouter {
|
|
|
594
711
|
additionalProperties: false,
|
|
595
712
|
},
|
|
596
713
|
},
|
|
714
|
+
{
|
|
715
|
+
name: 'job_list',
|
|
716
|
+
description: 'List this chat\'s background jobs (the same records task_run creates). '
|
|
717
|
+
+ 'Each row includes job_id (the taskId), status, and the task text.',
|
|
718
|
+
mode: 'read',
|
|
719
|
+
inputSchema: {
|
|
720
|
+
type: 'object',
|
|
721
|
+
properties: {},
|
|
722
|
+
additionalProperties: false,
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
name: 'job_output',
|
|
727
|
+
description: 'Read the final answer of a background job. job_id is the taskId from task_run or job_list. '
|
|
728
|
+
+ 'Fails with needsFollowup while the job is still running.',
|
|
729
|
+
mode: 'read',
|
|
730
|
+
inputSchema: {
|
|
731
|
+
type: 'object',
|
|
732
|
+
properties: {
|
|
733
|
+
job_id: { type: 'string', description: 'taskId of the background job.' },
|
|
734
|
+
},
|
|
735
|
+
required: ['job_id'],
|
|
736
|
+
additionalProperties: false,
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
name: 'job_kill',
|
|
741
|
+
description: 'Stop a running or blocked background job. A finished job cannot be killed. '
|
|
742
|
+
+ 'job_id is the taskId from task_run or job_list.',
|
|
743
|
+
mode: 'destructive',
|
|
744
|
+
inputSchema: {
|
|
745
|
+
type: 'object',
|
|
746
|
+
properties: {
|
|
747
|
+
job_id: { type: 'string', description: 'taskId of the background job.' },
|
|
748
|
+
reason: { type: 'string', description: 'Optional note stored on the job.' },
|
|
749
|
+
},
|
|
750
|
+
required: ['job_id'],
|
|
751
|
+
additionalProperties: false,
|
|
752
|
+
},
|
|
753
|
+
},
|
|
597
754
|
{
|
|
598
755
|
name: 'worktree_remove',
|
|
599
756
|
description: 'Remove a managed worktree and delete its branch — the DISCARD path, any unmerged changes are lost. '
|
|
@@ -738,7 +895,8 @@ export class ToolRouter {
|
|
|
738
895
|
command: String(args.command || ''),
|
|
739
896
|
cwd: typeof args.cwd === 'string' ? args.cwd : undefined,
|
|
740
897
|
timeout: typeof args.timeout === 'number' ? args.timeout : undefined,
|
|
741
|
-
|
|
898
|
+
pty: args.pty !== false,
|
|
899
|
+
}, projectRoot, context?.additionalWriteRoots ?? null);
|
|
742
900
|
case 'local_read_file':
|
|
743
901
|
return await this.localExecutor.readLocalFile({
|
|
744
902
|
path: String(args.path || ''),
|
|
@@ -776,7 +934,7 @@ export class ToolRouter {
|
|
|
776
934
|
content: String(args.content || ''),
|
|
777
935
|
createDirs: Boolean(args.createDirs),
|
|
778
936
|
expectedVersion: typeof args.expectedVersion === 'string' ? args.expectedVersion : undefined,
|
|
779
|
-
}, projectRoot);
|
|
937
|
+
}, projectRoot, context?.additionalWriteRoots ?? null);
|
|
780
938
|
this.autoInstallSkillOnSuccess(written, args.path);
|
|
781
939
|
return written;
|
|
782
940
|
}
|
|
@@ -793,7 +951,7 @@ export class ToolRouter {
|
|
|
793
951
|
path: String(args.path || ''),
|
|
794
952
|
edits,
|
|
795
953
|
expectedVersion: typeof args.expectedVersion === 'string' ? args.expectedVersion : undefined,
|
|
796
|
-
}, projectRoot);
|
|
954
|
+
}, projectRoot, context?.additionalWriteRoots ?? null);
|
|
797
955
|
this.autoInstallSkillOnSuccess(edited, args.path);
|
|
798
956
|
return edited;
|
|
799
957
|
}
|
|
@@ -805,7 +963,7 @@ export class ToolRouter {
|
|
|
805
963
|
});
|
|
806
964
|
case 'local_run_snippet': {
|
|
807
965
|
// 与 local_exec_shell 同款项目沙箱:cwd 默认项目根,显式越界拒绝。
|
|
808
|
-
const sandboxed = this.applyProjectCwdSandbox({ cwd: typeof args.cwd === 'string' ? args.cwd : undefined }, projectRoot);
|
|
966
|
+
const sandboxed = this.applyProjectCwdSandbox({ cwd: typeof args.cwd === 'string' ? args.cwd : undefined }, projectRoot, context?.additionalWriteRoots ?? null);
|
|
809
967
|
if ('error' in sandboxed)
|
|
810
968
|
return sandboxed.error;
|
|
811
969
|
return await this.localExecutor.runCode({
|
|
@@ -859,6 +1017,47 @@ export class ToolRouter {
|
|
|
859
1017
|
await this.refreshPluginTools();
|
|
860
1018
|
return result;
|
|
861
1019
|
}
|
|
1020
|
+
case 'grep':
|
|
1021
|
+
case 'glob': {
|
|
1022
|
+
const requested = typeof args.path === 'string' ? args.path.trim() : '';
|
|
1023
|
+
const start = requested
|
|
1024
|
+
? this.resolveReadablePath(requested, projectRoot, context?.additionalReadRoots ?? null)
|
|
1025
|
+
: { path: projectRoot ? expandFencePath(projectRoot) : process.cwd() };
|
|
1026
|
+
if ('error' in start) {
|
|
1027
|
+
return { success: false, error: start.error, needsFollowup: true };
|
|
1028
|
+
}
|
|
1029
|
+
if (call.name === 'grep') {
|
|
1030
|
+
return searchGrep({
|
|
1031
|
+
root: start.path,
|
|
1032
|
+
pattern: String(args.pattern || ''),
|
|
1033
|
+
glob: typeof args.glob === 'string' ? args.glob : undefined,
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
return searchGlob({ root: start.path, pattern: String(args.pattern || '') });
|
|
1037
|
+
}
|
|
1038
|
+
case 'pwsh': {
|
|
1039
|
+
const script = String(args.command || '');
|
|
1040
|
+
if (!script.trim()) {
|
|
1041
|
+
return { success: false, error: 'command is required', needsFollowup: true };
|
|
1042
|
+
}
|
|
1043
|
+
const command = process.platform === 'win32'
|
|
1044
|
+
? script
|
|
1045
|
+
: `pwsh -NoProfile -NonInteractive -Command ${JSON.stringify(script)}`;
|
|
1046
|
+
return await this.executeShell({
|
|
1047
|
+
command,
|
|
1048
|
+
cwd: typeof args.cwd === 'string' ? args.cwd : undefined,
|
|
1049
|
+
timeout: typeof args.timeout === 'number' ? args.timeout : undefined,
|
|
1050
|
+
shell: process.platform === 'win32' ? 'powershell' : undefined,
|
|
1051
|
+
}, projectRoot, context?.additionalWriteRoots ?? null);
|
|
1052
|
+
}
|
|
1053
|
+
case 'get_goal':
|
|
1054
|
+
case 'create_goal':
|
|
1055
|
+
case 'update_goal':
|
|
1056
|
+
return this.executeGoalTool(call.name, args, context);
|
|
1057
|
+
case 'job_list':
|
|
1058
|
+
case 'job_output':
|
|
1059
|
+
case 'job_kill':
|
|
1060
|
+
return this.executeJobTool(call.name, args, context);
|
|
862
1061
|
case 'task_run': {
|
|
863
1062
|
const chatId = this.requireTaskContext(context);
|
|
864
1063
|
const dependsOn = Array.isArray(args.dependsOn)
|
|
@@ -927,6 +1126,46 @@ export class ToolRouter {
|
|
|
927
1126
|
* 项目围栏按 chat 解析)。schema 出场 ⇒ 服务已接线,这里只缺 chatId 时
|
|
928
1127
|
* 抛错(reverse-tools 总是透传;缺了就是接线 bug,响亮失败)。
|
|
929
1128
|
*/
|
|
1129
|
+
async executeGoalTool(name, args, context) {
|
|
1130
|
+
const chatId = context?.chatId;
|
|
1131
|
+
if (!chatId) {
|
|
1132
|
+
return { success: false, error: '目标工具需要 chatId 调用上下文', needsFollowup: true };
|
|
1133
|
+
}
|
|
1134
|
+
const store = this.goals();
|
|
1135
|
+
if (name === 'get_goal')
|
|
1136
|
+
return store.get(chatId);
|
|
1137
|
+
if (name === 'create_goal')
|
|
1138
|
+
return store.create(chatId, String(args.objective || ''));
|
|
1139
|
+
return store.update({
|
|
1140
|
+
chatId,
|
|
1141
|
+
id: String(args.id || ''),
|
|
1142
|
+
revision: typeof args.revision === 'number' ? args.revision : Number.NaN,
|
|
1143
|
+
action: String(args.action || ''),
|
|
1144
|
+
objective: typeof args.objective === 'string' ? args.objective : undefined,
|
|
1145
|
+
reason: typeof args.reason === 'string' ? args.reason : undefined,
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
async executeJobTool(name, args, context) {
|
|
1149
|
+
const chatId = this.requireTaskContext(context);
|
|
1150
|
+
const jobId = String(args.job_id || '');
|
|
1151
|
+
if (name === 'job_list') {
|
|
1152
|
+
const listed = await this.taskService.status(chatId);
|
|
1153
|
+
const tasks = Array.isArray(listed.tasks) ? listed.tasks : [];
|
|
1154
|
+
return {
|
|
1155
|
+
...listed,
|
|
1156
|
+
jobs: tasks.map((task) => {
|
|
1157
|
+
const row = task;
|
|
1158
|
+
return { ...row, job_id: row.taskId };
|
|
1159
|
+
}),
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
if (!jobId)
|
|
1163
|
+
return { success: false, error: 'job_id 不能为空', needsFollowup: true };
|
|
1164
|
+
if (name === 'job_output')
|
|
1165
|
+
return this.taskService.result(chatId, jobId);
|
|
1166
|
+
const reason = typeof args.reason === 'string' && args.reason.trim() ? args.reason.trim() : undefined;
|
|
1167
|
+
return this.taskService.kill(chatId, jobId, reason);
|
|
1168
|
+
}
|
|
930
1169
|
requireTaskContext(context) {
|
|
931
1170
|
if (!this.taskService || !this.worktreeService) {
|
|
932
1171
|
throw new Error('任务/worktree 服务未接线(main.ts setTaskServices)');
|
|
@@ -955,41 +1194,44 @@ export class ToolRouter {
|
|
|
955
1194
|
}
|
|
956
1195
|
}
|
|
957
1196
|
/**
|
|
958
|
-
* 项目模式硬沙箱(cwd
|
|
959
|
-
* 模型显式给的 cwd
|
|
960
|
-
*
|
|
1197
|
+
* 项目模式硬沙箱(cwd 维度):有可写根时,cwd 默认收到项目家目录;
|
|
1198
|
+
* 模型显式给的 cwd 必须落在家目录或额外可写根(源文件夹)内,含各自子目录。
|
|
1199
|
+
* 相对 cwd 按家目录解析(而不是进程 cwd)。
|
|
961
1200
|
*
|
|
962
1201
|
* 注意边界:shell 命令文本里内嵌的绝对路径无法可靠解析,围栏只覆盖
|
|
963
|
-
* cwd
|
|
1202
|
+
* cwd 与文件读写工具。命令实际写盘还受 exec 沙箱的 writableRoots 约束,
|
|
1203
|
+
* 那些根同样包含子目录。
|
|
964
1204
|
*
|
|
965
1205
|
* local_exec_shell 与 local_run_snippet 共用本 helper。
|
|
966
1206
|
*/
|
|
967
|
-
applyProjectCwdSandbox(request, projectRoot) {
|
|
968
|
-
|
|
1207
|
+
applyProjectCwdSandbox(request, projectRoot, additionalWriteRoots) {
|
|
1208
|
+
const roots = writableFenceRoots(projectRoot, additionalWriteRoots);
|
|
1209
|
+
if (roots.length === 0)
|
|
969
1210
|
return { request };
|
|
970
|
-
const
|
|
1211
|
+
const home = projectRoot ? expandFencePath(projectRoot) : roots[0];
|
|
971
1212
|
if (request.cwd) {
|
|
972
1213
|
const expanded = request.cwd.startsWith('~')
|
|
973
1214
|
? path.join(os.homedir(), request.cwd.slice(1))
|
|
974
1215
|
: request.cwd;
|
|
975
1216
|
const resolvedCwd = path.isAbsolute(expanded)
|
|
976
1217
|
? path.resolve(expanded)
|
|
977
|
-
: path.resolve(
|
|
978
|
-
|
|
1218
|
+
: path.resolve(home, expanded);
|
|
1219
|
+
const violation = writableFenceViolation(resolvedCwd, roots);
|
|
1220
|
+
if (violation) {
|
|
979
1221
|
return {
|
|
980
1222
|
error: {
|
|
981
1223
|
success: false,
|
|
982
|
-
error:
|
|
1224
|
+
error: violation,
|
|
983
1225
|
platform: process.platform,
|
|
984
1226
|
},
|
|
985
1227
|
};
|
|
986
1228
|
}
|
|
987
1229
|
return { request: { ...request, cwd: resolvedCwd } };
|
|
988
1230
|
}
|
|
989
|
-
return { request: { ...request, cwd:
|
|
1231
|
+
return { request: { ...request, cwd: home } };
|
|
990
1232
|
}
|
|
991
|
-
async executeShell(request, projectRoot) {
|
|
992
|
-
const sandboxed = this.applyProjectCwdSandbox(request, projectRoot);
|
|
1233
|
+
async executeShell(request, projectRoot, additionalWriteRoots) {
|
|
1234
|
+
const sandboxed = this.applyProjectCwdSandbox(request, projectRoot, additionalWriteRoots);
|
|
993
1235
|
if ('error' in sandboxed)
|
|
994
1236
|
return sandboxed.error;
|
|
995
1237
|
if (this.shellExecutor) {
|
|
@@ -1021,8 +1263,17 @@ export class ToolRouter {
|
|
|
1021
1263
|
return { path: resolved };
|
|
1022
1264
|
}
|
|
1023
1265
|
resolveMcpConfig(args) {
|
|
1266
|
+
const command = typeof args.command === 'string' ? args.command.trim() : '';
|
|
1267
|
+
const url = typeof args.url === 'string' ? args.url.trim() : '';
|
|
1268
|
+
if (command && url)
|
|
1269
|
+
throw new Error('MCP 调用只能提供 command 或 url 之一');
|
|
1270
|
+
if (url)
|
|
1271
|
+
return { transport: 'streamable-http', url };
|
|
1272
|
+
if (!command)
|
|
1273
|
+
throw new Error('MCP 调用需要 command(stdio)或 url(Streamable HTTP)');
|
|
1024
1274
|
return {
|
|
1025
|
-
|
|
1275
|
+
transport: 'stdio',
|
|
1276
|
+
command,
|
|
1026
1277
|
args: Array.isArray(args.args) ? args.args.map(item => String(item)) : undefined,
|
|
1027
1278
|
cwd: typeof args.cwd === 'string' ? args.cwd : undefined,
|
|
1028
1279
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主侧 grep / glob。
|
|
3
|
+
*
|
|
4
|
+
* 调用方先把起点收进项目围栏。这里只在该目录内遍历,跳过符号链接,
|
|
5
|
+
* 并丢掉依赖目录和超大文件。命中数有上限。
|
|
6
|
+
*/
|
|
7
|
+
export interface SearchHit {
|
|
8
|
+
path: string;
|
|
9
|
+
line: number;
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
export interface GrepResult {
|
|
13
|
+
success: boolean;
|
|
14
|
+
matches?: SearchHit[];
|
|
15
|
+
truncated?: boolean;
|
|
16
|
+
filesScanned?: number;
|
|
17
|
+
error?: string;
|
|
18
|
+
needsFollowup?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface GlobResult {
|
|
21
|
+
success: boolean;
|
|
22
|
+
files?: string[];
|
|
23
|
+
truncated?: boolean;
|
|
24
|
+
error?: string;
|
|
25
|
+
needsFollowup?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function searchGrep(input: {
|
|
28
|
+
root: string;
|
|
29
|
+
pattern: string;
|
|
30
|
+
glob?: string;
|
|
31
|
+
}): Promise<GrepResult>;
|
|
32
|
+
export declare function searchGlob(input: {
|
|
33
|
+
root: string;
|
|
34
|
+
pattern: string;
|
|
35
|
+
}): Promise<GlobResult>;
|