minimal-agent 0.12.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 +3 -2
- 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 +45 -17
- package/plugins/workflow-runner/src/index.ts +44 -18
- 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/tool.js +29 -10
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +29 -11
- 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/types.js +113 -7
- package/plugins/workflow-runner/src/types.ts +111 -7
- 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/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/illustrate.load.test.ts +2 -2
- 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/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 +17 -0
- package/src/cli/print.ts +25 -0
- package/src/config/configService.js +24 -0
- package/src/config/configService.ts +23 -0
- package/src/infra/verification.js +25 -18
- package/src/infra/verification.ts +50 -18
- package/src/plugins/pluginRunner.js +18 -7
- package/src/plugins/pluginRunner.ts +20 -6
- package/src/prompts/policyRegistry.js +28 -1
- package/src/prompts/policyRegistry.ts +28 -1
- package/src/tools/advisor/advisor.js +76 -9
- package/src/tools/advisor/advisor.ts +79 -9
- package/src/tools/bash/bash.js +51 -6
- package/src/tools/bash/bash.ts +56 -7
- package/src/tools/powershell/powershell.js +18 -1
- package/src/tools/powershell/powershell.ts +18 -2
- package/src/tools/read/read.js +9 -4
- package/src/tools/read/read.ts +9 -4
- package/src/tools/shared/permanentDeleteGuard.js +34 -3
- package/src/tools/shared/permanentDeleteGuard.ts +43 -6
- 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/types.ts +10 -4
- 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
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* 2) / 输入 → discoverPlugins → resolveCommand
|
|
10
10
|
* a) 命中插件且其 plugin.ts 暴露 runCommand → 让 plugin.ts 接管
|
|
11
11
|
* b) 命中插件但只有声明式 commands/*.md → 把 prompt 拼好喂给 runQuery
|
|
12
|
-
* c) 未命中
|
|
13
|
-
*
|
|
12
|
+
* c) 未命中 -> warning 提示未知命令后透传 runQuery(/ 只是激活前缀,
|
|
13
|
+
* 不是严格命令系统;未知的 /xxx 当普通用户输入处理,不报错)
|
|
14
14
|
* ============================================================
|
|
15
15
|
*/
|
|
16
16
|
|
|
@@ -19,6 +19,7 @@ import type { LoopEvent, Message, Provider } from '../types.js';
|
|
|
19
19
|
import {
|
|
20
20
|
buildCommandInput,
|
|
21
21
|
discoverPlugins,
|
|
22
|
+
listAvailableCommands,
|
|
22
23
|
resolveCommand,
|
|
23
24
|
} from './commandRouter.js';
|
|
24
25
|
import type { PluginEvent } from './pluginApi.js';
|
|
@@ -106,10 +107,23 @@ export async function* runWithPlugins(
|
|
|
106
107
|
const matched = resolveCommand(userInput);
|
|
107
108
|
|
|
108
109
|
if (!matched) {
|
|
109
|
-
// 未命中任何插件命令
|
|
110
|
-
// / 只是激活前缀(类似 Claude Code 的 /
|
|
111
|
-
//
|
|
112
|
-
//
|
|
110
|
+
// 未命中任何插件命令 -> 提示后透传 runQuery。
|
|
111
|
+
// / 只是激活前缀(类似 Claude Code 的 / 模式),不是严格命令系统:
|
|
112
|
+
// 不匹配仍当普通对话处理,让 LLM 自行理解意图。但 -p 等无人值守场景里
|
|
113
|
+
// 拼错的 /reseach 会被静默当文本烧一轮 LLM(实战:用户以为在跑工作流,
|
|
114
|
+
// 实际什么都没发生)--先 warning 列出已注册命令,零成本纠正拼写。
|
|
115
|
+
const trimmedUnknown = userInput.trimStart();
|
|
116
|
+
const spaceUnknown = trimmedUnknown.indexOf(' ', 1);
|
|
117
|
+
const unknownCmd = (
|
|
118
|
+
spaceUnknown >= 0 ? trimmedUnknown.slice(1, spaceUnknown) : trimmedUnknown.slice(1)
|
|
119
|
+
).trim();
|
|
120
|
+
if (unknownCmd) {
|
|
121
|
+
const available = listAvailableCommands().map((c) => c.command);
|
|
122
|
+
yield {
|
|
123
|
+
type: 'warning',
|
|
124
|
+
message: `未知命令 /${unknownCmd}(已注册:${available.length ? available.join('、') : '无'}),已按普通对话处理。`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
113
127
|
yield* runQuery(userInput, {
|
|
114
128
|
provider,
|
|
115
129
|
history,
|
|
@@ -232,7 +232,34 @@ const CAPABILITY_POLICIES = [
|
|
|
232
232
|
{
|
|
233
233
|
id: 'capability.advisor', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|
|
234
234
|
requiresTools: ['Advisor'],
|
|
235
|
-
|
|
235
|
+
// 注意:文本内不得硬编码其他工具名(如 PresentResult),否则该工具缺席时这里
|
|
236
|
+
// 成为唯一残留的「幽灵引用」——capability 规则交叉引用必须在 toolNames 条件下拼接。
|
|
237
|
+
render: ({ toolNames }) => `- Advisor(顾问模型)使用规则:
|
|
238
|
+
**在动手之前问,不是出事之后问**:顾问的价值在方案定型前最大——代码一旦写下去,换方向的成本就变了。
|
|
239
|
+
「摸清现状」不算动手(找文件、读代码、跑命令看状态,这些先做完再问);
|
|
240
|
+
「动手」指写文件、认定某个解释、在假设上继续搭、给出结论。
|
|
241
|
+
**当 Advisor 工具出现在当前可用工具列表中时**,以下 5 种场景必须主动调用,不要等到出错或用户问了才想起来:
|
|
242
|
+
1. 方案/开发计划成型后、动手执行前(先让顾问挑刺,避免做一半才发现方向错了)
|
|
243
|
+
2. 最终结果提交用户前${toolNames.has('PresentResult') ? '(PresentResult 之前)' : ''}:代码评审、评估报告、技术选型、重构方案等所有"给出结论"的场合
|
|
244
|
+
3. 连续 2 次遇到同类错误或修复不收敛(死循环前兆,立刻换思路)
|
|
245
|
+
4. 同一轮内用户第 2 次反馈不满意(不要在原地打转,先找顾问换角度)
|
|
246
|
+
5. 涉及安全性、数据丢失、不可逆操作的决策(顾问双保险)
|
|
247
|
+
调用时 note 里必须包含:你的完整结论/方案 + 关键代码原文或证据摘要,不要只写结论摘要。
|
|
248
|
+
场景 2 有个前置动作:**先把产出落盘再调用**(写文件、保存结果)。这一次调用要花时间,
|
|
249
|
+
回合若在此期间结束,写下的文件还在,没写的答案就没了。
|
|
250
|
+
长任务至少咨询两次:方案定型前一次、宣布完成前一次;短的、下一步由刚读到的输出直接决定的任务,一次即可。
|
|
251
|
+
**顾问返回后必须做验证,不能盲信:**
|
|
252
|
+
- 顾问的意见只是第二视角的候选诊断,不是事实。它没有工具访问权,无法自行验证。
|
|
253
|
+
- 对顾问提出的每一条严重问题/反例,都必须回到代码、测试、原始资料或工具输出核实。
|
|
254
|
+
- 核实后分类:真问题(有证据支撑)、误报(被事实推翻)、待澄清(暂时无法验证)。
|
|
255
|
+
- 未经核实的顾问意见,不得写成确定结论;与实测冲突时,以实测为准。
|
|
256
|
+
- ⚠️ 但「自测通过」不等于顾问说错了:那往往只说明你的测试没测到它指的那一层。
|
|
257
|
+
- 与你已掌握的证据冲突时**不要默默改方向**:再调一次顾问,把冲突摆出来
|
|
258
|
+
(「我实测是 X,你建议 Y,哪个约束更硬?」)。它看过你的证据但可能低估了,
|
|
259
|
+
一次对账远比顺着错误分支做下去便宜。
|
|
260
|
+
**如果 Advisor 不在工具列表中**(未配置、已关闭、仅手动模式或当前不可用):视为用户明确选择跳过,不得尝试调用,也不阻塞流程,无需在输出中专门说明。
|
|
261
|
+
咨询前先落盘成果,顾问意见与实测冲突时以实测为准并明确核对冲突。
|
|
262
|
+
补充:任何评审/评估/结论输出前,先用 adversarial-review 的方法论自我审查(拆原子主张、反向提问、去现场核对),再决定是否需要顾问。`,
|
|
236
263
|
},
|
|
237
264
|
{
|
|
238
265
|
id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|
|
@@ -274,7 +274,34 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
|
|
|
274
274
|
{
|
|
275
275
|
id: 'capability.advisor', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|
|
276
276
|
requiresTools: ['Advisor'],
|
|
277
|
-
|
|
277
|
+
// 注意:文本内不得硬编码其他工具名(如 PresentResult),否则该工具缺席时这里
|
|
278
|
+
// 成为唯一残留的「幽灵引用」——capability 规则交叉引用必须在 toolNames 条件下拼接。
|
|
279
|
+
render: ({ toolNames }) => `- Advisor(顾问模型)使用规则:
|
|
280
|
+
**在动手之前问,不是出事之后问**:顾问的价值在方案定型前最大——代码一旦写下去,换方向的成本就变了。
|
|
281
|
+
「摸清现状」不算动手(找文件、读代码、跑命令看状态,这些先做完再问);
|
|
282
|
+
「动手」指写文件、认定某个解释、在假设上继续搭、给出结论。
|
|
283
|
+
**当 Advisor 工具出现在当前可用工具列表中时**,以下 5 种场景必须主动调用,不要等到出错或用户问了才想起来:
|
|
284
|
+
1. 方案/开发计划成型后、动手执行前(先让顾问挑刺,避免做一半才发现方向错了)
|
|
285
|
+
2. 最终结果提交用户前${toolNames.has('PresentResult') ? '(PresentResult 之前)' : ''}:代码评审、评估报告、技术选型、重构方案等所有"给出结论"的场合
|
|
286
|
+
3. 连续 2 次遇到同类错误或修复不收敛(死循环前兆,立刻换思路)
|
|
287
|
+
4. 同一轮内用户第 2 次反馈不满意(不要在原地打转,先找顾问换角度)
|
|
288
|
+
5. 涉及安全性、数据丢失、不可逆操作的决策(顾问双保险)
|
|
289
|
+
调用时 note 里必须包含:你的完整结论/方案 + 关键代码原文或证据摘要,不要只写结论摘要。
|
|
290
|
+
场景 2 有个前置动作:**先把产出落盘再调用**(写文件、保存结果)。这一次调用要花时间,
|
|
291
|
+
回合若在此期间结束,写下的文件还在,没写的答案就没了。
|
|
292
|
+
长任务至少咨询两次:方案定型前一次、宣布完成前一次;短的、下一步由刚读到的输出直接决定的任务,一次即可。
|
|
293
|
+
**顾问返回后必须做验证,不能盲信:**
|
|
294
|
+
- 顾问的意见只是第二视角的候选诊断,不是事实。它没有工具访问权,无法自行验证。
|
|
295
|
+
- 对顾问提出的每一条严重问题/反例,都必须回到代码、测试、原始资料或工具输出核实。
|
|
296
|
+
- 核实后分类:真问题(有证据支撑)、误报(被事实推翻)、待澄清(暂时无法验证)。
|
|
297
|
+
- 未经核实的顾问意见,不得写成确定结论;与实测冲突时,以实测为准。
|
|
298
|
+
- ⚠️ 但「自测通过」不等于顾问说错了:那往往只说明你的测试没测到它指的那一层。
|
|
299
|
+
- 与你已掌握的证据冲突时**不要默默改方向**:再调一次顾问,把冲突摆出来
|
|
300
|
+
(「我实测是 X,你建议 Y,哪个约束更硬?」)。它看过你的证据但可能低估了,
|
|
301
|
+
一次对账远比顺着错误分支做下去便宜。
|
|
302
|
+
**如果 Advisor 不在工具列表中**(未配置、已关闭、仅手动模式或当前不可用):视为用户明确选择跳过,不得尝试调用,也不阻塞流程,无需在输出中专门说明。
|
|
303
|
+
咨询前先落盘成果,顾问意见与实测冲突时以实测为准并明确核对冲突。
|
|
304
|
+
补充:任何评审/评估/结论输出前,先用 adversarial-review 的方法论自我审查(拆原子主张、反向提问、去现场核对),再决定是否需要顾问。`,
|
|
278
305
|
},
|
|
279
306
|
{
|
|
280
307
|
id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|
|
@@ -42,11 +42,23 @@ const inputSchema = z.object({
|
|
|
42
42
|
const parameters = toToolParameters(inputSchema);
|
|
43
43
|
// ---------------- 2. Description(给 LLM 看的工具说明) ----------------
|
|
44
44
|
const description = `- Consults an independent, separately-configured second LLM ("顾问模型" / advisor) for a second opinion on your current plan, diagnosis, or progress.
|
|
45
|
-
- Call with no arguments in most cases — the advisor receives a bounded, relevant view of the current session (the original request, recent discussion, current plan/diagnosis, and key tool results).
|
|
46
|
-
- Exception: if your current plan/diagnosis relies on things you saw in images (screenshots, photos, diagrams), summarize the key visual facts in \`note\` in 1-2 sentences (what each image shows and what you concluded from it) — the advisor may not be able to see the images themselves
|
|
47
|
-
- Call
|
|
45
|
+
- Call with no arguments in most cases — the advisor receives a bounded, relevant view of the current session (the original request, recent discussion, current plan/diagnosis, and key tool results). Always pass key evidence (original code snippets, raw data, full plan text) in \`note\`, not just your summary — the advisor cannot verify things you never wrote down.
|
|
46
|
+
- Exception: if your current plan/diagnosis relies on things you saw in images (screenshots, photos, diagrams), summarize the key visual facts in \`note\` in 1-2 sentences (what each image shows and what you concluded from it) — the advisor may not be able to see the images themselves.
|
|
47
|
+
- **Call it BEFORE substantive work, not after things go wrong.** The advisor is worth the most before an approach hardens — once you have written the code, the plan is expensive to abandon.
|
|
48
|
+
- Orientation is NOT substantive work: finding files, reading code, running a command to see the current state. Do that first, then consult.
|
|
49
|
+
- Substantive work IS: writing or editing files, committing to an interpretation, building on an assumption, declaring an answer.
|
|
50
|
+
- **MUST call Advisor in all 5 of these situations — do not wait for errors or user prompts:**
|
|
51
|
+
1. After a plan / development plan is finalized, **before** executing it (get critique first, avoid discovering wrong direction halfway)
|
|
52
|
+
2. **Before** submitting final results to the user (before PresentResult): code reviews, assessment reports, technical selections, refactoring plans — any time you "deliver a conclusion"
|
|
53
|
+
3. The same error occurs 2+ times in a row, or fixes are not converging (early sign of infinite loop — switch approach immediately)
|
|
54
|
+
4. The user gives unsatisfied feedback a **second time** in the same turn (stop spinning in place, get a fresh perspective first)
|
|
55
|
+
5. Decisions involving security, data loss, or irreversible operations (advisor double-check)
|
|
56
|
+
- **Make your work durable BEFORE calling in situation 2.** Write the file, save the result. This call takes time; if the turn ends during it, a written file survives and an unwritten answer does not.
|
|
57
|
+
- On a long task, consult at least twice: once before the approach is locked in, once before you declare it done. On a short reactive task whose next step is dictated by output you just read, one call is enough — the advisor adds most of its value early.
|
|
58
|
+
- **Take the advice seriously, but do not obey it blindly.** Adapt if you follow a step and it empirically fails, or you hold primary-source evidence contradicting a specific claim (the file says X, the test output says Y). A passing self-test is NOT evidence the advice is wrong — it is evidence your test does not check what the advice checks.
|
|
59
|
+
- **If the advisor contradicts evidence you already gathered, do not silently switch.** Call Advisor once more stating the conflict outright ("I measured X, you advise Y — which constraint breaks the tie?"). The advisor saw your evidence but may have underweighted it; one reconciling call is far cheaper than committing to the wrong branch.
|
|
48
60
|
- The advisor has no tool access and does a single read-only pass over the transcript — it cannot verify anything itself, only reason about what it's shown.
|
|
49
|
-
- If this tool errors (advisor not configured in shared settings, disabled, manual-only, or a call failure), that's not a signal to stop — treat it as "no second opinion available this time" and continue with your own judgment
|
|
61
|
+
- If this tool errors (advisor not configured in shared settings, disabled, manual-only, or a call failure), that's not a signal to stop — treat it as "no second opinion available this time" and continue with your own judgment, but **explicitly note "not cross-verified by advisor" in your final output**. Do not block or retry in a loop.`;
|
|
50
62
|
// ---------------- 3. 顾问 persona system prompt ----------------
|
|
51
63
|
const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技术顾问,为正在独立执行任务的另一个 AI 助手提供第二意见评审。
|
|
52
64
|
你的职责:技术方案评估、代码评审、疑难问题的根因定位与解决方向判断。
|
|
@@ -54,6 +66,14 @@ const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技
|
|
|
54
66
|
你会看到该助手当前会话中受预算约束的相关记录——包括首要需求、近期交流、它的思考过程、当前方案
|
|
55
67
|
和关键工具结果与执行结果,以及它现在想让你重点关注的内容(如果有)。
|
|
56
68
|
|
|
69
|
+
重要:你**没有工具访问权,无法自行验证任何事实**。你只是提供一个不同视角的怀疑和推理,
|
|
70
|
+
不能把自己的推测当成确定结论。每条指出的问题都要明确标注:
|
|
71
|
+
- 【有证据】:基于记录里明确出现的代码/测试结果/事实
|
|
72
|
+
- 【推断】:基于已有信息的合理推断,但未被证实
|
|
73
|
+
- 【存疑】:信息不足,需要执行助手回去核实
|
|
74
|
+
不要过度自信,不要给出确定性语气的未验证判断——执行助手会对你的意见逐条做验证,
|
|
75
|
+
夸大其词会被判定为误报。
|
|
76
|
+
|
|
57
77
|
审视时请做到:
|
|
58
78
|
- 像资深工程师做方案评审/代码评审一样直接、具体——发现问题就明说,不要无条件附和或说套话。
|
|
59
79
|
- 优先检查:方案是否遗漏了风险或边界情况、诊断是否真的找到了根因还是只是让症状消失、
|
|
@@ -66,7 +86,16 @@ const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技
|
|
|
66
86
|
或换个方式绕开,都要点出来。
|
|
67
87
|
- 结论必须基于你在历史记录里看到的实际证据,不要脱离上下文给泛泛的通用建议;
|
|
68
88
|
被省略的记录(尤其工具结果原文)所涉及的判断,明说你无法核实。
|
|
69
|
-
- 篇幅精炼:只讲最关键的 1-3
|
|
89
|
+
- 篇幅精炼:只讲最关键的 1-3 点,不要复述历史、不要客套开场白。
|
|
90
|
+
|
|
91
|
+
关于「带着反证回来」的那一次咨询(助手被要求这样做,不是在质疑你):
|
|
92
|
+
当它说「我实测是 X,你上次建议 Y」时,它手里的是一手证据,你手里的是推理——
|
|
93
|
+
默认它的观测为真,然后只回答一个问题:**哪个约束更硬**。
|
|
94
|
+
- 你上次漏看了它给出的证据 → 直接认,并说清结论怎么变。
|
|
95
|
+
- 它的观测其实没测到你担心的那一层(测了被改的函数,而真实调用路径根本走不到)
|
|
96
|
+
→ 指出这个错位,并说要测什么才算数。
|
|
97
|
+
- 两者都成立、只是适用范围不同 → 说清各自的边界,别硬选一个。
|
|
98
|
+
不要为了显得一致而坚持原判,也不要因为被反驳就整个收回——两种都会让它失去判断依据。`;
|
|
70
99
|
// ---------------- 4. 消息构造(纯函数,单独导出可测) ----------------
|
|
71
100
|
/**
|
|
72
101
|
* 把主循环的 history 转成喂给顾问模型的 messages。
|
|
@@ -203,6 +232,13 @@ function fitAdvisorBudget(messages, inputBudget) {
|
|
|
203
232
|
selected.push(first);
|
|
204
233
|
used += countMessagesTokens([first]);
|
|
205
234
|
}
|
|
235
|
+
// 从最近往前收,**装不下就停**(break,不是 continue)。
|
|
236
|
+
//
|
|
237
|
+
// ⚠️ 曾经是 continue:一条大消息放不下就跳过它、继续往前捡更小的旧消息。
|
|
238
|
+
// 结果是给顾问一段**中间有洞**的对话 —— 而洞恰好开在信息量最大的地方(长工具
|
|
239
|
+
// 结果、完整方案文本),偏偏下面那句省略提示还写着「保留最初需求和最近相关记录」,
|
|
240
|
+
// 顾问于是把两条本不相邻的消息读成前后相接,在残缺的因果链上给出自信结论。
|
|
241
|
+
// 宁可少给,不可给错:连续的尾部 + 如实说明从哪里开始省略。
|
|
206
242
|
const recent = [];
|
|
207
243
|
for (let index = body.length - 1; index >= 0; index--) {
|
|
208
244
|
if (index === firstUserIndex)
|
|
@@ -210,7 +246,7 @@ function fitAdvisorBudget(messages, inputBudget) {
|
|
|
210
246
|
const message = body[index];
|
|
211
247
|
const cost = countMessagesTokens([message]);
|
|
212
248
|
if (used + cost > inputBudget)
|
|
213
|
-
|
|
249
|
+
break;
|
|
214
250
|
recent.unshift(message);
|
|
215
251
|
used += cost;
|
|
216
252
|
}
|
|
@@ -287,17 +323,22 @@ export async function runAdvisor(input, signal, ctx, chatFn = chat, runtime = {}
|
|
|
287
323
|
return {
|
|
288
324
|
ok: false,
|
|
289
325
|
error: '顾问模型未配置。请在设置 → 模型中完成专家顾问配置。' +
|
|
290
|
-
'
|
|
326
|
+
'本回合后续不要再次尝试调用 Advisor;可继续凭自己判断推进,不必等待顾问。',
|
|
291
327
|
};
|
|
292
328
|
}
|
|
293
329
|
if (!advisor.enabled) {
|
|
294
330
|
return {
|
|
295
331
|
ok: false,
|
|
296
|
-
error: '顾问模型当前已关闭(/advisor on
|
|
332
|
+
error: '顾问模型当前已关闭(/advisor on 开启)。' +
|
|
333
|
+
'本回合后续不要再次尝试调用 Advisor;可继续凭自己判断推进,不必等待顾问。',
|
|
297
334
|
};
|
|
298
335
|
}
|
|
299
336
|
if (advisor.mode === 'manual' && ctx?.advisorInvocation !== 'manual') {
|
|
300
|
-
return {
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
error: '顾问模型当前为"仅手动"模式;请使用 /advisor ask 发起咨询。' +
|
|
340
|
+
'本回合后续不要再次尝试自动调用 Advisor。',
|
|
341
|
+
};
|
|
301
342
|
}
|
|
302
343
|
if (ctx?.advisorCalls && ctx.advisorCalls.used >= ctx.advisorCalls.max) {
|
|
303
344
|
// ⚠️ 必须说明「配额会增长」:配额随本回合工作量上调(loop.ts::advisorQuotaFor),
|
|
@@ -435,6 +476,32 @@ export const advisorTool = {
|
|
|
435
476
|
isConcurrencySafe: false,
|
|
436
477
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
437
478
|
interruptBehavior: () => 'cancel',
|
|
479
|
+
/**
|
|
480
|
+
* 动态可用性探测:未配置 / 已关闭 / 仅手动模式 → 工具不暴露给模型。
|
|
481
|
+
* 对齐 ComputerUse / WebBrowser 的模式:关闭时工具直接从 LLM 视野消失,
|
|
482
|
+
* 避免"看到工具但调不了→反复尝试"的流畅度问题。
|
|
483
|
+
*
|
|
484
|
+
* signature 随 enabled/mode/modelId 变化,确保配置改动后工具集签名更新、
|
|
485
|
+
* 模型下一轮拿到的是新工具列表。
|
|
486
|
+
*/
|
|
487
|
+
resolveAvailability() {
|
|
488
|
+
// 走同步探测(读内存快照),不触发磁盘 IO;
|
|
489
|
+
// resolveAvailability 是工具集构造阶段的同步调用,不能 await。
|
|
490
|
+
const avail = configService.advisorAvailabilitySync();
|
|
491
|
+
// ⚠️ 安全兜底:如果 snapshot 尚未初始化(极端情况:工具集构造发生在任何
|
|
492
|
+
// configService 异步调用之前),宁可返回 available=true 让工具显示出来
|
|
493
|
+
// (调用时 runAdvisor 会做真正的 fresh reload 校验),也不能误判为
|
|
494
|
+
// "未配置" 导致 Advisor 从工具列表里消失——少一个工具比多一个工具严重得多。
|
|
495
|
+
if (avail.configured && !avail.enabled) {
|
|
496
|
+
return { available: false, signature: 'disabled' };
|
|
497
|
+
}
|
|
498
|
+
if (avail.configured && avail.mode === 'manual') {
|
|
499
|
+
return { available: false, signature: 'manual-only' };
|
|
500
|
+
}
|
|
501
|
+
// 未初始化 / 已配置且可用 → 都显示工具(未配置时调用会有明确错误提示)
|
|
502
|
+
// modelId 进签名:不同顾问模型 = 不同能力 = 工具语义不同。
|
|
503
|
+
return { available: true, signature: avail.modelId ?? 'initializing' };
|
|
504
|
+
},
|
|
438
505
|
call,
|
|
439
506
|
callWithContext: runAdvisor,
|
|
440
507
|
};
|
|
@@ -51,11 +51,23 @@ const parameters = toToolParameters(inputSchema);
|
|
|
51
51
|
|
|
52
52
|
// ---------------- 2. Description(给 LLM 看的工具说明) ----------------
|
|
53
53
|
const description = `- Consults an independent, separately-configured second LLM ("顾问模型" / advisor) for a second opinion on your current plan, diagnosis, or progress.
|
|
54
|
-
- Call with no arguments in most cases — the advisor receives a bounded, relevant view of the current session (the original request, recent discussion, current plan/diagnosis, and key tool results).
|
|
55
|
-
- Exception: if your current plan/diagnosis relies on things you saw in images (screenshots, photos, diagrams), summarize the key visual facts in \`note\` in 1-2 sentences (what each image shows and what you concluded from it) — the advisor may not be able to see the images themselves
|
|
56
|
-
- Call
|
|
54
|
+
- Call with no arguments in most cases — the advisor receives a bounded, relevant view of the current session (the original request, recent discussion, current plan/diagnosis, and key tool results). Always pass key evidence (original code snippets, raw data, full plan text) in \`note\`, not just your summary — the advisor cannot verify things you never wrote down.
|
|
55
|
+
- Exception: if your current plan/diagnosis relies on things you saw in images (screenshots, photos, diagrams), summarize the key visual facts in \`note\` in 1-2 sentences (what each image shows and what you concluded from it) — the advisor may not be able to see the images themselves.
|
|
56
|
+
- **Call it BEFORE substantive work, not after things go wrong.** The advisor is worth the most before an approach hardens — once you have written the code, the plan is expensive to abandon.
|
|
57
|
+
- Orientation is NOT substantive work: finding files, reading code, running a command to see the current state. Do that first, then consult.
|
|
58
|
+
- Substantive work IS: writing or editing files, committing to an interpretation, building on an assumption, declaring an answer.
|
|
59
|
+
- **MUST call Advisor in all 5 of these situations — do not wait for errors or user prompts:**
|
|
60
|
+
1. After a plan / development plan is finalized, **before** executing it (get critique first, avoid discovering wrong direction halfway)
|
|
61
|
+
2. **Before** submitting final results to the user (before PresentResult): code reviews, assessment reports, technical selections, refactoring plans — any time you "deliver a conclusion"
|
|
62
|
+
3. The same error occurs 2+ times in a row, or fixes are not converging (early sign of infinite loop — switch approach immediately)
|
|
63
|
+
4. The user gives unsatisfied feedback a **second time** in the same turn (stop spinning in place, get a fresh perspective first)
|
|
64
|
+
5. Decisions involving security, data loss, or irreversible operations (advisor double-check)
|
|
65
|
+
- **Make your work durable BEFORE calling in situation 2.** Write the file, save the result. This call takes time; if the turn ends during it, a written file survives and an unwritten answer does not.
|
|
66
|
+
- On a long task, consult at least twice: once before the approach is locked in, once before you declare it done. On a short reactive task whose next step is dictated by output you just read, one call is enough — the advisor adds most of its value early.
|
|
67
|
+
- **Take the advice seriously, but do not obey it blindly.** Adapt if you follow a step and it empirically fails, or you hold primary-source evidence contradicting a specific claim (the file says X, the test output says Y). A passing self-test is NOT evidence the advice is wrong — it is evidence your test does not check what the advice checks.
|
|
68
|
+
- **If the advisor contradicts evidence you already gathered, do not silently switch.** Call Advisor once more stating the conflict outright ("I measured X, you advise Y — which constraint breaks the tie?"). The advisor saw your evidence but may have underweighted it; one reconciling call is far cheaper than committing to the wrong branch.
|
|
57
69
|
- The advisor has no tool access and does a single read-only pass over the transcript — it cannot verify anything itself, only reason about what it's shown.
|
|
58
|
-
- If this tool errors (advisor not configured in shared settings, disabled, manual-only, or a call failure), that's not a signal to stop — treat it as "no second opinion available this time" and continue with your own judgment
|
|
70
|
+
- If this tool errors (advisor not configured in shared settings, disabled, manual-only, or a call failure), that's not a signal to stop — treat it as "no second opinion available this time" and continue with your own judgment, but **explicitly note "not cross-verified by advisor" in your final output**. Do not block or retry in a loop.`;
|
|
59
71
|
|
|
60
72
|
// ---------------- 3. 顾问 persona system prompt ----------------
|
|
61
73
|
const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技术顾问,为正在独立执行任务的另一个 AI 助手提供第二意见评审。
|
|
@@ -64,6 +76,14 @@ const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技
|
|
|
64
76
|
你会看到该助手当前会话中受预算约束的相关记录——包括首要需求、近期交流、它的思考过程、当前方案
|
|
65
77
|
和关键工具结果与执行结果,以及它现在想让你重点关注的内容(如果有)。
|
|
66
78
|
|
|
79
|
+
重要:你**没有工具访问权,无法自行验证任何事实**。你只是提供一个不同视角的怀疑和推理,
|
|
80
|
+
不能把自己的推测当成确定结论。每条指出的问题都要明确标注:
|
|
81
|
+
- 【有证据】:基于记录里明确出现的代码/测试结果/事实
|
|
82
|
+
- 【推断】:基于已有信息的合理推断,但未被证实
|
|
83
|
+
- 【存疑】:信息不足,需要执行助手回去核实
|
|
84
|
+
不要过度自信,不要给出确定性语气的未验证判断——执行助手会对你的意见逐条做验证,
|
|
85
|
+
夸大其词会被判定为误报。
|
|
86
|
+
|
|
67
87
|
审视时请做到:
|
|
68
88
|
- 像资深工程师做方案评审/代码评审一样直接、具体——发现问题就明说,不要无条件附和或说套话。
|
|
69
89
|
- 优先检查:方案是否遗漏了风险或边界情况、诊断是否真的找到了根因还是只是让症状消失、
|
|
@@ -76,7 +96,16 @@ const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技
|
|
|
76
96
|
或换个方式绕开,都要点出来。
|
|
77
97
|
- 结论必须基于你在历史记录里看到的实际证据,不要脱离上下文给泛泛的通用建议;
|
|
78
98
|
被省略的记录(尤其工具结果原文)所涉及的判断,明说你无法核实。
|
|
79
|
-
- 篇幅精炼:只讲最关键的 1-3
|
|
99
|
+
- 篇幅精炼:只讲最关键的 1-3 点,不要复述历史、不要客套开场白。
|
|
100
|
+
|
|
101
|
+
关于「带着反证回来」的那一次咨询(助手被要求这样做,不是在质疑你):
|
|
102
|
+
当它说「我实测是 X,你上次建议 Y」时,它手里的是一手证据,你手里的是推理——
|
|
103
|
+
默认它的观测为真,然后只回答一个问题:**哪个约束更硬**。
|
|
104
|
+
- 你上次漏看了它给出的证据 → 直接认,并说清结论怎么变。
|
|
105
|
+
- 它的观测其实没测到你担心的那一层(测了被改的函数,而真实调用路径根本走不到)
|
|
106
|
+
→ 指出这个错位,并说要测什么才算数。
|
|
107
|
+
- 两者都成立、只是适用范围不同 → 说清各自的边界,别硬选一个。
|
|
108
|
+
不要为了显得一致而坚持原判,也不要因为被反驳就整个收回——两种都会让它失去判断依据。`;
|
|
80
109
|
|
|
81
110
|
// ---------------- 4. 消息构造(纯函数,单独导出可测) ----------------
|
|
82
111
|
|
|
@@ -234,12 +263,19 @@ function fitAdvisorBudget(messages: Message[], inputBudget: number): Message[] {
|
|
|
234
263
|
selected.push(first);
|
|
235
264
|
used += countMessagesTokens([first]);
|
|
236
265
|
}
|
|
266
|
+
// 从最近往前收,**装不下就停**(break,不是 continue)。
|
|
267
|
+
//
|
|
268
|
+
// ⚠️ 曾经是 continue:一条大消息放不下就跳过它、继续往前捡更小的旧消息。
|
|
269
|
+
// 结果是给顾问一段**中间有洞**的对话 —— 而洞恰好开在信息量最大的地方(长工具
|
|
270
|
+
// 结果、完整方案文本),偏偏下面那句省略提示还写着「保留最初需求和最近相关记录」,
|
|
271
|
+
// 顾问于是把两条本不相邻的消息读成前后相接,在残缺的因果链上给出自信结论。
|
|
272
|
+
// 宁可少给,不可给错:连续的尾部 + 如实说明从哪里开始省略。
|
|
237
273
|
const recent: Message[] = [];
|
|
238
274
|
for (let index = body.length - 1; index >= 0; index--) {
|
|
239
275
|
if (index === firstUserIndex) continue;
|
|
240
276
|
const message = body[index]!;
|
|
241
277
|
const cost = countMessagesTokens([message]);
|
|
242
|
-
if (used + cost > inputBudget)
|
|
278
|
+
if (used + cost > inputBudget) break;
|
|
243
279
|
recent.unshift(message);
|
|
244
280
|
used += cost;
|
|
245
281
|
}
|
|
@@ -348,19 +384,26 @@ export async function runAdvisor(
|
|
|
348
384
|
ok: false,
|
|
349
385
|
error:
|
|
350
386
|
'顾问模型未配置。请在设置 → 模型中完成专家顾问配置。' +
|
|
351
|
-
'
|
|
387
|
+
'本回合后续不要再次尝试调用 Advisor;可继续凭自己判断推进,不必等待顾问。',
|
|
352
388
|
};
|
|
353
389
|
}
|
|
354
390
|
|
|
355
391
|
if (!advisor.enabled) {
|
|
356
392
|
return {
|
|
357
393
|
ok: false,
|
|
358
|
-
error:
|
|
394
|
+
error:
|
|
395
|
+
'顾问模型当前已关闭(/advisor on 开启)。' +
|
|
396
|
+
'本回合后续不要再次尝试调用 Advisor;可继续凭自己判断推进,不必等待顾问。',
|
|
359
397
|
};
|
|
360
398
|
}
|
|
361
399
|
|
|
362
400
|
if (advisor.mode === 'manual' && ctx?.advisorInvocation !== 'manual') {
|
|
363
|
-
return {
|
|
401
|
+
return {
|
|
402
|
+
ok: false,
|
|
403
|
+
error:
|
|
404
|
+
'顾问模型当前为"仅手动"模式;请使用 /advisor ask 发起咨询。' +
|
|
405
|
+
'本回合后续不要再次尝试自动调用 Advisor。',
|
|
406
|
+
};
|
|
364
407
|
}
|
|
365
408
|
|
|
366
409
|
if (ctx?.advisorCalls && ctx.advisorCalls.used >= ctx.advisorCalls.max) {
|
|
@@ -507,6 +550,33 @@ export const advisorTool: Tool<AdvisorInput> = {
|
|
|
507
550
|
isConcurrencySafe: false,
|
|
508
551
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
509
552
|
interruptBehavior: () => 'cancel',
|
|
553
|
+
/**
|
|
554
|
+
* 动态可用性探测:未配置 / 已关闭 / 仅手动模式 → 工具不暴露给模型。
|
|
555
|
+
* 对齐 ComputerUse / WebBrowser 的模式:关闭时工具直接从 LLM 视野消失,
|
|
556
|
+
* 避免"看到工具但调不了→反复尝试"的流畅度问题。
|
|
557
|
+
*
|
|
558
|
+
* signature 随 enabled/mode/modelId 变化,确保配置改动后工具集签名更新、
|
|
559
|
+
* 模型下一轮拿到的是新工具列表。
|
|
560
|
+
*/
|
|
561
|
+
resolveAvailability() {
|
|
562
|
+
// 走同步探测(读内存快照),不触发磁盘 IO;
|
|
563
|
+
// resolveAvailability 是工具集构造阶段的同步调用,不能 await。
|
|
564
|
+
const avail = configService.advisorAvailabilitySync();
|
|
565
|
+
|
|
566
|
+
// ⚠️ 安全兜底:如果 snapshot 尚未初始化(极端情况:工具集构造发生在任何
|
|
567
|
+
// configService 异步调用之前),宁可返回 available=true 让工具显示出来
|
|
568
|
+
// (调用时 runAdvisor 会做真正的 fresh reload 校验),也不能误判为
|
|
569
|
+
// "未配置" 导致 Advisor 从工具列表里消失——少一个工具比多一个工具严重得多。
|
|
570
|
+
if (avail.configured && !avail.enabled) {
|
|
571
|
+
return { available: false, signature: 'disabled' };
|
|
572
|
+
}
|
|
573
|
+
if (avail.configured && avail.mode === 'manual') {
|
|
574
|
+
return { available: false, signature: 'manual-only' };
|
|
575
|
+
}
|
|
576
|
+
// 未初始化 / 已配置且可用 → 都显示工具(未配置时调用会有明确错误提示)
|
|
577
|
+
// modelId 进签名:不同顾问模型 = 不同能力 = 工具语义不同。
|
|
578
|
+
return { available: true, signature: avail.modelId ?? 'initializing' };
|
|
579
|
+
},
|
|
510
580
|
call,
|
|
511
581
|
callWithContext: runAdvisor,
|
|
512
582
|
};
|
package/src/tools/bash/bash.js
CHANGED
|
@@ -56,7 +56,7 @@ import { requestUserChoice } from '../../askBridge.js';
|
|
|
56
56
|
import { classifyCommand } from '../../irreversible/rules.js';
|
|
57
57
|
import { startBackgroundTask } from '../tasks/backgroundTaskService.js';
|
|
58
58
|
import { processExecutionMetadata } from '../tasks/processEvidence.js';
|
|
59
|
-
import { permanentDeleteReason } from '../shared/permanentDeleteGuard.js';
|
|
59
|
+
import { permanentDeleteReason, } from '../shared/permanentDeleteGuard.js';
|
|
60
60
|
import { ToolOutputStore } from '../shared/toolOutputStore.js';
|
|
61
61
|
// ---------------- 0. 常量 ----------------
|
|
62
62
|
const DEFAULT_TIMEOUT_MS = 120_000; // 2 min
|
|
@@ -105,6 +105,16 @@ function detectWindowsBash() {
|
|
|
105
105
|
return null;
|
|
106
106
|
}
|
|
107
107
|
const WIN_BASH = detectWindowsBash();
|
|
108
|
+
/**
|
|
109
|
+
* 本工具这一刻**真正**把命令交给谁执行。
|
|
110
|
+
*
|
|
111
|
+
* 单一出处:description(告诉模型该写哪种语法)与 permanentDeleteGuard(按哪套动词
|
|
112
|
+
* 匹配删除命令)必须看同一个判断,否则就会出现「description 说这是 cmd、守卫却按
|
|
113
|
+
* bash 匹配」这种两头对不上的裂缝 —— del / rd 会因此直接穿过全部三层防护。
|
|
114
|
+
*/
|
|
115
|
+
function effectiveShellKind() {
|
|
116
|
+
return process.platform === 'win32' && !WIN_BASH ? 'cmd' : 'bash';
|
|
117
|
+
}
|
|
108
118
|
/**
|
|
109
119
|
* G1:stdout / stderr 即时缓冲上限(各自)。命令狂输出(如 cat 大文件 / yes)时,
|
|
110
120
|
* 在最终统一截断之前先把内存封顶,防 OOM。取最终展示截断阈值的 2 倍 —— 正常输出不受影响,
|
|
@@ -311,8 +321,11 @@ Some commands return non-zero exit codes for informational (non-error) reasons.
|
|
|
311
321
|
- \`test\` / \`[\` exit 1 → condition is false (the answer to a question, not a failure)
|
|
312
322
|
Only exit codes ≥ 2 from these commands indicate a real failure. For all other commands, non-zero exit codes are treated as failures normally.
|
|
313
323
|
|
|
314
|
-
# Safety
|
|
315
|
-
|
|
324
|
+
# Safety - three levels, from strict to lenient
|
|
325
|
+
|
|
326
|
+
## 1. Blocked outright (fails before execution - do NOT retry with variants)
|
|
327
|
+
|
|
328
|
+
Catastrophic commands (no legitimate use):
|
|
316
329
|
- \`rm -rf /\` and variants targeting root, $HOME, ~, or system directories (/etc, /usr, /bin, /Windows, /Users, /home, ...)
|
|
317
330
|
- Filesystem destruction: \`mkfs\`, \`format X:\`, \`dd of=/dev/sdX\`, redirects to raw block devices
|
|
318
331
|
- Power management: \`shutdown\`, \`reboot\`, \`halt\`, \`poweroff\`, \`init 0/6\`
|
|
@@ -320,9 +333,37 @@ The following command patterns are blocked at the tool level and will fail befor
|
|
|
320
333
|
- Pipe-to-shell from network: \`curl ... | sh\`, \`wget ... | bash\`, etc.
|
|
321
334
|
- \`chmod 777 /\`, Windows full-disk \`del /s\` / \`rmdir /s\`, \`diskpart\`
|
|
322
335
|
|
|
323
|
-
|
|
336
|
+
Permanent deletion (bypasses the OS Trash / Recycle Bin - use the Trash tool instead):
|
|
337
|
+
- \`rm\`, \`unlink\`, \`rmdir\` (any shell-level file/dir deletion, any target)
|
|
338
|
+
- \`find ... -delete\`
|
|
339
|
+
- \`git clean -f\` / \`git clean --force\` (\`-n\` / \`--dry-run\` is fine)
|
|
340
|
+
|
|
341
|
+
To delete files or directories use the **Trash** tool (\`action: "move"\` - recoverable); to clean Git untracked files use Trash with \`action: "git-clean"\`.
|
|
342
|
+
Do NOT work around this by switching to \`find -delete\`, Python \`os.remove\`, Node \`fs.rm\`, or other shell variants - they are blocked the same way.
|
|
343
|
+
If a permanent deletion is truly required, ask the user to run it themselves in their terminal.
|
|
344
|
+
|
|
345
|
+
## 2. Requires user confirmation (irreversible)
|
|
346
|
+
|
|
347
|
+
These have legitimate uses but destroy the last copy - execution pauses and asks the user to confirm. In non-interactive contexts (sub-agents, cron, batch mode) they are automatically cancelled.
|
|
348
|
+
- \`git push --force\` / \`-f\`, and \`git push origin +branch\` (+refspec = force push)
|
|
349
|
+
- \`git push --delete <branch>\` / \`git push :<branch>\` (deleting remote branches)
|
|
350
|
+
- \`git filter-branch\` / \`git filter-repo\` (rewriting history)
|
|
351
|
+
- \`git reflog expire --expire=now\` / \`git gc --prune=now\` (destroying reflog)
|
|
352
|
+
- \`shred -u\` / \`shred --remove\` (overwrite and delete - unrecoverable)
|
|
353
|
+
- SQL data destruction: \`DROP DATABASE\`, \`DROP TABLE\`, \`TRUNCATE TABLE\`
|
|
354
|
+
|
|
355
|
+
## 3. Warning only (command still runs)
|
|
356
|
+
|
|
357
|
+
These execute normally; a \`⚠️ 警告:\` line is prepended to the output as a reminder. They are NOT blocked.
|
|
358
|
+
- \`git reset --hard\` (reflog keeps it recoverable)
|
|
359
|
+
- \`git push --force-with-lease\` (rejects if the remote has unseen commits - the safe force-push form)
|
|
360
|
+
- \`git checkout -- .\` / \`git restore .\` (discards uncommitted changes)
|
|
361
|
+
- \`git branch -D\`, \`git stash drop\` / \`git stash clear\` (local, recoverable via reflog in most cases)
|
|
362
|
+
- \`git commit --amend\`, \`git commit --no-verify\`
|
|
363
|
+
- \`DELETE FROM ...\` (SQL row deletion; DROP/TRUNCATE are in level 2)
|
|
364
|
+
- \`kubectl delete\`, \`terraform destroy\`
|
|
324
365
|
|
|
325
|
-
|
|
366
|
+
Regular Git operations (commit, add, status, log, diff, pull, fetch, checkout <branch>, merge, rebase, etc.) run without extra restrictions.${process.platform !== 'win32'
|
|
326
367
|
? ''
|
|
327
368
|
: WIN_BASH
|
|
328
369
|
? `
|
|
@@ -349,7 +390,11 @@ async function call(input, signal, context) {
|
|
|
349
390
|
error: `安全检查拒绝执行:${forbiddenReason}\n命令:${command}`,
|
|
350
391
|
};
|
|
351
392
|
}
|
|
352
|
-
|
|
393
|
+
// 传**实际执行命令的 shell**,不是工具名:win32 上找不到 Git Bash 时下面走的是
|
|
394
|
+
// cmd.exe(见 WIN_BASH 探测),description 也会告诉模型「这里是 cmd」,模型于是写
|
|
395
|
+
// del / rd —— 按 'bash' 匹配的话这两个词一个都拦不住(黑名单与 irreversible 规则表
|
|
396
|
+
// 同样只认 POSIX 形态),等于三层全空。
|
|
397
|
+
const permanentDelete = permanentDeleteReason(command, effectiveShellKind());
|
|
353
398
|
if (permanentDelete)
|
|
354
399
|
return { ok: false, error: `永久删除检查拒绝执行:${permanentDelete}\n命令:${command}` };
|
|
355
400
|
// 4a-1. 不可逆动作闸门:毁掉「最后一份副本」的命令要**停下来问人**。
|
package/src/tools/bash/bash.ts
CHANGED
|
@@ -63,7 +63,10 @@ import { requestUserChoice } from '../../askBridge.js';
|
|
|
63
63
|
import { classifyCommand } from '../../irreversible/rules.js';
|
|
64
64
|
import { startBackgroundTask } from '../tasks/backgroundTaskService.js';
|
|
65
65
|
import { processExecutionMetadata } from '../tasks/processEvidence.js';
|
|
66
|
-
import {
|
|
66
|
+
import {
|
|
67
|
+
permanentDeleteReason,
|
|
68
|
+
type ShellKind,
|
|
69
|
+
} from '../shared/permanentDeleteGuard.js';
|
|
67
70
|
import { ToolOutputStore } from '../shared/toolOutputStore.js';
|
|
68
71
|
|
|
69
72
|
// ---------------- 0. 常量 ----------------
|
|
@@ -112,6 +115,17 @@ function detectWindowsBash(): string | null {
|
|
|
112
115
|
|
|
113
116
|
const WIN_BASH = detectWindowsBash();
|
|
114
117
|
|
|
118
|
+
/**
|
|
119
|
+
* 本工具这一刻**真正**把命令交给谁执行。
|
|
120
|
+
*
|
|
121
|
+
* 单一出处:description(告诉模型该写哪种语法)与 permanentDeleteGuard(按哪套动词
|
|
122
|
+
* 匹配删除命令)必须看同一个判断,否则就会出现「description 说这是 cmd、守卫却按
|
|
123
|
+
* bash 匹配」这种两头对不上的裂缝 —— del / rd 会因此直接穿过全部三层防护。
|
|
124
|
+
*/
|
|
125
|
+
function effectiveShellKind(): ShellKind {
|
|
126
|
+
return process.platform === 'win32' && !WIN_BASH ? 'cmd' : 'bash';
|
|
127
|
+
}
|
|
128
|
+
|
|
115
129
|
/**
|
|
116
130
|
* G1:stdout / stderr 即时缓冲上限(各自)。命令狂输出(如 cat 大文件 / yes)时,
|
|
117
131
|
* 在最终统一截断之前先把内存封顶,防 OOM。取最终展示截断阈值的 2 倍 —— 正常输出不受影响,
|
|
@@ -332,8 +346,11 @@ Some commands return non-zero exit codes for informational (non-error) reasons.
|
|
|
332
346
|
- \`test\` / \`[\` exit 1 → condition is false (the answer to a question, not a failure)
|
|
333
347
|
Only exit codes ≥ 2 from these commands indicate a real failure. For all other commands, non-zero exit codes are treated as failures normally.
|
|
334
348
|
|
|
335
|
-
# Safety
|
|
336
|
-
|
|
349
|
+
# Safety - three levels, from strict to lenient
|
|
350
|
+
|
|
351
|
+
## 1. Blocked outright (fails before execution - do NOT retry with variants)
|
|
352
|
+
|
|
353
|
+
Catastrophic commands (no legitimate use):
|
|
337
354
|
- \`rm -rf /\` and variants targeting root, $HOME, ~, or system directories (/etc, /usr, /bin, /Windows, /Users, /home, ...)
|
|
338
355
|
- Filesystem destruction: \`mkfs\`, \`format X:\`, \`dd of=/dev/sdX\`, redirects to raw block devices
|
|
339
356
|
- Power management: \`shutdown\`, \`reboot\`, \`halt\`, \`poweroff\`, \`init 0/6\`
|
|
@@ -341,9 +358,37 @@ The following command patterns are blocked at the tool level and will fail befor
|
|
|
341
358
|
- Pipe-to-shell from network: \`curl ... | sh\`, \`wget ... | bash\`, etc.
|
|
342
359
|
- \`chmod 777 /\`, Windows full-disk \`del /s\` / \`rmdir /s\`, \`diskpart\`
|
|
343
360
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
361
|
+
Permanent deletion (bypasses the OS Trash / Recycle Bin - use the Trash tool instead):
|
|
362
|
+
- \`rm\`, \`unlink\`, \`rmdir\` (any shell-level file/dir deletion, any target)
|
|
363
|
+
- \`find ... -delete\`
|
|
364
|
+
- \`git clean -f\` / \`git clean --force\` (\`-n\` / \`--dry-run\` is fine)
|
|
365
|
+
|
|
366
|
+
To delete files or directories use the **Trash** tool (\`action: "move"\` - recoverable); to clean Git untracked files use Trash with \`action: "git-clean"\`.
|
|
367
|
+
Do NOT work around this by switching to \`find -delete\`, Python \`os.remove\`, Node \`fs.rm\`, or other shell variants - they are blocked the same way.
|
|
368
|
+
If a permanent deletion is truly required, ask the user to run it themselves in their terminal.
|
|
369
|
+
|
|
370
|
+
## 2. Requires user confirmation (irreversible)
|
|
371
|
+
|
|
372
|
+
These have legitimate uses but destroy the last copy - execution pauses and asks the user to confirm. In non-interactive contexts (sub-agents, cron, batch mode) they are automatically cancelled.
|
|
373
|
+
- \`git push --force\` / \`-f\`, and \`git push origin +branch\` (+refspec = force push)
|
|
374
|
+
- \`git push --delete <branch>\` / \`git push :<branch>\` (deleting remote branches)
|
|
375
|
+
- \`git filter-branch\` / \`git filter-repo\` (rewriting history)
|
|
376
|
+
- \`git reflog expire --expire=now\` / \`git gc --prune=now\` (destroying reflog)
|
|
377
|
+
- \`shred -u\` / \`shred --remove\` (overwrite and delete - unrecoverable)
|
|
378
|
+
- SQL data destruction: \`DROP DATABASE\`, \`DROP TABLE\`, \`TRUNCATE TABLE\`
|
|
379
|
+
|
|
380
|
+
## 3. Warning only (command still runs)
|
|
381
|
+
|
|
382
|
+
These execute normally; a \`⚠️ 警告:\` line is prepended to the output as a reminder. They are NOT blocked.
|
|
383
|
+
- \`git reset --hard\` (reflog keeps it recoverable)
|
|
384
|
+
- \`git push --force-with-lease\` (rejects if the remote has unseen commits - the safe force-push form)
|
|
385
|
+
- \`git checkout -- .\` / \`git restore .\` (discards uncommitted changes)
|
|
386
|
+
- \`git branch -D\`, \`git stash drop\` / \`git stash clear\` (local, recoverable via reflog in most cases)
|
|
387
|
+
- \`git commit --amend\`, \`git commit --no-verify\`
|
|
388
|
+
- \`DELETE FROM ...\` (SQL row deletion; DROP/TRUNCATE are in level 2)
|
|
389
|
+
- \`kubectl delete\`, \`terraform destroy\`
|
|
390
|
+
|
|
391
|
+
Regular Git operations (commit, add, status, log, diff, pull, fetch, checkout <branch>, merge, rebase, etc.) run without extra restrictions.${
|
|
347
392
|
process.platform !== 'win32'
|
|
348
393
|
? ''
|
|
349
394
|
: WIN_BASH
|
|
@@ -374,7 +419,11 @@ async function call(input: BashInput, signal?: AbortSignal, context?: ToolCallCo
|
|
|
374
419
|
error: `安全检查拒绝执行:${forbiddenReason}\n命令:${command}`,
|
|
375
420
|
};
|
|
376
421
|
}
|
|
377
|
-
|
|
422
|
+
// 传**实际执行命令的 shell**,不是工具名:win32 上找不到 Git Bash 时下面走的是
|
|
423
|
+
// cmd.exe(见 WIN_BASH 探测),description 也会告诉模型「这里是 cmd」,模型于是写
|
|
424
|
+
// del / rd —— 按 'bash' 匹配的话这两个词一个都拦不住(黑名单与 irreversible 规则表
|
|
425
|
+
// 同样只认 POSIX 形态),等于三层全空。
|
|
426
|
+
const permanentDelete = permanentDeleteReason(command, effectiveShellKind());
|
|
378
427
|
if (permanentDelete) return { ok:false, error:`永久删除检查拒绝执行:${permanentDelete}\n命令:${command}` };
|
|
379
428
|
|
|
380
429
|
// 4a-1. 不可逆动作闸门:毁掉「最后一份副本」的命令要**停下来问人**。
|