evolcore 0.0.28 → 0.0.29

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 (183) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +19 -9
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +6 -2
  7. package/dist/agents/codex/diagnostics.js +4 -3
  8. package/dist/agents/codex-runner.js +396 -95
  9. package/dist/agents/gemini-runner.js +35 -17
  10. package/dist/aun/group/history.js +130 -90
  11. package/dist/aun/group/operations.js +1 -3
  12. package/dist/aun/menu/transport.js +5 -3
  13. package/dist/aun/msg/managed-operation.js +70 -0
  14. package/dist/aun/msg/p2p.js +1 -3
  15. package/dist/aun/msg/upload.js +2 -2
  16. package/dist/aun/outbox.js +1 -37
  17. package/dist/channels/aun.js +249 -89
  18. package/dist/channels/daemon.js +18 -1
  19. package/dist/channels/feishu.js +1 -2
  20. package/dist/cli/agent-command.js +23 -3
  21. package/dist/cli/agent-create.js +68 -2
  22. package/dist/cli/agent.js +12 -2
  23. package/dist/cli/aun-commands.js +87 -13
  24. package/dist/cli/cli-argv.js +26 -0
  25. package/dist/cli/daemon-commands.js +40 -10
  26. package/dist/cli/data-command.js +4 -4
  27. package/dist/cli/handoff-command.js +2 -0
  28. package/dist/cli/index.js +18 -1
  29. package/dist/cli/managed-command-guard.js +10 -0
  30. package/dist/cli/memory-command.js +441 -0
  31. package/dist/cli/restart-monitor.js +62 -6
  32. package/dist/cli/skill-command.js +41 -0
  33. package/dist/cli/task-context.js +2 -23
  34. package/dist/cli/trigger-command.js +9 -1
  35. package/dist/cli/watch-msg-diag.js +209 -0
  36. package/dist/config/config-field-policy.js +52 -3
  37. package/dist/config/config-manager.js +17 -4
  38. package/dist/config/contact-book-store.js +2 -2
  39. package/dist/config/mention-mode.js +7 -2
  40. package/dist/config/merge.js +28 -9
  41. package/dist/config/resolved-config-op.js +1 -1
  42. package/dist/config/runtime-env.js +137 -0
  43. package/dist/config/schema-registry.js +11 -1
  44. package/dist/config-store.js +10 -10
  45. package/dist/core/audit/event-key.js +3 -1
  46. package/dist/core/audit/log-integrity.js +4 -0
  47. package/dist/core/auth/agent-delegation.js +2 -1
  48. package/dist/core/auth/auth-gateway.js +13 -6
  49. package/dist/core/auth/authorization-audit.js +54 -19
  50. package/dist/core/auth/operation-authorizer.js +17 -0
  51. package/dist/core/auth/operation-catalog.js +9 -0
  52. package/dist/core/causation/context.js +5 -1
  53. package/dist/core/command/agent-control.js +1 -2
  54. package/dist/core/command/command-handler.js +51 -23
  55. package/dist/core/command/memory-slash.js +270 -0
  56. package/dist/core/command/menu-catalog.js +1 -0
  57. package/dist/core/command/menu-handler.js +315 -60
  58. package/dist/core/command/menu-protocol.js +42 -4
  59. package/dist/core/command/slash-gate.js +10 -10
  60. package/dist/core/command/slash-handler.js +206 -64
  61. package/dist/core/data-migration.js +282 -505
  62. package/dist/core/event-catalog.js +26 -0
  63. package/dist/core/evolagent-registry.js +6 -0
  64. package/dist/core/evolagent.js +0 -10
  65. package/dist/core/handoff/store.js +1 -20
  66. package/dist/core/inference/text-inference.js +57 -1
  67. package/dist/core/memory/consolidation-context.js +505 -0
  68. package/dist/core/memory/consolidation-ops-records.js +124 -0
  69. package/dist/core/memory/consolidation-trigger.js +394 -0
  70. package/dist/core/memory/index-manager.js +308 -0
  71. package/dist/core/memory/launch-consolidation.js +395 -0
  72. package/dist/core/memory/memory-admin.js +234 -0
  73. package/dist/core/memory/memory-document.js +100 -0
  74. package/dist/core/memory/memory-session-policy.js +41 -0
  75. package/dist/core/memory/memory-signature.js +257 -0
  76. package/dist/core/memory/memory-writer.js +432 -0
  77. package/dist/core/memory/staleness.js +117 -0
  78. package/dist/core/message/auto-mention-gate.js +454 -0
  79. package/dist/core/message/im-renderer.js +69 -16
  80. package/dist/core/message/message-bridge.js +287 -21
  81. package/dist/core/message/message-log.js +1 -0
  82. package/dist/core/message/message-queue.js +114 -27
  83. package/dist/core/message/pending-hints.js +329 -161
  84. package/dist/core/message/response-engine.js +567 -80
  85. package/dist/core/permission/ec-command-parser.js +1029 -80
  86. package/dist/core/permission/protected-paths.js +376 -126
  87. package/dist/core/permission/sandbox-runtime.js +40 -12
  88. package/dist/core/permission/tool-error-code.js +96 -13
  89. package/dist/core/permission/tool-policy.js +276 -23
  90. package/dist/core/role/runtime-policy.js +3 -7
  91. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  92. package/dist/core/session/session-fs-store.js +0 -76
  93. package/dist/core/session/session-idle-monitor.js +355 -0
  94. package/dist/core/session/session-manager.js +478 -17
  95. package/dist/core/session/session-mapper.js +33 -16
  96. package/dist/core/session/session-renew.js +217 -155
  97. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  98. package/dist/core/skill/global-watcher.js +282 -0
  99. package/dist/core/skill/index-generator.js +84 -0
  100. package/dist/core/skill/startup.js +73 -0
  101. package/dist/core/skill/switch-resolver.js +115 -0
  102. package/dist/core/task/task-activity-store.js +219 -0
  103. package/dist/eck/kit-renderer.js +30 -3
  104. package/dist/eck/manifest-engine.js +26 -4
  105. package/dist/index.js +708 -18
  106. package/dist/ipc.js +564 -2
  107. package/dist/paths.js +30 -9
  108. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  109. package/dist/response-system/modes/single-session/index.js +3 -0
  110. package/dist/trigger/feedback.js +1 -0
  111. package/dist/trigger/manager.js +0 -10
  112. package/dist/trigger/scheduler.js +11 -0
  113. package/dist/trigger/validation.js +22 -0
  114. package/dist/utils/avatar-upload.js +59 -18
  115. package/dist/utils/error-dict.json +6 -3
  116. package/dist/utils/error-utils.js +27 -0
  117. package/dist/utils/logger.js +32 -10
  118. package/dist/utils/media-cache.js +84 -0
  119. package/dist/utils/msg-diag.js +97 -0
  120. package/dist/utils/welcome.js +37 -8
  121. package/kits/docs/AUTHORING.md +314 -0
  122. package/kits/docs/GUIDE.md +2 -0
  123. package/kits/docs/INDEX.md +2 -0
  124. package/kits/docs/aun/CHEATSHEET.md +4 -0
  125. package/kits/docs/aun/SDK.md +60 -0
  126. package/kits/docs/context-assembly.md +10 -6
  127. package/kits/docs/evolcore/INDEX.md +17 -3
  128. package/kits/docs/evolcore/agent-create.md +24 -0
  129. package/kits/docs/evolcore/agent.md +5 -2
  130. package/kits/docs/evolcore/config.md +1 -1
  131. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  132. package/kits/docs/evolcore/group.md +34 -18
  133. package/kits/docs/evolcore/memory-architecture.md +78 -0
  134. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  135. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  136. package/kits/docs/evolcore/memory-tags.md +63 -0
  137. package/kits/docs/evolcore/memory.md +110 -0
  138. package/kits/docs/evolcore/menu-client.md +27 -11
  139. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  140. package/kits/docs/evolcore/menu-protocol.md +213 -0
  141. package/kits/docs/evolcore/msg.md +2 -0
  142. package/kits/docs/evolcore/trigger.md +7 -5
  143. package/kits/docs/path-registry.md +6 -8
  144. package/kits/docs/prompt-loading-architecture.md +5 -3
  145. package/kits/docs/venues/group.md +5 -4
  146. package/kits/eck_manifest.json +294 -88
  147. package/kits/eck_manifest.memory.json +257 -0
  148. package/kits/rules/03-identity.md +8 -8
  149. package/kits/rules/05-venue.md +4 -2
  150. package/kits/rules/06-channel.md +1 -1
  151. package/kits/schemas/_meta.json +276 -47
  152. package/kits/schemas/agent-config.schema.12.json +38 -2
  153. package/kits/schemas/agent-config.schema.13.json +591 -0
  154. package/kits/schemas/daemon.schema.6.json +9 -9
  155. package/kits/schemas/defaults.schema.5.json +29 -0
  156. package/kits/schemas/relation-config.schema.8.json +14 -1
  157. package/kits/schemas/relation-config.schema.9.json +129 -0
  158. package/kits/schemas/role-config.schema.1.json +3 -3
  159. package/kits/skills/.switches.json +3 -0
  160. package/kits/skills/ec-skill/SKILL.md +217 -0
  161. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  162. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  163. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  164. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  165. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  166. package/kits/templates/decision/auto-mention.md +25 -0
  167. package/kits/templates/decision/session-renew.md +22 -0
  168. package/kits/templates/memory-consolidation/forget.md +104 -0
  169. package/kits/templates/memory-consolidation/generalize.md +76 -0
  170. package/kits/templates/memory-consolidation/notes.md +133 -0
  171. package/kits/templates/memory-consolidation/profile.md +58 -0
  172. package/kits/templates/memory-consolidation/promote.md +72 -0
  173. package/kits/templates/memory-consolidation/workflow.md +75 -0
  174. package/kits/templates/system-fragments/bootstrap.md +14 -5
  175. package/kits/templates/system-fragments/commands.md +1 -0
  176. package/kits/templates/system-fragments/group-history.md +6 -0
  177. package/kits/templates/system-fragments/identity.md +0 -3
  178. package/kits/templates/system-fragments/memory.md +79 -0
  179. package/kits/templates/system-fragments/reply.md +1 -0
  180. package/kits/templates/system-fragments/venue.md +1 -1
  181. package/package.json +4 -6
  182. package/dist/trigger/legacy-session-history.js +0 -19
  183. package/kits/templates/system-fragments/group-unread-history.md +0 -7
