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
|
@@ -21,10 +21,13 @@
|
|
|
21
21
|
* - `{ args.<任意字段>: var_name }` 绑入参的具名字段
|
|
22
22
|
* ============================================================
|
|
23
23
|
*/
|
|
24
|
+
import { randomUUID } from 'node:crypto';
|
|
24
25
|
import { existsSync } from 'node:fs';
|
|
25
26
|
import { resolve } from 'node:path';
|
|
26
27
|
import { executeTool, getToolByName, recordRead, } from '../../../../src/plugin-sdk.js';
|
|
27
28
|
import { interpolateDeep } from '../expressions.js';
|
|
29
|
+
import { buildWorkflowToolContext, effectiveWorkflowCwd, } from '../toolRuntime.js';
|
|
30
|
+
import { safeArtifactPath } from '../types.js';
|
|
28
31
|
export async function* execToolStep(step, vars, ctx) {
|
|
29
32
|
if (!step.tool)
|
|
30
33
|
throw new Error(`step ${step.id}: 缺少 tool 字段`);
|
|
@@ -56,8 +59,8 @@ export async function* execToolStep(step, vars, ctx) {
|
|
|
56
59
|
argsFriendly: friendlyToolDesc(tool.name, args),
|
|
57
60
|
};
|
|
58
61
|
// ════ 声明产物覆盖判定(解 Write/Edit 的 pre-read 守卫) ════
|
|
59
|
-
// 内核写类工具有「文件已存在必须先 Read」守卫(fileState
|
|
60
|
-
// 跨进程 / resume /
|
|
62
|
+
// 内核写类工具有「文件已存在必须先 Read」守卫(fileState 按会话隔离,
|
|
63
|
+
// 跨进程 / resume / 残留旧产物时可能为空)。确定性 tool 步的 args 是 yaml 作者
|
|
61
64
|
// 写死的,模型没有机会先 Read —— 目标文件若是工作流**声明的产物**
|
|
62
65
|
// (def 级 + step 级 artifacts:,= 工作流自己拥有、本就要覆盖的文件),
|
|
63
66
|
// 这里自动 pre-Read 一次解除守卫(管线动作,不 yield 事件、结果丢弃)。
|
|
@@ -65,16 +68,60 @@ export async function* execToolStep(step, vars, ctx) {
|
|
|
65
68
|
await preReadDeclaredArtifact(tool, args, step, ctx);
|
|
66
69
|
// 宿主能力按 `uses:` 切片后转发。`executeTool` 的第四参此前从未传过 ——
|
|
67
70
|
// 这正是「workflow 里 ComputerUse 恒失败」的末端表现。
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
const pendingProgress = [];
|
|
72
|
+
let wakeProgress;
|
|
73
|
+
const toolContext = buildWorkflowToolContext(step, ctx, toolCallId, (message, detail) => {
|
|
74
|
+
pendingProgress.push({
|
|
75
|
+
type: 'tool_progress',
|
|
76
|
+
toolName: tool.name,
|
|
77
|
+
toolCallId,
|
|
78
|
+
message,
|
|
79
|
+
detail,
|
|
80
|
+
});
|
|
81
|
+
wakeProgress?.();
|
|
82
|
+
});
|
|
83
|
+
if ('error' in toolContext) {
|
|
84
|
+
throw new Error(`step ${step.id}: ${toolContext.error}`);
|
|
85
|
+
}
|
|
86
|
+
const dispatch = ctx.executeTool ?? executeTool;
|
|
87
|
+
let settled = false;
|
|
88
|
+
let result;
|
|
89
|
+
void dispatch(tool.name, argsJson, ctx.signal, toolContext)
|
|
90
|
+
.then((value) => {
|
|
91
|
+
result = value;
|
|
92
|
+
})
|
|
93
|
+
.catch((error) => {
|
|
94
|
+
result = {
|
|
95
|
+
ok: false,
|
|
96
|
+
error: error instanceof Error ? error.message : String(error),
|
|
97
|
+
};
|
|
98
|
+
})
|
|
99
|
+
.finally(() => {
|
|
100
|
+
settled = true;
|
|
101
|
+
wakeProgress?.();
|
|
102
|
+
});
|
|
103
|
+
while (!settled || pendingProgress.length > 0) {
|
|
104
|
+
const progress = pendingProgress.shift();
|
|
105
|
+
if (progress) {
|
|
106
|
+
yield progress;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
await new Promise((resolveWake) => {
|
|
110
|
+
wakeProgress = resolveWake;
|
|
111
|
+
if (settled || pendingProgress.length > 0)
|
|
112
|
+
resolveWake();
|
|
113
|
+
});
|
|
114
|
+
wakeProgress = undefined;
|
|
115
|
+
}
|
|
116
|
+
if (!result)
|
|
117
|
+
throw new Error(`step ${step.id}: 工具执行未返回结果`);
|
|
72
118
|
yield {
|
|
73
119
|
type: 'tool_end',
|
|
74
120
|
toolName: tool.name,
|
|
75
121
|
toolCallId,
|
|
76
122
|
ok: result.ok,
|
|
77
123
|
content: result.ok ? result.content : result.error,
|
|
124
|
+
metadata: result.metadata,
|
|
78
125
|
};
|
|
79
126
|
if (!result.ok) {
|
|
80
127
|
throw new Error(result.error);
|
|
@@ -91,7 +138,7 @@ export async function* execToolStep(step, vars, ctx) {
|
|
|
91
138
|
role: 'assistant',
|
|
92
139
|
content: `[workflow:${step.id}] ✅ ${tool.name} 执行完成:${summary}`,
|
|
93
140
|
timestamp: Date.now(),
|
|
94
|
-
id:
|
|
141
|
+
id: randomUUID(),
|
|
95
142
|
});
|
|
96
143
|
// ⚠️ 工具返回的图片**不能丢**。此前这里只取 content,`visionImages` 被静默丢弃 ——
|
|
97
144
|
// 于是任何「截图/看图」类工作流都拿不到产物,而且不会报错,只是后面的评估步没图可看。
|
|
@@ -101,77 +148,60 @@ export async function* execToolStep(step, vars, ctx) {
|
|
|
101
148
|
.map((image) => (typeof image.source === 'string' ? image.source : ''))
|
|
102
149
|
.filter((path) => path.length > 0);
|
|
103
150
|
return {
|
|
104
|
-
raw: {
|
|
151
|
+
raw: {
|
|
152
|
+
content,
|
|
153
|
+
result: content,
|
|
154
|
+
args,
|
|
155
|
+
images,
|
|
156
|
+
metadata: result.metadata,
|
|
157
|
+
recovery: result.recovery,
|
|
158
|
+
sensitivity: result.sensitivity,
|
|
159
|
+
visionNote: result.visionNote,
|
|
160
|
+
},
|
|
105
161
|
preview: content.length > 200 ? `${content.slice(0, 200)}...` : content,
|
|
106
162
|
};
|
|
107
163
|
}
|
|
108
164
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* 三条语义,缺一条这个机制就退化:
|
|
112
|
-
* · 未声明 `uses` → 返回**空上下文**(不是 undefined,`toolGate` 这类与能力无关的
|
|
113
|
-
* 宿主策略仍要生效)。工具因此走自己的清晰报错,不会静默降级;
|
|
114
|
-
* · 声明了但宿主没提供 → **响亮报错**。yaml 说了要用电脑,而这次运行根本没开
|
|
115
|
-
* `--computer-use`,那是配置错误,应该在这一步就停,不是跑到一半才发现;
|
|
116
|
-
* · 只转发声明过的那一片 —— 声明 `[computer]` 的步拿不到 `browserRun`。
|
|
117
|
-
*
|
|
118
|
-
* 单一出处:切片逻辑只在这里,runner / 其它 executor 都不许自己拼。
|
|
119
|
-
*/
|
|
120
|
-
function hostContextFor(step, ctx) {
|
|
121
|
-
// toolGate 是宿主的通用策略缝(与具体能力无关),任何步都该受它约束。
|
|
122
|
-
const base = ctx.host?.toolGate
|
|
123
|
-
? { toolGate: ctx.host.toolGate }
|
|
124
|
-
: {};
|
|
125
|
-
const uses = step.uses ?? [];
|
|
126
|
-
if (uses.length === 0)
|
|
127
|
-
return { ctx: base };
|
|
128
|
-
const out = { ...base };
|
|
129
|
-
for (const capability of uses) {
|
|
130
|
-
if (capability === 'computer') {
|
|
131
|
-
if (!ctx.host?.computerRun) {
|
|
132
|
-
return {
|
|
133
|
-
error: 'uses:[computer] 声明了需要电脑操控能力,但本次运行的宿主没有提供 —— '
|
|
134
|
-
+ '请用 `--computer-use interactive` 启动,或去掉该步的 uses 声明。',
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
out.computerRun = ctx.host.computerRun;
|
|
138
|
-
// 人工接管通道与能力同进退:没有它,遇到验证/密码只能硬失败。
|
|
139
|
-
if (ctx.host.computerIntervention)
|
|
140
|
-
out.computerIntervention = ctx.host.computerIntervention;
|
|
141
|
-
}
|
|
142
|
-
else if (capability === 'browser') {
|
|
143
|
-
if (!ctx.host?.browserRun) {
|
|
144
|
-
return {
|
|
145
|
-
error: 'uses:[browser] 声明了需要浏览器授权上下文,但本次运行的宿主没有提供。'
|
|
146
|
-
+ '(基础浏览操作不声明 uses 也能用;upload/submit 这类才需要它。)',
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
out.browserRun = ctx.host.browserRun;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return { ctx: out };
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* 写类 tool 步的目标文件若命中声明产物且已存在 → pre-Read 解除内核守卫。
|
|
165
|
+
* 写类 tool 步的目标文件若命中【安全的声明产物】且已存在 → pre-Read 解除内核守卫。
|
|
156
166
|
*
|
|
157
167
|
* 路径归一化:artifacts 声明相对 cwd;args.file_path 可能是 ./相对 或 绝对,
|
|
158
168
|
* 双方都 resolve(cwd, …) 后比较(win32 文件系统大小写不敏感 → lowercase 比较;
|
|
159
169
|
* 其余平台保持大小写敏感,避免 Plan.md / plan.md 误判同文件)。
|
|
170
|
+
*
|
|
171
|
+
* 安全守卫(修复路径遍历):
|
|
172
|
+
* · 声明的 artifacts 路径先经 safeArtifactPath 校验【必须落在 cwd 内】,
|
|
173
|
+
* 越界声明直接从命中列表移除——不解守卫,让内核写保护照常生效(fail-closed)。
|
|
174
|
+
* · args.file_path 目标自身也做 cwd 内校验:目标越界(工作流步骤想写 cwd 外)
|
|
175
|
+
* → 同样不解守卫,让写工具在执行时自己报错(通常是权限/文件不存在提示,
|
|
176
|
+
* 而不是静默放行盲写,双重保险)。
|
|
160
177
|
*/
|
|
161
178
|
async function preReadDeclaredArtifact(tool, args, step, ctx) {
|
|
162
179
|
if (tool.isReadOnly !== false)
|
|
163
180
|
return; // 只对写类工具生效
|
|
164
181
|
if (typeof args.file_path !== 'string')
|
|
165
182
|
return;
|
|
183
|
+
const cwd = effectiveWorkflowCwd(ctx);
|
|
166
184
|
const norm = (p) => {
|
|
167
|
-
const abs = resolve(
|
|
185
|
+
const abs = resolve(cwd, p);
|
|
168
186
|
return process.platform === 'win32' ? abs.toLowerCase() : abs;
|
|
169
187
|
};
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
188
|
+
// 1. 目标文件先过 cwd 安全关(越界直接不解守卫)
|
|
189
|
+
const targetRaw = String(args.file_path);
|
|
190
|
+
const targetSafe = safeArtifactPath(cwd, targetRaw);
|
|
191
|
+
if (targetSafe === null)
|
|
192
|
+
return;
|
|
193
|
+
const target = norm(targetSafe);
|
|
194
|
+
// 2. 声明产物过 cwd 安全关(过滤掉越界的声明)
|
|
195
|
+
const rawDeclared = [...(ctx.declaredArtifacts ?? []), ...(step.artifacts ?? [])];
|
|
196
|
+
const declaredAbs = [];
|
|
197
|
+
for (const raw of rawDeclared) {
|
|
198
|
+
const safe = safeArtifactPath(cwd, raw);
|
|
199
|
+
if (safe !== null)
|
|
200
|
+
declaredAbs.push(norm(safe));
|
|
201
|
+
}
|
|
202
|
+
if (!declaredAbs.some((a) => a === target))
|
|
173
203
|
return;
|
|
174
|
-
const abs = resolve(
|
|
204
|
+
const abs = resolve(cwd, targetSafe);
|
|
175
205
|
if (!existsSync(abs))
|
|
176
206
|
return; // 新建文件不需要解守卫
|
|
177
207
|
// 直接登记一次「完整已读」状态解除内核 pre-read 守卫——不真正 Read 内容:
|
|
@@ -182,7 +212,7 @@ async function preReadDeclaredArtifact(tool, args, step, ctx) {
|
|
|
182
212
|
// (首轮新建 PLAN.md 不存在直接放行,审批反馈后回写已存在的大 PLAN.md 被挡死)。
|
|
183
213
|
// 两道守卫(never-read + truncated)必须一并解除,声明产物的整体覆盖才算真正授权。
|
|
184
214
|
// recordRead 内部自带 statSync 失败兜底(返回 1、不抛),无需再包 try/catch。
|
|
185
|
-
recordRead(abs, { truncated: false });
|
|
215
|
+
recordRead(abs, { truncated: false }, ctx.sessionState?.fileReads);
|
|
186
216
|
}
|
|
187
217
|
/**
|
|
188
218
|
* 给 tool_start 生成人话描述(LiveArea spinner 行用)。
|
|
@@ -22,18 +22,23 @@
|
|
|
22
22
|
* ============================================================
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import { randomUUID } from 'node:crypto';
|
|
25
26
|
import { existsSync } from 'node:fs';
|
|
26
27
|
import { resolve } from 'node:path';
|
|
27
28
|
|
|
28
29
|
import {
|
|
30
|
+
type LoopEvent,
|
|
31
|
+
type ToolResult,
|
|
29
32
|
executeTool,
|
|
30
33
|
getToolByName,
|
|
31
34
|
recordRead,
|
|
32
|
-
type LoopEvent,
|
|
33
|
-
type ToolCallContext,
|
|
34
35
|
} from '../../../../src/plugin-sdk.js';
|
|
35
36
|
import { interpolateDeep } from '../expressions.js';
|
|
36
|
-
import
|
|
37
|
+
import {
|
|
38
|
+
buildWorkflowToolContext,
|
|
39
|
+
effectiveWorkflowCwd,
|
|
40
|
+
} from '../toolRuntime.js';
|
|
41
|
+
import { safeArtifactPath, type RunContext, type StepDef, type StepResult, type VarStack } from '../types.js';
|
|
37
42
|
|
|
38
43
|
export async function* execToolStep(
|
|
39
44
|
step: StepDef,
|
|
@@ -74,19 +79,73 @@ export async function* execToolStep(
|
|
|
74
79
|
};
|
|
75
80
|
|
|
76
81
|
// ════ 声明产物覆盖判定(解 Write/Edit 的 pre-read 守卫) ════
|
|
77
|
-
// 内核写类工具有「文件已存在必须先 Read」守卫(fileState
|
|
78
|
-
// 跨进程 / resume /
|
|
82
|
+
// 内核写类工具有「文件已存在必须先 Read」守卫(fileState 按会话隔离,
|
|
83
|
+
// 跨进程 / resume / 残留旧产物时可能为空)。确定性 tool 步的 args 是 yaml 作者
|
|
79
84
|
// 写死的,模型没有机会先 Read —— 目标文件若是工作流**声明的产物**
|
|
80
85
|
// (def 级 + step 级 artifacts:,= 工作流自己拥有、本就要覆盖的文件),
|
|
81
86
|
// 这里自动 pre-Read 一次解除守卫(管线动作,不 yield 事件、结果丢弃)。
|
|
82
87
|
// 非声明产物的盲写保护原样生效;pre-Read 失败也不拦,让写工具自己报错。
|
|
83
|
-
await preReadDeclaredArtifact(
|
|
88
|
+
await preReadDeclaredArtifact(
|
|
89
|
+
tool,
|
|
90
|
+
args as Record<string, unknown>,
|
|
91
|
+
step,
|
|
92
|
+
ctx,
|
|
93
|
+
);
|
|
84
94
|
|
|
85
95
|
// 宿主能力按 `uses:` 切片后转发。`executeTool` 的第四参此前从未传过 ——
|
|
86
96
|
// 这正是「workflow 里 ComputerUse 恒失败」的末端表现。
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const
|
|
97
|
+
const pendingProgress: LoopEvent[] = [];
|
|
98
|
+
let wakeProgress: (() => void) | undefined;
|
|
99
|
+
const toolContext = buildWorkflowToolContext(
|
|
100
|
+
step,
|
|
101
|
+
ctx,
|
|
102
|
+
toolCallId,
|
|
103
|
+
(message, detail) => {
|
|
104
|
+
pendingProgress.push({
|
|
105
|
+
type: 'tool_progress',
|
|
106
|
+
toolName: tool.name,
|
|
107
|
+
toolCallId,
|
|
108
|
+
message,
|
|
109
|
+
detail,
|
|
110
|
+
});
|
|
111
|
+
wakeProgress?.();
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
if ('error' in toolContext) {
|
|
115
|
+
throw new Error(`step ${step.id}: ${toolContext.error}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const dispatch = ctx.executeTool ?? executeTool;
|
|
119
|
+
let settled = false;
|
|
120
|
+
let result: ToolResult | undefined;
|
|
121
|
+
void dispatch(tool.name, argsJson, ctx.signal, toolContext)
|
|
122
|
+
.then((value) => {
|
|
123
|
+
result = value;
|
|
124
|
+
})
|
|
125
|
+
.catch((error: unknown) => {
|
|
126
|
+
result = {
|
|
127
|
+
ok: false,
|
|
128
|
+
error: error instanceof Error ? error.message : String(error),
|
|
129
|
+
};
|
|
130
|
+
})
|
|
131
|
+
.finally(() => {
|
|
132
|
+
settled = true;
|
|
133
|
+
wakeProgress?.();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
while (!settled || pendingProgress.length > 0) {
|
|
137
|
+
const progress = pendingProgress.shift();
|
|
138
|
+
if (progress) {
|
|
139
|
+
yield progress;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
await new Promise<void>((resolveWake) => {
|
|
143
|
+
wakeProgress = resolveWake;
|
|
144
|
+
if (settled || pendingProgress.length > 0) resolveWake();
|
|
145
|
+
});
|
|
146
|
+
wakeProgress = undefined;
|
|
147
|
+
}
|
|
148
|
+
if (!result) throw new Error(`step ${step.id}: 工具执行未返回结果`);
|
|
90
149
|
|
|
91
150
|
yield {
|
|
92
151
|
type: 'tool_end',
|
|
@@ -94,6 +153,7 @@ export async function* execToolStep(
|
|
|
94
153
|
toolCallId,
|
|
95
154
|
ok: result.ok,
|
|
96
155
|
content: result.ok ? result.content : result.error,
|
|
156
|
+
metadata: result.metadata,
|
|
97
157
|
};
|
|
98
158
|
|
|
99
159
|
if (!result.ok) {
|
|
@@ -105,14 +165,15 @@ export async function* execToolStep(
|
|
|
105
165
|
// 后续 agent/llm/skill 步骤走 runQuery() 时,LLM 能在上下文中看到
|
|
106
166
|
// 这步做了什么(与正常 TAOR 循环中 tool_call → tool_result 的记录方式一致)。
|
|
107
167
|
// 只记录精简摘要(不把完整工具回执塞入 history,避免 token 浪费)。
|
|
108
|
-
const summary =
|
|
109
|
-
|
|
110
|
-
|
|
168
|
+
const summary =
|
|
169
|
+
content.length > 500
|
|
170
|
+
? `${content.slice(0, 500)}…(已截断,共 ${content.length} 字符)`
|
|
171
|
+
: content;
|
|
111
172
|
ctx.history.push({
|
|
112
173
|
role: 'assistant',
|
|
113
174
|
content: `[workflow:${step.id}] ✅ ${tool.name} 执行完成:${summary}`,
|
|
114
175
|
timestamp: Date.now(),
|
|
115
|
-
id:
|
|
176
|
+
id: randomUUID(),
|
|
116
177
|
});
|
|
117
178
|
|
|
118
179
|
// ⚠️ 工具返回的图片**不能丢**。此前这里只取 content,`visionImages` 被静默丢弃 ——
|
|
@@ -124,67 +185,33 @@ export async function* execToolStep(
|
|
|
124
185
|
.filter((path) => path.length > 0);
|
|
125
186
|
|
|
126
187
|
return {
|
|
127
|
-
raw: {
|
|
188
|
+
raw: {
|
|
189
|
+
content,
|
|
190
|
+
result: content,
|
|
191
|
+
args,
|
|
192
|
+
images,
|
|
193
|
+
metadata: result.metadata,
|
|
194
|
+
recovery: result.recovery,
|
|
195
|
+
sensitivity: result.sensitivity,
|
|
196
|
+
visionNote: result.visionNote,
|
|
197
|
+
},
|
|
128
198
|
preview: content.length > 200 ? `${content.slice(0, 200)}...` : content,
|
|
129
199
|
};
|
|
130
200
|
}
|
|
131
201
|
|
|
132
202
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* 三条语义,缺一条这个机制就退化:
|
|
136
|
-
* · 未声明 `uses` → 返回**空上下文**(不是 undefined,`toolGate` 这类与能力无关的
|
|
137
|
-
* 宿主策略仍要生效)。工具因此走自己的清晰报错,不会静默降级;
|
|
138
|
-
* · 声明了但宿主没提供 → **响亮报错**。yaml 说了要用电脑,而这次运行根本没开
|
|
139
|
-
* `--computer-use`,那是配置错误,应该在这一步就停,不是跑到一半才发现;
|
|
140
|
-
* · 只转发声明过的那一片 —— 声明 `[computer]` 的步拿不到 `browserRun`。
|
|
141
|
-
*
|
|
142
|
-
* 单一出处:切片逻辑只在这里,runner / 其它 executor 都不许自己拼。
|
|
143
|
-
*/
|
|
144
|
-
function hostContextFor(
|
|
145
|
-
step: StepDef,
|
|
146
|
-
ctx: RunContext,
|
|
147
|
-
): { ctx: ToolCallContext } | { error: string } {
|
|
148
|
-
// toolGate 是宿主的通用策略缝(与具体能力无关),任何步都该受它约束。
|
|
149
|
-
const base: ToolCallContext = ctx.host?.toolGate
|
|
150
|
-
? { toolGate: ctx.host.toolGate }
|
|
151
|
-
: {};
|
|
152
|
-
const uses = step.uses ?? [];
|
|
153
|
-
if (uses.length === 0) return { ctx: base };
|
|
154
|
-
|
|
155
|
-
const out: ToolCallContext = { ...base };
|
|
156
|
-
for (const capability of uses) {
|
|
157
|
-
if (capability === 'computer') {
|
|
158
|
-
if (!ctx.host?.computerRun) {
|
|
159
|
-
return {
|
|
160
|
-
error:
|
|
161
|
-
'uses:[computer] 声明了需要电脑操控能力,但本次运行的宿主没有提供 —— '
|
|
162
|
-
+ '请用 `--computer-use interactive` 启动,或去掉该步的 uses 声明。',
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
out.computerRun = ctx.host.computerRun;
|
|
166
|
-
// 人工接管通道与能力同进退:没有它,遇到验证/密码只能硬失败。
|
|
167
|
-
if (ctx.host.computerIntervention) out.computerIntervention = ctx.host.computerIntervention;
|
|
168
|
-
} else if (capability === 'browser') {
|
|
169
|
-
if (!ctx.host?.browserRun) {
|
|
170
|
-
return {
|
|
171
|
-
error:
|
|
172
|
-
'uses:[browser] 声明了需要浏览器授权上下文,但本次运行的宿主没有提供。'
|
|
173
|
-
+ '(基础浏览操作不声明 uses 也能用;upload/submit 这类才需要它。)',
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
out.browserRun = ctx.host.browserRun;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return { ctx: out };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* 写类 tool 步的目标文件若命中声明产物且已存在 → pre-Read 解除内核守卫。
|
|
203
|
+
* 写类 tool 步的目标文件若命中【安全的声明产物】且已存在 → pre-Read 解除内核守卫。
|
|
184
204
|
*
|
|
185
205
|
* 路径归一化:artifacts 声明相对 cwd;args.file_path 可能是 ./相对 或 绝对,
|
|
186
206
|
* 双方都 resolve(cwd, …) 后比较(win32 文件系统大小写不敏感 → lowercase 比较;
|
|
187
207
|
* 其余平台保持大小写敏感,避免 Plan.md / plan.md 误判同文件)。
|
|
208
|
+
*
|
|
209
|
+
* 安全守卫(修复路径遍历):
|
|
210
|
+
* · 声明的 artifacts 路径先经 safeArtifactPath 校验【必须落在 cwd 内】,
|
|
211
|
+
* 越界声明直接从命中列表移除——不解守卫,让内核写保护照常生效(fail-closed)。
|
|
212
|
+
* · args.file_path 目标自身也做 cwd 内校验:目标越界(工作流步骤想写 cwd 外)
|
|
213
|
+
* → 同样不解守卫,让写工具在执行时自己报错(通常是权限/文件不存在提示,
|
|
214
|
+
* 而不是静默放行盲写,双重保险)。
|
|
188
215
|
*/
|
|
189
216
|
async function preReadDeclaredArtifact(
|
|
190
217
|
tool: { name: string; isReadOnly?: boolean },
|
|
@@ -195,15 +222,28 @@ async function preReadDeclaredArtifact(
|
|
|
195
222
|
if (tool.isReadOnly !== false) return; // 只对写类工具生效
|
|
196
223
|
if (typeof args.file_path !== 'string') return;
|
|
197
224
|
|
|
225
|
+
const cwd = effectiveWorkflowCwd(ctx);
|
|
198
226
|
const norm = (p: string): string => {
|
|
199
|
-
const abs = resolve(
|
|
227
|
+
const abs = resolve(cwd, p);
|
|
200
228
|
return process.platform === 'win32' ? abs.toLowerCase() : abs;
|
|
201
229
|
};
|
|
202
|
-
const target = norm(args.file_path);
|
|
203
|
-
const declared = [...(ctx.declaredArtifacts ?? []), ...(step.artifacts ?? [])];
|
|
204
|
-
if (!declared.some((a) => norm(a) === target)) return;
|
|
205
230
|
|
|
206
|
-
|
|
231
|
+
// 1. 目标文件先过 cwd 安全关(越界直接不解守卫)
|
|
232
|
+
const targetRaw = String(args.file_path);
|
|
233
|
+
const targetSafe = safeArtifactPath(cwd, targetRaw);
|
|
234
|
+
if (targetSafe === null) return;
|
|
235
|
+
const target = norm(targetSafe);
|
|
236
|
+
|
|
237
|
+
// 2. 声明产物过 cwd 安全关(过滤掉越界的声明)
|
|
238
|
+
const rawDeclared = [...(ctx.declaredArtifacts ?? []), ...(step.artifacts ?? [])];
|
|
239
|
+
const declaredAbs: string[] = [];
|
|
240
|
+
for (const raw of rawDeclared) {
|
|
241
|
+
const safe = safeArtifactPath(cwd, raw);
|
|
242
|
+
if (safe !== null) declaredAbs.push(norm(safe));
|
|
243
|
+
}
|
|
244
|
+
if (!declaredAbs.some((a) => a === target)) return;
|
|
245
|
+
|
|
246
|
+
const abs = resolve(cwd, targetSafe);
|
|
207
247
|
if (!existsSync(abs)) return; // 新建文件不需要解守卫
|
|
208
248
|
|
|
209
249
|
// 直接登记一次「完整已读」状态解除内核 pre-read 守卫——不真正 Read 内容:
|
|
@@ -214,7 +254,7 @@ async function preReadDeclaredArtifact(
|
|
|
214
254
|
// (首轮新建 PLAN.md 不存在直接放行,审批反馈后回写已存在的大 PLAN.md 被挡死)。
|
|
215
255
|
// 两道守卫(never-read + truncated)必须一并解除,声明产物的整体覆盖才算真正授权。
|
|
216
256
|
// recordRead 内部自带 statSync 失败兜底(返回 1、不抛),无需再包 try/catch。
|
|
217
|
-
recordRead(abs, { truncated: false });
|
|
257
|
+
recordRead(abs, { truncated: false }, ctx.sessionState?.fileReads);
|
|
218
258
|
}
|
|
219
259
|
|
|
220
260
|
/**
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { parseCheckString, runVerification } from '../../../../src/plugin-sdk.js';
|
|
14
14
|
import { interpolate } from '../expressions.js';
|
|
15
|
-
|
|
15
|
+
import { effectiveWorkflowCwd } from '../toolRuntime.js';
|
|
16
|
+
export async function execVerifyStep(step, vars, ctx) {
|
|
16
17
|
// 兼容两种格式:YAML 数组 或逗号分隔字符串。
|
|
17
18
|
// 关键:【先插值再按逗号分割】。checks 可能是 "${verify_checks}" 这类模板,插值后才
|
|
18
19
|
// 展开成逗号分隔的多项(如 "shell:bun test,file_exists:x")。若先分割再插值,整段模板
|
|
@@ -43,8 +44,10 @@ export async function execVerifyStep(step, vars) {
|
|
|
43
44
|
}
|
|
44
45
|
checks.push(check);
|
|
45
46
|
}
|
|
46
|
-
//
|
|
47
|
-
|
|
47
|
+
// 执行所有检查。
|
|
48
|
+
// cwd 必须和写入侧同源:会话进了 session worktree 时产物落在 worktree 里,
|
|
49
|
+
// 验收若锚原 workspace 会在空目录找文件,恒判失败并触发无意义的重试。
|
|
50
|
+
const result = await runVerification(checks, effectiveWorkflowCwd(ctx));
|
|
48
51
|
if (!result.passed) {
|
|
49
52
|
const msg = step.onFail
|
|
50
53
|
? interpolate(step.onFail, vars)
|
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
import { parseCheckString, runVerification, type Check } from '../../../../src/plugin-sdk.js';
|
|
15
15
|
import { interpolate } from '../expressions.js';
|
|
16
|
-
import
|
|
16
|
+
import { effectiveWorkflowCwd } from '../toolRuntime.js';
|
|
17
|
+
import type { RunContext, StepDef, StepResult, VarStack } from '../types.js';
|
|
17
18
|
|
|
18
19
|
export async function execVerifyStep(
|
|
19
20
|
step: StepDef,
|
|
20
21
|
vars: VarStack,
|
|
22
|
+
ctx: RunContext,
|
|
21
23
|
): Promise<StepResult> {
|
|
22
24
|
// 兼容两种格式:YAML 数组 或逗号分隔字符串。
|
|
23
25
|
// 关键:【先插值再按逗号分割】。checks 可能是 "${verify_checks}" 这类模板,插值后才
|
|
@@ -51,8 +53,10 @@ export async function execVerifyStep(
|
|
|
51
53
|
checks.push(check);
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
+
// 执行所有检查。
|
|
57
|
+
// cwd 必须和写入侧同源:会话进了 session worktree 时产物落在 worktree 里,
|
|
58
|
+
// 验收若锚原 workspace 会在空目录找文件,恒判失败并触发无意义的重试。
|
|
59
|
+
const result = await runVerification(checks, effectiveWorkflowCwd(ctx));
|
|
56
60
|
|
|
57
61
|
if (!result.passed) {
|
|
58
62
|
const msg = step.onFail
|