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.
- package/package.json +5 -1
- package/plugins/workflow-runner/plugin.js +27 -15
- package/plugins/workflow-runner/plugin.ts +34 -20
- package/plugins/workflow-runner/src/agentCommon.js +13 -12
- package/plugins/workflow-runner/src/agentCommon.ts +22 -13
- package/plugins/workflow-runner/src/expressions.js +29 -8
- package/plugins/workflow-runner/src/expressions.ts +28 -10
- package/plugins/workflow-runner/src/index.js +50 -20
- package/plugins/workflow-runner/src/index.ts +75 -31
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +2 -1
- package/plugins/workflow-runner/src/recovery.js +9 -2
- package/plugins/workflow-runner/src/recovery.ts +9 -2
- package/plugins/workflow-runner/src/runner.js +31 -5
- package/plugins/workflow-runner/src/runner.ts +33 -5
- package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -1
- package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
- package/plugins/workflow-runner/src/stepExecutors/tool.js +91 -61
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +111 -71
- package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
- package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
- package/plugins/workflow-runner/src/taskIdentity.js +197 -21
- package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.js +113 -7
- package/plugins/workflow-runner/src/types.ts +137 -14
- package/plugins/workflow-runner/src/workflowState.js +455 -54
- package/plugins/workflow-runner/src/workflowState.ts +508 -63
- package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
- package/plugins/workflow-runner/test/context-files.test.ts +47 -16
- package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
- package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
- package/plugins/workflow-runner/test/expressions.test.ts +42 -0
- package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
- package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
- package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
- package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
- package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
- package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
- package/plugins/workflow-runner/test/research.load.test.ts +328 -0
- package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
- package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
- package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
- package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
- package/plugins/workflow-runner/test/word.load.test.ts +9 -9
- package/skills/skill-creator/scripts/run_eval.py +3 -2
- package/src/cli/print.js +36 -4
- package/src/cli/print.ts +42 -4
- package/src/cli/streamJson.js +19 -4
- package/src/cli/streamJson.ts +17 -4
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configFile.js +2 -21
- package/src/config/configFile.ts +11 -24
- package/src/config/configManagement.js +1 -1
- package/src/config/configManagement.ts +2 -2
- package/src/config/configService.js +24 -0
- package/src/config/configService.ts +23 -0
- package/src/context/compact.js +4 -0
- package/src/context/compact.ts +4 -0
- package/src/context/contextBundle.js +38 -13
- package/src/context/contextBundle.ts +47 -13
- package/src/harness/stallGuard.js +19 -4
- package/src/harness/stallGuard.ts +30 -7
- package/src/infra/verification.js +25 -18
- package/src/infra/verification.ts +50 -18
- package/src/loop.js +95 -11
- package/src/loop.ts +125 -16
- package/src/main.js +8 -1
- package/src/main.tsx +7 -1
- package/src/memory/constants.js +10 -0
- package/src/memory/constants.ts +13 -0
- package/src/memory/episodicMemory.js +274 -2
- package/src/memory/episodicMemory.ts +306 -1
- package/src/memory/fileStore.js +24 -4
- package/src/memory/fileStore.ts +22 -2
- package/src/memory/memoryService.js +79 -12
- package/src/memory/memoryService.ts +108 -9
- package/src/memory/repository.js +415 -85
- package/src/memory/repository.ts +437 -86
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.ts +16 -0
- package/src/plugins/pluginApi.ts +30 -6
- package/src/plugins/pluginRunner.js +23 -8
- package/src/plugins/pluginRunner.ts +32 -13
- package/src/prompts/policyRegistry.js +55 -1
- package/src/prompts/policyRegistry.ts +55 -1
- package/src/prompts/promptAssembly.js +39 -7
- package/src/prompts/promptAssembly.ts +145 -59
- package/src/session/runAgentTurn.js +43 -21
- package/src/session/runAgentTurn.ts +68 -26
- package/src/session/runTurn.js +11 -46
- package/src/session/runTurn.ts +12 -46
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +5 -0
- package/src/session/sessionState.ts +14 -0
- package/src/tools/advisor/advisor.js +76 -9
- package/src/tools/advisor/advisor.ts +79 -9
- package/src/tools/agent/agent.js +143 -14
- package/src/tools/agent/agent.ts +149 -14
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +5 -0
- package/src/tools/agent/agentTypes.ts +5 -0
- package/src/tools/bash/bash.js +80 -15
- package/src/tools/bash/bash.ts +83 -15
- package/src/tools/capabilities.js +5 -0
- package/src/tools/capabilities.ts +11 -1
- package/src/tools/edit/edit.js +7 -4
- package/src/tools/edit/edit.ts +7 -4
- package/src/tools/edit/multi-edit.js +4 -2
- package/src/tools/edit/multi-edit.ts +4 -2
- package/src/tools/glob/glob.js +156 -40
- package/src/tools/glob/glob.ts +185 -46
- package/src/tools/index.js +35 -17
- package/src/tools/index.ts +81 -20
- package/src/tools/lsp/lsp.js +165 -22
- package/src/tools/lsp/lsp.ts +206 -36
- package/src/tools/powershell/powershell.js +134 -27
- package/src/tools/powershell/powershell.ts +171 -30
- package/src/tools/presentresult/presentresult.js +10 -0
- package/src/tools/presentresult/presentresult.ts +10 -0
- package/src/tools/read/read.js +249 -29
- package/src/tools/read/read.ts +396 -59
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +2 -1
- package/src/tools/shared/fileActivity.ts +3 -0
- package/src/tools/shared/permanentDeleteGuard.js +57 -0
- package/src/tools/shared/permanentDeleteGuard.ts +71 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +79 -28
- package/src/tools/tasks/backgroundTaskService.ts +165 -49
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +64 -14
- package/src/tools/tasks/taskOutput.ts +102 -23
- package/src/tools/tasks/taskStop.js +48 -8
- package/src/tools/tasks/taskStop.ts +69 -9
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- package/src/tools/webbrowser/browserControl.js +5 -1
- package/src/tools/webbrowser/browserControl.ts +7 -1
- package/src/tools/webbrowser/webbrowser.js +8 -4
- package/src/tools/webbrowser/webbrowser.ts +8 -4
- package/src/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +6 -2
- package/src/tools/write/write.ts +6 -2
- package/src/types.ts +22 -4
- package/src/ui/hooks/useChat.js +31 -11
- package/src/ui/hooks/useChat.ts +31 -13
- package/src/web/protocol.ts +78 -3
- package/workflows/adaptive-research.yaml +5 -5
- package/workflows/book-review-short.yaml +5 -5
- package/workflows/e2e-write-greet.yaml +2 -2
- package/workflows/illustrate.yaml +12 -12
- package/workflows/plan.yaml +18 -18
- package/workflows/ppt.yaml +201 -201
- package/workflows/ralph-loop.yaml +3 -1
- package/workflows/research.md +449 -0
- package/workflows/research.yaml +873 -0
- package/workflows/webppt.yaml +47 -47
- package/workflows/word.yaml +80 -80
- 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 [
|
|
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 {
|
|
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
|
-
|
|
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
|
-
}
|
|
54
|
-
|
|
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({
|
|
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({
|
|
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 {
|
|
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, {
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
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(() => {
|
|
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) => {
|
|
94
|
-
stdout
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
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(
|
|
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 [
|
|
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(
|
|
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)
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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(
|
|
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(
|
|
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({
|
|
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) {
|
|
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, {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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(() => {
|
|
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');
|
|
88
|
-
child.
|
|
89
|
-
child.
|
|
90
|
-
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
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');
|