evolcore 0.0.27 → 0.0.29-test.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 (195) hide show
  1. package/CHANGELOG.md +38 -15
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +29 -14
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +13 -2
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex/diagnostics.js +4 -3
  9. package/dist/agents/codex-runner.js +444 -106
  10. package/dist/agents/gemini-runner.js +35 -17
  11. package/dist/aun/group/history.js +130 -90
  12. package/dist/aun/group/operations.js +1 -3
  13. package/dist/aun/menu/client.js +1 -0
  14. package/dist/aun/menu/transport.js +5 -3
  15. package/dist/aun/msg/managed-operation.js +70 -0
  16. package/dist/aun/msg/p2p.js +1 -3
  17. package/dist/aun/msg/upload.js +2 -2
  18. package/dist/aun/outbox.js +1 -37
  19. package/dist/channels/aun.js +262 -86
  20. package/dist/channels/daemon.js +18 -1
  21. package/dist/channels/feishu.js +1 -2
  22. package/dist/cli/agent-command.js +23 -3
  23. package/dist/cli/agent-create.js +68 -2
  24. package/dist/cli/agent.js +12 -2
  25. package/dist/cli/aun-commands.js +87 -13
  26. package/dist/cli/cli-argv.js +26 -0
  27. package/dist/cli/daemon-commands.js +116 -149
  28. package/dist/cli/data-command.js +4 -4
  29. package/dist/cli/diag.js +1 -1
  30. package/dist/cli/handoff-command.js +2 -0
  31. package/dist/cli/index.js +18 -1
  32. package/dist/cli/managed-command-guard.js +10 -0
  33. package/dist/cli/memory-command.js +441 -0
  34. package/dist/cli/menu-client.js +4 -0
  35. package/dist/cli/restart-monitor.js +127 -23
  36. package/dist/cli/skill-command.js +41 -0
  37. package/dist/cli/task-context.js +2 -23
  38. package/dist/cli/trigger-command.js +9 -1
  39. package/dist/cli/watch-msg-diag.js +209 -0
  40. package/dist/config/config-field-policy.js +52 -3
  41. package/dist/config/config-manager.js +17 -4
  42. package/dist/config/contact-book-store.js +2 -2
  43. package/dist/config/mention-mode.js +7 -2
  44. package/dist/config/merge.js +28 -9
  45. package/dist/config/resolved-config-op.js +1 -1
  46. package/dist/config/runtime-env.js +137 -0
  47. package/dist/config/schema-registry.js +11 -1
  48. package/dist/config-store.js +10 -10
  49. package/dist/core/audit/event-key.js +3 -1
  50. package/dist/core/audit/log-integrity.js +4 -0
  51. package/dist/core/auth/agent-delegation.js +2 -1
  52. package/dist/core/auth/auth-gateway.js +13 -6
  53. package/dist/core/auth/authorization-audit.js +54 -19
  54. package/dist/core/auth/operation-authorizer.js +17 -0
  55. package/dist/core/auth/operation-catalog.js +9 -0
  56. package/dist/core/causation/context.js +5 -1
  57. package/dist/core/command/agent-control.js +1 -2
  58. package/dist/core/command/command-handler.js +80 -24
  59. package/dist/core/command/diag-service.js +292 -27
  60. package/dist/core/command/memory-slash.js +270 -0
  61. package/dist/core/command/menu-catalog.js +1 -0
  62. package/dist/core/command/menu-handler.js +331 -79
  63. package/dist/core/command/menu-protocol.js +45 -4
  64. package/dist/core/command/slash-gate.js +10 -10
  65. package/dist/core/command/slash-handler.js +230 -79
  66. package/dist/core/data-migration.js +282 -505
  67. package/dist/core/event-catalog.js +26 -0
  68. package/dist/core/evolagent-registry.js +6 -0
  69. package/dist/core/evolagent.js +0 -10
  70. package/dist/core/handoff/store.js +1 -20
  71. package/dist/core/inference/text-inference.js +57 -1
  72. package/dist/core/memory/consolidation-context.js +505 -0
  73. package/dist/core/memory/consolidation-ops-records.js +124 -0
  74. package/dist/core/memory/consolidation-trigger.js +394 -0
  75. package/dist/core/memory/index-manager.js +308 -0
  76. package/dist/core/memory/launch-consolidation.js +395 -0
  77. package/dist/core/memory/memory-admin.js +234 -0
  78. package/dist/core/memory/memory-document.js +100 -0
  79. package/dist/core/memory/memory-session-policy.js +41 -0
  80. package/dist/core/memory/memory-signature.js +257 -0
  81. package/dist/core/memory/memory-writer.js +432 -0
  82. package/dist/core/memory/staleness.js +117 -0
  83. package/dist/core/message/auto-mention-gate.js +454 -0
  84. package/dist/core/message/im-renderer.js +90 -20
  85. package/dist/core/message/message-bridge.js +333 -23
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/message-queue.js +114 -27
  88. package/dist/core/message/pending-hints.js +329 -161
  89. package/dist/core/message/response-engine.js +596 -90
  90. package/dist/core/permission/ec-command-parser.js +1044 -87
  91. package/dist/core/permission/protected-paths.js +376 -126
  92. package/dist/core/permission/sandbox-runtime.js +40 -12
  93. package/dist/core/permission/tool-error-code.js +96 -13
  94. package/dist/core/permission/tool-policy.js +277 -24
  95. package/dist/core/role/runtime-policy.js +3 -7
  96. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  97. package/dist/core/session/session-fs-store.js +0 -76
  98. package/dist/core/session/session-idle-monitor.js +355 -0
  99. package/dist/core/session/session-manager.js +482 -17
  100. package/dist/core/session/session-mapper.js +33 -16
  101. package/dist/core/session/session-renew.js +217 -155
  102. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  103. package/dist/core/skill/global-watcher.js +282 -0
  104. package/dist/core/skill/index-generator.js +84 -0
  105. package/dist/core/skill/startup.js +73 -0
  106. package/dist/core/skill/switch-resolver.js +115 -0
  107. package/dist/core/task/task-activity-store.js +219 -0
  108. package/dist/eck/kit-renderer.js +30 -3
  109. package/dist/eck/manifest-engine.js +26 -4
  110. package/dist/index.js +722 -171
  111. package/dist/ipc.js +564 -2
  112. package/dist/paths.js +30 -9
  113. package/dist/product.js +1 -0
  114. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  115. package/dist/response-system/modes/single-session/index.js +3 -0
  116. package/dist/trigger/feedback.js +1 -0
  117. package/dist/trigger/manager.js +0 -10
  118. package/dist/trigger/scheduler.js +11 -0
  119. package/dist/trigger/validation.js +22 -0
  120. package/dist/utils/atomic-write.js +98 -0
  121. package/dist/utils/avatar-upload.js +59 -18
  122. package/dist/utils/cross-platform.js +87 -16
  123. package/dist/utils/error-dict.json +6 -3
  124. package/dist/utils/error-utils.js +27 -0
  125. package/dist/utils/logger.js +32 -10
  126. package/dist/utils/media-cache.js +84 -0
  127. package/dist/utils/msg-diag.js +97 -0
  128. package/dist/utils/npm-ops.js +119 -4
  129. package/dist/utils/package-version-info.js +244 -0
  130. package/dist/utils/welcome.js +37 -8
  131. package/dist/utils/windows-shell-trust.js +37 -0
  132. package/kits/docs/AUTHORING.md +314 -0
  133. package/kits/docs/GUIDE.md +2 -0
  134. package/kits/docs/INDEX.md +2 -0
  135. package/kits/docs/aun/CHEATSHEET.md +4 -0
  136. package/kits/docs/aun/SDK.md +60 -0
  137. package/kits/docs/channels/aun.md +8 -0
  138. package/kits/docs/context-assembly.md +10 -6
  139. package/kits/docs/evolcore/INDEX.md +17 -3
  140. package/kits/docs/evolcore/agent-create.md +24 -0
  141. package/kits/docs/evolcore/agent.md +5 -2
  142. package/kits/docs/evolcore/config.md +1 -1
  143. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  144. package/kits/docs/evolcore/group.md +34 -18
  145. package/kits/docs/evolcore/memory-architecture.md +78 -0
  146. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  147. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  148. package/kits/docs/evolcore/memory-tags.md +63 -0
  149. package/kits/docs/evolcore/memory.md +110 -0
  150. package/kits/docs/evolcore/menu-client.md +27 -11
  151. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  152. package/kits/docs/evolcore/menu-protocol.md +213 -0
  153. package/kits/docs/evolcore/msg.md +2 -0
  154. package/kits/docs/evolcore/trigger.md +7 -5
  155. package/kits/docs/path-registry.md +6 -8
  156. package/kits/docs/prompt-loading-architecture.md +5 -3
  157. package/kits/docs/venues/group.md +5 -4
  158. package/kits/eck_manifest.json +294 -88
  159. package/kits/eck_manifest.memory.json +257 -0
  160. package/kits/rules/03-identity.md +8 -8
  161. package/kits/rules/05-venue.md +4 -2
  162. package/kits/rules/06-channel.md +1 -1
  163. package/kits/schemas/_meta.json +276 -47
  164. package/kits/schemas/agent-config.schema.12.json +38 -2
  165. package/kits/schemas/agent-config.schema.13.json +591 -0
  166. package/kits/schemas/daemon.schema.6.json +9 -9
  167. package/kits/schemas/defaults.schema.5.json +29 -0
  168. package/kits/schemas/relation-config.schema.8.json +14 -1
  169. package/kits/schemas/relation-config.schema.9.json +129 -0
  170. package/kits/schemas/role-config.schema.1.json +3 -3
  171. package/kits/skills/.switches.json +3 -0
  172. package/kits/skills/ec-skill/SKILL.md +217 -0
  173. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  174. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  175. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  176. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  177. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  178. package/kits/templates/decision/auto-mention.md +25 -0
  179. package/kits/templates/decision/session-renew.md +22 -0
  180. package/kits/templates/memory-consolidation/forget.md +104 -0
  181. package/kits/templates/memory-consolidation/generalize.md +76 -0
  182. package/kits/templates/memory-consolidation/notes.md +133 -0
  183. package/kits/templates/memory-consolidation/profile.md +58 -0
  184. package/kits/templates/memory-consolidation/promote.md +72 -0
  185. package/kits/templates/memory-consolidation/workflow.md +75 -0
  186. package/kits/templates/system-fragments/bootstrap.md +14 -5
  187. package/kits/templates/system-fragments/commands.md +1 -0
  188. package/kits/templates/system-fragments/group-history.md +6 -0
  189. package/kits/templates/system-fragments/identity.md +0 -3
  190. package/kits/templates/system-fragments/memory.md +79 -0
  191. package/kits/templates/system-fragments/reply.md +1 -0
  192. package/kits/templates/system-fragments/venue.md +1 -1
  193. package/package.json +4 -6
  194. package/dist/trigger/legacy-session-history.js +0 -19
  195. package/kits/templates/system-fragments/group-unread-history.md +0 -7
