minimal-agent 0.11.0 → 0.13.0

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 (181) hide show
  1. package/package.json +5 -1
  2. package/plugins/workflow-runner/plugin.js +27 -15
  3. package/plugins/workflow-runner/plugin.ts +34 -20
  4. package/plugins/workflow-runner/src/agentCommon.js +13 -12
  5. package/plugins/workflow-runner/src/agentCommon.ts +22 -13
  6. package/plugins/workflow-runner/src/expressions.js +29 -8
  7. package/plugins/workflow-runner/src/expressions.ts +28 -10
  8. package/plugins/workflow-runner/src/index.js +50 -20
  9. package/plugins/workflow-runner/src/index.ts +75 -31
  10. package/plugins/workflow-runner/src/multimodal.js +2 -1
  11. package/plugins/workflow-runner/src/multimodal.ts +2 -1
  12. package/plugins/workflow-runner/src/recovery.js +9 -2
  13. package/plugins/workflow-runner/src/recovery.ts +9 -2
  14. package/plugins/workflow-runner/src/runner.js +31 -5
  15. package/plugins/workflow-runner/src/runner.ts +33 -5
  16. package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
  17. package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -1
  18. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  19. package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
  20. package/plugins/workflow-runner/src/stepExecutors/tool.js +91 -61
  21. package/plugins/workflow-runner/src/stepExecutors/tool.ts +111 -71
  22. package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
  23. package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
  24. package/plugins/workflow-runner/src/taskIdentity.js +197 -21
  25. package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
  26. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  27. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  28. package/plugins/workflow-runner/src/types.js +113 -7
  29. package/plugins/workflow-runner/src/types.ts +137 -14
  30. package/plugins/workflow-runner/src/workflowState.js +455 -54
  31. package/plugins/workflow-runner/src/workflowState.ts +508 -63
  32. package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
  33. package/plugins/workflow-runner/test/context-files.test.ts +47 -16
  34. package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
  35. package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
  36. package/plugins/workflow-runner/test/expressions.test.ts +42 -0
  37. package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
  38. package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
  39. package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
  40. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
  41. package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
  42. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
  43. package/plugins/workflow-runner/test/research.load.test.ts +328 -0
  44. package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
  45. package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
  46. package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
  47. package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
  48. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  49. package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
  50. package/plugins/workflow-runner/test/word.load.test.ts +9 -9
  51. package/skills/skill-creator/scripts/run_eval.py +3 -2
  52. package/src/cli/print.js +36 -4
  53. package/src/cli/print.ts +42 -4
  54. package/src/cli/streamJson.js +19 -4
  55. package/src/cli/streamJson.ts +17 -4
  56. package/src/config/configDefaults.js +25 -0
  57. package/src/config/configDefaults.ts +33 -0
  58. package/src/config/configFile.js +2 -21
  59. package/src/config/configFile.ts +11 -24
  60. package/src/config/configManagement.js +1 -1
  61. package/src/config/configManagement.ts +2 -2
  62. package/src/config/configService.js +24 -0
  63. package/src/config/configService.ts +23 -0
  64. package/src/context/compact.js +4 -0
  65. package/src/context/compact.ts +4 -0
  66. package/src/context/contextBundle.js +38 -13
  67. package/src/context/contextBundle.ts +47 -13
  68. package/src/harness/stallGuard.js +19 -4
  69. package/src/harness/stallGuard.ts +30 -7
  70. package/src/infra/verification.js +25 -18
  71. package/src/infra/verification.ts +50 -18
  72. package/src/loop.js +95 -11
  73. package/src/loop.ts +125 -16
  74. package/src/main.js +8 -1
  75. package/src/main.tsx +7 -1
  76. package/src/memory/constants.js +10 -0
  77. package/src/memory/constants.ts +13 -0
  78. package/src/memory/episodicMemory.js +274 -2
  79. package/src/memory/episodicMemory.ts +306 -1
  80. package/src/memory/fileStore.js +24 -4
  81. package/src/memory/fileStore.ts +22 -2
  82. package/src/memory/memoryService.js +79 -12
  83. package/src/memory/memoryService.ts +108 -9
  84. package/src/memory/repository.js +415 -85
  85. package/src/memory/repository.ts +437 -86
  86. package/src/memory/turnJournal.js +95 -0
  87. package/src/memory/turnJournal.ts +120 -0
  88. package/src/memory/types.ts +16 -0
  89. package/src/plugins/pluginApi.ts +30 -6
  90. package/src/plugins/pluginRunner.js +23 -8
  91. package/src/plugins/pluginRunner.ts +32 -13
  92. package/src/prompts/policyRegistry.js +55 -1
  93. package/src/prompts/policyRegistry.ts +55 -1
  94. package/src/prompts/promptAssembly.js +39 -7
  95. package/src/prompts/promptAssembly.ts +145 -59
  96. package/src/session/runAgentTurn.js +43 -21
  97. package/src/session/runAgentTurn.ts +68 -26
  98. package/src/session/runTurn.js +11 -46
  99. package/src/session/runTurn.ts +12 -46
  100. package/src/session/runtimeStore.js +149 -0
  101. package/src/session/runtimeStore.ts +197 -0
  102. package/src/session/sessionState.js +5 -0
  103. package/src/session/sessionState.ts +14 -0
  104. package/src/tools/advisor/advisor.js +76 -9
  105. package/src/tools/advisor/advisor.ts +79 -9
  106. package/src/tools/agent/agent.js +143 -14
  107. package/src/tools/agent/agent.ts +149 -14
  108. package/src/tools/agent/agentTaskService.js +60 -0
  109. package/src/tools/agent/agentTaskService.ts +113 -0
  110. package/src/tools/agent/agentTypes.js +5 -0
  111. package/src/tools/agent/agentTypes.ts +5 -0
  112. package/src/tools/bash/bash.js +80 -15
  113. package/src/tools/bash/bash.ts +83 -15
  114. package/src/tools/capabilities.js +5 -0
  115. package/src/tools/capabilities.ts +11 -1
  116. package/src/tools/edit/edit.js +7 -4
  117. package/src/tools/edit/edit.ts +7 -4
  118. package/src/tools/edit/multi-edit.js +4 -2
  119. package/src/tools/edit/multi-edit.ts +4 -2
  120. package/src/tools/glob/glob.js +156 -40
  121. package/src/tools/glob/glob.ts +185 -46
  122. package/src/tools/index.js +35 -17
  123. package/src/tools/index.ts +81 -20
  124. package/src/tools/lsp/lsp.js +165 -22
  125. package/src/tools/lsp/lsp.ts +206 -36
  126. package/src/tools/powershell/powershell.js +134 -27
  127. package/src/tools/powershell/powershell.ts +171 -30
  128. package/src/tools/presentresult/presentresult.js +10 -0
  129. package/src/tools/presentresult/presentresult.ts +10 -0
  130. package/src/tools/read/read.js +249 -29
  131. package/src/tools/read/read.ts +396 -59
  132. package/src/tools/shared/diffSummary.js +44 -0
  133. package/src/tools/shared/diffSummary.ts +63 -0
  134. package/src/tools/shared/fileActivity.js +2 -1
  135. package/src/tools/shared/fileActivity.ts +3 -0
  136. package/src/tools/shared/permanentDeleteGuard.js +57 -0
  137. package/src/tools/shared/permanentDeleteGuard.ts +71 -0
  138. package/src/tools/shared/toolOutputStore.js +97 -0
  139. package/src/tools/shared/toolOutputStore.ts +110 -0
  140. package/src/tools/skill/skill.js +86 -0
  141. package/src/tools/skill/skill.ts +94 -0
  142. package/src/tools/snip/snip.js +67 -0
  143. package/src/tools/snip/snip.ts +80 -0
  144. package/src/tools/tasks/backgroundTaskService.js +79 -28
  145. package/src/tools/tasks/backgroundTaskService.ts +165 -49
  146. package/src/tools/tasks/taskManagement.js +163 -0
  147. package/src/tools/tasks/taskManagement.ts +180 -0
  148. package/src/tools/tasks/taskOutput.js +64 -14
  149. package/src/tools/tasks/taskOutput.ts +102 -23
  150. package/src/tools/tasks/taskStop.js +48 -8
  151. package/src/tools/tasks/taskStop.ts +69 -9
  152. package/src/tools/tasks/taskStore.js +46 -0
  153. package/src/tools/tasks/taskStore.ts +77 -0
  154. package/src/tools/terminal/terminalCapture.js +167 -0
  155. package/src/tools/terminal/terminalCapture.ts +179 -0
  156. package/src/tools/trash/trash.js +233 -0
  157. package/src/tools/trash/trash.ts +283 -0
  158. package/src/tools/webbrowser/browserControl.js +5 -1
  159. package/src/tools/webbrowser/browserControl.ts +7 -1
  160. package/src/tools/webbrowser/webbrowser.js +8 -4
  161. package/src/tools/webbrowser/webbrowser.ts +8 -4
  162. package/src/tools/worktree/sessionWorktree.js +128 -0
  163. package/src/tools/worktree/sessionWorktree.ts +158 -0
  164. package/src/tools/write/write.js +6 -2
  165. package/src/tools/write/write.ts +6 -2
  166. package/src/types.ts +22 -4
  167. package/src/ui/hooks/useChat.js +31 -11
  168. package/src/ui/hooks/useChat.ts +31 -13
  169. package/src/web/protocol.ts +78 -3
  170. package/workflows/adaptive-research.yaml +5 -5
  171. package/workflows/book-review-short.yaml +5 -5
  172. package/workflows/e2e-write-greet.yaml +2 -2
  173. package/workflows/illustrate.yaml +12 -12
  174. package/workflows/plan.yaml +18 -18
  175. package/workflows/ppt.yaml +201 -201
  176. package/workflows/ralph-loop.yaml +3 -1
  177. package/workflows/research.md +449 -0
  178. package/workflows/research.yaml +873 -0
  179. package/workflows/webppt.yaml +47 -47
  180. package/workflows/word.yaml +80 -80
  181. package/workflows/youtube-shorts.yaml +25 -25
