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.
Files changed (60) hide show
  1. package/dist/cli-commands.d.ts +1 -0
  2. package/dist/cli-commands.js +11 -2
  3. package/dist/context/domain/types.d.ts +37 -0
  4. package/dist/context/services/context-orchestrator.js +2 -0
  5. package/dist/conversation-utility-host.bundle.cjs +1259 -132
  6. package/dist/conversation-utility-host.js +3 -0
  7. package/dist/core/agent.d.ts +139 -5
  8. package/dist/core/agent.js +964 -82
  9. package/dist/core/agentKernel/agent-loop.js +29 -3
  10. package/dist/core/agentKernel/types.d.ts +7 -0
  11. package/dist/core/agentKernelRunner.d.ts +2 -0
  12. package/dist/core/agentKernelRunner.js +121 -19
  13. package/dist/core/conversationKernel.d.ts +5 -0
  14. package/dist/core/conversationKernel.js +29 -0
  15. package/dist/core/dshCompatibility.d.ts +198 -0
  16. package/dist/core/dshCompatibility.js +600 -0
  17. package/dist/core/electronUtilityAgentClient.d.ts +4 -0
  18. package/dist/core/electronUtilityAgentClient.js +4 -0
  19. package/dist/core/electronUtilityRuntimePool.d.ts +8 -0
  20. package/dist/core/electronUtilityRuntimePool.js +14 -0
  21. package/dist/core/mcpManager.d.ts +1 -0
  22. package/dist/core/mcpManager.js +100 -10
  23. package/dist/core/subagent.d.ts +6 -0
  24. package/dist/core/subagent.js +22 -1
  25. package/dist/core/toolPolicy.d.ts +15 -0
  26. package/dist/core/toolPolicy.js +174 -1
  27. package/dist/core/types.d.ts +1 -1
  28. package/dist/core/utilityAgentProtocol.d.ts +8 -1
  29. package/dist/core/workspace.d.ts +9 -0
  30. package/dist/core/workspace.js +48 -1
  31. package/dist/core/wslAgentClient.d.ts +4 -0
  32. package/dist/core/wslAgentClient.js +4 -0
  33. package/dist/core/wslAgentProtocol.d.ts +8 -1
  34. package/dist/core/wslAgentRuntimePool.d.ts +8 -0
  35. package/dist/core/wslAgentRuntimePool.js +15 -0
  36. package/dist/launcher.js +8 -0
  37. package/dist/llm/provider.d.ts +1 -1
  38. package/dist/llm/provider.js +4 -3
  39. package/dist/main.js +163 -11
  40. package/dist/preload.js +11 -0
  41. package/dist/providers/chat-completions.adapter.js +41 -15
  42. package/dist/providers/provider-adapter.d.ts +3 -0
  43. package/dist/toolchain/registry/tool-registry.d.ts +13 -1
  44. package/dist/toolchain/registry/tool-registry.js +8 -0
  45. package/dist/toolchain/registry-seeder.js +52 -6
  46. package/dist/tools/index.d.ts +1 -0
  47. package/dist/tools/index.js +39 -2
  48. package/dist/tools/nativeTools.js +6 -1
  49. package/dist/tui/src/app.js +24 -0
  50. package/dist/tui/src/i18n.js +151 -0
  51. package/dist/tui/src/render.js +152 -61
  52. package/dist/tui/src/state.js +83 -0
  53. package/dist/ui/index.html +2669 -234
  54. package/dist/ui/lucide-sprite.svg +31 -0
  55. package/dist/wsl-agent-host.bundle.cjs +1259 -132
  56. package/dist/wsl-agent-host.js +3 -0
  57. package/package.json +6 -10
  58. package/Flow/Electron-Debug-Release.Flow.json +0 -43
  59. package/Flow/Flow.md +0 -9
  60. 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[];
@@ -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
- return value.map(item => String(item)).filter(item => item.length <= 4000).slice(0, 100);
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
- const existing = this.state.servers.find(server => server.id === id);
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
- const url = String(input.url ?? existing?.url ?? '').trim();
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 === 'http' && !/^https?:\/\//i.test(url))
94
- throw new Error('An HTTP MCP server requires an http(s) URL.');
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 !== false,
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(Boolean) : [] };
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
- const tempPath = `${this.filePath}.${process.pid}.tmp`;
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
  }
@@ -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;
@@ -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
@@ -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
@@ -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';
@@ -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;
@@ -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
- this.external = Array.isArray(ext)
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';