minimal-agent 0.2.0 → 0.3.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/README.md +50 -72
- package/package.json +18 -13
- package/plugins/ralph-wiggum/plugin.js +205 -0
- package/plugins/ralph-wiggum/src/goalState.js +260 -0
- package/plugins/ralph-wiggum/src/{sentinels.ts → sentinels.js} +4 -7
- package/plugins/ralph-wiggum/src/stopHookRunner.js +104 -0
- package/plugins/ralph-wiggum/src/verificationGate.js +202 -0
- package/plugins/workflow-runner/{plugin.ts → plugin.js} +20 -26
- package/plugins/workflow-runner/src/expressions.js +369 -0
- package/plugins/workflow-runner/src/index.js +174 -0
- package/plugins/workflow-runner/src/loader.js +183 -0
- package/plugins/workflow-runner/src/runner.js +290 -0
- package/plugins/workflow-runner/src/stepExecutors/assert.js +28 -0
- package/plugins/workflow-runner/src/stepExecutors/llm.js +44 -0
- package/plugins/workflow-runner/src/stepExecutors/skill.js +103 -0
- package/plugins/workflow-runner/src/stepExecutors/{tool.ts → tool.js} +19 -25
- package/plugins/workflow-runner/src/types.js +59 -0
- package/plugins/workflow-runner/src/{workflowState.ts → workflowState.js} +21 -40
- package/src/bootstrap/cwdArg.js +22 -0
- package/src/bootstrap/workingDir.js +31 -0
- package/src/cli/configWizard.js +272 -0
- package/src/cli/print.js +192 -0
- package/src/config/configFile.js +78 -0
- package/src/config.js +118 -0
- package/src/context/compact.js +357 -0
- package/src/context/microCompactLite.js +151 -0
- package/src/context/persistContext.js +109 -0
- package/src/context/reactiveCompact.js +121 -0
- package/src/context/sessionPath.js +58 -0
- package/src/context/snipCompact.js +112 -0
- package/src/context/tokenCounter.js +66 -0
- package/src/llm/client.js +182 -0
- package/src/loop.js +230 -0
- package/src/main.js +116 -0
- package/src/plugin-sdk.js +24 -0
- package/src/plugins/commandRouter.js +169 -0
- package/src/plugins/hookEngine.js +258 -0
- package/src/plugins/pluginApi.js +23 -0
- package/src/plugins/pluginLoader.js +71 -0
- package/src/plugins/pluginRunner.js +65 -0
- package/src/plugins/transcript.js +171 -0
- package/src/prompts/projectInstructions.js +48 -0
- package/src/prompts/skillList.js +126 -0
- package/src/prompts/system.js +155 -0
- package/src/session/runTurn.js +41 -0
- package/src/session/sessionState.js +19 -0
- package/src/tools/bash/bash.js +352 -0
- package/src/tools/bash/semantics.js +85 -0
- package/src/tools/bash/warnings.js +98 -0
- package/src/tools/edit/edit.js +253 -0
- package/src/tools/edit/multi-edit.js +155 -0
- package/src/tools/glob/glob.js +97 -0
- package/src/tools/grep/grep.js +185 -0
- package/src/tools/grep/rgPath.js +173 -0
- package/src/tools/index.js +94 -0
- package/src/tools/read/read.js +209 -0
- package/src/tools/shared/fileState.js +61 -0
- package/src/tools/shared/fileUtils.js +281 -0
- package/src/tools/shared/schemas.js +16 -0
- package/src/tools/types.js +21 -0
- package/src/tools/webbrowser/browser.js +55 -0
- package/src/tools/webbrowser/webbrowser.js +194 -0
- package/src/tools/webfetch/preapproved.js +267 -0
- package/src/tools/webfetch/webfetch.js +317 -0
- package/src/tools/websearch/websearch.js +161 -0
- package/src/tools/write/write.js +125 -0
- package/src/types/turndown.d.ts +23 -0
- package/src/types.js +16 -0
- package/src/ui/App.js +37 -0
- package/src/ui/InputBox.js +240 -0
- package/src/ui/MessageList.js +28 -0
- package/src/ui/Root.js +70 -0
- package/src/ui/StatusLine.js +41 -0
- package/src/ui/ToolStatus.js +11 -0
- package/src/ui/hooks/useChat.js +234 -0
- package/src/ui/hooks/usePasteHandler.js +137 -0
- package/src/ui/hooks/useTextBuffer.js +55 -0
- package/src/ui/hooks/useTokenUsage.js +30 -0
- package/src/ui/textBuffer.js +217 -0
- package/src/utils/packageRoot.js +37 -0
- package/src/utils/resourcePaths.js +49 -0
- package/src/utils/zodToJson.js +29 -0
- package/dist/main.js +0 -5315
- package/plugins/ralph-wiggum/plugin.ts +0 -275
- package/plugins/ralph-wiggum/scripts/setup-ralph-loop.sh +0 -203
- package/plugins/ralph-wiggum/src/goalState.ts +0 -310
- package/plugins/ralph-wiggum/src/stopHookRunner.ts +0 -136
- package/plugins/ralph-wiggum/src/verificationGate.ts +0 -252
- package/plugins/ralph-wiggum/test/goalState.test.ts +0 -410
- package/plugins/ralph-wiggum/test/verificationGate.test.ts +0 -122
- package/plugins/workflow-runner/src/expressions.ts +0 -371
- package/plugins/workflow-runner/src/index.ts +0 -194
- package/plugins/workflow-runner/src/loader.ts +0 -193
- package/plugins/workflow-runner/src/runner.ts +0 -313
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +0 -30
- package/plugins/workflow-runner/src/stepExecutors/llm.ts +0 -54
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +0 -115
- package/plugins/workflow-runner/src/types.ts +0 -183
- package/plugins/workflow-runner/test/cli.e2e.test.ts +0 -114
- package/plugins/workflow-runner/test/e2e.test.ts +0 -268
- package/plugins/workflow-runner/test/expressions.test.ts +0 -140
- package/plugins/workflow-runner/test/fixtures/cli-e2e.yaml +0 -27
- package/plugins/workflow-runner/test/fixtures/hello-workflow.yaml +0 -49
- package/plugins/workflow-runner/test/graceful.test.ts +0 -139
- package/plugins/workflow-runner/test/loader.test.ts +0 -216
- package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +0 -230
- package/plugins/workflow-runner/test/runner.test.ts +0 -511
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* src/tools/bash.ts —— Bash 工具(万能命令兜底)
|
|
4
|
+
* ------------------------------------------------------------
|
|
5
|
+
* 对应 kakadeai 主仓库的 BashTool。
|
|
6
|
+
* 这是 minimal-agent 的"瑞士军刀" —— 当 Read/Edit/Write/Glob/Grep
|
|
7
|
+
* 搞不定时(rm 文件、chmod、git、npm/curl、跑测试...),就用它。
|
|
8
|
+
*
|
|
9
|
+
* 设计原则(重要):
|
|
10
|
+
*
|
|
11
|
+
* 1. 优先用专用工具,Bash 只做兜底
|
|
12
|
+
* - 读文件 → Read(不要 cat/head/tail)
|
|
13
|
+
* - 写文件 → Write(不要 echo > / cat <<EOF)
|
|
14
|
+
* - 改文件 → Edit(不要 sed/awk)
|
|
15
|
+
* - 列文件 → Glob(不要 find/ls)
|
|
16
|
+
* - 搜内容 → Grep(不要 grep/rg)
|
|
17
|
+
* 这条放进 description 里反复强调,让模型自觉。
|
|
18
|
+
*
|
|
19
|
+
* 2. 安全黑名单:拦截灾难性命令
|
|
20
|
+
* - rm -rf / 、 rm -rf $HOME、 rm -rf ~
|
|
21
|
+
* - mkfs / format c: / dd if=...of=/dev/sd*
|
|
22
|
+
* - shutdown / reboot / halt / poweroff
|
|
23
|
+
* - fork bomb :(){ :|:& };:
|
|
24
|
+
* - curl ... | sh / wget ... | bash (管道到 shell)
|
|
25
|
+
* 不可绕过:在 call() 入口正则匹配,命中直接拒绝。
|
|
26
|
+
* 注意:黑名单是兜底,不是真正的沙盒;模型规规矩矩工作时不会误伤。
|
|
27
|
+
*
|
|
28
|
+
* 3. 超时保护:默认 120s,可显式设到最多 600s(10 分钟)
|
|
29
|
+
* - 防止跑死的命令吃光会话;
|
|
30
|
+
* - AbortSignal + timer 双保险。
|
|
31
|
+
*
|
|
32
|
+
* 4. 跨平台:
|
|
33
|
+
* - 用 spawn({ shell: true }),让平台自己挑 shell
|
|
34
|
+
* (Unix → /bin/sh -c,Windows → cmd.exe /d /s /c)
|
|
35
|
+
* - 对模型透明:在 prompt 里说"假定 bash 语法",
|
|
36
|
+
* Windows 用户可能要自行调整跨平台命令(这是 minimal 版本,不做转译)
|
|
37
|
+
*
|
|
38
|
+
* 5. 输出兜底:
|
|
39
|
+
* - stdout / stderr / exit code 一起返回(结构化文本)
|
|
40
|
+
* - 超过 30K 字符截断,避免把 npm install 的 200K 日志灌进上下文
|
|
41
|
+
* ============================================================
|
|
42
|
+
*/
|
|
43
|
+
import { spawn } from 'node:child_process';
|
|
44
|
+
import { z } from 'zod';
|
|
45
|
+
import { getWorkingDir } from '../../bootstrap/workingDir.js';
|
|
46
|
+
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
|
|
47
|
+
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
48
|
+
import { interpretCommandResult } from './semantics.js';
|
|
49
|
+
import { scanDestructiveCommand } from './warnings.js';
|
|
50
|
+
// ---------------- 0. 常量 ----------------
|
|
51
|
+
const DEFAULT_TIMEOUT_MS = 120_000; // 2 min
|
|
52
|
+
const MAX_TIMEOUT_MS = 600_000; // 10 min
|
|
53
|
+
/**
|
|
54
|
+
* 灾难性命令黑名单。
|
|
55
|
+
* 命中任意一条就直接拒绝执行 —— 这层是"防猪队友"而不是真沙盒。
|
|
56
|
+
*
|
|
57
|
+
* 选词依据(每条注释解释为啥要拦):
|
|
58
|
+
* - 历史上"误删生产/格盘/锁机"的真实事故都对应一类模式
|
|
59
|
+
* - 工程协作里这些命令几乎不会被合法用到(如要用,肯定是用户在终端自己手敲)
|
|
60
|
+
*
|
|
61
|
+
* 工具仅是"模型可调用的能力",模型规规矩矩地写代码、跑测试时永远不会触到这些。
|
|
62
|
+
*/
|
|
63
|
+
const FORBIDDEN_PATTERNS = [
|
|
64
|
+
// ---- rm -rf / 及变体 ----
|
|
65
|
+
// 经典:"rm -rf /"、"rm -rf /*"、"rm -fr /usr"、"rm -rf $HOME"、"rm -rf ~"
|
|
66
|
+
// 也覆盖 sudo rm -rf /
|
|
67
|
+
{
|
|
68
|
+
pattern: /\brm\s+(?:-[a-zA-Z]*[rRf][a-zA-Z]*\s+)+(?:\/+\*?|\$HOME|~)(?:\s|$)/,
|
|
69
|
+
reason: '禁止递归删除根目录或家目录(rm -rf / 类)',
|
|
70
|
+
},
|
|
71
|
+
// 拦截 "rm -rf <绝对路径>" 中目标是关键系统目录的情况(防误伤其它正常 rm -rf ./tmp)
|
|
72
|
+
{
|
|
73
|
+
pattern: /\brm\s+(?:-[a-zA-Z]*[rRf][a-zA-Z]*\s+)+(?:\/etc|\/usr|\/bin|\/sbin|\/var|\/boot|\/lib|\/System|\/Windows|\/Users|\/home)(?:\/|\s|$)/,
|
|
74
|
+
reason: '禁止递归删除系统关键目录',
|
|
75
|
+
},
|
|
76
|
+
// ---- 格盘 / 改写裸设备 ----
|
|
77
|
+
{
|
|
78
|
+
pattern: /\bmkfs(?:\.[a-z0-9]+)?\b/i,
|
|
79
|
+
reason: '禁止格式化文件系统(mkfs)',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
// 末尾不能用 \b:盘符 "C:" 后是非 word char,\b 匹配不到。
|
|
83
|
+
// 改成"盘符后跟空白/反斜杠/结尾"。
|
|
84
|
+
pattern: /\bformat\s+[A-Za-z]:(?=\s|\\|$)/i,
|
|
85
|
+
reason: '禁止 Windows 格盘命令(format X:)',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
pattern: /\bdd\s+[^|;&]*\bof=\/dev\/(?:sd[a-z]|nvme|disk|hd[a-z]|mmcblk)/i,
|
|
89
|
+
reason: '禁止 dd 写入裸磁盘设备',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
pattern: />\s*\/dev\/(?:sd[a-z]|nvme|disk|hd[a-z]|mmcblk)/i,
|
|
93
|
+
reason: '禁止重定向到裸磁盘设备',
|
|
94
|
+
},
|
|
95
|
+
// ---- 关机 / 重启 ----
|
|
96
|
+
{
|
|
97
|
+
pattern: /\b(?:shutdown|reboot|halt|poweroff)\b/i,
|
|
98
|
+
reason: '禁止关机/重启命令',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
pattern: /\binit\s+[06]\b/,
|
|
102
|
+
reason: '禁止 init 0/6(关机/重启)',
|
|
103
|
+
},
|
|
104
|
+
// ---- Fork bomb ----
|
|
105
|
+
{
|
|
106
|
+
pattern: /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/,
|
|
107
|
+
reason: '禁止 fork bomb',
|
|
108
|
+
},
|
|
109
|
+
// ---- 管道到 shell(典型恶意安装脚本模式) ----
|
|
110
|
+
// curl ... | sh / wget ... | bash / curl ... | python
|
|
111
|
+
{
|
|
112
|
+
pattern: /\b(?:curl|wget|fetch)\b[^|;&]*\|\s*(?:sh|bash|zsh|dash|python|perl|ruby|node)\b/i,
|
|
113
|
+
reason: '禁止从网络管道到 shell 解释器(curl ... | sh 模式)',
|
|
114
|
+
},
|
|
115
|
+
// ---- chmod 整盘 777 ----
|
|
116
|
+
{
|
|
117
|
+
pattern: /\bchmod\s+(?:-R\s+)?[0-7]*7[0-7]*\s+\/(?:\s|$)/,
|
|
118
|
+
reason: '禁止对根目录 chmod 777',
|
|
119
|
+
},
|
|
120
|
+
// ---- Windows 系统级删除 ----
|
|
121
|
+
{
|
|
122
|
+
pattern: /\b(?:del|erase)\s+\/[sS][^|;&]*[A-Za-z]:\\?\s*$/,
|
|
123
|
+
reason: '禁止 Windows 全盘删除(del /s 根目录)',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
pattern: /\brmdir\s+\/[sS][^|;&]*[A-Za-z]:\\?\s*$/,
|
|
127
|
+
reason: '禁止 Windows 全盘 rmdir',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
pattern: /\bdiskpart\b/i,
|
|
131
|
+
reason: '禁止 diskpart',
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
/**
|
|
135
|
+
* 检查命令是否命中黑名单。
|
|
136
|
+
* 返回命中的原因字符串,否则返回 null。
|
|
137
|
+
*/
|
|
138
|
+
export function checkForbidden(command) {
|
|
139
|
+
for (const { pattern, reason } of FORBIDDEN_PATTERNS) {
|
|
140
|
+
if (pattern.test(command))
|
|
141
|
+
return reason;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
// ---------------- 1. Zod 输入 schema ----------------
|
|
146
|
+
const inputSchema = z.object({
|
|
147
|
+
command: z
|
|
148
|
+
.string()
|
|
149
|
+
.min(1, '必须提供 command')
|
|
150
|
+
.describe('要执行的 shell 命令(假定 bash 语法)'),
|
|
151
|
+
timeout: z
|
|
152
|
+
.number()
|
|
153
|
+
.int()
|
|
154
|
+
.positive()
|
|
155
|
+
.max(MAX_TIMEOUT_MS)
|
|
156
|
+
.optional()
|
|
157
|
+
.describe(`超时(毫秒),最多 ${MAX_TIMEOUT_MS}(${MAX_TIMEOUT_MS / 60_000} 分钟);不填默认 ${DEFAULT_TIMEOUT_MS}`),
|
|
158
|
+
description: z
|
|
159
|
+
.string()
|
|
160
|
+
.optional()
|
|
161
|
+
.describe('用一句话主动语态描述命令做什么(5-10 词),如 "List files in current directory"'),
|
|
162
|
+
});
|
|
163
|
+
// ---------------- 2. JSON Schema(由 Zod 自动派生) ----------------
|
|
164
|
+
const parameters = toToolParameters(inputSchema);
|
|
165
|
+
// ---------------- 3. Description(给 LLM 看的工具说明) ----------------
|
|
166
|
+
//
|
|
167
|
+
// 提炼自 kakadeai/tools/BashTool/prompt.ts 的 getSimplePrompt()。
|
|
168
|
+
// 核心逻辑:
|
|
169
|
+
// - 先点明"专用工具优先"的原则,把每类操作映射到对应工具
|
|
170
|
+
// - 再列 Bash 自身的使用规范(绝对路径 / 不 cd / 超时 / 多命令并行...)
|
|
171
|
+
// - 最后强调安全黑名单(让模型知道哪些命令会被拒,避免反复试)
|
|
172
|
+
const description = `Executes a given bash command and returns its output.
|
|
173
|
+
|
|
174
|
+
The working directory persists between commands, but shell state does not. The shell environment is initialized from the user's profile.
|
|
175
|
+
|
|
176
|
+
IMPORTANT: Avoid using this tool to run \`find\`, \`grep\`, \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or after you have verified that a dedicated tool cannot accomplish your task. Instead, use the appropriate dedicated tool as this will provide a much better experience for the user:
|
|
177
|
+
|
|
178
|
+
- File search: Use Glob (NOT find or ls)
|
|
179
|
+
- Content search: Use Grep (NOT grep or rg)
|
|
180
|
+
- Read files: Use Read (NOT cat/head/tail)
|
|
181
|
+
- Edit files: Use Edit (NOT sed/awk)
|
|
182
|
+
- Write files: Use Write (NOT echo >/cat <<EOF)
|
|
183
|
+
- Communication: Output text directly (NOT echo/printf)
|
|
184
|
+
While the Bash tool can do similar things, it's better to use the built-in tools as they provide a better user experience and make it easier to review tool calls and give permission.
|
|
185
|
+
|
|
186
|
+
# Instructions
|
|
187
|
+
- If your command will create new directories or files, first use this tool to run \`ls\` to verify the parent directory exists and is the correct location.
|
|
188
|
+
- Always quote file paths that contain spaces with double quotes in your command (e.g., cd "path with spaces/file.txt").
|
|
189
|
+
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it.
|
|
190
|
+
- You may specify an optional timeout in milliseconds (up to ${MAX_TIMEOUT_MS}ms / ${MAX_TIMEOUT_MS / 60_000} minutes). By default, your command will timeout after ${DEFAULT_TIMEOUT_MS}ms (${DEFAULT_TIMEOUT_MS / 60_000} minutes).
|
|
191
|
+
- When issuing multiple commands:
|
|
192
|
+
- If the commands are independent and can run in parallel, make multiple Bash tool calls in a single message.
|
|
193
|
+
- If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them.
|
|
194
|
+
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail.
|
|
195
|
+
- DO NOT use newlines to separate commands (newlines are ok in quoted strings and here-strings).
|
|
196
|
+
- For git commands:
|
|
197
|
+
- Prefer to create a new commit rather than amending an existing commit.
|
|
198
|
+
- Before running destructive operations (e.g., git reset --hard, git push --force, git checkout --), consider whether there is a safer alternative that achieves the same goal. Only use destructive operations when they are truly the best approach.
|
|
199
|
+
- Never skip hooks (--no-verify) or bypass signing (--no-gpg-sign, -c commit.gpgsign=false) unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue.
|
|
200
|
+
- Avoid unnecessary \`sleep\` commands; do not retry failing commands in a sleep loop — diagnose the root cause.
|
|
201
|
+
|
|
202
|
+
# Exit code semantics
|
|
203
|
+
Some commands return non-zero exit codes for informational (non-error) reasons. Bash recognizes these and reports them as success (ok=true) — do NOT retry just because exit code is 1:
|
|
204
|
+
- \`grep\` / \`rg\` exit 1 → no match found (not an error)
|
|
205
|
+
- \`find\` exit 1 → some directories inaccessible (non-fatal, partial results still returned)
|
|
206
|
+
- \`diff\` / \`cmp\` exit 1 → files differ (informational, not an error)
|
|
207
|
+
- \`test\` / \`[\` exit 1 → condition is false (the answer to a question, not a failure)
|
|
208
|
+
Only exit codes ≥ 2 from these commands indicate a real failure. For all other commands, non-zero exit codes are treated as failures normally.
|
|
209
|
+
|
|
210
|
+
# Safety
|
|
211
|
+
The following command patterns are blocked at the tool level and will fail before execution (no need to try them):
|
|
212
|
+
- \`rm -rf /\` and variants targeting root, $HOME, ~, or system directories (/etc, /usr, /bin, /Windows, /Users, /home, ...)
|
|
213
|
+
- Filesystem destruction: \`mkfs\`, \`format X:\`, \`dd of=/dev/sdX\`, redirects to raw block devices
|
|
214
|
+
- Power management: \`shutdown\`, \`reboot\`, \`halt\`, \`poweroff\`, \`init 0/6\`
|
|
215
|
+
- Fork bombs
|
|
216
|
+
- Pipe-to-shell from network: \`curl ... | sh\`, \`wget ... | bash\`, etc.
|
|
217
|
+
- \`chmod 777 /\`, Windows full-disk \`del /s\` / \`rmdir /s\`, \`diskpart\`
|
|
218
|
+
|
|
219
|
+
If you have a legitimate use case that requires one of the above patterns, ask the user to run the command themselves in their terminal — do not try to bypass the check.
|
|
220
|
+
|
|
221
|
+
Separately, Bash scans for common destructive-but-recoverable patterns (\`git reset --hard\`, \`git push -f\` / \`--force\` / \`--force-with-lease\`, \`git checkout .\`, \`git restore .\`, \`git clean -f\`, \`git stash drop/clear\`, \`git branch -D\`, \`git commit --amend\` / \`--no-verify\`, \`rm -rf <path>\`, \`DROP TABLE\`, \`TRUNCATE\`, \`DELETE FROM\`, \`kubectl delete\`, \`terraform destroy\`, etc.) and prepends a \`⚠️ 警告:\` line to the output. These commands are NOT blocked — the warning is informational. Treat it as a signal to double-check intent and surface the warning to the user when relevant.`;
|
|
222
|
+
// ---------------- 4. call() —— 真正干活 ----------------
|
|
223
|
+
async function call(input, signal) {
|
|
224
|
+
const command = input.command;
|
|
225
|
+
const timeoutMs = Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
226
|
+
// 4a. 安全黑名单(命中即拒)
|
|
227
|
+
const forbiddenReason = checkForbidden(command);
|
|
228
|
+
if (forbiddenReason) {
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
error: `安全检查拒绝执行:${forbiddenReason}\n命令:${command}`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// 4a-2. 破坏性命令告警扫描(仅警告,不阻塞)
|
|
235
|
+
// 命中后把警告挂到结果输出顶部,让模型/用户看见苗头。
|
|
236
|
+
const destructiveWarning = scanDestructiveCommand(command);
|
|
237
|
+
// 4b. spawn —— shell:true 让平台自己挑 shell
|
|
238
|
+
// Unix: /bin/sh -c <command>
|
|
239
|
+
// Windows: cmd.exe /d /s /c "<command>"
|
|
240
|
+
let stdout = '';
|
|
241
|
+
let stderr = '';
|
|
242
|
+
let exitCode = null;
|
|
243
|
+
let timedOut = false;
|
|
244
|
+
let killedBySignal = false;
|
|
245
|
+
// 用 AbortController 把外部 signal + 我们自己的 timeout 合并成一个
|
|
246
|
+
const ac = new AbortController();
|
|
247
|
+
const onAbort = () => ac.abort();
|
|
248
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
249
|
+
const timer = setTimeout(() => {
|
|
250
|
+
timedOut = true;
|
|
251
|
+
ac.abort();
|
|
252
|
+
}, timeoutMs);
|
|
253
|
+
try {
|
|
254
|
+
await new Promise((resolveP, rejectP) => {
|
|
255
|
+
const child = spawn(command, {
|
|
256
|
+
shell: true,
|
|
257
|
+
cwd: getWorkingDir(),
|
|
258
|
+
signal: ac.signal,
|
|
259
|
+
env: process.env,
|
|
260
|
+
windowsHide: true,
|
|
261
|
+
});
|
|
262
|
+
child.stdout?.setEncoding('utf8');
|
|
263
|
+
child.stderr?.setEncoding('utf8');
|
|
264
|
+
child.stdout?.on('data', (chunk) => {
|
|
265
|
+
stdout += chunk;
|
|
266
|
+
// 读到一半就超额时不强行 kill;最后再统一截断,避免日志噪声
|
|
267
|
+
});
|
|
268
|
+
child.stderr?.on('data', (chunk) => {
|
|
269
|
+
stderr += chunk;
|
|
270
|
+
});
|
|
271
|
+
child.on('error', (e) => {
|
|
272
|
+
// signal abort 会先触发 error('AbortError');把这种 case 转成正常分支
|
|
273
|
+
if (e.code === 'ABORT_ERR') {
|
|
274
|
+
killedBySignal = true;
|
|
275
|
+
resolveP();
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
rejectP(e);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
child.on('close', (code, sig) => {
|
|
282
|
+
exitCode = code;
|
|
283
|
+
if (sig)
|
|
284
|
+
killedBySignal = true;
|
|
285
|
+
resolveP();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
catch (e) {
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
error: `执行命令失败:${e.message}\n命令:${command}`,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
clearTimeout(timer);
|
|
297
|
+
signal?.removeEventListener('abort', onAbort);
|
|
298
|
+
}
|
|
299
|
+
// 4c. 拼输出
|
|
300
|
+
const parts = [];
|
|
301
|
+
if (stdout)
|
|
302
|
+
parts.push(`<stdout>\n${stdout.replace(/\s+$/, '')}\n</stdout>`);
|
|
303
|
+
if (stderr)
|
|
304
|
+
parts.push(`<stderr>\n${stderr.replace(/\s+$/, '')}\n</stderr>`);
|
|
305
|
+
if (parts.length === 0)
|
|
306
|
+
parts.push('<no output>');
|
|
307
|
+
if (timedOut) {
|
|
308
|
+
parts.push(`\n[超时 ${timeoutMs}ms 后被中断]`);
|
|
309
|
+
}
|
|
310
|
+
else if (killedBySignal && !timedOut) {
|
|
311
|
+
parts.push(`\n[被信号中断]`);
|
|
312
|
+
}
|
|
313
|
+
parts.push(`\n[exit code: ${exitCode === null ? 'killed' : exitCode}]`);
|
|
314
|
+
let combinedOutput = parts.join('\n');
|
|
315
|
+
// 4d. 大小兜底
|
|
316
|
+
if (combinedOutput.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
|
|
317
|
+
combinedOutput =
|
|
318
|
+
combinedOutput.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) +
|
|
319
|
+
`\n\n... (输出超过 ${DEFAULT_MAX_RESULT_SIZE_CHARS} 字符,已截断)`;
|
|
320
|
+
}
|
|
321
|
+
// 4e. 中断 / 超时仍然算硬失败
|
|
322
|
+
if (timedOut || killedBySignal) {
|
|
323
|
+
return { ok: false, error: combinedOutput };
|
|
324
|
+
}
|
|
325
|
+
// 4f. 语义化退出码判定
|
|
326
|
+
// 交给 semantics 模块:grep/rg/find/diff/test 有"信息性退出码",
|
|
327
|
+
// 其它命令用默认语义(exitCode !== 0 即错)。
|
|
328
|
+
// destructiveWarning 命中时把告警贴在输出头部,模型/用户都能看见。
|
|
329
|
+
const semantic = interpretCommandResult(command, exitCode ?? 0, stdout, stderr);
|
|
330
|
+
const finalContent = destructiveWarning
|
|
331
|
+
? `⚠️ 警告: ${destructiveWarning}\n\n${combinedOutput}`
|
|
332
|
+
: combinedOutput;
|
|
333
|
+
if (semantic.isError) {
|
|
334
|
+
return {
|
|
335
|
+
ok: false,
|
|
336
|
+
error: `命令失败 (exit ${exitCode}): ${stderr || stdout || semantic.message || ''}`.trim() +
|
|
337
|
+
`\n\n${finalContent}`,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return { ok: true, content: finalContent };
|
|
341
|
+
}
|
|
342
|
+
// ---------------- 5. 导出 ----------------
|
|
343
|
+
export const bashTool = {
|
|
344
|
+
name: 'Bash',
|
|
345
|
+
description,
|
|
346
|
+
inputSchema,
|
|
347
|
+
parameters,
|
|
348
|
+
isReadOnly: false,
|
|
349
|
+
isConcurrencySafe: false, // 默认按"会改状态"算;保守不并发
|
|
350
|
+
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
351
|
+
call,
|
|
352
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* src/tools/bash/semantics.ts —— 命令退出码语义解释
|
|
4
|
+
* ------------------------------------------------------------
|
|
5
|
+
* 许多命令用 exit code 传递"信息"而不是"成功/失败"。
|
|
6
|
+
* 最典型的是 grep:没匹配返回 1,但这不算错误。
|
|
7
|
+
*
|
|
8
|
+
* 这里对一小撮"退出码语义特殊"的命令做白名单化处理,
|
|
9
|
+
* 让 Bash 工具能正确地把它们的"信息性退出码"识别成非错误,
|
|
10
|
+
* 避免模型看到 ok=false 后无意义重试。
|
|
11
|
+
*
|
|
12
|
+
* 纯函数模块,无 I/O,便于单测。
|
|
13
|
+
* ============================================================
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_SEMANTIC = (exitCode) => ({
|
|
16
|
+
isError: exitCode !== 0,
|
|
17
|
+
message: exitCode !== 0 ? `Command failed with exit code ${exitCode}` : undefined,
|
|
18
|
+
});
|
|
19
|
+
export const COMMAND_SEMANTICS = new Map([
|
|
20
|
+
// grep: 0=找到匹配, 1=无匹配(非错误), 2+=真错误
|
|
21
|
+
['grep', (exitCode) => ({
|
|
22
|
+
isError: exitCode >= 2,
|
|
23
|
+
message: exitCode === 1 ? 'No matches found' : undefined,
|
|
24
|
+
})],
|
|
25
|
+
// ripgrep 与 grep 同义
|
|
26
|
+
['rg', (exitCode) => ({
|
|
27
|
+
isError: exitCode >= 2,
|
|
28
|
+
message: exitCode === 1 ? 'No matches found' : undefined,
|
|
29
|
+
})],
|
|
30
|
+
// find: 1=部分目录不可达(仍有结果,非致命), 2+=错误
|
|
31
|
+
['find', (exitCode) => ({
|
|
32
|
+
isError: exitCode >= 2,
|
|
33
|
+
message: exitCode === 1 ? 'Some directories were inaccessible' : undefined,
|
|
34
|
+
})],
|
|
35
|
+
// diff: 0=相同, 1=有差异(非错误), 2+=错误
|
|
36
|
+
['diff', (exitCode) => ({
|
|
37
|
+
isError: exitCode >= 2,
|
|
38
|
+
message: exitCode === 1 ? 'Files differ' : undefined,
|
|
39
|
+
})],
|
|
40
|
+
// test: 0=真, 1=假(非错误), 2+=错误
|
|
41
|
+
['test', (exitCode) => ({
|
|
42
|
+
isError: exitCode >= 2,
|
|
43
|
+
message: exitCode === 1 ? 'Condition is false' : undefined,
|
|
44
|
+
})],
|
|
45
|
+
// [ 是 test 的别名
|
|
46
|
+
['[', (exitCode) => ({
|
|
47
|
+
isError: exitCode >= 2,
|
|
48
|
+
message: exitCode === 1 ? 'Condition is false' : undefined,
|
|
49
|
+
})],
|
|
50
|
+
]);
|
|
51
|
+
/**
|
|
52
|
+
* 从复合命令串里启发式抽出"最终决定 exit code 的那个命令"。
|
|
53
|
+
*
|
|
54
|
+
* 规则(够用即可,不追求严谨 shell 解析):
|
|
55
|
+
* 1. 去掉前缀 `bash -c "..."` / `sh -c '...'`
|
|
56
|
+
* 2. 按 ` | `、`&&`、`||`、`;` 分段,取最后一段(决定退出码的是最后一个命令)
|
|
57
|
+
* 3. 去掉前置 `env VAR=val` / `VAR=val` 形态的环境变量赋值
|
|
58
|
+
* 4. 取第一个 token 作为命令名
|
|
59
|
+
*
|
|
60
|
+
* 不依赖 shell-quote,避免引新依赖。
|
|
61
|
+
*/
|
|
62
|
+
function extractPrimaryCommand(command) {
|
|
63
|
+
let cmd = command.trim();
|
|
64
|
+
// 剥 bash -c "..." / sh -c '...' 外壳
|
|
65
|
+
const wrapMatch = cmd.match(/^(?:bash|sh|zsh|dash)\s+-c\s+(['"])(.+)\1\s*$/s);
|
|
66
|
+
if (wrapMatch)
|
|
67
|
+
cmd = wrapMatch[2].trim();
|
|
68
|
+
// 按 shell 控制操作符切,取最后一段
|
|
69
|
+
const segments = cmd.split(/\s*(?:\|\||&&|;|\|)\s*/).filter((s) => s.length > 0);
|
|
70
|
+
let last = segments[segments.length - 1] ?? cmd;
|
|
71
|
+
last = last.trim();
|
|
72
|
+
// 去掉前置环境变量赋值:`FOO=bar BAZ=qux cmd ...` 或 `env FOO=bar cmd ...`
|
|
73
|
+
const tokens = last.split(/\s+/);
|
|
74
|
+
let i = 0;
|
|
75
|
+
if (tokens[i] === 'env')
|
|
76
|
+
i++;
|
|
77
|
+
while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i]))
|
|
78
|
+
i++;
|
|
79
|
+
return tokens[i] ?? '';
|
|
80
|
+
}
|
|
81
|
+
export function interpretCommandResult(command, exitCode, stdout, stderr) {
|
|
82
|
+
const base = extractPrimaryCommand(command);
|
|
83
|
+
const fn = COMMAND_SEMANTICS.get(base) ?? DEFAULT_SEMANTIC;
|
|
84
|
+
return fn(exitCode, stdout, stderr);
|
|
85
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* src/tools/bash/warnings.ts —— 破坏性命令告警扫描
|
|
4
|
+
* ------------------------------------------------------------
|
|
5
|
+
* 对应 kakadeai 主仓库 tools/BashTool/destructiveCommandWarning.ts。
|
|
6
|
+
*
|
|
7
|
+
* 与 bash.ts 里的"黑名单"区别(不要混淆):
|
|
8
|
+
* - 黑名单:灾难性命令(rm -rf /、mkfs、shutdown...),命中直接拒绝执行
|
|
9
|
+
* - 告警扫描:"看起来有点危险但合法"的命令(git reset --hard、git push -f、
|
|
10
|
+
* kubectl delete...),命中后只输出警告字符串、**不阻塞执行**,
|
|
11
|
+
* 让模型/用户看到苗头但保留行动自由。
|
|
12
|
+
*
|
|
13
|
+
* 纯函数模块。
|
|
14
|
+
* ============================================================
|
|
15
|
+
*/
|
|
16
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
17
|
+
// Git —— 数据丢失 / 难回退
|
|
18
|
+
{
|
|
19
|
+
pattern: /\bgit\s+reset\s+--hard\b/,
|
|
20
|
+
warning: 'Note: may discard uncommitted changes',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
pattern: /\bgit\s+push\b[^;&|\n]*[ \t](--force|--force-with-lease|-f)\b/,
|
|
24
|
+
warning: 'Note: may overwrite remote history',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
pattern: /\bgit\s+clean\b(?![^;&|\n]*(?:-[a-zA-Z]*n|--dry-run))[^;&|\n]*-[a-zA-Z]*f/,
|
|
28
|
+
warning: 'Note: may permanently delete untracked files',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
pattern: /\bgit\s+checkout\s+(--\s+)?\.[ \t]*($|[;&|\n])/,
|
|
32
|
+
warning: 'Note: may discard all working tree changes',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
pattern: /\bgit\s+restore\s+(--\s+)?\.[ \t]*($|[;&|\n])/,
|
|
36
|
+
warning: 'Note: may discard all working tree changes',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
pattern: /\bgit\s+stash[ \t]+(drop|clear)\b/,
|
|
40
|
+
warning: 'Note: may permanently remove stashed changes',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
pattern: /\bgit\s+branch\s+(-D[ \t]|--delete\s+--force|--force\s+--delete)\b/,
|
|
44
|
+
warning: 'Note: may force-delete a branch',
|
|
45
|
+
},
|
|
46
|
+
// Git —— 安全绕过
|
|
47
|
+
{
|
|
48
|
+
pattern: /\bgit\s+(commit|push|merge)\b[^;&|\n]*--no-verify\b/,
|
|
49
|
+
warning: 'Note: may skip safety hooks',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
pattern: /\bgit\s+commit\b[^;&|\n]*--amend\b/,
|
|
53
|
+
warning: 'Note: may rewrite the last commit',
|
|
54
|
+
},
|
|
55
|
+
// 文件删除(rm -rf / 之类的致命形式由 bash.ts 黑名单处理;这里只做"未到致命"的提醒)
|
|
56
|
+
{
|
|
57
|
+
pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*[rR][a-zA-Z]*f|(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*f[a-zA-Z]*[rR]/,
|
|
58
|
+
warning: 'Note: may recursively force-remove files',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*[rR]/,
|
|
62
|
+
warning: 'Note: may recursively remove files',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*f/,
|
|
66
|
+
warning: 'Note: may force-remove files',
|
|
67
|
+
},
|
|
68
|
+
// 数据库
|
|
69
|
+
{
|
|
70
|
+
pattern: /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA)\b/i,
|
|
71
|
+
warning: 'Note: may drop or truncate database objects',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
pattern: /\bDELETE\s+FROM\s+\w+[ \t]*(;|"|'|\n|$)/i,
|
|
75
|
+
warning: 'Note: may delete all rows from a database table',
|
|
76
|
+
},
|
|
77
|
+
// 基础设施
|
|
78
|
+
{
|
|
79
|
+
pattern: /\bkubectl\s+delete\b/,
|
|
80
|
+
warning: 'Note: may delete Kubernetes resources',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
pattern: /\bterraform\s+destroy\b/,
|
|
84
|
+
warning: 'Note: may destroy Terraform infrastructure',
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
/**
|
|
88
|
+
* 扫描命令是否命中已知破坏性模式。
|
|
89
|
+
* 命中返回人类可读的告警字符串;未命中返回 null。
|
|
90
|
+
* **仅警告,不阻塞**。
|
|
91
|
+
*/
|
|
92
|
+
export function scanDestructiveCommand(command) {
|
|
93
|
+
for (const { pattern, warning } of DESTRUCTIVE_PATTERNS) {
|
|
94
|
+
if (pattern.test(command))
|
|
95
|
+
return warning;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|