@@ -11,6 +11,8 @@ import { killTree } from '../shared/killTree.js';
11
11
  import { startBackgroundTask } from '../tasks/backgroundTaskService.js';
12
12
  import { processExecutionMetadata } from '../tasks/processEvidence.js';
13
13
  import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
14
+ import { permanentDeleteReason } from '../shared/permanentDeleteGuard.js';
15
+ import { ToolOutputStore } from '../shared/toolOutputStore.js';
14
16
  const MAX_TIMEOUT_MS = 600_000;
15
17
  function findPowerShell() {
16
18
  if (process.platform !== 'win32')
@@ -35,23 +37,42 @@ const inputSchema = z.object({
35
37
  run_in_background: z.boolean().optional(),
36
38
  });
37
39
  function invocation(command) {
38
- return ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', `$OutputEncoding=[Console]::OutputEncoding=[Text.UTF8Encoding]::new(); ${command}`];
40
+ return [
41
+ '-NoLogo',
42
+ '-NoProfile',
43
+ '-NonInteractive',
44
+ '-Command',
45
+ `$OutputEncoding=[Console]::OutputEncoding=[Text.UTF8Encoding]::new(); ${command}`,
46
+ ];
39
47
  }
40
48
  async function approved(command, signal) {
49
+ const permanentDelete = permanentDeleteReason(command, 'powershell');
50
+ if (permanentDelete)
51
+ return {
52
+ ok: false,
53
+ error: `永久删除检查拒绝执行:${permanentDelete}\n命令:${command}`,
54
+ };
41
55
  const forbidden = checkForbidden(command);
42
56
  if (forbidden)
43
- return { ok: false, error: `安全检查拒绝执行:${forbidden}\n命令:${command}` };
57
+ return {
58
+ ok: false,
59
+ error: `安全检查拒绝执行:${forbidden}\n命令:${command}`,
60
+ };
44
61
  const irreversible = classifyCommand(command);
45
62
  if (!irreversible)
46
63
  return undefined;
47
- const [answer] = await requestUserChoice([{
48
- question: `这条命令不可逆:${irreversible.why}。\n\n命令:${command}\n\n确认执行吗?`, header: '不可逆操作',
64
+ const [answer] = await requestUserChoice([
65
+ {
66
+ question: `这条命令不可逆:${irreversible.why}。\n\n命令:${command}\n\n确认执行吗?`,
67
+ header: '不可逆操作',
49
68
  options: [
50
69
  { label: '取消,不执行 (推荐)', description: '放弃并改用可回滚做法' },
51
70
  { label: '我已确认后果,执行', description: '继续执行不可逆操作' },
52
71
  ],
53
- }], signal);
54
- return answer?.selected?.[0]?.includes('执行') && !answer.selected[0]?.includes('取消')
72
+ },
73
+ ], signal);
74
+ return answer?.selected?.[0]?.includes('执行') &&
75
+ !answer.selected[0]?.includes('取消')
55
76
  ? undefined
56
77
  : { ok: false, error: `已拒绝执行不可逆 PowerShell 命令:${command}` };
57
78
  }
@@ -66,45 +87,128 @@ async function call(input, signal, context) {
66
87
  const cwd = context?.cwd ?? getWorkingDir();
67
88
  if (input.run_in_background) {
68
89
  try {
69
- const task = await startBackgroundTask({ command: input.command, executable, args, cwd, env: sanitizedChildEnv(), sessionId: context?.sessionId, shellKind: 'powershell' }, context?.sessionState?.backgroundTasks);
90
+ const task = await startBackgroundTask({
91
+ command: input.command,
92
+ executable,
93
+ args,
94
+ cwd,
95
+ env: sanitizedChildEnv(),
96
+ sessionId: context?.sessionId,
97
+ workspaceId: context?.workspaceId,
98
+ shellKind: 'powershell',
99
+ }, context?.sessionState?.backgroundTasks);
70
100
  return {
71
101
  ok: true,
72
- content: `PowerShell 后台任务已启动。task_id=${task.id}\nlog=${task.logPath}`,
73
- metadata: processExecutionMetadata({ shell: 'powershell', mode: 'background', command: input.command, status: 'started', taskId: task.id }),
102
+ content: `PowerShell 后台任务已启动。task_id=${task.id.replace(/^proc_/, '')}\nprocess_id=${task.id}\nlog=${task.logPath}`,
103
+ metadata: processExecutionMetadata({
104
+ shell: 'powershell',
105
+ mode: 'background',
106
+ command: input.command,
107
+ status: 'started',
108
+ taskId: task.id,
109
+ }),
74
110
  };
75
111
  }
76
112
  catch (error) {
77
- return { ok: false, error: `PowerShell 后台任务启动失败:${error.message}` };
113
+ return {
114
+ ok: false,
115
+ error: `PowerShell 后台任务启动失败:${error.message}`,
116
+ };
78
117
  }
79
118
  }
80
119
  return await new Promise((resolvePromise) => {
81
- const child = spawn(executable, args, { cwd, env: sanitizedChildEnv(), windowsHide: true, detached: false });
82
- let stdout = '';
83
- let stderr = '';
120
+ const child = spawn(executable, args, {
121
+ cwd,
122
+ env: sanitizedChildEnv(),
123
+ windowsHide: true,
124
+ detached: false,
125
+ });
84
126
  let finished = false;
85
- const finish = (result) => { if (finished)
86
- return; finished = true; clearTimeout(timer); signal?.removeEventListener('abort', abort); resolvePromise(result); };
87
- const abort = () => { killTree(child); finish({ ok: false, error: `PowerShell 命令被中断\n${stdout}\n${stderr}`.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS), metadata: processExecutionMetadata({ shell: 'powershell', mode: 'foreground', command: input.command, status: 'stopped', exitCode: null }) }); };
127
+ const store = new ToolOutputStore({
128
+ workspaceId: context?.workspaceId,
129
+ sessionId: context?.sessionId,
130
+ });
131
+ const finish = async (status, code, prefix = '') => {
132
+ if (finished)
133
+ return;
134
+ finished = true;
135
+ clearTimeout(timer);
136
+ signal?.removeEventListener('abort', abort);
137
+ const output = await store.finish();
138
+ const reference = output.path
139
+ ? `\n[完整输出: ${output.path}; bytes=${output.bytes}; sha256=${output.sha256}; complete=${output.complete}]`
140
+ : '';
141
+ let text = `${prefix}${output.stdout ? `<stdout>\n${output.stdout.trimEnd()}\n</stdout>\n` : ''}${output.stderr ? `<stderr>\n${output.stderr.trimEnd()}\n</stderr>\n` : ''}[exit code: ${code ?? 'killed'}]`;
142
+ if (text.length + reference.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
143
+ const suffix = `\n... (输出预览已截断)${reference}`;
144
+ text =
145
+ text.slice(0, Math.max(0, DEFAULT_MAX_RESULT_SIZE_CHARS - suffix.length)) + suffix;
146
+ }
147
+ else
148
+ text += reference;
149
+ const metadata = processExecutionMetadata({
150
+ shell: 'powershell',
151
+ mode: 'foreground',
152
+ command: input.command,
153
+ status,
154
+ exitCode: code,
155
+ });
156
+ resolvePromise(status === 'completed'
157
+ ? { ok: true, content: text, metadata }
158
+ : { ok: false, error: text, metadata });
159
+ };
160
+ const abort = () => {
161
+ killTree(child);
162
+ void finish('stopped', null, 'PowerShell 命令被中断\n');
163
+ };
88
164
  const timeout = input.timeout ?? 120_000;
89
- const timer = setTimeout(() => { killTree(child); finish({ ok: false, error: `PowerShell 命令超时(${timeout}ms)\n${stdout}\n${stderr}`.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS), metadata: processExecutionMetadata({ shell: 'powershell', mode: 'foreground', command: input.command, status: 'failed', exitCode: null }) }); }, timeout);
165
+ const timer = setTimeout(() => {
166
+ killTree(child);
167
+ void finish('failed', null, `PowerShell 命令超时(${timeout}ms)\n`);
168
+ }, timeout);
90
169
  signal?.addEventListener('abort', abort, { once: true });
91
170
  child.stdout?.setEncoding('utf8');
92
171
  child.stderr?.setEncoding('utf8');
93
- child.stdout?.on('data', (chunk) => { if (stdout.length < DEFAULT_MAX_RESULT_SIZE_CHARS * 2)
94
- stdout += chunk; });
95
- child.stderr?.on('data', (chunk) => { if (stderr.length < DEFAULT_MAX_RESULT_SIZE_CHARS * 2)
96
- stderr += chunk; });
97
- child.once('error', (error) => finish({ ok: false, error: `PowerShell 启动失败:${error.message}`, metadata: processExecutionMetadata({ shell: 'powershell', mode: 'foreground', command: input.command, status: 'failed', exitCode: null }) }));
172
+ child.stdout?.on('data', (chunk) => {
173
+ if (!store.append('stdout', chunk)) {
174
+ child.stdout?.pause();
175
+ store.onDrain(() => child.stdout?.resume());
176
+ }
177
+ });
178
+ child.stderr?.on('data', (chunk) => {
179
+ if (!store.append('stderr', chunk)) {
180
+ child.stderr?.pause();
181
+ store.onDrain(() => child.stderr?.resume());
182
+ }
183
+ });
184
+ child.once('error', (error) => {
185
+ void finish('failed', null, `PowerShell 启动失败:${error.message}\n`);
186
+ });
98
187
  child.once('close', (code) => {
99
- const text = `${stdout ? `<stdout>\n${stdout.trimEnd()}\n</stdout>\n` : ''}${stderr ? `<stderr>\n${stderr.trimEnd()}\n</stderr>\n` : ''}[exit code: ${code}]`.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS);
100
- const metadata = processExecutionMetadata({ shell: 'powershell', mode: 'foreground', command: input.command, status: code === 0 ? 'completed' : 'failed', exitCode: code });
101
- finish(code === 0 ? { ok: true, content: text, metadata } : { ok: false, error: text, metadata });
188
+ void finish(code === 0 ? 'completed' : 'failed', code);
102
189
  });
103
190
  });