package/CHANGELOG.md CHANGED
@@ -5,24 +5,47 @@
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
+
20
+ ## 0.0.28 (2026-09-24)
21
+
22
+ - 增加 npm 版本缓存,改进升级状态展示及启动、重启流程。
23
+ - 改进 AUN 回复投递与消息展示。
24
+ - 加固 Windows CLI 启动和 PowerShell 命令解析,完善 Codex CLI 诊断。
25
+ - 强化 Menu 请求校验与错误处理。
26
+
8
27
  ## 0.0.27 (2026-09-24)
9
28
 
29
+ ### AUN、群聊与消息
30
+
10
31
  - 持久化 AUN 群邀请的 Owner 审核流程,并增加 Base Agent 清单查询入口。
11
- - 支持 EC Agent 的角色级模型与推理强度策略;为 Claude、Codex、EC Agent runner 会话增加任务历史聚合,并在未确认中断后设置会话重用栅栏。
12
- - 模型类 Trigger 支持单次执行时限;目标会话失联时明确记录为孤儿执行失败。
13
- - 修复 AUN 撤回事件导致的私聊游标跳跃;手动停止的 Agent reload 后恢复渠道连接。
14
- - 规范 Shell 工具活动中的命令展示;本地缺少证书时跳过控制 AID 名片发布。
15
- - 收紧 Codex 分支会话的权限上下文,避免从旧会话缓存继承过期授权。
16
- - 移除 CodeBuddy、QwenWork 非标准 Base Agent 的运行时自动管理路径;EC Agent 默认配置恢复显式凭证要求。
17
- - 移除运行时命令索引中尚未实现的组织管理、任务、资源和工作流命令说明。
18
- - 修复 AUN 出站 payload 截断按原始字符串大小判断的问题;现在按最终 JSON/UTF-8 大小压缩嵌套日志和转义输出,避免可截断负载被误判为仍超限。
19
- - 升级 `@agentunion/fastaun` 至 `0.5.17`,同步运行时最低版本要求;包含 Storage / Group FS 上传 Content-Type 一致性修复,避免 OSS 预签名校验不一致导致的 403。
20
- - 附件 MIME 采用 SDK 字节识别及存储结果;头像与 Storage 上传、删除复用 SDK 文件系统,公开文件返回稳定 AID URL。
21
- - 订阅 `agentmd.changed` 失效 Core 身份缓存,按需刷新并保留失败重试,避免旧查询覆盖更新。
22
- - 对齐无持久序号消息的 Pull 类型,按实际返回透传 volatile 元数据,并修正 SDK 未返回 count 时的消息数量。
23
- - 群命令与托管群文件操作支持 `{base}.{issuer}` 群 AID;不再接受或转换 `group.{issuer}/{base}`,保留当前群权限检查。
24
- - 默认建群名称改为 `g-` 加五位随机数字(10000–99999),明确身份占用时最多尝试 5 次;自定义名称完整保留 `g-`,并提示实际创建身份与请求不一致的情况。
25
- - 群参数格式校验与主体类型提示分离;关系角色查询优先采用已存储的关系类型和已验签身份信息。
32
+ - 支持 EC Agent 的角色级模型与推理强度策略;完善 Claude、Codex、EC Agent 会话的任务历史和中断恢复。
33
+ - 模型类 Trigger 支持单次执行时限;目标会话失联时明确记录为失败。
34
+ - 修复 AUN 撤回消息造成的私聊游标跳跃;手动停止的 Agent 重新加载后可恢复渠道连接。
35
+ - 规范工具活动中的 Shell 命令展示,并在缺少本地证书时跳过控制 AID 名片发布。
36
+ - 优化消息大小处理,提升长消息和复杂内容的发送稳定性。
37
+ - 升级 AUN SDK 至 `0.5.17`,改进消息、群文件和存储上传的兼容性。
38
+ - 改进附件、头像和文件存储处理,提升文件类型识别、上传和公开访问的稳定性。
39
+ - 改进身份信息缓存刷新,确保对端资料更新后及时生效。
40
+ - 支持无连续编号消息的完整拉取信息,并修正消息数量统计。
41
+ - 群命令和托管群文件操作支持标准群 AID 格式。
42
+ - 优化群名称生成和冲突处理,自定义名称保持原样。
43
+ - 改进群组参数校验和成员身份识别,提升群聊操作准确性。
44
+
45
+ ### Agent、权限与运行稳定性
46
+
47
+ - 收紧 Codex 分支会话的权限上下文,避免旧会话继承过期授权。
48
+ - 完善 Agent 初始化、会话恢复、权限边界和运行状态处理。
26
49
 
