@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/brand.d.ts
CHANGED
|
@@ -18,6 +18,11 @@ export interface Brand {
|
|
|
18
18
|
agentName: string;
|
|
19
19
|
/** 一句话定位(fallback prompt 用) */
|
|
20
20
|
tagline: string;
|
|
21
|
+
/**
|
|
22
|
+
* 新对话落地页 / 空会话首屏副文案。省略用 shell 默认句;
|
|
23
|
+
* 空字符串表示不显示。只进渲染层,不进系统提示词。
|
|
24
|
+
*/
|
|
25
|
+
homeHint?: string;
|
|
21
26
|
/** 新对话 / 首页未选手动专家时绑定的内置智能体 id */
|
|
22
27
|
defaultAgentId: string;
|
|
23
28
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话目标。每个 chat 一份,写在用户数据目录的 goals.json。
|
|
3
|
+
*
|
|
4
|
+
* 修订号做乐观并发:update_goal 必须带上一次读到的 revision。
|
|
5
|
+
*/
|
|
6
|
+
export type GoalPhase = 'active' | 'paused' | 'blocked' | 'complete';
|
|
7
|
+
export type GoalAction = 'edit' | 'pause' | 'resume' | 'complete' | 'blocked';
|
|
8
|
+
export declare const GOAL_ACTIONS: readonly GoalAction[];
|
|
9
|
+
export interface StoredGoal {
|
|
10
|
+
id: string;
|
|
11
|
+
chatId: string;
|
|
12
|
+
revision: number;
|
|
13
|
+
objective: string;
|
|
14
|
+
phase: GoalPhase;
|
|
15
|
+
blockedReason?: string;
|
|
16
|
+
updatedAt: number;
|
|
17
|
+
}
|
|
18
|
+
export interface GoalToolResult {
|
|
19
|
+
success: boolean;
|
|
20
|
+
goal?: StoredGoal | null;
|
|
21
|
+
error?: string;
|
|
22
|
+
needsFollowup?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare class GoalStore {
|
|
25
|
+
private readonly filePath;
|
|
26
|
+
private pending;
|
|
27
|
+
constructor(filePath: string);
|
|
28
|
+
static default(): GoalStore;
|
|
29
|
+
get(chatId: string): Promise<GoalToolResult>;
|
|
30
|
+
create(chatId: string, objective: string): Promise<GoalToolResult>;
|
|
31
|
+
update(input: {
|
|
32
|
+
chatId: string;
|
|
33
|
+
id: string;
|
|
34
|
+
revision: number;
|
|
35
|
+
action: string;
|
|
36
|
+
objective?: string;
|
|
37
|
+
reason?: string;
|
|
38
|
+
}): Promise<GoalToolResult>;
|
|
39
|
+
private queue;
|
|
40
|
+
private load;
|
|
41
|
+
private read;
|
|
42
|
+
private write;
|
|
43
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话目标。每个 chat 一份,写在用户数据目录的 goals.json。
|
|
3
|
+
*
|
|
4
|
+
* 修订号做乐观并发:update_goal 必须带上一次读到的 revision。
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { getUserDataDir } from './runtime.js';
|
|
10
|
+
export const GOAL_ACTIONS = [
|
|
11
|
+
'edit',
|
|
12
|
+
'pause',
|
|
13
|
+
'resume',
|
|
14
|
+
'complete',
|
|
15
|
+
'blocked',
|
|
16
|
+
];
|
|
17
|
+
export class GoalStore {
|
|
18
|
+
filePath;
|
|
19
|
+
pending = Promise.resolve();
|
|
20
|
+
constructor(filePath) {
|
|
21
|
+
this.filePath = filePath;
|
|
22
|
+
}
|
|
23
|
+
static default() {
|
|
24
|
+
return new GoalStore(path.join(getUserDataDir(), 'goals.json'));
|
|
25
|
+
}
|
|
26
|
+
async get(chatId) {
|
|
27
|
+
const data = await this.load();
|
|
28
|
+
return { success: true, goal: data.goals[chatId] ?? null };
|
|
29
|
+
}
|
|
30
|
+
async create(chatId, objective) {
|
|
31
|
+
const text = objective.trim();
|
|
32
|
+
if (!text)
|
|
33
|
+
return { success: false, error: 'objective 不能为空', needsFollowup: true };
|
|
34
|
+
return this.queue(async () => {
|
|
35
|
+
const data = await this.read();
|
|
36
|
+
const existing = data.goals[chatId];
|
|
37
|
+
if (existing && existing.phase !== 'complete') {
|
|
38
|
+
return {
|
|
39
|
+
success: false,
|
|
40
|
+
error: `本会话已有未完成目标(${existing.id},revision ${existing.revision})。先 update_goal 完成或改写它。`,
|
|
41
|
+
goal: existing,
|
|
42
|
+
needsFollowup: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const goal = {
|
|
46
|
+
id: randomUUID(),
|
|
47
|
+
chatId,
|
|
48
|
+
revision: 1,
|
|
49
|
+
objective: text,
|
|
50
|
+
phase: 'active',
|
|
51
|
+
updatedAt: Date.now(),
|
|
52
|
+
};
|
|
53
|
+
data.goals[chatId] = goal;
|
|
54
|
+
await this.write(data);
|
|
55
|
+
return { success: true, goal };
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async update(input) {
|
|
59
|
+
if (!GOAL_ACTIONS.includes(input.action)) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
error: `action 必须是 ${GOAL_ACTIONS.join(' | ')}`,
|
|
63
|
+
needsFollowup: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const action = input.action;
|
|
67
|
+
return this.queue(async () => {
|
|
68
|
+
const data = await this.read();
|
|
69
|
+
const current = data.goals[input.chatId];
|
|
70
|
+
if (!current || current.id !== input.id) {
|
|
71
|
+
return { success: false, error: '目标不存在或 id 不匹配。先 get_goal。', needsFollowup: true };
|
|
72
|
+
}
|
|
73
|
+
if (current.revision !== input.revision) {
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
error: `revision 已变(当前 ${current.revision})。重新 get_goal 后再更新。`,
|
|
77
|
+
goal: current,
|
|
78
|
+
needsFollowup: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const next = applyAction(current, action, input.objective, input.reason);
|
|
82
|
+
if ('error' in next)
|
|
83
|
+
return { success: false, error: next.error, goal: current, needsFollowup: true };
|
|
84
|
+
data.goals[input.chatId] = next;
|
|
85
|
+
await this.write(data);
|
|
86
|
+
return { success: true, goal: next };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
queue(fn) {
|
|
90
|
+
const run = this.pending.then(fn, fn);
|
|
91
|
+
this.pending = run.then(() => undefined, () => undefined);
|
|
92
|
+
return run;
|
|
93
|
+
}
|
|
94
|
+
async load() {
|
|
95
|
+
return this.queue(() => this.read());
|
|
96
|
+
}
|
|
97
|
+
async read() {
|
|
98
|
+
try {
|
|
99
|
+
const raw = await readFile(this.filePath, 'utf8');
|
|
100
|
+
const parsed = JSON.parse(raw);
|
|
101
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.goals)
|
|
102
|
+
return { goals: {} };
|
|
103
|
+
return parsed;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const code = err.code;
|
|
107
|
+
if (code === 'ENOENT')
|
|
108
|
+
return { goals: {} };
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async write(data) {
|
|
113
|
+
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
114
|
+
await writeFile(this.filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function applyAction(current, action, objective, reason) {
|
|
118
|
+
const next = { ...current, revision: current.revision + 1, updatedAt: Date.now() };
|
|
119
|
+
if (action === 'edit') {
|
|
120
|
+
const text = objective?.trim() ?? '';
|
|
121
|
+
if (!text)
|
|
122
|
+
return { error: 'edit 需要 objective' };
|
|
123
|
+
next.objective = text;
|
|
124
|
+
return next;
|
|
125
|
+
}
|
|
126
|
+
if (action === 'pause') {
|
|
127
|
+
if (current.phase !== 'active')
|
|
128
|
+
return { error: `只有 active 目标可以暂停(当前 ${current.phase})` };
|
|
129
|
+
next.phase = 'paused';
|
|
130
|
+
return next;
|
|
131
|
+
}
|
|
132
|
+
if (action === 'resume') {
|
|
133
|
+
if (current.phase !== 'paused' && current.phase !== 'blocked') {
|
|
134
|
+
return { error: `只有 paused 或 blocked 目标可以恢复(当前 ${current.phase})` };
|
|
135
|
+
}
|
|
136
|
+
next.phase = 'active';
|
|
137
|
+
delete next.blockedReason;
|
|
138
|
+
return next;
|
|
139
|
+
}
|
|
140
|
+
if (action === 'complete') {
|
|
141
|
+
if (current.phase === 'complete')
|
|
142
|
+
return { error: '目标已经完成' };
|
|
143
|
+
next.phase = 'complete';
|
|
144
|
+
return next;
|
|
145
|
+
}
|
|
146
|
+
const text = reason?.trim() ?? '';
|
|
147
|
+
if (!text)
|
|
148
|
+
return { error: 'blocked 需要 reason' };
|
|
149
|
+
if (current.phase !== 'active' && current.phase !== 'paused') {
|
|
150
|
+
return { error: `当前阶段 ${current.phase} 不能标为 blocked` };
|
|
151
|
+
}
|
|
152
|
+
next.phase = 'blocked';
|
|
153
|
+
next.blockedReason = text;
|
|
154
|
+
return next;
|
|
155
|
+
}
|
package/dist/host/runtime.js
CHANGED
|
@@ -16,6 +16,7 @@ import { LocalScriptRegistry } from '../local-script-registry.js';
|
|
|
16
16
|
import { TerminalManager } from '../terminal-manager.js';
|
|
17
17
|
import { ToolRouter } from '../tool-router.js';
|
|
18
18
|
import { McpServerRegistry, } from '../mcp-server-registry.js';
|
|
19
|
+
import { mcpExecutor } from '../mcp-executor.js';
|
|
19
20
|
import { ProjectRegistry } from '../project-registry.js';
|
|
20
21
|
import { getProductConfig } from '../product-config.js';
|
|
21
22
|
import { LocalBackendRouter } from '../local-backend/router.js';
|
|
@@ -120,7 +121,13 @@ export async function createHostRuntime(options) {
|
|
|
120
121
|
if (!projectId)
|
|
121
122
|
return null;
|
|
122
123
|
const project = projectRegistry.get(projectId);
|
|
123
|
-
return project
|
|
124
|
+
return project
|
|
125
|
+
? {
|
|
126
|
+
name: project.name,
|
|
127
|
+
folderPath: project.folderPath,
|
|
128
|
+
sourceFolders: project.sourceFolders ?? [],
|
|
129
|
+
}
|
|
130
|
+
: null;
|
|
124
131
|
};
|
|
125
132
|
// 场景包装配(2.3):产品组装根(products/<id>/active.ts)在 import 期
|
|
126
133
|
// 把包装配函数注册进 pack-assembly 注册表;这里统一装配。宿主不 import
|
|
@@ -214,8 +221,8 @@ export async function createHostRuntime(options) {
|
|
|
214
221
|
store: defaultStore,
|
|
215
222
|
toolRouter,
|
|
216
223
|
resolveProjectRoot: async (chatId) => localBackendRouter.resolveChatWorkspaceRoot(chatId),
|
|
217
|
-
//
|
|
218
|
-
//
|
|
224
|
+
// 读:项目家目录之外,附件目录和源文件夹(含各自子目录)也可读。
|
|
225
|
+
// 写:源文件夹及其子目录与家目录同一档;附件目录保持只读。
|
|
219
226
|
resolveAdditionalReadRoots: async (chatId) => {
|
|
220
227
|
const project = await localBackendRouter.resolveChatProject(chatId);
|
|
221
228
|
return [
|
|
@@ -223,6 +230,10 @@ export async function createHostRuntime(options) {
|
|
|
223
230
|
...(project?.sourceFolders ?? []),
|
|
224
231
|
];
|
|
225
232
|
},
|
|
233
|
+
resolveAdditionalWriteRoots: async (chatId) => {
|
|
234
|
+
const project = await localBackendRouter.resolveChatProject(chatId);
|
|
235
|
+
return project?.sourceFolders ?? [];
|
|
236
|
+
},
|
|
226
237
|
approvalHandler: approvalBridge.handler,
|
|
227
238
|
askUserHandler: askUserBridge.handler,
|
|
228
239
|
// P2b: resume 时 sidecar 把记录里的读证据推给 LocalExecutor 的
|
|
@@ -254,6 +265,7 @@ export async function createHostRuntime(options) {
|
|
|
254
265
|
for (const handle of [...packHandles.values()].reverse()) {
|
|
255
266
|
await handle.dispose?.();
|
|
256
267
|
}
|
|
268
|
+
await mcpExecutor.shutdownAll();
|
|
257
269
|
await shutdownHostSidecar();
|
|
258
270
|
await closeStorage();
|
|
259
271
|
},
|
package/dist/host-tools.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export type ResolvedSettingsChrome = Record<SettingsItemId, boolean>;
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function resolveSettingsChrome(value?: unknown, tools?: ResolvedHostTools): ResolvedSettingsChrome;
|
|
36
36
|
export declare function hasGeneralSettingsChrome(chrome?: ResolvedSettingsChrome): boolean;
|
|
37
|
-
export declare const LOCAL_FS_TOOL_NAMES: readonly ["local_exec_shell", "local_read_file", "view_image", "local_write_file", "local_edit_file", "present_files", "local_open_path", "local_run_snippet", "local_list_scripts", "local_run_script"];
|
|
37
|
+
export declare const LOCAL_FS_TOOL_NAMES: readonly ["local_exec_shell", "local_read_file", "view_image", "local_write_file", "local_edit_file", "present_files", "local_open_path", "local_run_snippet", "local_list_scripts", "local_run_script", "grep", "glob", "pwsh"];
|
|
38
38
|
export declare function resolveHostToolSurface(value: boolean | {
|
|
39
39
|
capability?: boolean;
|
|
40
40
|
chrome?: boolean;
|
package/dist/host-tools.js
CHANGED
|
@@ -84,6 +84,9 @@ export const LOCAL_FS_TOOL_NAMES = [
|
|
|
84
84
|
'local_run_snippet',
|
|
85
85
|
'local_list_scripts',
|
|
86
86
|
'local_run_script',
|
|
87
|
+
'grep',
|
|
88
|
+
'glob',
|
|
89
|
+
'pwsh',
|
|
87
90
|
];
|
|
88
91
|
const FAMILY_TOOL_PREFIX = {
|
|
89
92
|
terminal: [],
|
|
@@ -97,6 +100,9 @@ const FAMILY_TOOL_PREFIX = {
|
|
|
97
100
|
'worktree_create',
|
|
98
101
|
'worktree_list',
|
|
99
102
|
'worktree_remove',
|
|
103
|
+
'job_list',
|
|
104
|
+
'job_output',
|
|
105
|
+
'job_kill',
|
|
100
106
|
],
|
|
101
107
|
mcp: ['mcp_list_tools', 'mcp_tool_exec'],
|
|
102
108
|
web: ['web_search', 'web_fetch'],
|
|
@@ -69,9 +69,14 @@ export interface PortableWebSearchSection {
|
|
|
69
69
|
}
|
|
70
70
|
export interface PortableMcpSection {
|
|
71
71
|
name: string;
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
transport: 'stdio' | 'streamable-http';
|
|
73
|
+
command?: string;
|
|
74
|
+
args?: string[];
|
|
74
75
|
cwd?: string;
|
|
76
|
+
url?: string;
|
|
77
|
+
headers?: Record<string, string>;
|
|
78
|
+
headersFromEnv?: Record<string, string>;
|
|
79
|
+
bearerTokenEnvVar?: string;
|
|
75
80
|
enabled: boolean;
|
|
76
81
|
envIncluded: boolean;
|
|
77
82
|
env?: Record<string, string>;
|
|
@@ -178,11 +183,20 @@ export declare function webSearchSectionFromSettings(settings: {
|
|
|
178
183
|
}, includeSecrets: boolean): PortableWebSearchSection;
|
|
179
184
|
export declare function mcpSectionFromServer(server: {
|
|
180
185
|
name: string;
|
|
186
|
+
transport: 'stdio';
|
|
181
187
|
command: string;
|
|
182
188
|
args: string[];
|
|
183
189
|
cwd?: string;
|
|
184
190
|
enabled: boolean;
|
|
185
191
|
env: Record<string, string>;
|
|
192
|
+
} | {
|
|
193
|
+
name: string;
|
|
194
|
+
transport: 'streamable-http';
|
|
195
|
+
url: string;
|
|
196
|
+
headers: Record<string, string>;
|
|
197
|
+
headersFromEnv: Record<string, string>;
|
|
198
|
+
bearerTokenEnvVar?: string;
|
|
199
|
+
enabled: boolean;
|
|
186
200
|
}, includeSecrets: boolean): PortableMcpSection;
|
|
187
201
|
/**
|
|
188
202
|
* 把消息正文和 metadata 里的旧附件绝对路径换成新会话里的路径。
|
|
@@ -106,15 +106,27 @@ export function webSearchSectionFromSettings(settings, includeSecrets) {
|
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
export function mcpSectionFromServer(server, includeSecrets) {
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
109
|
+
return server.transport === 'stdio'
|
|
110
|
+
? {
|
|
111
|
+
name: server.name,
|
|
112
|
+
transport: 'stdio',
|
|
113
|
+
command: server.command,
|
|
114
|
+
args: [...server.args],
|
|
115
|
+
cwd: server.cwd,
|
|
116
|
+
enabled: server.enabled,
|
|
117
|
+
envIncluded: includeSecrets,
|
|
118
|
+
...(includeSecrets ? { env: { ...server.env } } : {}),
|
|
119
|
+
}
|
|
120
|
+
: {
|
|
121
|
+
name: server.name,
|
|
122
|
+
transport: 'streamable-http',
|
|
123
|
+
url: server.url,
|
|
124
|
+
headers: { ...server.headers },
|
|
125
|
+
headersFromEnv: { ...server.headersFromEnv },
|
|
126
|
+
bearerTokenEnvVar: server.bearerTokenEnvVar,
|
|
127
|
+
enabled: server.enabled,
|
|
128
|
+
envIncluded: false,
|
|
129
|
+
};
|
|
118
130
|
}
|
|
119
131
|
/**
|
|
120
132
|
* 把消息正文和 metadata 里的旧附件绝对路径换成新会话里的路径。
|
|
@@ -180,22 +180,36 @@ async function applyMcp(mcp, servers) {
|
|
|
180
180
|
return 'MCP 注册表不可用';
|
|
181
181
|
for (const server of servers) {
|
|
182
182
|
const name = server.name?.trim();
|
|
183
|
-
|
|
184
|
-
if (!name || !command)
|
|
183
|
+
if (!name)
|
|
185
184
|
continue;
|
|
186
|
-
const input =
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
185
|
+
const input = server.transport === 'streamable-http'
|
|
186
|
+
? {
|
|
187
|
+
name,
|
|
188
|
+
transport: 'streamable-http',
|
|
189
|
+
url: server.url?.trim() ?? '',
|
|
190
|
+
headers: server.headers ?? {},
|
|
191
|
+
headersFromEnv: server.headersFromEnv ?? {},
|
|
192
|
+
bearerTokenEnvVar: server.bearerTokenEnvVar,
|
|
193
|
+
enabled: server.enabled !== false,
|
|
194
|
+
}
|
|
195
|
+
: {
|
|
196
|
+
name,
|
|
197
|
+
transport: 'stdio',
|
|
198
|
+
command: server.command?.trim() ?? '',
|
|
199
|
+
args: Array.isArray(server.args) ? server.args.map((arg) => String(arg)) : [],
|
|
200
|
+
cwd: server.cwd,
|
|
201
|
+
enabled: server.enabled !== false,
|
|
202
|
+
...(server.envIncluded && server.env ? { env: server.env } : {}),
|
|
203
|
+
};
|
|
204
|
+
if ((input.transport === 'stdio' && !input.command) ||
|
|
205
|
+
(input.transport === 'streamable-http' && !input.url)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
194
208
|
const existing = mcp.list().find((item) => item.name.toLowerCase() === name.toLowerCase());
|
|
195
209
|
if (existing)
|
|
196
210
|
mcp.update(existing.id, input);
|
|
197
211
|
else
|
|
198
|
-
mcp.create(
|
|
212
|
+
mcp.create(input);
|
|
199
213
|
}
|
|
200
214
|
return null;
|
|
201
215
|
}
|
|
@@ -10,6 +10,7 @@ import { buildWorldState, getActiveCoreLoopStreamId, streamCoreLoopTurn, } from
|
|
|
10
10
|
import { driveWithAutoContinue, resolveAutoContinueMax, } from './auto-continue-helper.js';
|
|
11
11
|
import { buildAmbientDelegateRoster, buildDelegateDispatchInstruction, buildDelegateRosterHint, buildMentionDelegateRoster, BUILTIN_SUBAGENT_PROFILES, mergeTurnSubagentParam, } from './subagent-profiles.js';
|
|
12
12
|
import { resolveMentionedAgentIds } from './mention-targets.js';
|
|
13
|
+
import { presentToolCall } from '../tool-presentation.js';
|
|
13
14
|
import { readSidecarHistoryEntries, timelineFromHistoryEntries, } from './task-process.js';
|
|
14
15
|
import { appendTimelineDelta, freezeTimelineReasoning, sealLastTimelineBlock, syncTimelineTools, } from './turn-timeline.js';
|
|
15
16
|
import { turnDurationMs } from './turn-duration.js';
|
|
@@ -50,6 +51,14 @@ import { matchPackBackendRoute } from './pack-backend-routes.js';
|
|
|
50
51
|
import { collectTurnFiles } from './turn-files.js';
|
|
51
52
|
import { resolveMentionedPaths } from './mentioned-paths.js';
|
|
52
53
|
import { getAuthProvider } from '../auth/index.js';
|
|
54
|
+
function stringRecord(value) {
|
|
55
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
56
|
+
return {};
|
|
57
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
58
|
+
key,
|
|
59
|
+
String(item),
|
|
60
|
+
]));
|
|
61
|
+
}
|
|
53
62
|
function parseSourceFolders(value) {
|
|
54
63
|
if (!Array.isArray(value))
|
|
55
64
|
return [];
|
|
@@ -1139,7 +1148,7 @@ export class LocalBackendRouter {
|
|
|
1139
1148
|
const servers = registry.list().map((s) => {
|
|
1140
1149
|
const cached = registry.getCachedTools(s.id);
|
|
1141
1150
|
return {
|
|
1142
|
-
...s,
|
|
1151
|
+
...registry.toPublicEntry(s),
|
|
1143
1152
|
serverKey: registry.serverKey(s),
|
|
1144
1153
|
toolCount: cached?.tools.length ?? 0,
|
|
1145
1154
|
toolsPreview: cached?.tools.slice(0, 8).map((t) => t.name) ?? [],
|
|
@@ -1155,19 +1164,35 @@ export class LocalBackendRouter {
|
|
|
1155
1164
|
return { status: 503, data: { error: 'MCP 注册表不可用' } };
|
|
1156
1165
|
try {
|
|
1157
1166
|
const payload = this.toRecord(request.body);
|
|
1158
|
-
const entry =
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
:
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1167
|
+
const entry = payload.transport === 'streamable-http'
|
|
1168
|
+
? registry.create({
|
|
1169
|
+
name: String(payload.name ?? ''),
|
|
1170
|
+
transport: 'streamable-http',
|
|
1171
|
+
url: String(payload.url ?? ''),
|
|
1172
|
+
headers: stringRecord(payload.headers),
|
|
1173
|
+
headersFromEnv: stringRecord(payload.headersFromEnv),
|
|
1174
|
+
bearerTokenEnvVar: typeof payload.bearerTokenEnvVar === 'string'
|
|
1175
|
+
? payload.bearerTokenEnvVar
|
|
1176
|
+
: undefined,
|
|
1177
|
+
reconnect: payload.reconnect &&
|
|
1178
|
+
typeof payload.reconnect === 'object' &&
|
|
1179
|
+
!Array.isArray(payload.reconnect)
|
|
1180
|
+
? payload.reconnect
|
|
1181
|
+
: undefined,
|
|
1182
|
+
enabled: payload.enabled !== false,
|
|
1183
|
+
})
|
|
1184
|
+
: registry.create({
|
|
1185
|
+
name: String(payload.name ?? ''),
|
|
1186
|
+
transport: 'stdio',
|
|
1187
|
+
command: String(payload.command ?? ''),
|
|
1188
|
+
args: Array.isArray(payload.args) ? payload.args.map((a) => String(a)) : [],
|
|
1189
|
+
env: stringRecord(payload.env),
|
|
1190
|
+
cwd: typeof payload.cwd === 'string' ? payload.cwd : undefined,
|
|
1191
|
+
enabled: payload.enabled !== false,
|
|
1192
|
+
});
|
|
1168
1193
|
// 后台拉一次工具列表,下一轮对话即可用;失败不阻塞创建
|
|
1169
1194
|
void registry.refreshTools(entry.id).catch(() => { });
|
|
1170
|
-
return { status: 200, data: { server: entry } };
|
|
1195
|
+
return { status: 200, data: { server: registry.toPublicEntry(entry) } };
|
|
1171
1196
|
}
|
|
1172
1197
|
catch (err) {
|
|
1173
1198
|
return { status: 400, data: { error: err instanceof Error ? err.message : String(err) } };
|
|
@@ -1187,7 +1212,7 @@ export class LocalBackendRouter {
|
|
|
1187
1212
|
return {
|
|
1188
1213
|
status: 200,
|
|
1189
1214
|
data: {
|
|
1190
|
-
added: result.added,
|
|
1215
|
+
added: result.added.map((entry) => registry.toPublicEntry(entry)),
|
|
1191
1216
|
skipped: result.skipped,
|
|
1192
1217
|
},
|
|
1193
1218
|
};
|
|
@@ -1210,17 +1235,32 @@ export class LocalBackendRouter {
|
|
|
1210
1235
|
const payload = this.toRecord(request.body);
|
|
1211
1236
|
const updated = registry.update(serverId, {
|
|
1212
1237
|
name: typeof payload.name === 'string' ? payload.name : undefined,
|
|
1238
|
+
transport: payload.transport === 'stdio' || payload.transport === 'streamable-http'
|
|
1239
|
+
? payload.transport
|
|
1240
|
+
: undefined,
|
|
1213
1241
|
command: typeof payload.command === 'string' ? payload.command : undefined,
|
|
1214
1242
|
args: Array.isArray(payload.args) ? payload.args.map((a) => String(a)) : undefined,
|
|
1215
1243
|
env: payload.env && typeof payload.env === 'object' && !Array.isArray(payload.env)
|
|
1216
|
-
?
|
|
1244
|
+
? stringRecord(payload.env)
|
|
1217
1245
|
: undefined,
|
|
1218
1246
|
cwd: typeof payload.cwd === 'string' ? payload.cwd : undefined,
|
|
1247
|
+
url: typeof payload.url === 'string' ? payload.url : undefined,
|
|
1248
|
+
headers: payload.headers && typeof payload.headers === 'object' && !Array.isArray(payload.headers)
|
|
1249
|
+
? stringRecord(payload.headers)
|
|
1250
|
+
: undefined,
|
|
1251
|
+
headersFromEnv: payload.headersFromEnv &&
|
|
1252
|
+
typeof payload.headersFromEnv === 'object' &&
|
|
1253
|
+
!Array.isArray(payload.headersFromEnv)
|
|
1254
|
+
? stringRecord(payload.headersFromEnv)
|
|
1255
|
+
: undefined,
|
|
1256
|
+
bearerTokenEnvVar: typeof payload.bearerTokenEnvVar === 'string'
|
|
1257
|
+
? payload.bearerTokenEnvVar
|
|
1258
|
+
: undefined,
|
|
1219
1259
|
enabled: typeof payload.enabled === 'boolean' ? payload.enabled : undefined,
|
|
1220
1260
|
});
|
|
1221
1261
|
if (updated.enabled)
|
|
1222
1262
|
void registry.refreshTools(updated.id).catch(() => { });
|
|
1223
|
-
return { status: 200, data: { server: updated } };
|
|
1263
|
+
return { status: 200, data: { server: registry.toPublicEntry(updated) } };
|
|
1224
1264
|
}
|
|
1225
1265
|
catch (err) {
|
|
1226
1266
|
return { status: 400, data: { error: err instanceof Error ? err.message : String(err) } };
|
|
@@ -2448,17 +2488,17 @@ export class LocalBackendRouter {
|
|
|
2448
2488
|
? chatProject.folderPath
|
|
2449
2489
|
: await this.resolveChatWorkspaceRoot(chatId);
|
|
2450
2490
|
if (chatProject) {
|
|
2491
|
+
const sourceFolderLine = chatProject.sourceFolders.length > 0
|
|
2492
|
+
? `源文件夹及其子目录也可读写:${chatProject.sourceFolders.join('、')}。`
|
|
2493
|
+
: '如确需读写家目录外的目录,可请用户把该目录附加为源文件夹。';
|
|
2451
2494
|
systemPrompt +=
|
|
2452
2495
|
`\n\n【项目模式】当前对话绑定项目「${chatProject.name}」,家目录:${chatProject.folderPath}\n` +
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
`如确需读取项目外的文件,可请用户在项目里附加为源文件夹。\n` +
|
|
2460
|
-
`例外:本会话的用户上传附件目录 ${chatAttachmentsDirPath(chatId)} 也在项目家目录外,` +
|
|
2461
|
-
`但它已作为**只读放行根**开放给 local_read_file(用绝对路径可直接读取,不会被项目围栏拒绝)。` +
|
|
2496
|
+
`文件写入(local_write_file / local_edit_file)和命令写入可以落在家目录及其全部子目录内。` +
|
|
2497
|
+
`${sourceFolderLine}` +
|
|
2498
|
+
`这些范围之外的写入会被拒绝。命令未指定 cwd 时在家目录运行;显式 cwd 必须落在上述可写目录(含子目录)内。` +
|
|
2499
|
+
`相对路径按家目录解析。\n` +
|
|
2500
|
+
`例外:本会话的用户上传附件目录 ${chatAttachmentsDirPath(chatId)} 只读,不在可写范围内,` +
|
|
2501
|
+
`但 local_read_file 可以用绝对路径读取。` +
|
|
2462
2502
|
/* shell-neutral:allow generic document extensions, not product identity */ `用户附到本条消息的文件都放在那里;需要转换(docx / pdf / xlsx / pptx 等)时,` +
|
|
2463
2503
|
`把该绝对路径交给 local_exec_shell 的转换脚本处理即可,**不要**因为它在项目家目录外就拒绝读取或要求用户重新拷贝。`;
|
|
2464
2504
|
// W6-5 + W6-7a:项目级规则文件(AGENTS.md / CLAUDE.md)是不可信输入,
|
|
@@ -2481,7 +2521,7 @@ export class LocalBackendRouter {
|
|
|
2481
2521
|
else {
|
|
2482
2522
|
systemPrompt +=
|
|
2483
2523
|
`\n\n【对话工作区】当前对话没有绑定项目。工作区:${workspaceRoot}\n` +
|
|
2484
|
-
`你的文件写入(local_write_file)和命令执行(local_exec_shell
|
|
2524
|
+
`你的文件写入(local_write_file)和命令执行(local_exec_shell)都被限制在该目录及其子目录内:` +
|
|
2485
2525
|
`写入路径越界会被拒绝;命令默认在工作区下运行,显式指定的 cwd 越界也会被拒绝。` +
|
|
2486
2526
|
`请一律使用工作区内的路径(相对路径按工作区解析)。` +
|
|
2487
2527
|
`用户若要把对话绑到已有项目,可在输入框上方选择项目。`;
|
|
@@ -2940,7 +2980,7 @@ export class LocalBackendRouter {
|
|
|
2940
2980
|
// spec (default.harness.yaml) is the single source of truth for
|
|
2941
2981
|
// maxRounds / maxToolErrors, and an explicit request param overrides it.
|
|
2942
2982
|
// W4-2: per-exec sandbox for shell tool calls, default-on. Writable
|
|
2943
|
-
// roots =
|
|
2983
|
+
// roots = 项目家目录(或无项目时的对话工作区)+ 源文件夹。每个根含其子目录。
|
|
2944
2984
|
// `execPolicy: 'full'`(输入框「完整权限」)关闭这一层,让 mkdir
|
|
2945
2985
|
// Downloads 这类工作区外写入不再被 Seatbelt 拦成 Operation not permitted。
|
|
2946
2986
|
const chatProject = await this.resolveChatProject(chatId);
|
|
@@ -2952,6 +2992,7 @@ export class LocalBackendRouter {
|
|
|
2952
2992
|
// 工作区)。
|
|
2953
2993
|
...collectPackExecWritableRoots(chatId),
|
|
2954
2994
|
workspaceRoot,
|
|
2995
|
+
...(chatProject?.sourceFolders ?? []),
|
|
2955
2996
|
], { policy: clampExecPolicy(parseExecPolicy(payload.execPolicy), getResolvedHostTools()) });
|
|
2956
2997
|
// W4-1: approval algebra, host mode — every tool call is gated by the
|
|
2957
2998
|
// Electron approval UI over the reverse channel. The durable scope
|
|
@@ -2989,6 +3030,7 @@ export class LocalBackendRouter {
|
|
|
2989
3030
|
const turnFileRoots = [
|
|
2990
3031
|
...collectPackExecWritableRoots(chatId),
|
|
2991
3032
|
workspaceRoot,
|
|
3033
|
+
...(chatProject?.sourceFolders ?? []),
|
|
2992
3034
|
];
|
|
2993
3035
|
try {
|
|
2994
3036
|
const coreLoopOptions = {
|
|
@@ -3077,7 +3119,7 @@ export class LocalBackendRouter {
|
|
|
3077
3119
|
...(toolPolicy.mode === 'all' ? {} : { toolPolicy }),
|
|
3078
3120
|
},
|
|
3079
3121
|
execSandbox,
|
|
3080
|
-
approval,
|
|
3122
|
+
approval: approval ? { ...approval, writableRoots: turnFileRoots } : undefined,
|
|
3081
3123
|
// delegate-on-pool 统一:模型的多代理面收敛为单工具
|
|
3082
3124
|
// delegate_subagent(默认开)。内置画像集(explore/research/coder)
|
|
3083
3125
|
// 带工具域/轮次/并发/系统提示,模型按画像 description 选委派
|
|
@@ -3110,6 +3152,7 @@ export class LocalBackendRouter {
|
|
|
3110
3152
|
tool: call.tool,
|
|
3111
3153
|
mode: chatMode,
|
|
3112
3154
|
arguments: call.arguments,
|
|
3155
|
+
view: presentToolCall(call.tool, call.arguments),
|
|
3113
3156
|
threw: false,
|
|
3114
3157
|
});
|
|
3115
3158
|
syncTimelineTools(timeline, executedActions);
|
|
@@ -3124,6 +3167,7 @@ export class LocalBackendRouter {
|
|
|
3124
3167
|
tool: action.tool,
|
|
3125
3168
|
mode: chatMode,
|
|
3126
3169
|
arguments: action.arguments,
|
|
3170
|
+
view: presentToolCall(action.tool, action.arguments, action.result),
|
|
3127
3171
|
result: action.result,
|
|
3128
3172
|
success: action.success,
|
|
3129
3173
|
error: action.error,
|
|
@@ -20,8 +20,12 @@ import { loadSkills } from './skill-loader.js';
|
|
|
20
20
|
/** 只读工具域:探索与调研画像共享的「摸不到写操作」基线。 */
|
|
21
21
|
const READ_ONLY_TOOLS = [
|
|
22
22
|
'local_read_file',
|
|
23
|
+
'view_image',
|
|
23
24
|
'local_list_scripts',
|
|
24
25
|
'local_open_path',
|
|
26
|
+
'grep',
|
|
27
|
+
'glob',
|
|
28
|
+
'get_goal',
|
|
25
29
|
'web_search',
|
|
26
30
|
'web_fetch',
|
|
27
31
|
'mcp_list_tools',
|
|
@@ -54,6 +54,7 @@ export interface TaskServiceDeps {
|
|
|
54
54
|
resolveChatProject: (chatId: string) => Promise<{
|
|
55
55
|
name: string;
|
|
56
56
|
folderPath: string;
|
|
57
|
+
sourceFolders?: string[];
|
|
57
58
|
} | null>;
|
|
58
59
|
/**
|
|
59
60
|
* 无项目对话的可写根(Documents/<应用>/conversations/<chatId>)。
|
|
@@ -120,6 +121,11 @@ export declare class TaskService {
|
|
|
120
121
|
private executeTask;
|
|
121
122
|
/** task_status 工具:单任务或本 chat 全部任务的快照。 */
|
|
122
123
|
status(chatId: string, taskId?: string): Promise<Record<string, unknown>>;
|
|
124
|
+
/**
|
|
125
|
+
* job_kill:结束 running 或 blocked 任务。running 先打标记再取消流,
|
|
126
|
+
* 终态回写看到标记后保持 failed,避免流的正常完成把杀死盖掉。
|
|
127
|
+
*/
|
|
128
|
+
kill(chatId: string, taskId: string, reason?: string): Promise<Record<string, unknown>>;
|
|
123
129
|
/** task_result 工具:取终态任务的完整结果。 */
|
|
124
130
|
result(chatId: string, taskId: string): Promise<Record<string, unknown>>;
|
|
125
131
|
/** 喂给模型的任务视图(完整字段,answer 截断防爆上下文)。 */
|