104
191
  }
105
192
  export const powerShellTool = {
106
193
  name: 'PowerShell',
107
- description: 'Executes native PowerShell on Windows. Use for Windows-specific administration and scripts; use run_in_background for long builds, tests, or servers, then TaskOutput/TaskStop.',
194
+ description: `Executes native PowerShell on Windows. Use for Windows-specific administration and scripts; use run_in_background for long builds, tests, or servers, then TaskOutput/TaskStop.
195
+
196
+ # Safety - two levels, from strict to lenient
197
+
198
+ ## 1. Blocked outright (fails before execution - do NOT retry with variants)
199
+
200
+ Catastrophic commands (no legitimate use): \`rm -rf /\`-class deletions of root/$HOME/system directories, \`mkfs\`, \`format X:\`, \`dd\` to raw devices, shutdown/reboot, fork bombs, pipe-to-shell from network (\`curl ... | sh\`), \`chmod 777 /\`, full-disk \`del /s\` / \`rmdir /s\`, \`diskpart\`.
201
+
202
+ Permanent deletion (bypasses the Recycle Bin - use the Trash tool instead): \`Remove-Item\`, \`Clear-RecycleBin\`, \`del\` / \`erase\` / \`rd\` / \`rmdir\` / \`rm\` / \`ri\`, and \`.Delete()\` method calls - all fail before execution.
203
+ To delete files or directories use the **Trash** tool (\`action: "move"\` - recoverable); to clean Git untracked files use Trash with \`action: "git-clean"\`. Do NOT try other deletion spellings - they are blocked the same way. If a permanent deletion is truly required, ask the user to run it themselves.
204
+
205
+ ## 2. Requires user confirmation (irreversible)
206
+
207
+ These have legitimate uses but destroy the last copy - execution pauses and asks the user to confirm. In non-interactive contexts they are automatically cancelled.
208
+ - \`git push --force\` / \`-f\` / \`+refspec\`, \`git push --delete <branch>\`
209
+ - \`git filter-branch\` / \`git filter-repo\`, \`git reflog expire --expire=now\` / \`git gc --prune=now\`
210
+ - \`shred -u\` / \`shred --remove\`
211
+ - SQL data destruction: \`DROP DATABASE\`, \`DROP TABLE\`, \`TRUNCATE TABLE\``,
108
212
  inputSchema,
109
213
  parameters: toToolParameters(inputSchema),
110
214
  isReadOnly: false,
@@ -113,7 +217,10 @@ export const powerShellTool = {
113
217
  interruptBehavior: () => 'block',
114
218
  resolveAvailability: () => {
115
219
  const executable = findPowerShell();
116
- return { available: Boolean(executable), signature: executable ?? 'unavailable' };
220
+ return {
221
+ available: Boolean(executable),
222
+ signature: executable ?? 'unavailable',
223
+ };
117
224
  },
118
225
  call: (input, signal) => call(input, signal),
119
226
  callWithContext: (input, signal, context) => call(input, signal, context),
@@ -13,6 +13,8 @@ import { killTree } from '../shared/killTree.js';
13
13
  import { startBackgroundTask } from '../tasks/backgroundTaskService.js';
14
14
  import { processExecutionMetadata } from '../tasks/processEvidence.js';
15
15
  import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
16
+ import { permanentDeleteReason } from '../shared/permanentDeleteGuard.js';
17
+ import { ToolOutputStore } from '../shared/toolOutputStore.js';
16
18
 
17
19
  const MAX_TIMEOUT_MS = 600_000;
18
20
  function findPowerShell(): string | undefined {
@@ -25,7 +27,13 @@ function findPowerShell(): string | undefined {
25
27
  if (existsSync(path)) return path;
26
28
  }
27
29
  }
28
- const system = join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
30
+ const system = join(
31
+ process.env.SystemRoot ?? 'C:\\Windows',
32
+ 'System32',
33
+ 'WindowsPowerShell',
34
+ 'v1.0',
35
+ 'powershell.exe',
36
+ );
29
37
  return existsSync(system) ? system : undefined;
30
38
  }
31
39
 
@@ -38,27 +46,57 @@ const inputSchema = z.object({
38
46
  type PowerShellInput = z.infer<typeof inputSchema>;
39
47
 
40
48
  function invocation(command: string): string[] {
41
- return ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', `$OutputEncoding=[Console]::OutputEncoding=[Text.UTF8Encoding]::new(); ${command}`];
49
+ return [
50
+ '-NoLogo',
51
+ '-NoProfile',
52
+ '-NonInteractive',
53
+ '-Command',
54
+ `$OutputEncoding=[Console]::OutputEncoding=[Text.UTF8Encoding]::new(); ${command}`,
55
+ ];
42
56
  }
43
57
 
44
- async function approved(command: string, signal?: AbortSignal): Promise<ToolResult | undefined> {
58
+ async function approved(
59
+ command: string,
60
+ signal?: AbortSignal,
61
+ ): Promise<ToolResult | undefined> {
62
+ const permanentDelete = permanentDeleteReason(command, 'powershell');
63
+ if (permanentDelete)
64
+ return {
65
+ ok: false,
66
+ error: `永久删除检查拒绝执行:${permanentDelete}\n命令:${command}`,
67
+ };
45
68
  const forbidden = checkForbidden(command);
46
- if (forbidden) return { ok: false, error: `安全检查拒绝执行:${forbidden}\n命令:${command}` };
69
+ if (forbidden)
70
+ return {
71
+ ok: false,
72
+ error: `安全检查拒绝执行:${forbidden}\n命令:${command}`,
73
+ };
47
74
  const irreversible = classifyCommand(command);
48
75
  if (!irreversible) return undefined;
49
- const [answer] = await requestUserChoice([{
50
- question: `这条命令不可逆:${irreversible.why}。\n\n命令:${command}\n\n确认执行吗?`, header: '不可逆操作',
51
- options: [
52
- { label: '取消,不执行 (推荐)', description: '放弃并改用可回滚做法' },
53
- { label: '我已确认后果,执行', description: '继续执行不可逆操作' },
76
+ const [answer] = await requestUserChoice(
77
+ [
78
+ {
79
+ question: `这条命令不可逆:${irreversible.why}。\n\n命令:${command}\n\n确认执行吗?`,
80
+ header: '不可逆操作',
81
+ options: [
82
+ { label: '取消,不执行 (推荐)', description: '放弃并改用可回滚做法' },
83
+ { label: '我已确认后果,执行', description: '继续执行不可逆操作' },
84
+ ],
85
+ },
54
86
  ],
55
- }], signal);
56
- return answer?.selected?.[0]?.includes('执行') && !answer.selected[0]?.includes('取消')
87
+ signal,
88
+ );
89
+ return answer?.selected?.[0]?.includes('执行') &&
90
+ !answer.selected[0]?.includes('取消')
57
91
  ? undefined
58
92
  : { ok: false, error: `已拒绝执行不可逆 PowerShell 命令:${command}` };
59
93
  }
60
94
 
61
- async function call(input: PowerShellInput, signal?: AbortSignal, context?: ToolCallContext): Promise<ToolResult> {
95
+ async function call(
96
+ input: PowerShellInput,
97
+ signal?: AbortSignal,
98
+ context?: ToolCallContext,
99
+ ): Promise<ToolResult> {
62
100
  const executable = findPowerShell();
63
101
  if (!executable) return { ok: false, error: '当前宿主没有可用的 PowerShell' };
64
102
  const refusal = await approved(input.command, signal);
@@ -67,38 +105,138 @@ async function call(input: PowerShellInput, signal?: AbortSignal, context?: Tool
67
105
  const cwd = context?.cwd ?? getWorkingDir();
68
106
  if (input.run_in_background) {
69
107
  try {
70
- const task = await startBackgroundTask({ command: input.command, executable, args, cwd, env: sanitizedChildEnv(), sessionId: context?.sessionId, shellKind: 'powershell' }, context?.sessionState?.backgroundTasks);
108
+ const task = await startBackgroundTask(
109
+ {
110
+ command: input.command,
111
+ executable,
112
+ args,
113
+ cwd,
114
+ env: sanitizedChildEnv(),
115
+ sessionId: context?.sessionId,
116
+ workspaceId: context?.workspaceId,
117
+ shellKind: 'powershell',
118
+ },
119
+ context?.sessionState?.backgroundTasks,
120
+ );
71
121
  return {
72
122
  ok: true,
73
- content: `PowerShell 后台任务已启动。task_id=${task.id}\nlog=${task.logPath}`,
74
- metadata: processExecutionMetadata({ shell: 'powershell', mode: 'background', command: input.command, status: 'started', taskId: task.id }),
123
+ content: `PowerShell 后台任务已启动。task_id=${task.id.replace(/^proc_/, '')}\nprocess_id=${task.id}\nlog=${task.logPath}`,
124
+ metadata: processExecutionMetadata({
125
+ shell: 'powershell',
126
+ mode: 'background',
127
+ command: input.command,
128
+ status: 'started',
129
+ taskId: task.id,
130
+ }),
75
131
  };
76
- } catch (error) { return { ok: false, error: `PowerShell 后台任务启动失败:${(error as Error).message}` }; }
132
+ } catch (error) {
133
+ return {
134
+ ok: false,
135
+ error: `PowerShell 后台任务启动失败:${(error as Error).message}`,
136
+ };
137
+ }
77
138
  }
78
139
 
79
140
  return await new Promise<ToolResult>((resolvePromise) => {
80
- const child = spawn(executable, args, { cwd, env: sanitizedChildEnv(), windowsHide: true, detached: false });
81
- let stdout = ''; let stderr = ''; let finished = false;
82
- const finish = (result: ToolResult) => { if (finished) return; finished = true; clearTimeout(timer); signal?.removeEventListener('abort', abort); resolvePromise(result); };
83
- const abort = () => { killTree(child); finish({ ok: false, error: `PowerShell 命令被中断\n${stdout}\n${stderr}`.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS), metadata: processExecutionMetadata({ shell: 'powershell', mode: 'foreground', command: input.command, status: 'stopped', exitCode: null }) }); };
141
+ const child = spawn(executable, args, {
142
+ cwd,
143
+ env: sanitizedChildEnv(),
144
+ windowsHide: true,
145
+ detached: false,
146
+ });
147
+ let finished = false;
148
+ const store = new ToolOutputStore({
149
+ workspaceId: context?.workspaceId,
150
+ sessionId: context?.sessionId,
151
+ });
152
+ const finish = async (
153
+ status: 'completed' | 'failed' | 'stopped',
154
+ code: number | null,
155
+ prefix = '',
156
+ ) => {
157
+ if (finished) return;
158
+ finished = true;
159
+ clearTimeout(timer);
160
+ signal?.removeEventListener('abort', abort);
161
+ const output = await store.finish();
162
+ const reference = output.path
163
+ ? `\n[完整输出: ${output.path}; bytes=${output.bytes}; sha256=${output.sha256}; complete=${output.complete}]`
164
+ : '';
165
+ let text = `${prefix}${output.stdout ? `<stdout>\n${output.stdout.trimEnd()}\n</stdout>\n` : ''}${output.stderr ? `<stderr>\n${output.stderr.trimEnd()}\n</stderr>\n` : ''}[exit code: ${code ?? 'killed'}]`;
166
+ if (text.length + reference.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
167
+ const suffix = `\n... (输出预览已截断)${reference}`;
168
+ text =
169
+ text.slice(
170
+ 0,
171
+ Math.max(0, DEFAULT_MAX_RESULT_SIZE_CHARS - suffix.length),
172
+ ) + suffix;
173
+ } else text += reference;
174
+ const metadata = processExecutionMetadata({
175
+ shell: 'powershell',
176
+ mode: 'foreground',
177
+ command: input.command,
178
+ status,
179
+ exitCode: code,
180
+ });
181
+ resolvePromise(
182
+ status === 'completed'
183
+ ? { ok: true, content: text, metadata }
184
+ : { ok: false, error: text, metadata },
185
+ );
186
+ };
187
+ const abort = () => {
188
+ killTree(child);
189
+ void finish('stopped', null, 'PowerShell 命令被中断\n');
190
+ };
84
191
  const timeout = input.timeout ?? 120_000;
85
- const timer = setTimeout(() => { killTree(child); finish({ ok: false, error: `PowerShell 命令超时(${timeout}ms)\n${stdout}\n${stderr}`.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS), metadata: processExecutionMetadata({ shell: 'powershell', mode: 'foreground', command: input.command, status: 'failed', exitCode: null }) }); }, timeout);
192
+ const timer = setTimeout(() => {
193
+ killTree(child);
194
+ void finish('failed', null, `PowerShell 命令超时(${timeout}ms)\n`);
195
+ }, timeout);
86
196
  signal?.addEventListener('abort', abort, { once: true });
87
- child.stdout?.setEncoding('utf8'); child.stderr?.setEncoding('utf8');
88
- child.stdout?.on('data', (chunk) => { if (stdout.length < DEFAULT_MAX_RESULT_SIZE_CHARS * 2) stdout += chunk; });
89
- child.stderr?.on('data', (chunk) => { if (stderr.length < DEFAULT_MAX_RESULT_SIZE_CHARS * 2) stderr += chunk; });
90
- child.once('error', (error) => finish({ ok: false, error: `PowerShell 启动失败:${error.message}`, metadata: processExecutionMetadata({ shell: 'powershell', mode: 'foreground', command: input.command, status: 'failed', exitCode: null }) }));
197
+ child.stdout?.setEncoding('utf8');
198
+ child.stderr?.setEncoding('utf8');
199
+ child.stdout?.on('data', (chunk) => {
200
+ if (!store.append('stdout', chunk)) {
201
+ child.stdout?.pause();
202
+ store.onDrain(() => child.stdout?.resume());
203
+ }
204
+ });
205
+ child.stderr?.on('data', (chunk) => {
206
+ if (!store.append('stderr', chunk)) {
207
+ child.stderr?.pause();
208
+ store.onDrain(() => child.stderr?.resume());
209
+ }
210
+ });
211
+ child.once('error', (error) => {
212
+ void finish('failed', null, `PowerShell 启动失败:${error.message}\n`);
213
+ });
91
214
  child.once('close', (code) => {
92
- const text = `${stdout ? `<stdout>\n${stdout.trimEnd()}\n</stdout>\n` : ''}${stderr ? `<stderr>\n${stderr.trimEnd()}\n</stderr>\n` : ''}[exit code: ${code}]`.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS);
93
- const metadata = processExecutionMetadata({ shell: 'powershell', mode: 'foreground', command: input.command, status: code === 0 ? 'completed' : 'failed', exitCode: code });
94
- finish(code === 0 ? { ok: true, content: text, metadata } : { ok: false, error: text, metadata });
215
+ void finish(code === 0 ? 'completed' : 'failed', code);
95
216
  });
96
217
  });
97
218
  }
98
219
 
99
220
  export const powerShellTool: Tool<PowerShellInput> = {
100
221
  name: 'PowerShell',
101
- description: 'Executes native PowerShell on Windows. Use for Windows-specific administration and scripts; use run_in_background for long builds, tests, or servers, then TaskOutput/TaskStop.',
222
+ description: `Executes native PowerShell on Windows. Use for Windows-specific administration and scripts; use run_in_background for long builds, tests, or servers, then TaskOutput/TaskStop.
223
+
224
+ # Safety - two levels, from strict to lenient
225
+
226
+ ## 1. Blocked outright (fails before execution - do NOT retry with variants)
227
+
228
+ Catastrophic commands (no legitimate use): \`rm -rf /\`-class deletions of root/$HOME/system directories, \`mkfs\`, \`format X:\`, \`dd\` to raw devices, shutdown/reboot, fork bombs, pipe-to-shell from network (\`curl ... | sh\`), \`chmod 777 /\`, full-disk \`del /s\` / \`rmdir /s\`, \`diskpart\`.
229
+
230
+ Permanent deletion (bypasses the Recycle Bin - use the Trash tool instead): \`Remove-Item\`, \`Clear-RecycleBin\`, \`del\` / \`erase\` / \`rd\` / \`rmdir\` / \`rm\` / \`ri\`, and \`.Delete()\` method calls - all fail before execution.
231
+ To delete files or directories use the **Trash** tool (\`action: "move"\` - recoverable); to clean Git untracked files use Trash with \`action: "git-clean"\`. Do NOT try other deletion spellings - they are blocked the same way. If a permanent deletion is truly required, ask the user to run it themselves.
232
+
233
+ ## 2. Requires user confirmation (irreversible)
234
+
235
+ These have legitimate uses but destroy the last copy - execution pauses and asks the user to confirm. In non-interactive contexts they are automatically cancelled.
236
+ - \`git push --force\` / \`-f\` / \`+refspec\`, \`git push --delete <branch>\`
237
+ - \`git filter-branch\` / \`git filter-repo\`, \`git reflog expire --expire=now\` / \`git gc --prune=now\`
238
+ - \`shred -u\` / \`shred --remove\`
239
+ - SQL data destruction: \`DROP DATABASE\`, \`DROP TABLE\`, \`TRUNCATE TABLE\``,
102
240
  inputSchema,
103
241
  parameters: toToolParameters(inputSchema),
104
242
  isReadOnly: false,
@@ -107,7 +245,10 @@ export const powerShellTool: Tool<PowerShellInput> = {
107
245
  interruptBehavior: () => 'block',
108
246
  resolveAvailability: () => {
109
247
  const executable = findPowerShell();
110
- return { available: Boolean(executable), signature: executable ?? 'unavailable' };
248
+ return {
249
+ available: Boolean(executable),
250
+ signature: executable ?? 'unavailable',
251
+ };
111
252
  },
112
253
  call: (input, signal) => call(input, signal),
113
254
  callWithContext: (input, signal, context) => call(input, signal, context),
@@ -7,17 +7,27 @@ import { getWorkingDir } from '../../bootstrap/workingDir.js';
7
7
  import { toToolParameters } from '../../utils/zodToJson.js';
8
8
  const MIME_BY_EXT = {
9
9
  '.csv': 'text/csv',
10
+ '.css': 'text/css',
10
11
  '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
12
+ '.gif': 'image/gif',
11
13
  '.html': 'text/html',
14
+ '.js': 'text/javascript',
12
15
  '.jpeg': 'image/jpeg',
13
16
  '.jpg': 'image/jpeg',
14
17
  '.json': 'application/json',
15
18
  '.md': 'text/markdown',
19
+ '.mp4': 'video/mp4',
16
20
  '.pdf': 'application/pdf',
17
21
  '.png': 'image/png',
18
22
  '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
23
+ '.py': 'text/x-python',
24
+ '.svg': 'image/svg+xml',
25
+ '.ts': 'text/typescript',
26
+ '.tsx': 'text/typescript',
19
27
  '.txt': 'text/plain',
28
+ '.webp': 'image/webp',
20
29
  '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
30
+ '.zip': 'application/zip',
21
31
  };
22
32
  const verificationSchema = z.enum(['passed', 'failed', 'not_run']).default('not_run');
23
33
  const inputSchema = z.object({
@@ -10,17 +10,27 @@ import { toToolParameters } from '../../utils/zodToJson.js';
10
10
 
11
11
  const MIME_BY_EXT: Record<string, string> = {
12
12
  '.csv': 'text/csv',
13
+ '.css': 'text/css',
13
14
  '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
15
+ '.gif': 'image/gif',
14
16
  '.html': 'text/html',
17
+ '.js': 'text/javascript',
15
18
  '.jpeg': 'image/jpeg',
16
19
  '.jpg': 'image/jpeg',
17
20
  '.json': 'application/json',
18
21
  '.md': 'text/markdown',
22
+ '.mp4': 'video/mp4',
19
23
  '.pdf': 'application/pdf',
20
24
  '.png': 'image/png',
21
25
  '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
26
+ '.py': 'text/x-python',
27
+ '.svg': 'image/svg+xml',
28
+ '.ts': 'text/typescript',
29
+ '.tsx': 'text/typescript',
22
30
  '.txt': 'text/plain',
31
+ '.webp': 'image/webp',
23
32
  '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
33
+ '.zip': 'application/zip',
24
34
  };
25
35
 
26
36
  const verificationSchema = z.enum(['passed', 'failed', 'not_run']).default('not_run');