27
50
  ## 0.0.26 (2026-09-18)
28
51
 
@@ -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
  {
@@ -12,22 +12,27 @@
12
12
  import fs from 'fs';
13
13
  import path from 'path';
14
14
  import os from 'os';
15
- import { commandExists, isWindows, resolveProbePath } from '../utils/cross-platform.js';
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');
21
22
  }
23
+ /** Return a PS1-only Codex shim for installation diagnostics, never execution. */
24
+ export function resolveCodexPowerShellShimPath() {
25
+ return resolvePowerShellShimPath('codex');
26
+ }
22
27
  export function resolveCodexLaunchCommand(args, options = {}) {
23
28
  const windows = options.isWindows ?? isWindows;
24
29
  const commandPath = options.commandPath !== undefined ? options.commandPath : resolveCodexCliPath();
25
30
  if (!commandPath)
26
31
  return null;
27
- if (windows && /\.(cmd|bat)$/i.test(commandPath)) {
28
- return { command: options.comSpec ?? process.env.ComSpec ?? 'cmd.exe', args: ['/d', '/s', '/c', commandPath, ...args] };
29
- }
30
- return { command: commandPath, args };
32
+ return buildCliLaunchCommand(commandPath, args, {
33
+ isWindows: windows,
34
+ comSpec: options.comSpec,
35
+ });
31
36
  }
