newmark-agent 0.3.12 → 0.4.2
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/cli-commands.d.ts +1 -0
- package/dist/cli-commands.js +11 -2
- package/dist/context/domain/types.d.ts +37 -0
- package/dist/context/services/context-orchestrator.js +2 -0
- package/dist/conversation-utility-host.bundle.cjs +1259 -132
- package/dist/conversation-utility-host.js +3 -0
- package/dist/core/agent.d.ts +139 -5
- package/dist/core/agent.js +964 -82
- package/dist/core/agentKernel/agent-loop.js +29 -3
- package/dist/core/agentKernel/types.d.ts +7 -0
- package/dist/core/agentKernelRunner.d.ts +2 -0
- package/dist/core/agentKernelRunner.js +121 -19
- package/dist/core/conversationKernel.d.ts +5 -0
- package/dist/core/conversationKernel.js +29 -0
- package/dist/core/dshCompatibility.d.ts +198 -0
- package/dist/core/dshCompatibility.js +600 -0
- package/dist/core/electronUtilityAgentClient.d.ts +4 -0
- package/dist/core/electronUtilityAgentClient.js +4 -0
- package/dist/core/electronUtilityRuntimePool.d.ts +8 -0
- package/dist/core/electronUtilityRuntimePool.js +14 -0
- package/dist/core/mcpManager.d.ts +1 -0
- package/dist/core/mcpManager.js +100 -10
- package/dist/core/subagent.d.ts +6 -0
- package/dist/core/subagent.js +22 -1
- package/dist/core/toolPolicy.d.ts +15 -0
- package/dist/core/toolPolicy.js +174 -1
- package/dist/core/types.d.ts +1 -1
- package/dist/core/utilityAgentProtocol.d.ts +8 -1
- package/dist/core/workspace.d.ts +9 -0
- package/dist/core/workspace.js +48 -1
- package/dist/core/wslAgentClient.d.ts +4 -0
- package/dist/core/wslAgentClient.js +4 -0
- package/dist/core/wslAgentProtocol.d.ts +8 -1
- package/dist/core/wslAgentRuntimePool.d.ts +8 -0
- package/dist/core/wslAgentRuntimePool.js +15 -0
- package/dist/launcher.js +8 -0
- package/dist/llm/provider.d.ts +1 -1
- package/dist/llm/provider.js +4 -3
- package/dist/main.js +163 -11
- package/dist/preload.js +11 -0
- package/dist/providers/chat-completions.adapter.js +41 -15
- package/dist/providers/provider-adapter.d.ts +3 -0
- package/dist/toolchain/registry/tool-registry.d.ts +13 -1
- package/dist/toolchain/registry/tool-registry.js +8 -0
- package/dist/toolchain/registry-seeder.js +52 -6
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +39 -2
- package/dist/tools/nativeTools.js +6 -1
- package/dist/tui/src/app.js +24 -0
- package/dist/tui/src/i18n.js +151 -0
- package/dist/tui/src/render.js +152 -61
- package/dist/tui/src/state.js +83 -0
- package/dist/ui/index.html +2669 -234
- package/dist/ui/lucide-sprite.svg +31 -0
- package/dist/wsl-agent-host.bundle.cjs +1259 -132
- package/dist/wsl-agent-host.js +3 -0
- package/package.json +6 -10
- package/Flow/Electron-Debug-Release.Flow.json +0 -43
- package/Flow/Flow.md +0 -9
- package/Flow/UI-Feature-Integration.Flow.json +0 -96
|
@@ -28,6 +28,7 @@ export interface McpServerInput {
|
|
|
28
28
|
export declare class McpManager {
|
|
29
29
|
private readonly filePath;
|
|
30
30
|
private state;
|
|
31
|
+
private preserveCorruptSource;
|
|
31
32
|
constructor(root: string);
|
|
32
33
|
list(): Array<Omit<McpServerConfig, 'env' | 'headers'> & {
|
|
33
34
|
envKeys: string[];
|
package/dist/core/mcpManager.js
CHANGED
|
@@ -45,10 +45,10 @@ function stringRecord(value, label) {
|
|
|
45
45
|
const output = {};
|
|
46
46
|
for (const [key, item] of Object.entries(value)) {
|
|
47
47
|
const cleanKey = String(key || '').trim();
|
|
48
|
-
if (!cleanKey || cleanKey.includes('\r') || cleanKey.includes('\n') || cleanKey.includes(String.fromCharCode(0)))
|
|
48
|
+
if (!cleanKey || cleanKey.includes('\r') || cleanKey.includes('\n') || cleanKey.includes(String.fromCharCode(0)) || ['__proto__', 'prototype', 'constructor'].includes(cleanKey))
|
|
49
49
|
throw new Error(`${label} contains an invalid key.`);
|
|
50
50
|
const cleanValue = String(item ?? '');
|
|
51
|
-
if (cleanValue.includes(String.fromCharCode(0)))
|
|
51
|
+
if (cleanValue.includes(String.fromCharCode(0)) || (label === 'headers' && /[\r\n]/.test(cleanValue)))
|
|
52
52
|
throw new Error(`${label} contains an invalid value.`);
|
|
53
53
|
output[cleanKey] = cleanValue;
|
|
54
54
|
}
|
|
@@ -59,11 +59,83 @@ function stringArray(value) {
|
|
|
59
59
|
return undefined;
|
|
60
60
|
if (!Array.isArray(value))
|
|
61
61
|
throw new Error('args must be a JSON array of strings.');
|
|
62
|
-
|
|
62
|
+
if (value.length > 100 || value.some(item => typeof item !== 'string' || item.length > 4000 || item.includes(String.fromCharCode(0)))) {
|
|
63
|
+
throw new Error('args must contain at most 100 strings, each no longer than 4000 characters.');
|
|
64
|
+
}
|
|
65
|
+
return [...value];
|
|
66
|
+
}
|
|
67
|
+
function publicHttpUrl(value) {
|
|
68
|
+
if (!value)
|
|
69
|
+
return undefined;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = new URL(value);
|
|
72
|
+
if (parsed.username)
|
|
73
|
+
parsed.username = '<redacted>';
|
|
74
|
+
if (parsed.password)
|
|
75
|
+
parsed.password = '<redacted>';
|
|
76
|
+
for (const key of Array.from(parsed.searchParams.keys())) {
|
|
77
|
+
if (/(?:api.?key|authorization|access.?token|secret|password|credential)/i.test(key))
|
|
78
|
+
parsed.searchParams.set(key, '<redacted>');
|
|
79
|
+
}
|
|
80
|
+
return parsed.toString();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function parseHttpUrl(value) {
|
|
87
|
+
let parsed;
|
|
88
|
+
try {
|
|
89
|
+
parsed = new URL(value);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new Error('An HTTP MCP server requires an http(s) URL.');
|
|
93
|
+
}
|
|
94
|
+
if (!['http:', 'https:'].includes(parsed.protocol))
|
|
95
|
+
throw new Error('An HTTP MCP server requires an http(s) URL.');
|
|
96
|
+
if (parsed.username || parsed.password)
|
|
97
|
+
throw new Error('Put HTTP credentials in headers, not in the MCP URL.');
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
function safeLoadedServer(value) {
|
|
101
|
+
try {
|
|
102
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
103
|
+
return undefined;
|
|
104
|
+
const item = value;
|
|
105
|
+
const id = String(item.id || '').trim();
|
|
106
|
+
const name = String(item.name || '').trim();
|
|
107
|
+
const transport = item.transport === 'http' ? 'http' : item.transport === 'stdio' ? 'stdio' : undefined;
|
|
108
|
+
if (!id || !name || !transport)
|
|
109
|
+
return undefined;
|
|
110
|
+
const command = String(item.command || '').trim();
|
|
111
|
+
const url = String(item.url || '').trim();
|
|
112
|
+
if (transport === 'stdio' && !command)
|
|
113
|
+
return undefined;
|
|
114
|
+
if (transport === 'http')
|
|
115
|
+
parseHttpUrl(url);
|
|
116
|
+
return {
|
|
117
|
+
id: id.slice(0, 200),
|
|
118
|
+
name: name.slice(0, 120),
|
|
119
|
+
enabled: item.enabled === true,
|
|
120
|
+
transport,
|
|
121
|
+
command: transport === 'stdio' ? command.slice(0, 2000) : undefined,
|
|
122
|
+
args: transport === 'stdio' ? (stringArray(item.args) || []) : undefined,
|
|
123
|
+
cwd: transport === 'stdio' ? String(item.cwd || '').trim().slice(0, 4000) || undefined : undefined,
|
|
124
|
+
url: transport === 'http' ? url.slice(0, 4000) : undefined,
|
|
125
|
+
env: transport === 'stdio' ? (stringRecord(item.env, 'env') || {}) : undefined,
|
|
126
|
+
headers: transport === 'http' ? (stringRecord(item.headers, 'headers') || {}) : undefined,
|
|
127
|
+
createdAt: String(item.createdAt || new Date(0).toISOString()),
|
|
128
|
+
updatedAt: String(item.updatedAt || item.createdAt || new Date(0).toISOString()),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
63
134
|
}
|
|
64
135
|
class McpManager {
|
|
65
136
|
filePath;
|
|
66
137
|
state;
|
|
138
|
+
preserveCorruptSource = false;
|
|
67
139
|
constructor(root) {
|
|
68
140
|
this.filePath = path.join(root, 'MCP.json');
|
|
69
141
|
this.state = this.load();
|
|
@@ -74,6 +146,7 @@ class McpManager {
|
|
|
74
146
|
return {
|
|
75
147
|
...publicServer,
|
|
76
148
|
args: [...(server.args || [])],
|
|
149
|
+
url: publicHttpUrl(server.url),
|
|
77
150
|
envKeys: Object.keys(env || {}).sort(),
|
|
78
151
|
headerKeys: Object.keys(headers || {}).sort(),
|
|
79
152
|
};
|
|
@@ -81,22 +154,33 @@ class McpManager {
|
|
|
81
154
|
}
|
|
82
155
|
upsert(input) {
|
|
83
156
|
const id = String(input.id || '').trim();
|
|
84
|
-
|
|
157
|
+
if (input.transport !== undefined && input.transport !== 'stdio' && input.transport !== 'http')
|
|
158
|
+
throw new Error('transport must be stdio or http.');
|
|
159
|
+
let existing = this.state.servers.find(server => server.id === id);
|
|
85
160
|
const name = String(input.name ?? existing?.name ?? '').trim().slice(0, 120);
|
|
86
161
|
if (!name)
|
|
87
162
|
throw new Error('MCP server name is required.');
|
|
163
|
+
if (/[\r\n\0]/.test(name))
|
|
164
|
+
throw new Error('MCP server name contains invalid characters.');
|
|
88
165
|
const transport = input.transport === 'http' ? 'http' : (input.transport === 'stdio' ? 'stdio' : existing?.transport || 'stdio');
|
|
89
166
|
const command = String(input.command ?? existing?.command ?? '').trim();
|
|
90
|
-
|
|
167
|
+
let url = String(input.url ?? existing?.url ?? '').trim();
|
|
91
168
|
if (transport === 'stdio' && !command)
|
|
92
169
|
throw new Error('A stdio MCP server requires a command.');
|
|
93
|
-
if (transport === '
|
|
94
|
-
throw new Error('
|
|
170
|
+
if (transport === 'stdio' && /[\r\n\0]/.test(command))
|
|
171
|
+
throw new Error('command contains an invalid value.');
|
|
172
|
+
if (transport === 'http')
|
|
173
|
+
url = parseHttpUrl(url);
|
|
174
|
+
if (!existing && !id) {
|
|
175
|
+
existing = this.state.servers.find(server => server.name.toLocaleLowerCase() === name.toLocaleLowerCase()
|
|
176
|
+
&& server.transport === transport
|
|
177
|
+
&& (transport === 'stdio' ? server.command === command : server.url === url));
|
|
178
|
+
}
|
|
95
179
|
const now = new Date().toISOString();
|
|
96
180
|
const server = {
|
|
97
181
|
id: existing?.id || `mcp-${(0, crypto_1.randomUUID)()}`,
|
|
98
182
|
name,
|
|
99
|
-
enabled: typeof input.enabled === 'boolean' ? input.enabled : existing?.enabled
|
|
183
|
+
enabled: typeof input.enabled === 'boolean' ? input.enabled : existing?.enabled === true,
|
|
100
184
|
transport,
|
|
101
185
|
command: transport === 'stdio' ? command.slice(0, 2000) : undefined,
|
|
102
186
|
args: transport === 'stdio' ? (stringArray(input.args) ?? existing?.args ?? []) : undefined,
|
|
@@ -134,15 +218,21 @@ class McpManager {
|
|
|
134
218
|
load() {
|
|
135
219
|
try {
|
|
136
220
|
const parsed = JSON.parse(fs.readFileSync(this.filePath, 'utf8'));
|
|
137
|
-
return { version: 1, servers: Array.isArray(parsed.servers) ? parsed.servers.filter(
|
|
221
|
+
return { version: 1, servers: Array.isArray(parsed.servers) ? parsed.servers.map(safeLoadedServer).filter((server) => !!server) : [] };
|
|
138
222
|
}
|
|
139
223
|
catch {
|
|
224
|
+
this.preserveCorruptSource = fs.existsSync(this.filePath);
|
|
140
225
|
return { version: 1, servers: [] };
|
|
141
226
|
}
|
|
142
227
|
}
|
|
143
228
|
save() {
|
|
144
229
|
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
145
|
-
|
|
230
|
+
if (this.preserveCorruptSource) {
|
|
231
|
+
const backupPath = `${this.filePath}.corrupt-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
232
|
+
fs.copyFileSync(this.filePath, backupPath);
|
|
233
|
+
this.preserveCorruptSource = false;
|
|
234
|
+
}
|
|
235
|
+
const tempPath = `${this.filePath}.${process.pid}-${(0, crypto_1.randomUUID)()}.tmp`;
|
|
146
236
|
fs.writeFileSync(tempPath, JSON.stringify(this.state, null, 2), 'utf8');
|
|
147
237
|
fs.renameSync(tempPath, this.filePath);
|
|
148
238
|
}
|
package/dist/core/subagent.d.ts
CHANGED
|
@@ -223,6 +223,12 @@ export declare class SubagentManager {
|
|
|
223
223
|
toRecord(idOrName: string): NewmarkSubagentRecord | undefined;
|
|
224
224
|
toToolResult(idOrName: string, output: string, ok?: boolean): NewmarkSubagentToolResult;
|
|
225
225
|
getResult(name: string): string;
|
|
226
|
+
/**
|
|
227
|
+
* 有界结果 transcript:subagent_result 注入主 Agent 上下文时,不再放大完整
|
|
228
|
+
* 消息历史(含所有中间 tool call/result 洪水)。只保留最近若干条非 tool 消息,
|
|
229
|
+
* 按字符上限截断,杜绝上下文回归。完整历史按需走 subagent_read(max_chars)。
|
|
230
|
+
*/
|
|
231
|
+
boundedResultTranscript(idOrName: string): string;
|
|
226
232
|
listActive(): SubagentInstance[];
|
|
227
233
|
listAll(): SubagentInstance[];
|
|
228
234
|
pauseScheduling(): void;
|
package/dist/core/subagent.js
CHANGED
|
@@ -193,7 +193,7 @@ class SubagentManager {
|
|
|
193
193
|
fromAgentId,
|
|
194
194
|
toAgentId: target.id,
|
|
195
195
|
kind,
|
|
196
|
-
body,
|
|
196
|
+
body: truncateText(body, 32000),
|
|
197
197
|
correlationId: details.correlationId,
|
|
198
198
|
replyTo: details.replyTo,
|
|
199
199
|
createdAt: now(),
|
|
@@ -481,6 +481,27 @@ class SubagentManager {
|
|
|
481
481
|
return '';
|
|
482
482
|
return record.result || record.messages.filter(message => message.role === 'assistant').map(message => message.content).join('\n');
|
|
483
483
|
}
|
|
484
|
+
/**
|
|
485
|
+
* 有界结果 transcript:subagent_result 注入主 Agent 上下文时,不再放大完整
|
|
486
|
+
* 消息历史(含所有中间 tool call/result 洪水)。只保留最近若干条非 tool 消息,
|
|
487
|
+
* 按字符上限截断,杜绝上下文回归。完整历史按需走 subagent_read(max_chars)。
|
|
488
|
+
*/
|
|
489
|
+
boundedResultTranscript(idOrName) {
|
|
490
|
+
const record = this.get(idOrName);
|
|
491
|
+
if (!record)
|
|
492
|
+
return '';
|
|
493
|
+
const MAX_MSG = 8; // 最近保留的消息条数
|
|
494
|
+
const MAX_CHARS = 8000; // 总字符上限
|
|
495
|
+
const messages = record.messages
|
|
496
|
+
.filter(message => message.role === 'assistant' || message.role === 'user' || message.role === 'system')
|
|
497
|
+
.slice(-MAX_MSG)
|
|
498
|
+
.map(message => `[${message.role}] ${truncateText(String(message.content || ''), 1200)}`);
|
|
499
|
+
let text = messages.join('\n');
|
|
500
|
+
if (text.length > MAX_CHARS) {
|
|
501
|
+
text = text.slice(0, MAX_CHARS) + `\n[...transcript truncated: ${record.messages.length} total messages, ${record.messages.length - MAX_MSG} older omitted; use subagent_read for full history...]`;
|
|
502
|
+
}
|
|
503
|
+
return text || '(no transcript)';
|
|
504
|
+
}
|
|
484
505
|
listActive() { return this.listAll().filter(item => item.status !== 'closed'); }
|
|
485
506
|
listAll() { return [...this.subs.values()].map(cloneRecord); }
|
|
486
507
|
pauseScheduling() {
|
|
@@ -14,9 +14,24 @@ export interface ToolPolicyDecision {
|
|
|
14
14
|
}
|
|
15
15
|
export declare const PLAN_COMPUTER_USE_ACTIONS: readonly ["observe", "app_list", "app_observe"];
|
|
16
16
|
export declare const PLAN_BROWSER_USE_ACTIONS: readonly ["observe", "navigate", "wait", "extract"];
|
|
17
|
+
/** 判断一个工具是否可参与并行调度。缺省 false(独占)。
|
|
18
|
+
* 优先看 toolchain registry 推断的 riskLevel('read' 工具天然并发安全,
|
|
19
|
+
* seedToolchainFromDefinitions 的 inferRiskLevel 自动推断),显式白名单作兜底。
|
|
20
|
+
* 这样后续新增工具只需在 ToolExecutor.definitions() 加 schema,seed 时其
|
|
21
|
+
* riskLevel 被自动推断,'read' 工具自动并发安全——无需再改本文件。 */
|
|
22
|
+
export declare function isConcurrencySafeTool(name: string, riskLevel?: string): boolean;
|
|
17
23
|
export declare function isReadOnlyScopedToolAction(name: string, action: string): boolean;
|
|
18
24
|
export declare function toolAvailability(name: string): ToolAvailability;
|
|
19
25
|
export declare function evaluateToolPolicy(request: ToolPolicyRequest): ToolPolicyDecision;
|
|
20
26
|
export declare function filterToolDefinitions<T>(definitions: T[], request: Omit<ToolPolicyRequest, 'name' | 'args'>): T[];
|
|
21
27
|
export declare function planModePolicyPrompt(): string;
|
|
28
|
+
export interface DeletionGuardDecision {
|
|
29
|
+
blocked: boolean;
|
|
30
|
+
reason?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 硬性删除命令审查。blocked=true 表示该命令构成脚本/命令批量删除,必须拒绝并
|
|
34
|
+
* 引导 Agent 使用受监管的 delete_file 工具逐个删除。
|
|
35
|
+
*/
|
|
36
|
+
export declare function evaluateDeletionGuard(command: string): DeletionGuardDecision;
|
|
22
37
|
//# sourceMappingURL=toolPolicy.d.ts.map
|
package/dist/core/toolPolicy.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PLAN_BROWSER_USE_ACTIONS = exports.PLAN_COMPUTER_USE_ACTIONS = void 0;
|
|
4
|
+
exports.isConcurrencySafeTool = isConcurrencySafeTool;
|
|
4
5
|
exports.isReadOnlyScopedToolAction = isReadOnlyScopedToolAction;
|
|
5
6
|
exports.toolAvailability = toolAvailability;
|
|
6
7
|
exports.evaluateToolPolicy = evaluateToolPolicy;
|
|
7
8
|
exports.filterToolDefinitions = filterToolDefinitions;
|
|
8
9
|
exports.planModePolicyPrompt = planModePolicyPrompt;
|
|
10
|
+
exports.evaluateDeletionGuard = evaluateDeletionGuard;
|
|
9
11
|
const REQUIRED_TOOLS = new Set(['pwd', 'read', 'glob', 'grep']);
|
|
10
12
|
const MODE_SCOPED_TOOLS = new Set([
|
|
11
13
|
'image_inspect',
|
|
@@ -16,6 +18,11 @@ const MODE_SCOPED_TOOLS = new Set([
|
|
|
16
18
|
'build_history_query',
|
|
17
19
|
'context_compress',
|
|
18
20
|
'context_history_manage',
|
|
21
|
+
'compress_tool_result',
|
|
22
|
+
'background_tool',
|
|
23
|
+
'read_tool_result',
|
|
24
|
+
'goal_manage',
|
|
25
|
+
'conversation_rename',
|
|
19
26
|
'question',
|
|
20
27
|
'task',
|
|
21
28
|
'subagent_list',
|
|
@@ -23,6 +30,10 @@ const MODE_SCOPED_TOOLS = new Set([
|
|
|
23
30
|
'subagent_send',
|
|
24
31
|
'subagent_result',
|
|
25
32
|
'subagent_close',
|
|
33
|
+
'branch_list',
|
|
34
|
+
'branch_send',
|
|
35
|
+
'branch_read',
|
|
36
|
+
'branch_create',
|
|
26
37
|
]);
|
|
27
38
|
const PLAN_READ_ONLY_TOOLS = new Set([
|
|
28
39
|
'pwd',
|
|
@@ -51,12 +62,50 @@ const PLAN_READ_ONLY_TOOLS = new Set([
|
|
|
51
62
|
'subagent_send',
|
|
52
63
|
'subagent_result',
|
|
53
64
|
'subagent_close',
|
|
65
|
+
'branch_list',
|
|
66
|
+
'branch_read',
|
|
54
67
|
'question',
|
|
55
68
|
]);
|
|
56
69
|
exports.PLAN_COMPUTER_USE_ACTIONS = ['observe', 'app_list', 'app_observe'];
|
|
57
70
|
exports.PLAN_BROWSER_USE_ACTIONS = ['observe', 'navigate', 'wait', 'extract'];
|
|
58
71
|
const PLAN_COMPUTER_USE_ACTION_SET = new Set(exports.PLAN_COMPUTER_USE_ACTIONS);
|
|
59
72
|
const PLAN_BROWSER_USE_ACTION_SET = new Set(exports.PLAN_BROWSER_USE_ACTIONS);
|
|
73
|
+
/**
|
|
74
|
+
* 并发安全工具集合(DSH isConcurrencySafe 语义的 Newmark 落地)。
|
|
75
|
+
*
|
|
76
|
+
* 只有确定性无副作用的只读工具才允许与兄弟 tool call 并发执行;任何写入、
|
|
77
|
+
* shell 命令、浏览器交互、子代理编排、以及会改变对话/文件/外部状态的工具
|
|
78
|
+
* 都保持独占串行,避免盲目 Promise.all 引发竞态。缺省保守:不在集合中的
|
|
79
|
+
* 工具一律视为独占。
|
|
80
|
+
*
|
|
81
|
+
* 注意:read/grep/glob/pwd 是同一进程内的内存/文件系统只读,可安全重叠;
|
|
82
|
+
* web_search/web_fetch 只读网络,可重叠;git_status/file_audit/repo_security_audit
|
|
83
|
+
* 只读审计,可重叠。其余(含 memory_lab_read 等读接口)因可能触发内部缓存/
|
|
84
|
+
* 重建副作用,默认保守串行。
|
|
85
|
+
*/
|
|
86
|
+
const CONCURRENCY_SAFE_TOOLS = new Set([
|
|
87
|
+
'pwd',
|
|
88
|
+
'read',
|
|
89
|
+
'glob',
|
|
90
|
+
'grep',
|
|
91
|
+
'web_search',
|
|
92
|
+
'web_fetch',
|
|
93
|
+
'git_status',
|
|
94
|
+
'file_audit',
|
|
95
|
+
'repo_security_audit',
|
|
96
|
+
]);
|
|
97
|
+
/** 判断一个工具是否可参与并行调度。缺省 false(独占)。
|
|
98
|
+
* 优先看 toolchain registry 推断的 riskLevel('read' 工具天然并发安全,
|
|
99
|
+
* seedToolchainFromDefinitions 的 inferRiskLevel 自动推断),显式白名单作兜底。
|
|
100
|
+
* 这样后续新增工具只需在 ToolExecutor.definitions() 加 schema,seed 时其
|
|
101
|
+
* riskLevel 被自动推断,'read' 工具自动并发安全——无需再改本文件。 */
|
|
102
|
+
function isConcurrencySafeTool(name, riskLevel) {
|
|
103
|
+
const toolName = String(name || '').trim();
|
|
104
|
+
if (CONCURRENCY_SAFE_TOOLS.has(toolName))
|
|
105
|
+
return true;
|
|
106
|
+
// 从 toolchain registry 派生的 riskLevel:read 工具默认并发安全。
|
|
107
|
+
return riskLevel === 'read';
|
|
108
|
+
}
|
|
60
109
|
function isReadOnlyScopedToolAction(name, action) {
|
|
61
110
|
if (name === 'computer_use')
|
|
62
111
|
return PLAN_COMPUTER_USE_ACTION_SET.has(action);
|
|
@@ -97,7 +146,7 @@ function evaluateToolPolicy(request) {
|
|
|
97
146
|
}
|
|
98
147
|
}
|
|
99
148
|
if (request.isSubagent) {
|
|
100
|
-
if (name === 'skill_download' || name === 'question' || name.startsWith('automation_')) {
|
|
149
|
+
if (name === 'skill_download' || name === 'question' || name.startsWith('automation_') || name === 'goal_manage' || name === 'conversation_rename') {
|
|
101
150
|
return { ...base, allowed: false, reason: `[Subagent sandbox] Tool '${name}' is disabled for peer agents.` };
|
|
102
151
|
}
|
|
103
152
|
}
|
|
@@ -119,4 +168,128 @@ function planModePolicyPrompt() {
|
|
|
119
168
|
'Runtime policy rejects stale or hidden mutating tool calls even if a prompt asks for them.',
|
|
120
169
|
].join(' ');
|
|
121
170
|
}
|
|
171
|
+
/** 删除命令动词(跨 POSIX / PowerShell / cmd)。注意:不含单独 "remove"(避免匹配普通英文)。 */
|
|
172
|
+
const DELETE_VERB_SOURCE = '(?:remove-item|rmdir|unlink|erase|del|rm|rd|ri)';
|
|
173
|
+
const DELETE_VERB_BOUNDARY = new RegExp(`(?:^|[\\s;&|()\\n])${DELETE_VERB_SOURCE}(?:\\s|$)`, 'i');
|
|
174
|
+
function hasDeletionVerb(text) {
|
|
175
|
+
return DELETE_VERB_BOUNDARY.test(text);
|
|
176
|
+
}
|
|
177
|
+
function deletionVerbCount(text) {
|
|
178
|
+
const matches = text.match(new RegExp(DELETE_VERB_BOUNDARY.source, 'gi'));
|
|
179
|
+
return matches ? matches.length : 0;
|
|
180
|
+
}
|
|
181
|
+
/** 循环结构批量删除:foreach / for…in / for( / while( / bash do…done。 */
|
|
182
|
+
function hasLoopDeletion(text) {
|
|
183
|
+
const lower = text.toLowerCase();
|
|
184
|
+
if (/\bforeach\b/.test(lower))
|
|
185
|
+
return true; // PowerShell foreach
|
|
186
|
+
if (/\bfor\b\s*[$({]/.test(lower))
|
|
187
|
+
return true; // PowerShell/C for(...)
|
|
188
|
+
if (/\bfor\b\s+\S+\s+in\b/.test(lower))
|
|
189
|
+
return true; // bash for f in ...
|
|
190
|
+
if (/\bwhile\b\s*[({]/.test(lower))
|
|
191
|
+
return true; // while(...)
|
|
192
|
+
if (/\bwhile\b\s+\S/.test(lower) && /\bdo\b/.test(lower))
|
|
193
|
+
return true; // bash while ... do
|
|
194
|
+
if (/\bdone\b/.test(lower))
|
|
195
|
+
return true; // bash 循环结束标记
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
/** find -delete / find -exec rm / xargs rm 批量删除。 */
|
|
199
|
+
function hasFindXargsDeletion(text) {
|
|
200
|
+
if (/\bfind\b[^\n;&|]*-(?:delete\b|exec(?:dir)?\s+(?:rm|del|erase)\b)/i.test(text))
|
|
201
|
+
return true;
|
|
202
|
+
if (/\bxargs\b[^\n;&|]*\b(?:rm|del|erase|remove-item)\b/i.test(text))
|
|
203
|
+
return true;
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
/** 按 shell 语义切分参数:引号内的空格不拆分,返回去引号后的 token。 */
|
|
207
|
+
function splitCommandArgs(args) {
|
|
208
|
+
const tokens = [];
|
|
209
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
210
|
+
let m;
|
|
211
|
+
while ((m = re.exec(args)) !== null) {
|
|
212
|
+
const token = m[1] ?? m[2] ?? m[3] ?? '';
|
|
213
|
+
if (token)
|
|
214
|
+
tokens.push(token);
|
|
215
|
+
}
|
|
216
|
+
return tokens;
|
|
217
|
+
}
|
|
218
|
+
/** 管道接收端删除:上游产出多项,删除动词作为接收端即批量删除。 */
|
|
219
|
+
function hasPipeDeletion(text) {
|
|
220
|
+
return new RegExp(`\\|\\s*${DELETE_VERB_SOURCE}\\b`, 'i').test(text);
|
|
221
|
+
}
|
|
222
|
+
/** 递归删除标志:rm -r/-R/--recursive、Remove-Item -Recurse、rmdir/rd /s、del /s。 */
|
|
223
|
+
function hasRecursiveDeletionFlag(text) {
|
|
224
|
+
const lower = text.toLowerCase();
|
|
225
|
+
if (/\brm\b\s+(-[a-z]*r[a-z]*|--recursive)\b/.test(lower))
|
|
226
|
+
return true;
|
|
227
|
+
if (/\bremove-item\b[^\n;&|]*\s+-(?:recurse|r)\b/.test(lower))
|
|
228
|
+
return true;
|
|
229
|
+
if (/\b(?:rmdir|rd)\b\s+(-r\b|\/[s]\b)/.test(lower))
|
|
230
|
+
return true;
|
|
231
|
+
if (/\bdel\b\s+\/[s]\b/.test(lower))
|
|
232
|
+
return true;
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
/** 删除命令后跟含通配符的目标 token。 */
|
|
236
|
+
function hasWildcardDeletionTarget(text) {
|
|
237
|
+
const segmentRe = new RegExp(`(?:^|[\\s;&|()\\n])${DELETE_VERB_SOURCE}([\\s][^;&|\\n]*)?`, 'gi');
|
|
238
|
+
let m;
|
|
239
|
+
while ((m = segmentRe.exec(text)) !== null) {
|
|
240
|
+
const args = m[1] || '';
|
|
241
|
+
for (const token of splitCommandArgs(args)) {
|
|
242
|
+
if (!token || token.startsWith('-') || /^\/[A-Za-z]/.test(token))
|
|
243
|
+
continue;
|
|
244
|
+
if (/[*?]/.test(token))
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
/** 单条删除命令后跟 >= 2 个明确目标(非 flag、非 shell 开关)。 */
|
|
251
|
+
function hasMultipleDeleteTargets(text) {
|
|
252
|
+
const segmentRe = new RegExp(`(?:^|[\\s;&|()\\n])${DELETE_VERB_SOURCE}([\\s][^;&|\\n]*)?`, 'gi');
|
|
253
|
+
let m;
|
|
254
|
+
while ((m = segmentRe.exec(text)) !== null) {
|
|
255
|
+
const args = m[1] || '';
|
|
256
|
+
const targets = splitCommandArgs(args)
|
|
257
|
+
.filter(t => t && !t.startsWith('-') && !/^\/[A-Za-z]/.test(t) && !/^(&&|\|\||;|\||&|>|>>|<|2>&1)$/.test(t));
|
|
258
|
+
if (targets.length >= 2)
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* 硬性删除命令审查。blocked=true 表示该命令构成脚本/命令批量删除,必须拒绝并
|
|
265
|
+
* 引导 Agent 使用受监管的 delete_file 工具逐个删除。
|
|
266
|
+
*/
|
|
267
|
+
function evaluateDeletionGuard(command) {
|
|
268
|
+
const text = String(command || '');
|
|
269
|
+
if (!text.trim())
|
|
270
|
+
return { blocked: false };
|
|
271
|
+
// find -delete / find -exec rm 中,-delete 不含标准删除动词,需在入口单独识别为批量删除意图。
|
|
272
|
+
const findXargs = hasFindXargsDeletion(text);
|
|
273
|
+
if (!hasDeletionVerb(text) && !findXargs)
|
|
274
|
+
return { blocked: false };
|
|
275
|
+
const refuse = (kind) => ({
|
|
276
|
+
blocked: true,
|
|
277
|
+
reason: `[deletion guard] ${kind} batch deletion is not allowed. Delete files one by one with the delete_file tool under Agent supervision.`,
|
|
278
|
+
});
|
|
279
|
+
if (hasLoopDeletion(text))
|
|
280
|
+
return refuse('Loop-based');
|
|
281
|
+
if (findXargs)
|
|
282
|
+
return refuse('find/xargs');
|
|
283
|
+
if (hasPipeDeletion(text))
|
|
284
|
+
return refuse('Pipe-fed');
|
|
285
|
+
if (hasRecursiveDeletionFlag(text))
|
|
286
|
+
return refuse('Recursive');
|
|
287
|
+
if (hasWildcardDeletionTarget(text))
|
|
288
|
+
return refuse('Wildcard');
|
|
289
|
+
if (hasMultipleDeleteTargets(text))
|
|
290
|
+
return refuse('Multiple-target');
|
|
291
|
+
if (deletionVerbCount(text) >= 2)
|
|
292
|
+
return refuse('Multiple-statement');
|
|
293
|
+
return { blocked: false };
|
|
294
|
+
}
|
|
122
295
|
//# sourceMappingURL=toolPolicy.js.map
|
package/dist/core/types.d.ts
CHANGED
|
@@ -100,7 +100,7 @@ export interface ChatMessage {
|
|
|
100
100
|
export interface AgentWorkEvent {
|
|
101
101
|
id: string;
|
|
102
102
|
conversationId: string;
|
|
103
|
-
type: 'start' | 'text' | 'response' | 'final_response' | 'tool_call' | 'tool_result' | 'status' | 'done' | 'error' | 'queue_update' | 'guide';
|
|
103
|
+
type: 'start' | 'text' | 'response' | 'final_response' | 'tool_call' | 'tool_result' | 'thought' | 'thought_result' | 'status' | 'done' | 'error' | 'queue_update' | 'guide';
|
|
104
104
|
content: string;
|
|
105
105
|
mode: string;
|
|
106
106
|
model: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BrowserControlRequest, BrowserControlResult } from './browserControl';
|
|
2
2
|
import { BrowserUseRequest } from './browserUse';
|
|
3
|
-
import { AgentPromptMessage, ConversationKernelRunOptions, ConversationKernelRunResult, ConversationQueueMode, ConversationRuntimeState, ConversationStopResult } from './conversationKernel';
|
|
3
|
+
import { AgentPromptMessage, ConversationContextCompressOptions, ConversationKernelRunOptions, ConversationKernelRunResult, ConversationQueueMode, ConversationRuntimeState, ConversationStopResult } from './conversationKernel';
|
|
4
4
|
import { ConversationRuntimeTarget, NormalizedConversationTarget } from './conversationTarget';
|
|
5
5
|
import { AgentMode, AgentWorkEvent, ConversationInputEnvelope, GuideReceipt } from './types';
|
|
6
6
|
import type { AutoRouteRatingResult, ConversationSnapshot } from './agent';
|
|
@@ -102,6 +102,13 @@ export type UtilityAgentRequest = {
|
|
|
102
102
|
params: {
|
|
103
103
|
target: ConversationRuntimeTarget;
|
|
104
104
|
};
|
|
105
|
+
} | {
|
|
106
|
+
id: string;
|
|
107
|
+
method: 'context_compress';
|
|
108
|
+
params: {
|
|
109
|
+
target: ConversationRuntimeTarget;
|
|
110
|
+
options?: ConversationContextCompressOptions;
|
|
111
|
+
};
|
|
105
112
|
} | {
|
|
106
113
|
id: string;
|
|
107
114
|
method: 'rate_auto_route';
|
package/dist/core/workspace.d.ts
CHANGED
|
@@ -23,6 +23,15 @@ export interface WorkspaceManagerOptions {
|
|
|
23
23
|
}
|
|
24
24
|
/** Normalize persisted Windows/WSL aliases and recover paths damaged by cross-host path.resolve calls. */
|
|
25
25
|
export declare function normalizeHostWorkspacePath(input: string, platform?: NodeJS.Platform): string;
|
|
26
|
+
/**
|
|
27
|
+
* An installed package is never a writable user workspace. Older builds could
|
|
28
|
+
* persist the install directory as an external workspace when the GUI was
|
|
29
|
+
* started from its shortcut working directory; restoring that entry then
|
|
30
|
+
* tried to create `<install>\\conversations` under Program Files. Keep this
|
|
31
|
+
* policy in the workspace layer so GUI, TUI, CLI, and detached runtimes all
|
|
32
|
+
* recover the same way.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isProtectedInstallWorkspacePath(candidate: string): boolean;
|
|
26
35
|
export declare class WorkspaceManager {
|
|
27
36
|
rootPath: string;
|
|
28
37
|
private config;
|
package/dist/core/workspace.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.WorkspaceManager = void 0;
|
|
37
37
|
exports.normalizeHostWorkspacePath = normalizeHostWorkspacePath;
|
|
38
|
+
exports.isProtectedInstallWorkspacePath = isProtectedInstallWorkspacePath;
|
|
38
39
|
const fs = __importStar(require("fs"));
|
|
39
40
|
const path = __importStar(require("path"));
|
|
40
41
|
const crypto = __importStar(require("crypto"));
|
|
@@ -64,6 +65,34 @@ function normalizeHostWorkspacePath(input, platform = process.platform) {
|
|
|
64
65
|
}
|
|
65
66
|
return path.posix.resolve(raw || '.');
|
|
66
67
|
}
|
|
68
|
+
function isPathInside(parent, child) {
|
|
69
|
+
try {
|
|
70
|
+
const relative = path.relative(path.resolve(parent), path.resolve(child));
|
|
71
|
+
return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* An installed package is never a writable user workspace. Older builds could
|
|
79
|
+
* persist the install directory as an external workspace when the GUI was
|
|
80
|
+
* started from its shortcut working directory; restoring that entry then
|
|
81
|
+
* tried to create `<install>\\conversations` under Program Files. Keep this
|
|
82
|
+
* policy in the workspace layer so GUI, TUI, CLI, and detached runtimes all
|
|
83
|
+
* recover the same way.
|
|
84
|
+
*/
|
|
85
|
+
function isProtectedInstallWorkspacePath(candidate) {
|
|
86
|
+
const value = String(candidate || '').trim();
|
|
87
|
+
if (!value)
|
|
88
|
+
return false;
|
|
89
|
+
const roots = [path.dirname(process.execPath)];
|
|
90
|
+
if (process.platform === 'win32') {
|
|
91
|
+
roots.push(process.env.ProgramFiles || '', process.env['ProgramFiles(x86)'] || '', process.env.ProgramW6432 || '');
|
|
92
|
+
}
|
|
93
|
+
const resolved = path.resolve(value);
|
|
94
|
+
return roots.filter(Boolean).some(root => isPathInside(root, resolved));
|
|
95
|
+
}
|
|
67
96
|
class WorkspaceManager {
|
|
68
97
|
rootPath;
|
|
69
98
|
config;
|
|
@@ -135,9 +164,18 @@ class WorkspaceManager {
|
|
|
135
164
|
catch { /* empty */ }
|
|
136
165
|
try {
|
|
137
166
|
const ext = JSON.parse(fs.readFileSync(path.join(w, 'External.json'), 'utf-8'));
|
|
138
|
-
|
|
167
|
+
const normalized = Array.isArray(ext)
|
|
139
168
|
? ext.map(item => this.normalizeExternalWorkspace(item, changed => { externalChanged = externalChanged || changed; }))
|
|
140
169
|
: [];
|
|
170
|
+
this.external = normalized.filter(workspace => {
|
|
171
|
+
if (!isProtectedInstallWorkspacePath(workspace.path))
|
|
172
|
+
return true;
|
|
173
|
+
// Never restore or persist the installation directory as a user
|
|
174
|
+
// workspace. It is a migration boundary for registries written by
|
|
175
|
+
// older versions and by package-level pressure tests.
|
|
176
|
+
externalChanged = true;
|
|
177
|
+
return false;
|
|
178
|
+
});
|
|
141
179
|
}
|
|
142
180
|
catch { /* empty */ }
|
|
143
181
|
// Scan for directories not in Local.json
|
|
@@ -344,6 +382,15 @@ class WorkspaceManager {
|
|
|
344
382
|
}
|
|
345
383
|
restoreCurrent() {
|
|
346
384
|
const stateCurrent = this.readState().current || null;
|
|
385
|
+
if (stateCurrent?.path && isProtectedInstallWorkspacePath(stateCurrent.path)) {
|
|
386
|
+
// The previous current workspace may have been filtered from
|
|
387
|
+
// External.json above. Clear the pointer instead of falling back to an
|
|
388
|
+
// arbitrary external directory; Agent can create/reuse a user-root
|
|
389
|
+
// internal workspace according to its normal configuration.
|
|
390
|
+
this.current = null;
|
|
391
|
+
this.saveState();
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
347
394
|
const stored = this.findWorkspace(stateCurrent);
|
|
348
395
|
if (stored) {
|
|
349
396
|
this.current = stored;
|
|
@@ -72,6 +72,10 @@ export declare class WslAgentClient {
|
|
|
72
72
|
rewind(target: ConversationRuntimeTarget, messageIndex: number): Promise<WslConversationRewindResult>;
|
|
73
73
|
enqueueGuide(target: ConversationRuntimeTarget, envelope: ConversationInputEnvelope): Promise<GuideReceipt>;
|
|
74
74
|
checkpoint(target: ConversationRuntimeTarget): Promise<Record<string, unknown>>;
|
|
75
|
+
contextCompress(target: ConversationRuntimeTarget, options?: {
|
|
76
|
+
keepRecent?: number;
|
|
77
|
+
force?: boolean;
|
|
78
|
+
}): Promise<Record<string, unknown>>;
|
|
75
79
|
rateAutoRoute(target: ConversationRuntimeTarget, score: number, routeId?: string): Promise<WslAutoRouteRatingResult>;
|
|
76
80
|
setWorkRunExpanded(target: ConversationRuntimeTarget, runId: string, expanded: boolean): Promise<boolean>;
|
|
77
81
|
setMode(target: ConversationRuntimeTarget, mode: AgentMode): Promise<AgentMode>;
|
|
@@ -317,6 +317,10 @@ class WslAgentClient {
|
|
|
317
317
|
await this.start();
|
|
318
318
|
return await this.request('checkpoint', { target: await this.mapTarget(target) }, 5_000);
|
|
319
319
|
}
|
|
320
|
+
async contextCompress(target, options = {}) {
|
|
321
|
+
await this.start();
|
|
322
|
+
return await this.request('context_compress', { target: await this.mapTarget(target), options }, 120_000);
|
|
323
|
+
}
|
|
320
324
|
async rateAutoRoute(target, score, routeId = '') {
|
|
321
325
|
await this.start();
|
|
322
326
|
return await this.request('rate_auto_route', {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AgentMode, AgentWorkEvent, ConversationInputEnvelope, GuideReceipt } from './types';
|
|
2
|
-
import { AgentPromptMessage, ConversationKernelRunOptions, ConversationKernelRunResult, ConversationQueueMode, ConversationStopResult } from './conversationKernel';
|
|
2
|
+
import { AgentPromptMessage, ConversationContextCompressOptions, ConversationKernelRunOptions, ConversationKernelRunResult, ConversationQueueMode, ConversationStopResult } from './conversationKernel';
|
|
3
3
|
import { ConversationRuntimeTarget } from './conversationTarget';
|
|
4
4
|
import { TerminalTakeoverEvent, TerminalTakeoverOwnerFilter, TerminalTakeoverState } from '../tools/terminalTakeover';
|
|
5
5
|
import { BrowserUseRequest } from './browserUse';
|
|
@@ -114,6 +114,13 @@ export type WslAgentRequest = {
|
|
|
114
114
|
params: {
|
|
115
115
|
target: ConversationRuntimeTarget;
|
|
116
116
|
};
|
|
117
|
+
} | {
|
|
118
|
+
id: string;
|
|
119
|
+
method: 'context_compress';
|
|
120
|
+
params: {
|
|
121
|
+
target: ConversationRuntimeTarget;
|
|
122
|
+
options?: ConversationContextCompressOptions;
|
|
123
|
+
};
|
|
117
124
|
} | {
|
|
118
125
|
id: string;
|
|
119
126
|
method: 'rate_auto_route';
|