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.
Files changed (181) hide show
  1. package/package.json +5 -1
  2. package/plugins/workflow-runner/plugin.js +27 -15
  3. package/plugins/workflow-runner/plugin.ts +34 -20
  4. package/plugins/workflow-runner/src/agentCommon.js +13 -12
  5. package/plugins/workflow-runner/src/agentCommon.ts +22 -13
  6. package/plugins/workflow-runner/src/expressions.js +29 -8
  7. package/plugins/workflow-runner/src/expressions.ts +28 -10
  8. package/plugins/workflow-runner/src/index.js +50 -20
  9. package/plugins/workflow-runner/src/index.ts +75 -31
  10. package/plugins/workflow-runner/src/multimodal.js +2 -1
  11. package/plugins/workflow-runner/src/multimodal.ts +2 -1
  12. package/plugins/workflow-runner/src/recovery.js +9 -2
  13. package/plugins/workflow-runner/src/recovery.ts +9 -2
  14. package/plugins/workflow-runner/src/runner.js +31 -5
  15. package/plugins/workflow-runner/src/runner.ts +33 -5
  16. package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
  17. package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -1
  18. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  19. package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
  20. package/plugins/workflow-runner/src/stepExecutors/tool.js +91 -61
  21. package/plugins/workflow-runner/src/stepExecutors/tool.ts +111 -71
  22. package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
  23. package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
  24. package/plugins/workflow-runner/src/taskIdentity.js +197 -21
  25. package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
  26. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  27. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  28. package/plugins/workflow-runner/src/types.js +113 -7
  29. package/plugins/workflow-runner/src/types.ts +137 -14
  30. package/plugins/workflow-runner/src/workflowState.js +455 -54
  31. package/plugins/workflow-runner/src/workflowState.ts +508 -63
  32. package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
  33. package/plugins/workflow-runner/test/context-files.test.ts +47 -16
  34. package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
  35. package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
  36. package/plugins/workflow-runner/test/expressions.test.ts +42 -0
  37. package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
  38. package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
  39. package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
  40. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
  41. package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
  42. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
  43. package/plugins/workflow-runner/test/research.load.test.ts +328 -0
  44. package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
  45. package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
  46. package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
  47. package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
  48. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  49. package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
  50. package/plugins/workflow-runner/test/word.load.test.ts +9 -9
  51. package/skills/skill-creator/scripts/run_eval.py +3 -2
  52. package/src/cli/print.js +36 -4
  53. package/src/cli/print.ts +42 -4
  54. package/src/cli/streamJson.js +19 -4
  55. package/src/cli/streamJson.ts +17 -4
  56. package/src/config/configDefaults.js +25 -0
  57. package/src/config/configDefaults.ts +33 -0
  58. package/src/config/configFile.js +2 -21
  59. package/src/config/configFile.ts +11 -24
  60. package/src/config/configManagement.js +1 -1
  61. package/src/config/configManagement.ts +2 -2
  62. package/src/config/configService.js +24 -0
  63. package/src/config/configService.ts +23 -0
  64. package/src/context/compact.js +4 -0
  65. package/src/context/compact.ts +4 -0
  66. package/src/context/contextBundle.js +38 -13
  67. package/src/context/contextBundle.ts +47 -13
  68. package/src/harness/stallGuard.js +19 -4
  69. package/src/harness/stallGuard.ts +30 -7
  70. package/src/infra/verification.js +25 -18
  71. package/src/infra/verification.ts +50 -18
  72. package/src/loop.js +95 -11
  73. package/src/loop.ts +125 -16
  74. package/src/main.js +8 -1
  75. package/src/main.tsx +7 -1
  76. package/src/memory/constants.js +10 -0
  77. package/src/memory/constants.ts +13 -0
  78. package/src/memory/episodicMemory.js +274 -2
  79. package/src/memory/episodicMemory.ts +306 -1
  80. package/src/memory/fileStore.js +24 -4
  81. package/src/memory/fileStore.ts +22 -2
  82. package/src/memory/memoryService.js +79 -12
  83. package/src/memory/memoryService.ts +108 -9
  84. package/src/memory/repository.js +415 -85
  85. package/src/memory/repository.ts +437 -86
  86. package/src/memory/turnJournal.js +95 -0
  87. package/src/memory/turnJournal.ts +120 -0
  88. package/src/memory/types.ts +16 -0
  89. package/src/plugins/pluginApi.ts +30 -6
  90. package/src/plugins/pluginRunner.js +23 -8
  91. package/src/plugins/pluginRunner.ts +32 -13
  92. package/src/prompts/policyRegistry.js +55 -1
  93. package/src/prompts/policyRegistry.ts +55 -1
  94. package/src/prompts/promptAssembly.js +39 -7
  95. package/src/prompts/promptAssembly.ts +145 -59
  96. package/src/session/runAgentTurn.js +43 -21
  97. package/src/session/runAgentTurn.ts +68 -26
  98. package/src/session/runTurn.js +11 -46
  99. package/src/session/runTurn.ts +12 -46
  100. package/src/session/runtimeStore.js +149 -0
  101. package/src/session/runtimeStore.ts +197 -0
  102. package/src/session/sessionState.js +5 -0
  103. package/src/session/sessionState.ts +14 -0
  104. package/src/tools/advisor/advisor.js +76 -9
  105. package/src/tools/advisor/advisor.ts +79 -9
  106. package/src/tools/agent/agent.js +143 -14
  107. package/src/tools/agent/agent.ts +149 -14
  108. package/src/tools/agent/agentTaskService.js +60 -0
  109. package/src/tools/agent/agentTaskService.ts +113 -0
  110. package/src/tools/agent/agentTypes.js +5 -0
  111. package/src/tools/agent/agentTypes.ts +5 -0
  112. package/src/tools/bash/bash.js +80 -15
  113. package/src/tools/bash/bash.ts +83 -15
  114. package/src/tools/capabilities.js +5 -0
  115. package/src/tools/capabilities.ts +11 -1
  116. package/src/tools/edit/edit.js +7 -4
  117. package/src/tools/edit/edit.ts +7 -4
  118. package/src/tools/edit/multi-edit.js +4 -2
  119. package/src/tools/edit/multi-edit.ts +4 -2
  120. package/src/tools/glob/glob.js +156 -40
  121. package/src/tools/glob/glob.ts +185 -46
  122. package/src/tools/index.js +35 -17
  123. package/src/tools/index.ts +81 -20
  124. package/src/tools/lsp/lsp.js +165 -22
  125. package/src/tools/lsp/lsp.ts +206 -36
  126. package/src/tools/powershell/powershell.js +134 -27
  127. package/src/tools/powershell/powershell.ts +171 -30
  128. package/src/tools/presentresult/presentresult.js +10 -0
  129. package/src/tools/presentresult/presentresult.ts +10 -0
  130. package/src/tools/read/read.js +249 -29
  131. package/src/tools/read/read.ts +396 -59
  132. package/src/tools/shared/diffSummary.js +44 -0
  133. package/src/tools/shared/diffSummary.ts +63 -0
  134. package/src/tools/shared/fileActivity.js +2 -1
  135. package/src/tools/shared/fileActivity.ts +3 -0
  136. package/src/tools/shared/permanentDeleteGuard.js +57 -0
  137. package/src/tools/shared/permanentDeleteGuard.ts +71 -0
  138. package/src/tools/shared/toolOutputStore.js +97 -0
  139. package/src/tools/shared/toolOutputStore.ts +110 -0
  140. package/src/tools/skill/skill.js +86 -0
  141. package/src/tools/skill/skill.ts +94 -0
  142. package/src/tools/snip/snip.js +67 -0
  143. package/src/tools/snip/snip.ts +80 -0
  144. package/src/tools/tasks/backgroundTaskService.js +79 -28
  145. package/src/tools/tasks/backgroundTaskService.ts +165 -49
  146. package/src/tools/tasks/taskManagement.js +163 -0
  147. package/src/tools/tasks/taskManagement.ts +180 -0
  148. package/src/tools/tasks/taskOutput.js +64 -14
  149. package/src/tools/tasks/taskOutput.ts +102 -23
  150. package/src/tools/tasks/taskStop.js +48 -8
  151. package/src/tools/tasks/taskStop.ts +69 -9
  152. package/src/tools/tasks/taskStore.js +46 -0
  153. package/src/tools/tasks/taskStore.ts +77 -0
  154. package/src/tools/terminal/terminalCapture.js +167 -0
  155. package/src/tools/terminal/terminalCapture.ts +179 -0
  156. package/src/tools/trash/trash.js +233 -0
  157. package/src/tools/trash/trash.ts +283 -0
  158. package/src/tools/webbrowser/browserControl.js +5 -1
  159. package/src/tools/webbrowser/browserControl.ts +7 -1
  160. package/src/tools/webbrowser/webbrowser.js +8 -4
  161. package/src/tools/webbrowser/webbrowser.ts +8 -4
  162. package/src/tools/worktree/sessionWorktree.js +128 -0
  163. package/src/tools/worktree/sessionWorktree.ts +158 -0
  164. package/src/tools/write/write.js +6 -2
  165. package/src/tools/write/write.ts +6 -2
  166. package/src/types.ts +22 -4
  167. package/src/ui/hooks/useChat.js +31 -11
  168. package/src/ui/hooks/useChat.ts +31 -13
  169. package/src/web/protocol.ts +78 -3
  170. package/workflows/adaptive-research.yaml +5 -5
  171. package/workflows/book-review-short.yaml +5 -5
  172. package/workflows/e2e-write-greet.yaml +2 -2
  173. package/workflows/illustrate.yaml +12 -12
  174. package/workflows/plan.yaml +18 -18
  175. package/workflows/ppt.yaml +201 -201
  176. package/workflows/ralph-loop.yaml +3 -1
  177. package/workflows/research.md +449 -0
  178. package/workflows/research.yaml +873 -0
  179. package/workflows/webppt.yaml +47 -47
  180. package/workflows/word.yaml +80 -80
  181. 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
- return Object.assign({}, ...this.frames);
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 节点的工具白名单;未声明时默认全量 ALL_TOOLS。 */
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
- return Object.assign({}, ...this.frames);
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
- * v0.9 重构后:agent/llm/skill 步委托 runQuery(TAOR) 执行,
441
- * runQuery 内部使用自己的 ALL_TOOLS 全量工具列表。
442
- * 此字段保留给 runner 内部确定性步(tool 步)的 executeTool 调度使用,
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<import('../../../src/plugin-sdk.js').LlmStreamEvent, void, undefined>;
539
+ }) => AsyncGenerator<
540
+ import('../../../src/plugin-sdk.js').LlmStreamEvent,
541
+ void,
542
+ undefined
543
+ >;
498
544
  /**
499
545
  * DI:executeTool 实现(仅测试用)。同上。
500
546
  */
501
- executeTool?: (name: string, rawArgsJson: string, signal?: AbortSignal) => Promise<import('../../../src/plugin-sdk.js').ToolResult>;
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']);