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
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* src/types.ts 的 LoopEvent union **不**包含 workflow_*,框架零插件耦合。
|
|
13
13
|
* ============================================================
|
|
14
14
|
*/
|
|
15
|
+
import { resolve, sep } from 'node:path';
|
|
15
16
|
// ---------------- 2. 变量栈(loop scope 用) ----------------
|
|
16
17
|
/**
|
|
17
18
|
* 简单的多帧变量栈:
|
|
@@ -20,9 +21,21 @@
|
|
|
20
21
|
* - 退出 loop 时 pop()
|
|
21
22
|
*
|
|
22
23
|
* get(key) 从栈顶往下找第一个匹配的 frame,类似 JS 的 lexical scope。
|
|
24
|
+
*
|
|
25
|
+
* ════════════════ 原型污染防护(v2) ════════════════
|
|
26
|
+
* 三层防线,任何一层破了都还有下一层兜底:
|
|
27
|
+
* ① 所有 frame 对象用 `Object.create(null)` 创建 → 根本没有原型访问器,
|
|
28
|
+
* obj["__proto__"] = {...} 不会触发 setter(因为 Object.create(null)
|
|
29
|
+
* 的 [[Prototype]] 是 null,没有 Object.prototype.__proto__ 那个 getter/
|
|
30
|
+
* setter),只会写成 own property。
|
|
31
|
+
* ② set* 写入口统一过滤 DANGEROUS_KEYS(`__proto__/constructor/prototype`)
|
|
32
|
+
* → 即使将来有人改回普通 {} 对象,写操作也到不了原型。
|
|
33
|
+
* ③ 读侧(expressions 点路径访问)另有 S1 黑名防护(parsePrimary L451),
|
|
34
|
+
* 即使值真写进去了也读不出来。
|
|
35
|
+
* ════════════════════════════════════════════════════
|
|
23
36
|
*/
|
|
24
37
|
export class VarStack {
|
|
25
|
-
frames = [
|
|
38
|
+
frames = [Object.create(null)];
|
|
26
39
|
/**
|
|
27
40
|
* 哪些帧是「循环迭代帧」—— 只为装 `as` / `as_idx` 这类**每轮重置**的迭代变量而生。
|
|
28
41
|
*
|
|
@@ -37,12 +50,24 @@ export class VarStack {
|
|
|
37
50
|
* 而不是「它只在这一轮迭代里活着」。
|
|
38
51
|
*/
|
|
39
52
|
iterationFrames = new Set();
|
|
53
|
+
/**
|
|
54
|
+
* 本次运行「当前有效工作目录」的解析器,供 fileExists / readJson 这类**读磁盘**的
|
|
55
|
+
* 内置函数取锚点用。不是 yaml 变量(不进 frames,`${cwd}` 取不到它)。
|
|
56
|
+
*
|
|
57
|
+
* ⚠️ 存的是**函数**不是字符串:worktree 是运行途中才进入的(模型调 EnterWorktree),
|
|
58
|
+
* 创建 VarStack 时固定成字符串的话,之后所有 done_when 仍会锚在旧目录上。
|
|
59
|
+
* 未设置时由调用方回落 getWorkingDir()。
|
|
60
|
+
*/
|
|
61
|
+
resolveCwd;
|
|
40
62
|
/**
|
|
41
63
|
* 写一个**作者命名**的变量(capture / set 步 / 引擎内部状态)。
|
|
42
64
|
*
|
|
43
65
|
* 穿透所有迭代帧,落到最近的非迭代帧上 —— 这样它在循环结束后依然存在。
|
|
66
|
+
* 危险键(__proto__/constructor/prototype)直接忽略(防线②)。
|
|
44
67
|
*/
|
|
45
68
|
set(key, val) {
|
|
69
|
+
if (DANGEROUS_KEYS.has(key))
|
|
70
|
+
return;
|
|
46
71
|
for (let i = this.frames.length - 1; i >= 0; i--) {
|
|
47
72
|
if (!this.iterationFrames.has(i)) {
|
|
48
73
|
this.frames[i][key] = val;
|
|
@@ -51,15 +76,24 @@ export class VarStack {
|
|
|
51
76
|
}
|
|
52
77
|
this.frames[0][key] = val;
|
|
53
78
|
}
|
|
54
|
-
/** 写一个**迭代变量**(`as` / `as_idx`):落在当前迭代帧,随 pop 一起消失。
|
|
79
|
+
/** 写一个**迭代变量**(`as` / `as_idx`):落在当前迭代帧,随 pop 一起消失。
|
|
80
|
+
* 危险键直接忽略(防线②)。 */
|
|
55
81
|
setIteration(key, val) {
|
|
82
|
+
if (DANGEROUS_KEYS.has(key))
|
|
83
|
+
return;
|
|
56
84
|
this.frames[this.frames.length - 1][key] = val;
|
|
57
85
|
}
|
|
58
|
-
/** 写到栈底(root frame),用于 inputs /
|
|
86
|
+
/** 写到栈底(root frame),用于 inputs / 全局常量。危险键直接忽略(防线②)。 */
|
|
59
87
|
setGlobal(key, val) {
|
|
88
|
+
if (DANGEROUS_KEYS.has(key))
|
|
89
|
+
return;
|
|
60
90
|
this.frames[0][key] = val;
|
|
61
91
|
}
|
|
62
92
|
get(key) {
|
|
93
|
+
// 读侧也过危险键:防止 Object.create(null) 上若后续被别的代码写入了 own property
|
|
94
|
+
// 形式的 __proto__,读出来再被其他地方当对象 key 造成二次污染。
|
|
95
|
+
if (DANGEROUS_KEYS.has(key))
|
|
96
|
+
return undefined;
|
|
63
97
|
for (let i = this.frames.length - 1; i >= 0; i--) {
|
|
64
98
|
if (key in this.frames[i])
|
|
65
99
|
return this.frames[i][key];
|
|
@@ -67,6 +101,8 @@ export class VarStack {
|
|
|
67
101
|
return undefined;
|
|
68
102
|
}
|
|
69
103
|
has(key) {
|
|
104
|
+
if (DANGEROUS_KEYS.has(key))
|
|
105
|
+
return false;
|
|
70
106
|
for (let i = this.frames.length - 1; i >= 0; i--) {
|
|
71
107
|
if (key in this.frames[i])
|
|
72
108
|
return true;
|
|
@@ -74,11 +110,11 @@ export class VarStack {
|
|
|
74
110
|
return false;
|
|
75
111
|
}
|
|
76
112
|
push() {
|
|
77
|
-
this.frames.push(
|
|
113
|
+
this.frames.push(Object.create(null));
|
|
78
114
|
}
|
|
79
115
|
/** 压一个迭代帧:只有 `setIteration` 写进来的东西会随 `pop` 消失。 */
|
|
80
116
|
pushIteration() {
|
|
81
|
-
this.frames.push(
|
|
117
|
+
this.frames.push(Object.create(null));
|
|
82
118
|
this.iterationFrames.add(this.frames.length - 1);
|
|
83
119
|
}
|
|
84
120
|
pop() {
|
|
@@ -87,9 +123,18 @@ export class VarStack {
|
|
|
87
123
|
this.frames.pop();
|
|
88
124
|
}
|
|
89
125
|
}
|
|
90
|
-
/** 合并所有帧为一个普通对象(外层覆盖内层),用于 vars
|
|
126
|
+
/** 合并所有帧为一个普通对象(外层覆盖内层),用于 vars 持久化与事件。
|
|
127
|
+
* 危险键不进入快照(双重保险:帧是 Object.create(null),读的时候也过不了 get)。*/
|
|
91
128
|
snapshot() {
|
|
92
|
-
|
|
129
|
+
const out = {};
|
|
130
|
+
for (const frame of this.frames) {
|
|
131
|
+
for (const k of Object.keys(frame)) {
|
|
132
|
+
if (DANGEROUS_KEYS.has(k))
|
|
133
|
+
continue;
|
|
134
|
+
out[k] = frame[k];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
93
138
|
}
|
|
94
139
|
}
|
|
95
140
|
/**
|
|
@@ -109,3 +154,64 @@ export function normalizeCapture(cap) {
|
|
|
109
154
|
return { result: cap };
|
|
110
155
|
return cap;
|
|
111
156
|
}
|
|
157
|
+
// ---------------- 6. 路径安全守卫(declaredArtifacts / 表达式层共用) ----------------
|
|
158
|
+
/**
|
|
159
|
+
* 规范化 + cwd 范围校验的产物路径。
|
|
160
|
+
*
|
|
161
|
+
* 安全背景:declaredArtifacts 来自 YAML 定义,若作者写 "../../.ssh/id_rsa",
|
|
162
|
+
* path.join(workingDir, p) 会跟随 ../ 跳出工作目录,new-task 清理时就可能
|
|
163
|
+
* 删掉工作目录外的文件(唯一能造成真实数据损失的入口)。
|
|
164
|
+
*
|
|
165
|
+
* 行为:
|
|
166
|
+
* - 先用 resolve(workingDir, rawPath) 得到绝对路径(自动归一化 ../)
|
|
167
|
+
* - 再检查它是否严格在 resolve(workingDir) 目录树之内
|
|
168
|
+
* - 命中:返回【相对 workingDir 的规范化相对路径】(./ 前缀去除、\\→/),
|
|
169
|
+
* 保证后续 join(workingDir, result) 一定不会越界
|
|
170
|
+
* - 越界/空串/解析失败:返回 null,调用方应跳过(fail-closed)
|
|
171
|
+
*
|
|
172
|
+
* 跨平台:win32 上 compare 前双方都 normalizeCase(lowercase),其他平台保持大小写敏感。
|
|
173
|
+
*/
|
|
174
|
+
export function safeArtifactPath(workingDir, rawPath) {
|
|
175
|
+
if (!rawPath || typeof rawPath !== 'string')
|
|
176
|
+
return null;
|
|
177
|
+
const base = resolve(workingDir);
|
|
178
|
+
// 必须加路径分隔符作为前缀匹配边界:防止 base=/repo 匹配到 /repo-hack/evil
|
|
179
|
+
const baseWithSep = base.endsWith(sep) ? base : base + sep;
|
|
180
|
+
let abs;
|
|
181
|
+
try {
|
|
182
|
+
abs = resolve(workingDir, rawPath);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const sameCase = (p) => process.platform === 'win32' ? p.toLowerCase() : p;
|
|
188
|
+
const nAbs = sameCase(abs);
|
|
189
|
+
const nBase = sameCase(base);
|
|
190
|
+
const nBaseSep = sameCase(baseWithSep);
|
|
191
|
+
// 恰好等于 base 本身(极端的 rawPath='.' 情形)也视为合法,但 caller 一般
|
|
192
|
+
// 用的是具体文件;这里允许不报错。
|
|
193
|
+
if (nAbs !== nBase && !nAbs.startsWith(nBaseSep))
|
|
194
|
+
return null;
|
|
195
|
+
// 导出【相对 workingDir 的相对路径】(去掉 base 前缀)
|
|
196
|
+
const rel = abs.slice(base.length).replace(/^[/\\]/, '');
|
|
197
|
+
return rel.length === 0 ? '.' : rel;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* 表达式/任意场景的通用「绝对路径是否在 cwd 内」判定。
|
|
201
|
+
* - abs 必须是【已 resolve 后的绝对路径】
|
|
202
|
+
* - 越界/空 → false
|
|
203
|
+
*/
|
|
204
|
+
export function isAbsWithinCwd(workingDir, abs) {
|
|
205
|
+
if (!abs)
|
|
206
|
+
return false;
|
|
207
|
+
const base = resolve(workingDir);
|
|
208
|
+
const baseWithSep = base.endsWith(sep) ? base : base + sep;
|
|
209
|
+
const sameCase = (p) => process.platform === 'win32' ? p.toLowerCase() : p;
|
|
210
|
+
const nAbs = sameCase(abs);
|
|
211
|
+
return nAbs === sameCase(base) || nAbs.startsWith(sameCase(baseWithSep));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 危险键黑名单:VarStack.set* / interpolateDeep 对象 key / 点路径访问统一过滤。
|
|
215
|
+
* 防止原型污染 / 构造函数逃逸。与 expressions.ts parsePrimary S1 白名单保持一致。
|
|
216
|
+
*/
|
|
217
|
+
export const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
@@ -13,12 +13,16 @@
|
|
|
13
13
|
* ============================================================
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { resolve, sep } from 'node:path';
|
|
17
|
+
|
|
16
18
|
import type {
|
|
17
19
|
ImageDetail,
|
|
18
20
|
Message,
|
|
21
|
+
PluginContext,
|
|
19
22
|
Provider,
|
|
20
23
|
SessionState,
|
|
21
24
|
Tool,
|
|
25
|
+
ToolCallContext,
|
|
22
26
|
} from '../../../src/plugin-sdk.js';
|
|
23
27
|
|
|
24
28
|
// ---------------- 1. Workflow / Step 定义 ----------------
|
|
@@ -237,7 +241,7 @@ export interface StepDef {
|
|
|
237
241
|
/** ADR-05: 仅 llm 节点声明的上下文文件清单(skill 节点 loader 拒绝);runner 拼 path+hint 进 prompt 并开放 Read 子循环,不 inline 内容。 */
|
|
238
242
|
context_files?: Array<{ path: string; hint?: string }>;
|
|
239
243
|
|
|
240
|
-
/** ADR-05: agent
|
|
244
|
+
/** ADR-05: agent 节点的工具白名单;未声明时继承宿主过滤后的有效工具集。 */
|
|
241
245
|
allowed_tools?: string[];
|
|
242
246
|
|
|
243
247
|
/**
|
|
@@ -318,9 +322,21 @@ export interface WorkflowDef {
|
|
|
318
322
|
* - 退出 loop 时 pop()
|
|
319
323
|
*
|
|
320
324
|
* get(key) 从栈顶往下找第一个匹配的 frame,类似 JS 的 lexical scope。
|
|
325
|
+
*
|
|
326
|
+
* ════════════════ 原型污染防护(v2) ════════════════
|
|
327
|
+
* 三层防线,任何一层破了都还有下一层兜底:
|
|
328
|
+
* ① 所有 frame 对象用 `Object.create(null)` 创建 → 根本没有原型访问器,
|
|
329
|
+
* obj["__proto__"] = {...} 不会触发 setter(因为 Object.create(null)
|
|
330
|
+
* 的 [[Prototype]] 是 null,没有 Object.prototype.__proto__ 那个 getter/
|
|
331
|
+
* setter),只会写成 own property。
|
|
332
|
+
* ② set* 写入口统一过滤 DANGEROUS_KEYS(`__proto__/constructor/prototype`)
|
|
333
|
+
* → 即使将来有人改回普通 {} 对象,写操作也到不了原型。
|
|
334
|
+
* ③ 读侧(expressions 点路径访问)另有 S1 黑名防护(parsePrimary L451),
|
|
335
|
+
* 即使值真写进去了也读不出来。
|
|
336
|
+
* ════════════════════════════════════════════════════
|
|
321
337
|
*/
|
|
322
338
|
export class VarStack {
|
|
323
|
-
private frames: Record<string, unknown>[] = [
|
|
339
|
+
private frames: Record<string, unknown>[] = [Object.create(null)];
|
|
324
340
|
/**
|
|
325
341
|
* 哪些帧是「循环迭代帧」—— 只为装 `as` / `as_idx` 这类**每轮重置**的迭代变量而生。
|
|
326
342
|
*
|
|
@@ -336,12 +352,24 @@ export class VarStack {
|
|
|
336
352
|
*/
|
|
337
353
|
private iterationFrames = new Set<number>();
|
|
338
354
|
|
|
355
|
+
/**
|
|
356
|
+
* 本次运行「当前有效工作目录」的解析器,供 fileExists / readJson 这类**读磁盘**的
|
|
357
|
+
* 内置函数取锚点用。不是 yaml 变量(不进 frames,`${cwd}` 取不到它)。
|
|
358
|
+
*
|
|
359
|
+
* ⚠️ 存的是**函数**不是字符串:worktree 是运行途中才进入的(模型调 EnterWorktree),
|
|
360
|
+
* 创建 VarStack 时固定成字符串的话,之后所有 done_when 仍会锚在旧目录上。
|
|
361
|
+
* 未设置时由调用方回落 getWorkingDir()。
|
|
362
|
+
*/
|
|
363
|
+
resolveCwd?: () => string;
|
|
364
|
+
|
|
339
365
|
/**
|
|
340
366
|
* 写一个**作者命名**的变量(capture / set 步 / 引擎内部状态)。
|
|
341
367
|
*
|
|
342
368
|
* 穿透所有迭代帧,落到最近的非迭代帧上 —— 这样它在循环结束后依然存在。
|
|
369
|
+
* 危险键(__proto__/constructor/prototype)直接忽略(防线②)。
|
|
343
370
|
*/
|
|
344
371
|
set(key: string, val: unknown): void {
|
|
372
|
+
if (DANGEROUS_KEYS.has(key)) return;
|
|
345
373
|
for (let i = this.frames.length - 1; i >= 0; i--) {
|
|
346
374
|
if (!this.iterationFrames.has(i)) {
|
|
347
375
|
this.frames[i][key] = val;
|
|
@@ -351,17 +379,23 @@ export class VarStack {
|
|
|
351
379
|
this.frames[0][key] = val;
|
|
352
380
|
}
|
|
353
381
|
|
|
354
|
-
/** 写一个**迭代变量**(`as` / `as_idx`):落在当前迭代帧,随 pop 一起消失。
|
|
382
|
+
/** 写一个**迭代变量**(`as` / `as_idx`):落在当前迭代帧,随 pop 一起消失。
|
|
383
|
+
* 危险键直接忽略(防线②)。 */
|
|
355
384
|
setIteration(key: string, val: unknown): void {
|
|
385
|
+
if (DANGEROUS_KEYS.has(key)) return;
|
|
356
386
|
this.frames[this.frames.length - 1][key] = val;
|
|
357
387
|
}
|
|
358
388
|
|
|
359
|
-
/** 写到栈底(root frame),用于 inputs /
|
|
389
|
+
/** 写到栈底(root frame),用于 inputs / 全局常量。危险键直接忽略(防线②)。 */
|
|
360
390
|
setGlobal(key: string, val: unknown): void {
|
|
391
|
+
if (DANGEROUS_KEYS.has(key)) return;
|
|
361
392
|
this.frames[0][key] = val;
|
|
362
393
|
}
|
|
363
394
|
|
|
364
395
|
get(key: string): unknown {
|
|
396
|
+
// 读侧也过危险键:防止 Object.create(null) 上若后续被别的代码写入了 own property
|
|
397
|
+
// 形式的 __proto__,读出来再被其他地方当对象 key 造成二次污染。
|
|
398
|
+
if (DANGEROUS_KEYS.has(key)) return undefined;
|
|
365
399
|
for (let i = this.frames.length - 1; i >= 0; i--) {
|
|
366
400
|
if (key in this.frames[i]) return this.frames[i][key];
|
|
367
401
|
}
|
|
@@ -369,6 +403,7 @@ export class VarStack {
|
|
|
369
403
|
}
|
|
370
404
|
|
|
371
405
|
has(key: string): boolean {
|
|
406
|
+
if (DANGEROUS_KEYS.has(key)) return false;
|
|
372
407
|
for (let i = this.frames.length - 1; i >= 0; i--) {
|
|
373
408
|
if (key in this.frames[i]) return true;
|
|
374
409
|
}
|
|
@@ -376,12 +411,12 @@ export class VarStack {
|
|
|
376
411
|
}
|
|
377
412
|
|
|
378
413
|
push(): void {
|
|
379
|
-
this.frames.push(
|
|
414
|
+
this.frames.push(Object.create(null));
|
|
380
415
|
}
|
|
381
416
|
|
|
382
417
|
/** 压一个迭代帧:只有 `setIteration` 写进来的东西会随 `pop` 消失。 */
|
|
383
418
|
pushIteration(): void {
|
|
384
|
-
this.frames.push(
|
|
419
|
+
this.frames.push(Object.create(null));
|
|
385
420
|
this.iterationFrames.add(this.frames.length - 1);
|
|
386
421
|
}
|
|
387
422
|
|
|
@@ -392,9 +427,17 @@ export class VarStack {
|
|
|
392
427
|
}
|
|
393
428
|
}
|
|
394
429
|
|
|
395
|
-
/** 合并所有帧为一个普通对象(外层覆盖内层),用于 vars
|
|
430
|
+
/** 合并所有帧为一个普通对象(外层覆盖内层),用于 vars 持久化与事件。
|
|
431
|
+
* 危险键不进入快照(双重保险:帧是 Object.create(null),读的时候也过不了 get)。*/
|
|
396
432
|
snapshot(): Record<string, unknown> {
|
|
397
|
-
|
|
433
|
+
const out: Record<string, unknown> = {};
|
|
434
|
+
for (const frame of this.frames) {
|
|
435
|
+
for (const k of Object.keys(frame)) {
|
|
436
|
+
if (DANGEROUS_KEYS.has(k)) continue;
|
|
437
|
+
out[k] = frame[k];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return out;
|
|
398
441
|
}
|
|
399
442
|
}
|
|
400
443
|
|
|
@@ -437,10 +480,9 @@ export interface RunContext {
|
|
|
437
480
|
cwd: string;
|
|
438
481
|
/**
|
|
439
482
|
* 可用工具列表。
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
* 以及测试兼容。生产路径由 plugin.ts 注入 ALL_TOOLS。
|
|
483
|
+
* agent/llm/skill 与确定性 tool 步都以此列表为宿主授权上限。
|
|
484
|
+
* plugin.ts 先按 runtime 和父宿主白名单过滤,再把同一列表交给提示词、
|
|
485
|
+
* Schema 和真实执行,任何工作流节点都不能从 ALL_TOOLS 恢复隐藏能力。
|
|
444
486
|
*/
|
|
445
487
|
tools: Tool[];
|
|
446
488
|
transientContext?: string;
|
|
@@ -494,11 +536,29 @@ export interface RunContext {
|
|
|
494
536
|
tools: import('../../../src/plugin-sdk.js').Tool[];
|
|
495
537
|
signal?: AbortSignal;
|
|
496
538
|
promptCache?: import('../../../src/plugin-sdk.js').PromptCacheContext;
|
|
497
|
-
}) => AsyncGenerator<
|
|
539
|
+
}) => AsyncGenerator<
|
|
540
|
+
import('../../../src/plugin-sdk.js').LlmStreamEvent,
|
|
541
|
+
void,
|
|
542
|
+
undefined
|
|
543
|
+
>;
|
|
498
544
|
/**
|
|
499
545
|
* DI:executeTool 实现(仅测试用)。同上。
|
|
500
546
|
*/
|
|
501
|
-
executeTool?:
|
|
547
|
+
executeTool?: PluginContext['executeTool'];
|
|
548
|
+
/**
|
|
549
|
+
* Canonical non-model-visible context shared by deterministic tool nodes.
|
|
550
|
+
* Browser/computer capabilities are deliberately excluded here and are only
|
|
551
|
+
* added by the step-level `uses` slicer.
|
|
552
|
+
*/
|
|
553
|
+
toolContext?: Omit<
|
|
554
|
+
ToolCallContext,
|
|
555
|
+
| 'browserRun'
|
|
556
|
+
| 'computerRun'
|
|
557
|
+
| 'computerIntervention'
|
|
558
|
+
| 'reportProgress'
|
|
559
|
+
| 'toolCallId'
|
|
560
|
+
| 'cwd'
|
|
561
|
+
>;
|
|
502
562
|
/**
|
|
503
563
|
* 会话状态(驱动 microCompact / reactiveCompact)。
|
|
504
564
|
*
|
|
@@ -581,3 +641,66 @@ export type WorkflowEvent =
|
|
|
581
641
|
}
|
|
582
642
|
| { type: 'workflow_step_skipped'; id: string; reason: string }
|
|
583
643
|
| { type: 'workflow_done'; name: string; vars: Record<string, unknown> };
|
|
644
|
+
|
|
645
|
+
// ---------------- 6. 路径安全守卫(declaredArtifacts / 表达式层共用) ----------------
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* 规范化 + cwd 范围校验的产物路径。
|
|
649
|
+
*
|
|
650
|
+
* 安全背景:declaredArtifacts 来自 YAML 定义,若作者写 "../../.ssh/id_rsa",
|
|
651
|
+
* path.join(workingDir, p) 会跟随 ../ 跳出工作目录,new-task 清理时就可能
|
|
652
|
+
* 删掉工作目录外的文件(唯一能造成真实数据损失的入口)。
|
|
653
|
+
*
|
|
654
|
+
* 行为:
|
|
655
|
+
* - 先用 resolve(workingDir, rawPath) 得到绝对路径(自动归一化 ../)
|
|
656
|
+
* - 再检查它是否严格在 resolve(workingDir) 目录树之内
|
|
657
|
+
* - 命中:返回【相对 workingDir 的规范化相对路径】(./ 前缀去除、\\→/),
|
|
658
|
+
* 保证后续 join(workingDir, result) 一定不会越界
|
|
659
|
+
* - 越界/空串/解析失败:返回 null,调用方应跳过(fail-closed)
|
|
660
|
+
*
|
|
661
|
+
* 跨平台:win32 上 compare 前双方都 normalizeCase(lowercase),其他平台保持大小写敏感。
|
|
662
|
+
*/
|
|
663
|
+
export function safeArtifactPath(workingDir: string, rawPath: string): string | null {
|
|
664
|
+
if (!rawPath || typeof rawPath !== 'string') return null;
|
|
665
|
+
const base = resolve(workingDir);
|
|
666
|
+
// 必须加路径分隔符作为前缀匹配边界:防止 base=/repo 匹配到 /repo-hack/evil
|
|
667
|
+
const baseWithSep = base.endsWith(sep) ? base : base + sep;
|
|
668
|
+
let abs: string;
|
|
669
|
+
try {
|
|
670
|
+
abs = resolve(workingDir, rawPath);
|
|
671
|
+
} catch {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
const sameCase = (p: string): string =>
|
|
675
|
+
process.platform === 'win32' ? p.toLowerCase() : p;
|
|
676
|
+
const nAbs = sameCase(abs);
|
|
677
|
+
const nBase = sameCase(base);
|
|
678
|
+
const nBaseSep = sameCase(baseWithSep);
|
|
679
|
+
// 恰好等于 base 本身(极端的 rawPath='.' 情形)也视为合法,但 caller 一般
|
|
680
|
+
// 用的是具体文件;这里允许不报错。
|
|
681
|
+
if (nAbs !== nBase && !nAbs.startsWith(nBaseSep)) return null;
|
|
682
|
+
// 导出【相对 workingDir 的相对路径】(去掉 base 前缀)
|
|
683
|
+
const rel = abs.slice(base.length).replace(/^[/\\]/, '');
|
|
684
|
+
return rel.length === 0 ? '.' : rel;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* 表达式/任意场景的通用「绝对路径是否在 cwd 内」判定。
|
|
689
|
+
* - abs 必须是【已 resolve 后的绝对路径】
|
|
690
|
+
* - 越界/空 → false
|
|
691
|
+
*/
|
|
692
|
+
export function isAbsWithinCwd(workingDir: string, abs: string): boolean {
|
|
693
|
+
if (!abs) return false;
|
|
694
|
+
const base = resolve(workingDir);
|
|
695
|
+
const baseWithSep = base.endsWith(sep) ? base : base + sep;
|
|
696
|
+
const sameCase = (p: string): string =>
|
|
697
|
+
process.platform === 'win32' ? p.toLowerCase() : p;
|
|
698
|
+
const nAbs = sameCase(abs);
|
|
699
|
+
return nAbs === sameCase(base) || nAbs.startsWith(sameCase(baseWithSep));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* 危险键黑名单:VarStack.set* / interpolateDeep 对象 key / 点路径访问统一过滤。
|
|
704
|
+
* 防止原型污染 / 构造函数逃逸。与 expressions.ts parsePrimary S1 白名单保持一致。
|
|
705
|
+
*/
|
|
706
|
+
export const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|