package/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.0.29 (2026-10-08)
9
+
10
+ - 完成记忆系统 v3:支持 note 标签、三作用域工作记忆、自动整理触发、整理队列与签名索引,并完善 `ec memory`、`/memory` 命令和 ECK 注入。
11
+ - 新增 EC Skill 开关、索引生成与变更监控;补充托管 CLI、依赖和 frontmatter 工具链。
12
+ - 强化 Shell 与工具安全:安全输出 payload/过滤器链、受管 `cd` 与临时目录边界、禁止通过 Shell 间接调用 `apply_patch`,并统一工具错误分类、审计元数据及 Agent 作用域/sandbox 路径边界。
13
+ - 改进 Codex 话题与会话分支:支持按消息锚点回退、持久化历史快照、并发安全的 rewind/resumeAt 管理;持久化任务 Activity,支持会话闲置事件触发 Trigger,并完善闲置监视和恢复诊断。
14
+ - 增强 AUN 群聊和主动响应:支持可配置的自动提及决策模型与主会话自动判断、`ec group history/mention`、观察者悄悄话失败重试与恢复,以及 Menu 协议分页拉取、TTL/设备定向投递和权限校验;升级 AUN SDK 至 `0.5.18`,将文件上传/拉取上限提升至 256MiB 并改进存储兼容性。
15
+ - 完善权限与运行稳定性:统一 Owner 鉴权,移除 daemon fullaccess 总开关,重构 Agent 数据迁移与重启预检,补充失败归档/恢复校验、重启失败自动修复配置和 Windows PowerShell 委托兼容。
16
+ - 改进 Base Agent 运行兼容性:支持 Windows 随包探测 Claude 可执行文件,增强 Codex app-server 会话、分页回退、凭据隔离和工具错误分类,并收紧 Gemini/Codex 的 Agent 作用域保护。
17
+ - Agent 创建与初始化支持本地文件或 HTTPS 地址头像上传,完善头像校验、托管任务附件来源和失败恢复流程。
18
+ - 新增消息生命周期诊断,支持从 SDK 入站到 AUN 出站的阶段追踪与实时查看;统一记忆文档块解析,并迁移整理批次目录和操作记录格式。
19
+
8
20
  ## 0.0.28 (2026-09-24)