32
37
  /** Execute a Codex CLI command using the resolved npm shim/path. */
33
38
  export function execCodexCliSync(args, options) {
@@ -40,6 +45,7 @@ export function execCodexCliSync(args, options) {
40
45
  return execFileSync(launchCommand.command, launchCommand.args, {
41
46
  ...(options ?? {}),
42
47
  windowsHide: isWindows,
48
+ windowsVerbatimArguments: launchCommand.windowsVerbatimArguments,
43
49
  });
44
50
  }
45
51
  const BASEAGENT_ALIASES = {
@@ -131,7 +137,7 @@ function loadClaudeSettings() {
131
137
  * A pair must come from one source so partial configuration cannot be mixed
132
138
  * across Agent config, the process environment, and Claude Code settings.
133
139
  */
134
- export function resolveAnthropicDirectConfig(override) {
140
+ export function resolveAnthropicDirectConfig(override, runtime) {
135
141
  const explicit = directInferencePair(override?.apiKey, override?.baseUrl);
136
142
  if (explicit) {
137
143
  return {
@@ -139,7 +145,7 @@ export function resolveAnthropicDirectConfig(override) {
139
145
  ...normalizeRequestMetadata(override),
140
146
  };
141
147
  }
142
- 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);
143
149
  if (environment) {
144
150
  return {
145
151
  ...environment,
@@ -155,21 +161,30 @@ export function resolveAnthropicDirectConfig(override) {
155
161
  }
156
162
  : undefined;
157
163
  }
158
- export function resolveAnthropicConfig(config, override) {
164
+ export function resolveAnthropicConfig(config, override, runtime) {
159
165
  const settings = loadClaudeSettings();
160
166
  const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
161
- const overrideApiKey = isPlaceholder(override?.apiKey) ? undefined : override?.apiKey;
162
- const globalApiKey = isPlaceholder(config.agents?.claude?.apiKey) ? undefined : config.agents?.claude?.apiKey;
163
- 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);
164
176
  if (!apiKey) {
165
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');
166
178
  }
167
179
  const isPlaceholderUrl = (v) => !v || v.includes('api.anthropic.com');
168
- const overrideBaseUrl = isPlaceholderUrl(override?.baseUrl) ? undefined : override?.baseUrl;
169
- 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;
170
185
  const baseUrl = overrideBaseUrl
171
186
  || globalBaseUrl
172
- || process.env.ANTHROPIC_BASE_URL
187
+ || runtimeBaseUrl
173
188
  || settings.env?.ANTHROPIC_BASE_URL;
174
189
  const headers = normalizeStringMap(override?.headers ?? config.agents?.claude?.headers, 'headers');
175
190
  const queryParams = normalizeStringMap(override?.queryParams ?? config.agents?.claude?.queryParams, 'queryParams');