9
21
 
10
22
  - 增加 npm 版本缓存,改进升级状态展示及启动、重启流程。
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
- import { spawn } from 'child_process';
4
+ import { spawn, spawnSync } from 'child_process';
5
5
  import { fileURLToPath } from 'url';
6
6
 
7
7
  const MAX_OUTPUT_BYTES = 1024 * 1024;
@@ -9,11 +9,102 @@ const EC_EXECUTABLES = new Set(['ec']);
9
9
  const HOST_PROCESS_EXECUTABLES = new Set(['ps', 'pgrep', 'pidof', 'kill', 'pkill', 'killall']);
10
10
  const IS_WINDOWS = process.platform === 'win32';
11
11
  const HELPER_DIR = path.dirname(fileURLToPath(import.meta.url));
12
- // A rooted POSIX path is not absolute on Windows: Node resolves `/usr/bin/head`
13
- // onto the current drive as `C:\usr\bin\head`. Keep these fixed candidates as
14
- // the trusted fast path where they are genuinely absolute, and discover the
15
- // executable through PATH everywhere else.
16
- const POSIX_HEAD_CANDIDATES = ['/usr/bin/head', '/bin/head'];
12
+
13
+ /**
14
+ * stdout filters this helper will spawn. This repeats the policy allowlist in
15
+ * `src/core/permission/ec-command-parser.ts` on purpose: the payload is data
16
+ * from another process, so the helper re-validates it instead of trusting it.
17
+ * Only filters that read stdin and write stdout may appear here, and each
18
+ * filter's option grammar is an allowlist so it cannot acquire a file operand,
19
+ * a write target, or a process spawn.
20
+ */
21
+ const STDOUT_FILTER_SPECS = new Map([
22
+ // `tail` follow/retry options are absent below, so `tail -f` is rejected
23
+ // rather than allowed to hold the pipe open.
24
+ ['head', {
25
+ flags: new Set(),
26
+ valueOptions: new Set(['-n', '-c', '--lines', '--bytes']),
27
+ maxOperands: 0,
28
+ numericShorthand: true,
29
+ }],
30
+ ['tail', {
31
+ flags: new Set(),
32
+ valueOptions: new Set(['-n', '-c', '--lines', '--bytes']),
33
+ maxOperands: 0,
34
+ numericShorthand: true,
35
+ }],
36
+ // `grep` takes either `-e`/`--regexp` or one leading pattern operand. With an
37
+ // explicit pattern option the operand budget is zero, because a bare operand
38
+ // would be a file to read. `-r`, `-R` and `-f` are absent, so recursion and
39
+ // pattern files stay out.
40
+ ['grep', {
41
+ flags: new Set([
42
+ '-i', '--ignore-case', '-v', '--invert-match', '-c', '--count',
43
+ '-l', '--files-with-matches', '-L', '--files-without-match', '-o', '--only-matching',
44
+ '-n', '--line-number', '-h', '--no-filename', '-H', '--with-filename',
45
+ '-q', '--quiet', '--silent', '-s', '--no-messages', '-w', '--word-regexp',
46
+ '-x', '--line-regexp', '-F', '--fixed-strings', '-E', '--extended-regexp',
47
+ '-G', '--basic-regexp', '-P', '--perl-regexp', '-a', '--text', '-I',
48
+ '-z', '--null-data', '-U', '--binary', '--line-buffered', '--no-group-separator',
49
+ ]),
50
+ valueOptions: new Set([
51
+ '-m', '--max-count', '-A', '--after-context', '-B', '--before-context',
52
+ '-C', '--context', '--color', '--colour', '--binary-files', '--label',
53
+ '--group-separator', '-d', '--directories', '--include', '--exclude', '--exclude-dir',
54
+ ]),
55
+ patternValueOptions: new Set(['-e', '--regexp']),
56
+ maxOperands: 1,
57
+ }],
58
+ // `wc`, `sort`, `cut` and `uniq` take no operands here: a path operand would
59
+ // read a file instead of the EC stdout. Options naming an input or output
60
+ // file (--files0-from, sort -o/-T/--random-source, uniq --group) are absent.
61
+ ['wc', {
62
+ flags: new Set([
63
+ '-c', '-m', '-l', '-w', '-L',
64
+ '--bytes', '--chars', '--lines', '--words', '--max-line-length',
65
+ ]),
66
+ valueOptions: new Set(),
67
+ maxOperands: 0,
68
+ }],
69
+ ['sort', {
70
+ flags: new Set([
71
+ '-b', '-d', '-f', '-g', '-h', '-i', '-M', '-n', '-r', '-R', '-s', '-u', '-V', '-z',
72
+ '--ignore-leading-blanks', '--dictionary-order', '--ignore-case', '--general-numeric-sort',
73
+ '--human-numeric-sort', '--ignore-nonprinting', '--month-sort', '--numeric-sort',
74
+ '--random-sort', '--reverse', '--stable', '--unique', '--version-sort', '--zero-terminated',
75
+ '--check', '--merge',
76
+ ]),
77
+ valueOptions: new Set([
78
+ '-k', '-t', '-S', '--key', '--field-separator',
79
+ '--batch-size', '--buffer-size', '--parallel', '--sort',
80
+ ]),
81
+ maxOperands: 0,
82
+ }],
83
+ ['cut', {
84
+ flags: new Set(['-s', '--only-delimited', '-n', '--complement', '-z', '--zero-terminated']),
85
+ valueOptions: new Set([
86
+ '-b', '-c', '-f', '-d', '--bytes', '--characters', '--fields', '--delimiter', '--output-delimiter',
87
+ ]),
88
+ maxOperands: 0,
89
+ }],
90
+ ['uniq', {
91
+ flags: new Set([
92
+ '-c', '-d', '-D', '-u', '-i', '-z',
93
+ '--count', '--repeated', '--all-repeated', '--unique', '--ignore-case', '--zero-terminated',
94
+ ]),
95
+ valueOptions: new Set(['-f', '-s', '--skip-fields', '--skip-chars']),
96
+ maxOperands: 0,
97
+ }],
98
+ // `tr` takes its character sets as operands, which are never files.
99
+ ['tr', {
100
+ flags: new Set([
101
+ '-c', '-C', '-d', '-s', '-t',
102
+ '--complement', '--delete', '--squeeze-repeats', '--truncate-set1',
103
+ ]),
104
+ valueOptions: new Set(),
105
+ maxOperands: 2,
106
+ }],
107
+ ]);
17
108
 
18
109
  function isRegularFile(candidate) {
19
110
  try {
@@ -53,16 +144,17 @@ function resolveExecutablePath(executable) {
53
144
  return undefined;
54
145
  }
55
146
 
56
- function resolveHeadExecutable() {
147
+ function resolveFilterExecutable(name) {
57
148
  if (!IS_WINDOWS) {
58
- const fixed = POSIX_HEAD_CANDIDATES.find(isRegularFile);
149
+ // A rooted POSIX path is not absolute on Windows: Node resolves
150
+ // `/usr/bin/head` onto the current drive as `C:\usr\bin\head`. On POSIX
151
+ // the fixed locations are genuinely absolute, so try them first.
152
+ const fixed = [`/usr/bin/${name}`, `/bin/${name}`].find(isRegularFile);
59
153
  if (fixed) return fixed;
60
154
  }
61
- return resolveExecutablePath('head');
155
+ return resolveExecutablePath(name);
62
156
  }
63
157
 
64
- const HEAD_EXECUTABLE = resolveHeadExecutable();
65
-
66
158
  /**
67
159
  * Resolve the concrete process for a validated payload.
68
160
  *
@@ -105,7 +197,10 @@ function sameOrDescendant(candidate, parent) {
105
197
  function decodePayload(encoded) {
106
198
  try {
107
199
  const value = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
108
- if (!value || value.v !== 1 || !Array.isArray(value.argv) || value.argv.some(arg => typeof arg !== 'string')) return null;
200
+ // v3 carries a chain of stdin-only filters in `filterChain`. v1 named only
201
+ // the filter's arguments and v2 carried a single `filterArgv`, so both are
202
+ // rejected rather than misread.
203
+ if (!value || value.v !== 3 || !Array.isArray(value.argv) || value.argv.some(arg => typeof arg !== 'string')) return null;
109
204
  return value;
110
205
  } catch {
111
206
  return null;
@@ -119,18 +214,33 @@ function validateCommand(payload) {
119
214
  return !payload.argv.some(arg => /\bnode(?:js|\.exe)?\b/i.test(arg));
120
215
  }
121
216
 
122
- function validateStdinOnlyHeadArgs(args) {
123
- if (args === undefined) return true;
124
- if (!Array.isArray(args) || args.some(arg => typeof arg !== 'string')) return false;
217
+ /**
218
+ * Re-validate the filter argv the policy layer chose. The helper never trusts
219
+ * the payload: the executable must be a known stdin-only filter, every option
220
+ * must appear in that filter's allowlist, and non-option operands must stay
221
+ * within the filter's operand budget.
222
+ */
223
+ function resolveFilterArgv(filterArgv) {
224
+ if (filterArgv === undefined) return { executable: undefined, args: [] };
225
+ if (!Array.isArray(filterArgv) || filterArgv.length === 0
226
+ || filterArgv.some(arg => typeof arg !== 'string')) return null;
227
+ const spec = STDOUT_FILTER_SPECS.get(filterArgv[0]);
228
+ if (!spec) return null;
229
+ // 单级校验:可执行文件名必须在白名单表内,选项必须逐项命中该过滤器的允许表,
230
+ // 非选项操作数不得超出其预算。链上每一级都走这里,所以多一级不放松任何一项。
231
+
232
+ let operands = 0;
233
+ let sawPatternValue = false;
125
234
  let optionsEnded = false;
126
235
  let expectsOptionValue = false;
127
- for (const arg of args) {
236
+ for (let index = 1; index < filterArgv.length; index++) {
237
+ const arg = filterArgv[index];
128
238
  if (expectsOptionValue) {
129
239
  expectsOptionValue = false;
130
240
  continue;
131
241
  }
132
242
  if (optionsEnded) {
133
- if (arg !== '-') return false;
243
+ if (arg !== '-') operands++;
134
244
  continue;
135
245
  }
136
246
  if (arg === '--') {
@@ -138,12 +248,49 @@ function validateStdinOnlyHeadArgs(args) {
138
248
  continue;
139
249
  }
140
250
  if (arg === '-') continue;
141
- if (!arg.startsWith('-')) return false;
142
- if (arg === '-n' || arg === '-c' || arg === '--lines' || arg === '--bytes') {
143
- expectsOptionValue = true;
251
+ if (!arg.startsWith('-')) {
252
+ operands++;
253
+ continue;
144
254
  }
255
+ if (spec.numericShorthand && /^-\d+$/.test(arg)) continue;
256
+ const option = arg.split('=', 1)[0];
257
+ if (spec.patternValueOptions?.has(option)) {
258
+ sawPatternValue = true;
259
+ if (!arg.includes('=') && arg.length === option.length) expectsOptionValue = true;
260
+ continue;
261
+ }
262
+ if (spec.valueOptions.has(option)) {
263
+ if (!arg.includes('=') && arg.length === option.length) expectsOptionValue = true;
264
+ continue;
265
+ }
266
+ if (spec.flags.has(arg)) continue;
267
+ return null;
268
+ }
269
+
270
+ const operandBudget = sawPatternValue ? 0 : spec.maxOperands;
271
+ if (operands > operandBudget) return null;
272
+ if (spec.patternValueOptions && !sawPatternValue && operands === 0) return null;
273
+
274
+ const executable = resolveFilterExecutable(filterArgv[0]);
275
+ if (!executable) return null;
276
+ return { executable, args: filterArgv.slice(1) };
277
+ }
278
+
279
+ /**
280
+ * 复验 policy 选出的过滤器链。每一级都必须独立通过 resolveFilterArgv,
281
+ * 任何一级不合格就整条拒绝——链式不放宽单级的标准。
282
+ * 返回 null 表示拒绝;返回 [] 表示无过滤器(直接输出)。
283
+ */
284
+ function resolveFilterChain(filterChain) {
285
+ if (filterChain === undefined) return [];
286
+ if (!Array.isArray(filterChain)) return null;
287
+ const resolved = [];
288
+ for (const stage of filterChain) {
289
+ const one = resolveFilterArgv(stage);
290
+ if (one === null || one.executable === undefined) return null;
291
+ resolved.push(one);
145
292
  }
146
- return true;
293
+ return resolved;
147
294
  }
148
295
 
149
296
  function resolveExecutionPaths(payload) {
@@ -218,8 +365,10 @@ function openOutputFile(payload, paths) {
218
365
  const payload = decodePayload(process.argv[2]);
219
366
  if (!payload) fail('invalid payload');
220
367
  if (!validateCommand(payload)) fail('command is not permitted');
221
- if (!validateStdinOnlyHeadArgs(payload.headArgs)) fail('head may only consume stdin');
222
- if (payload.headArgs !== undefined && !HEAD_EXECUTABLE) fail('trusted head executable is unavailable');
368
+ const filterChain = resolveFilterChain(payload.filterChain);
369
+ if (filterChain === null) fail('stdout filter may only consume stdin');
370
+ const suffixArgv = resolveSuffixArgv(payload.suffixArgv);
371
+ if (suffixArgv === null) fail('trailing command must be an inert echo/pwd');
223
372
 
224
373
  // Resolve the child executable before anything with a side effect: an
225
374
  // unresolvable command must not leave an empty create-only output file behind.
@@ -249,19 +398,25 @@ function writeLimited(chunk) {
249
398
  }
250
399
  }
251
400
 
252
- const head = payload.headArgs === undefined
253
- ? undefined
254
- : spawn(HEAD_EXECUTABLE, payload.headArgs, {
255
- cwd: paths.cwd,
256
- env: process.env,
257
- stdio: ['pipe', 'pipe', 'inherit'],
258
- });
259
- if (head) {
260
- head.stdout.on('data', writeLimited);
401
+ // 过滤器链:逐级 spawn,前一级的 stdout 接后一级的 stdin。
402
+ // 最后一级的输出才进入 writeLimited(终端或输出文件)。
403
+ const filterProcesses = filterChain.map((stage) => {
404
+ const proc = spawn(stage.executable, stage.args, {
405
+ cwd: paths.cwd,
406
+ env: process.env,
407
+ // stdout 恒为 pipe:链尾由本进程消费,中间级接给下一级。
408
+ stdio: ['pipe', 'pipe', 'inherit'],
409
+ });
261
410
  // Early-exit options such as --help or a small -n/-c close stdin before the
262
411
  // EC process finishes. Ignore EPIPE and keep draining EC output below.
263
- head.stdin.on('error', () => {});
264
- }
412
+ proc.stdin.on('error', () => {});
413
+ // 同理:下游提前退出(如 `| head -3`)后上游仍会写,忽略其 stdout EPIPE,
414
+ // 否则 Node 会把未处理的 'error' 事件升级成进程崩溃。
415
+ proc.stdout.on('error', () => {});
416
+ return proc;
417
+ });
418
+ const filterProcess = filterProcesses[filterProcesses.length - 1];
419
+ if (filterProcess) filterProcess.stdout.on('data', writeLimited);
265
420
 
266
421
  const child = spawn(childCommand.file, childCommand.args, {
267
422
  cwd: paths.cwd,
@@ -270,11 +425,35 @@ const child = spawn(childCommand.file, childCommand.args, {
270
425
  windowsHide: IS_WINDOWS,
271
426
  });
272
427
  const sourceStreams = [child.stdout, ...(payload.mergeStderr ? [child.stderr] : [])];
273
- if (head) {
274
- for (const stream of sourceStreams) {
275
- // The no-op data listener keeps draining the source if head exits early.
428
+
429
+ /**
430
+ * 把一组上游流接到某个下游 stdin,并保证 EOF 会传播。
431
+ *
432
+ * 这里刻意不用 `pipe(dst, { end: true })`:多个上游(stdout + 合并的 stderr)
433
+ * 共用一个下游时,任何一个上游结束都会提前关掉下游 stdin,丢掉另一路数据。
434
+ * 改为全部上游结束后再显式 end(),链上每一级都遵循同一约定,否则
435
+ * 下游永远等不到 EOF,整条链会挂住而不退出。
436
+ */
437
+ function pipeAllWithEnd(streams, dst) {
438
+ let pending = streams.length;
439
+ const onDone = () => {
440
+ if (--pending === 0 && !dst.destroyed) dst.end();
441
+ };
442
+ for (const stream of streams) {
443
+ // The no-op data listener keeps draining the source if the filter exits
444
+ // early, e.g. `head -n 1`.
276
445
  stream.on('data', () => {});
277
- stream.pipe(head.stdin, { end: false });
446
+ stream.on('end', onDone);
447
+ stream.on('close', onDone);
448
+ stream.pipe(dst, { end: false });
449
+ }
450
+ }
451
+
452
+ if (filterProcesses.length > 0) {
453
+ pipeAllWithEnd(sourceStreams, filterProcesses[0].stdin);
454
+ // 链内串接:第 i 级的 stdout → 第 i+1 级的 stdin(同样等上游结束后再 end)。
455
+ for (let index = 0; index + 1 < filterProcesses.length; index++) {
456
+ pipeAllWithEnd([filterProcesses[index].stdout], filterProcesses[index + 1].stdin);
278
457
  }
279
458
  } else {
280
459
  for (const stream of sourceStreams) stream.on('data', writeLimited);
@@ -290,11 +469,25 @@ child.once('error', error => {
290
469
 
291
470
  let sourceClosed = false;
292
471
  let sourceCode = 1;
293
- let headClosed = !head;
294
- let headCode = 1;
472
+ let filterClosed = filterProcesses.length === 0;
473
+ let filterStarted = false;
474
+
475
+ /**
476
+ * 复验 policy 选出的尾部命令。只允许 `echo` 与 `pwd`,实参必须是纯字面量
477
+ * (无 `$`/反引号/通配/重定向/管道),与 parser 侧的判据保持一致——
478
+ * helper 从不相信 payload。
479
+ */
480
+ function resolveSuffixArgv(suffixArgv) {
481
+ if (suffixArgv === undefined) return undefined;
482
+ if (!Array.isArray(suffixArgv) || suffixArgv.length === 0
483
+ || suffixArgv.some(arg => typeof arg !== 'string')) return null;
484
+ if (suffixArgv[0] !== 'echo' && suffixArgv[0] !== 'pwd') return null;
485
+ if (suffixArgv.some(arg => /[$`*?\[\]{}()<>|;&]/.test(arg))) return null;
486
+ return suffixArgv;
487
+ }
295
488
 
296
489
  function finish() {
297
- if (!sourceClosed || !headClosed) return;
490
+ if (!sourceClosed || !filterClosed) return;
298
491
  if (output) {
299
492
  try { fs.closeSync(output.fd); } catch { writeFailed = true; }
300
493
  }
@@ -303,25 +496,48 @@ function finish() {
303
496
  fail('failed to write command output');
304
497
  }
305
498
  if (output) process.stderr.write(`Saved output to ${output.target}\n`);
306
- process.exitCode = head ? headCode : sourceCode;
499
+ // 尾部零副作用命令(`&& echo done`)在 ec 输出之后执行一次,输出直连终端。
500
+ // 失败不影响退出码——它只是提示,不表达 ec 是否成功。
501
+ if (suffixArgv) {
502
+ try {
503
+ const res = spawnSync(suffixArgv[0], suffixArgv.slice(1), {
504
+ cwd: paths.cwd,
505
+ env: process.env,
506
+ stdio: ['ignore', 'inherit', 'inherit'],
507
+ windowsHide: IS_WINDOWS,
508
+ });
509
+ void res;
510
+ } catch { /* 忽略:尾部命令失败不改变 ec 的结果 */ }
511
+ }
512
+ // The caller's exit code describes the EC command, not the filter. `grep`
513
+ // and `uniq` exit 1 to mean "no lines matched"/"nothing unique", which says
514
+ // nothing about whether the EC command itself succeeded.
515
+ process.exitCode = sourceCode;
307
516
  }
308
517
 
309
- if (head) {
310
- head.once('error', error => {
311
- writeFailed = true;
312
- process.stderr.write(`ec-safe-output: unable to start head: ${error.message}\n`);
313
- });
314
- head.once('close', code => {
315
- headCode = code ?? 1;
316
- headClosed = true;
317
- for (const stream of sourceStreams) stream.unpipe(head.stdin);
318
- finish();
319
- });
518
+ if (filterProcesses.length > 0) {
519
+ for (const [index, proc] of filterProcesses.entries()) {
520
+ const isLast = index === filterProcesses.length - 1;
521
+ proc.once('error', error => {
522
+ writeFailed = true;
523
+ process.stderr.write(`ec-safe-output: unable to start stdout filter: ${error.message}\n`);
524
+ });
525
+ proc.once('spawn', () => { if (isLast) filterStarted = true; });
526
+ proc.once('close', () => {
527
+ // 只有链尾关闭才算整条链结束;中间级提前退出时下游仍可能有余量可读。
528
+ if (!isLast) return;
529
+ filterClosed = true;
530
+ for (const stream of sourceStreams) stream.unpipe(filterProcesses[0].stdin);
531
+ finish();
532
+ });
533
+ }
320
534
  }
321
535
 
322
536
  child.once('close', code => {
323
537
  sourceCode = code ?? 1;
324
538
  sourceClosed = true;
325
- if (head && !head.stdin.destroyed) head.stdin.end();
539
+ // EOF 由 pipeAllWithEnd 在 source 流真正结束后传播(child 'close' 早于
540
+ // stdout 'end',此处主动 end 会把还没读完的输出截断)。这里只在过滤器从未
541
+ // 启动成功时兜底收尾,避免 helper 永远等一个收不到 EOF 的进程。
326
542
  finish();
327
543
  });
@@ -1,6 +1,40 @@
1
1
  import { normalizeBaseagent } from './baseagent.js';
2
2
  import { commandExists } from '../utils/cross-platform.js';
3
3
  import { getCodexAppServerAvailability } from './codex-runner.js';
4
+ import { createRequire } from 'module';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ const _resolveModule = createRequire(import.meta.url);
8
+ /**
9
+ * Check whether the bundled Claude SDK native binary is present on disk.
10
+ * This covers Windows installations where the npm global shim may be absent
11
+ * but the SDK ships claude.exe alongside evolcore's own node_modules.
12
+ */
13
+ function isBundledClaudeAvailable() {
14
+ if (process.platform !== 'win32')
15
+ return false;
16
+ const nativePackage = process.arch === 'arm64'
17
+ ? '@anthropic-ai/claude-agent-sdk-win32-arm64'
18
+ : process.arch === 'x64'
19
+ ? '@anthropic-ai/claude-agent-sdk-win32-x64'
20
+ : undefined;
21
+ if (!nativePackage)
22
+ return false;
23
+ try {
24
+ const exe = _resolveModule.resolve(`${nativePackage}/claude.exe`);
25
+ if (fs.existsSync(exe))
26
+ return true;
27
+ }
28
+ catch { /* fall through */ }
29
+ try {
30
+ const sdkEntry = _resolveModule.resolve('@anthropic-ai/claude-agent-sdk');
31
+ const exe = path.resolve(path.dirname(sdkEntry), '..', '@anthropic-ai', nativePackage.slice('@anthropic-ai/'.length), 'claude.exe');
32
+ return fs.existsSync(exe);
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
4
38
  export const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini', 'ecagent'];
5
39
  export const AUTO_BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
6
40
  /**
@@ -14,11 +48,13 @@ export function detectAvailableBaseagents(codexAvailability = getCodexAppServerA
14
48
  return true;
15
49
  if (baseagent === 'codex')
16
50
  return codexAvailability.available;
51
+ if (baseagent === 'claude')
52
+ return commandExists(baseagent) || isBundledClaudeAvailable();
17
53
  return commandExists(baseagent);
18
54
  });
19
55
  }
20
56
  export function detectBaseagentDetails(codexAvailability = getCodexAppServerAvailability()) {
21
- const claude = commandExists('claude');
57
+ const claude = commandExists('claude') || isBundledClaudeAvailable();
22
58
  const gemini = commandExists('gemini');
23
59
  return [
24
60
  {
@@ -15,6 +15,7 @@ import os from 'os';
15
15
  import { buildCliLaunchCommand, commandExists, isWindows, resolvePowerShellShimPath, resolveProbePath, } from '../utils/cross-platform.js';
16
16
  import { execFileSync } from 'child_process';
17
17
  import { parse as parseToml } from 'smol-toml';
18
+ import { readRuntimeEnvironmentVariable } from '../config/runtime-env.js';
18
19
  /** Resolve the installed Codex CLI entrypoint, including npm shims on Windows. */
19
20
  export function resolveCodexCliPath() {
20
21
  return resolveProbePath('codex');
@@ -136,7 +137,7 @@ function loadClaudeSettings() {
136
137
  * A pair must come from one source so partial configuration cannot be mixed
137
138
  * across Agent config, the process environment, and Claude Code settings.
138
139
  */
139
- export function resolveAnthropicDirectConfig(override) {
140
+ export function resolveAnthropicDirectConfig(override, runtime) {
140
141
  const explicit = directInferencePair(override?.apiKey, override?.baseUrl);
141
142
  if (explicit) {
142
143
  return {
@@ -144,7 +145,7 @@ export function resolveAnthropicDirectConfig(override) {
144
145
  ...normalizeRequestMetadata(override),
145
146
  };
146
147
  }
147
- const environment = directInferencePair(directInferenceValue(process.env.ANTHROPIC_AUTH_TOKEN, process.env.ANTHROPIC_API_KEY), process.env.ANTHROPIC_BASE_URL);
148
+ const environment = directInferencePair(directInferenceValue(readRuntimeEnvironmentVariable('ANTHROPIC_AUTH_TOKEN', runtime?.agentAid, runtime?.projectPath).value, readRuntimeEnvironmentVariable('ANTHROPIC_API_KEY', runtime?.agentAid, runtime?.projectPath).value), readRuntimeEnvironmentVariable('ANTHROPIC_BASE_URL', runtime?.agentAid, runtime?.projectPath).value);
148
149
  if (environment) {
149
150
  return {
150
151
  ...environment,
@@ -160,21 +161,30 @@ export function resolveAnthropicDirectConfig(override) {
160
161
  }
161
162
  : undefined;
162
163
  }
163
- export function resolveAnthropicConfig(config, override) {
164
+ export function resolveAnthropicConfig(config, override, runtime) {
164
165
  const settings = loadClaudeSettings();
165
166
  const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
166
- const overrideApiKey = isPlaceholder(override?.apiKey) ? undefined : override?.apiKey;
167
- const globalApiKey = isPlaceholder(config.agents?.claude?.apiKey) ? undefined : config.agents?.claude?.apiKey;
168
- const apiKey = resolveAnthropicApiKey(overrideApiKey, globalApiKey, process.env.ANTHROPIC_AUTH_TOKEN, process.env.ANTHROPIC_API_KEY, settings.env?.ANTHROPIC_AUTH_TOKEN, settings.env?.ANTHROPIC_API_KEY);
167
+ const resolveGatewayReference = (value) => {
168
+ const name = value?.match(/^\$\{(ANTHROPIC_BASE_URL|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_API_KEY)\}$/u)?.[1];
169
+ return name ? readRuntimeEnvironmentVariable(name, runtime?.agentAid, runtime?.projectPath).value : value;
170
+ };
171
+ const overrideApiKey = resolveGatewayReference(override?.apiKey);
172
+ const globalApiKey = resolveGatewayReference(config.agents?.claude?.apiKey);
173
+ const runtimeAuthToken = readRuntimeEnvironmentVariable('ANTHROPIC_AUTH_TOKEN', runtime?.agentAid, runtime?.projectPath).value;
174
+ const runtimeApiKey = readRuntimeEnvironmentVariable('ANTHROPIC_API_KEY', runtime?.agentAid, runtime?.projectPath).value;
175
+ const apiKey = resolveAnthropicApiKey(isPlaceholder(overrideApiKey) ? undefined : overrideApiKey, isPlaceholder(globalApiKey) ? undefined : globalApiKey, runtimeAuthToken, runtimeApiKey, settings.env?.ANTHROPIC_AUTH_TOKEN, settings.env?.ANTHROPIC_API_KEY);
169
176
  if (!apiKey) {
170
177
  throw new Error('No API key found for Claude. Set baseagents.claude.apiKey, env ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY, or the corresponding variable in ~/.claude/settings.json');
171
178
  }
172
179
  const isPlaceholderUrl = (v) => !v || v.includes('api.anthropic.com');
173
- const overrideBaseUrl = isPlaceholderUrl(override?.baseUrl) ? undefined : override?.baseUrl;
174
- const globalBaseUrl = isPlaceholderUrl(config.agents?.claude?.baseUrl) ? undefined : config.agents?.claude?.baseUrl;
180
+ const overrideBaseUrlValue = resolveGatewayReference(override?.baseUrl);
181
+ const globalBaseUrlValue = resolveGatewayReference(config.agents?.claude?.baseUrl);
182
+ const overrideBaseUrl = isPlaceholderUrl(overrideBaseUrlValue) ? undefined : overrideBaseUrlValue;
183
+ const globalBaseUrl = isPlaceholderUrl(globalBaseUrlValue) ? undefined : globalBaseUrlValue;
184
+ const runtimeBaseUrl = readRuntimeEnvironmentVariable('ANTHROPIC_BASE_URL', runtime?.agentAid, runtime?.projectPath).value;
175
185
  const baseUrl = overrideBaseUrl
176
186
  || globalBaseUrl
177
- || process.env.ANTHROPIC_BASE_URL
187
+ || runtimeBaseUrl
178
188
  || settings.env?.ANTHROPIC_BASE_URL;
179
189
  const headers = normalizeStringMap(override?.headers ?? config.agents?.claude?.headers, 'headers');
180
190
  const queryParams = normalizeStringMap(override?.queryParams ?? config.agents?.claude?.queryParams, 'queryParams');