evolcore 0.0.3 → 0.0.5

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 (148) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +1 -31
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +157 -5
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/channels/aun.js +220 -112
  14. package/dist/channels/daemon.js +18 -4
  15. package/dist/channels/dingtalk.js +52 -137
  16. package/dist/channels/feishu.js +56 -4
  17. package/dist/channels/qqbot.js +23 -1
  18. package/dist/channels/wechat.js +303 -155
  19. package/dist/channels/wecom.js +383 -7
  20. package/dist/cli/aun-commands.js +11 -11
  21. package/dist/cli/daemon-commands.js +46 -15
  22. package/dist/cli/handoff-command.js +2 -1
  23. package/dist/cli/init-channel.js +185 -67
  24. package/dist/cli/init.js +49 -27
  25. package/dist/cli/trigger-command.js +92 -11
  26. package/dist/config/access-policy-domain.js +18 -0
  27. package/dist/config/access-policy.js +110 -0
  28. package/dist/config/builtin-role-templates.js +27 -14
  29. package/dist/config/builtin-roles.js +25 -6
  30. package/dist/config/config-field-policy.js +17 -5
  31. package/dist/config/config-manager.js +198 -22
  32. package/dist/config/config-operation-service.js +35 -38
  33. package/dist/config/contact-bind-code.js +274 -0
  34. package/dist/config/contact-book-store.js +173 -5
  35. package/dist/config/contact-book.js +32 -0
  36. package/dist/config/contact-operation-service.js +9 -3
  37. package/dist/config/contact-request-service.js +331 -0
  38. package/dist/config/peer-role-resolver.js +3 -1
  39. package/dist/config/schema-registry.js +49 -24
  40. package/dist/config-store.js +14 -2
  41. package/dist/core/auth/agent-delegation.js +105 -11
  42. package/dist/core/auth/authorization-audit.js +6 -0
  43. package/dist/core/auth/operation-authorizer.js +8 -0
  44. package/dist/core/auth/operation-catalog.js +51 -3
  45. package/dist/core/auth/trigger-authorization.js +15 -0
  46. package/dist/core/bootstrap-service.js +2 -9
  47. package/dist/core/channel-loader.js +6 -2
  48. package/dist/core/command/command-handler.js +10 -13
  49. package/dist/core/command/connect-menu.js +229 -26
  50. package/dist/core/command/evol-menu-version-gate.js +38 -0
  51. package/dist/core/command/group-menu.js +421 -0
  52. package/dist/core/command/menu-handler.js +269 -85
  53. package/dist/core/command/menu-protocol.js +8 -2
  54. package/dist/core/command/menu-token-store.js +102 -0
  55. package/dist/core/command/role-menu.js +16 -0
  56. package/dist/core/command/slash-gate.js +1 -1
  57. package/dist/core/command/slash-handler.js +158 -31
  58. package/dist/core/daemon-file-cache.js +28 -0
  59. package/dist/core/event-catalog.js +29 -0
  60. package/dist/core/evolagent-registry.js +4 -39
  61. package/dist/core/handoff/runtime.js +162 -37
  62. package/dist/core/handoff/store.js +46 -2
  63. package/dist/core/handoff/types.js +1 -0
  64. package/dist/core/inference/text-inference.js +16 -74
  65. package/dist/core/message/file-markers.js +7 -0
  66. package/dist/core/message/im-renderer.js +18 -14
  67. package/dist/core/message/inbound-admission.js +134 -0
  68. package/dist/core/message/message-bridge.js +616 -74
  69. package/dist/core/message/message-log.js +1 -0
  70. package/dist/core/message/message-queue.js +185 -10
  71. package/dist/core/message/peer-mode.js +7 -8
  72. package/dist/core/message/response-engine.js +381 -79
  73. package/dist/core/permission/approval-gateway.js +17 -10
  74. package/dist/core/permission/ec-command-parser.js +101 -46
  75. package/dist/core/permission/tool-policy.js +69 -24
  76. package/dist/core/protected-paths.js +36 -11
  77. package/dist/core/session/session-fs-store.js +19 -4
  78. package/dist/core/session/session-manager.js +232 -4
  79. package/dist/core/session/session-mapper.js +2 -0
  80. package/dist/core/session/session-renew.js +125 -69
  81. package/dist/core/session/session-turn-coordinator.js +5 -1
  82. package/dist/eck/kit-renderer.js +12 -1
  83. package/dist/eck/message-renderer.js +79 -1
  84. package/dist/index.js +224 -89
  85. package/dist/ipc.js +81 -4
  86. package/dist/paths.js +3 -0
  87. package/dist/response-system/config-resolver.js +29 -0
  88. package/dist/response-system/coordinator.js +8 -32
  89. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  90. package/dist/response-system/index.js +1 -0
  91. package/dist/response-system/modes/single-session/index.js +7 -4
  92. package/dist/trigger/parser.js +55 -15
  93. package/dist/trigger/patch.js +4 -1
  94. package/dist/trigger/scheduler.js +441 -39
  95. package/dist/utils/cross-platform.js +8 -2
  96. package/dist/utils/error-dict.json +7 -0
  97. package/dist/utils/evolcore-version.js +21 -0
  98. package/dist/utils/logger.js +2 -2
  99. package/dist/utils/process-introspect.js +19 -3
  100. package/dist/utils/stable-semver.js +21 -0
  101. package/dist/utils/stats.js +33 -9
  102. package/kits/docs/channels/aun.md +4 -13
  103. package/kits/docs/evolcore/INDEX.md +1 -1
  104. package/kits/docs/evolcore/config.md +47 -2
  105. package/kits/docs/evolcore/contact.md +8 -3
  106. package/kits/docs/evolcore/group-rules.md +46 -4
  107. package/kits/docs/evolcore/group.md +4 -4
  108. package/kits/docs/evolcore/msg.md +5 -5
  109. package/kits/docs/evolcore/trigger.md +25 -8
  110. package/kits/docs/path-registry.md +36 -17
  111. package/kits/eck_message_manifest.json +12 -1
  112. package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
  113. package/kits/rules/01-overview.md +17 -7
  114. package/kits/rules/02-navigation.md +38 -18
  115. package/kits/rules/03-identity.md +28 -24
  116. package/kits/rules/04-relation.md +44 -28
  117. package/kits/rules/05-venue.md +31 -15
  118. package/kits/rules/06-channel.md +11 -7
  119. package/kits/schemas/_meta.json +18 -7
  120. package/kits/schemas/agent-config.schema.7.json +303 -0
  121. package/kits/schemas/agent-config.schema.8.json +304 -0
  122. package/kits/schemas/agent-config.schema.9.json +364 -0
  123. package/kits/schemas/contact-book.schema.3.json +67 -0
  124. package/kits/schemas/daemon.schema.1.json +3 -2
  125. package/kits/schemas/daemon.schema.2.json +101 -0
  126. package/kits/schemas/daemon.schema.3.json +123 -0
  127. package/kits/schemas/defaults.schema.2.json +85 -0
  128. package/kits/schemas/defaults.schema.3.json +73 -0
  129. package/kits/schemas/relation-config.schema.6.json +46 -0
  130. package/kits/schemas/relation-config.schema.7.json +59 -0
  131. package/kits/schemas/single-session.schema.2.json +57 -0
  132. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  133. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  134. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  135. package/kits/templates/roles/admin.json +46 -0
  136. package/kits/templates/roles/member.json +4 -0
  137. package/kits/templates/roles/visitor.json +4 -0
  138. package/kits/templates/system-fragments/channel.md +12 -2
  139. package/kits/templates/system-fragments/identity.md +3 -1
  140. package/kits/templates/system-fragments/relation.md +1 -1
  141. package/kits/templates/system-fragments/session.md +6 -2
  142. package/package.json +4 -2
  143. package/MIGRATION-0.5.0.md +0 -378
  144. package/ROLE_ACCESS_CONTROL.md +0 -174
  145. package/dist/channels/contact-bind-code.js +0 -134
  146. package/dist/channels/wecom-card.js +0 -101
  147. package/dist/channels/wecom-onboarding.js +0 -82
  148. package/dist/channels/wecom-state.js +0 -191
package/CHANGELOG.md CHANGED
@@ -3,6 +3,89 @@
3
3
  本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
4
4
  [`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
5
5
 
6
+ ## 0.0.5 (2026-07-31)
7
+
8
+ ### 消息准入
9
+
10
+ - 群聊准入统一为开放、联系人、Owner 三种行为,移除额外的渠道白名单配置。
11
+ - 访问策略的修改权限收归 Agent Owner。
12
+ - 完善联系人准入、拉黑与 Owner 权限边界的判定。
13
+ - Connect 群组入口展示实时群组数量。
14
+
15
+ ### 运行可靠性
16
+
17
+ - 修复旧版本遗留的 AUN 会话目录身份恢复:可唯一判定身份时安全迁移,无法判定时保持原样并告警。
18
+ - 修正 Linux 容器内进程启动时间与运行时长的计算偏差。
19
+
20
+ ### 初始化体验
21
+
22
+ - 优化初始化向导的目录输入,支持家目录简写、错误重试与显式跳过。
23
+ - 未完成初始化时自动运行向导,完成后继续启动服务。
24
+ - 未配置 self-agent 时按实际可用入口给出创建提示。
25
+
26
+ ## 0.0.4 (2026-07-30)
27
+
28
+ ### 联系人身份与消息准入
29
+
30
+ - 联系人身份绑定统一扩展到飞书、微信、钉钉、QQ 和企业微信渠道,收敛为显式 `/bind` 命令。
31
+ - 支持通过 AUN 私聊自助申请绑定码,并可在保留机器人配置时单独重绑渠道联系人。
32
+ - 新增私聊入站消息准入链路,覆盖联系人申请、审批、拦截与过期处理。
33
+ - 新增联系人请求服务,申请与状态流转改为原子操作,避免并发请求产生不一致。
34
+ - 限制单实例和进程级待绑定请求数量,并限制无效绑定码的连续尝试次数。
35
+ - 拒绝对不存在的联系人执行身份绑定和状态变更。
36
+ - 群聊准入按 `access.policyMode` 分级:`open` 对所有已配置渠道开放,`contacts` 将 Agent 已加入并接收到的
37
+ 群聊视为授权,`owners` 仅 Owner 消息开放。
38
+
39
+ ### 菜单与授权
40
+
41
+ - 新增群组菜单协议,支持群列表查询、群级 @ 提及和群规则加载策略的查询与更新。
42
+ - 群操作纳入权限目录、角色能力判定和审计约束,菜单与斜杠命令的可见性统一走运行时授权检查。
43
+ - 群级策略通过版本比对和原子写入避免并发覆盖。
44
+ - AUN 菜单增加 Evol 最低版本门禁和加密访问令牌校验。
45
+ - 菜单令牌改为持久化哈希存储并按空闲时间失效,且不再进入追踪日志和观察转发。
46
+ - Connect 菜单准入策略的校验与写入收敛为原子操作,补齐策略域、操作目录和内置角色的授权边界。
47
+
48
+ ### 跨会话协作
49
+
50
+ - 跨会话消息必须显式声明 `return none` 或 `required`,Trigger 禁止使用 `required` 等待回流。
51
+ - 聚合同一目标会话的待处理请求和同一来源身份的返回结果,避免重复触发模型轮次。
52
+ - `ec handoff list` 输出新增 `RETURN_POLICY` 列。
53
+ - Trigger 创建必须显式指定 `--exec script|trigger-session|target-session`,不再隐式推断。
54
+ - 新增 `--prompt-file <path>` 承载多行或超长 prompt,与 `--prompt`、`--file` 互斥。
55
+
56
+ ### 安全边界
57
+
58
+ - AUN keystore `encryptionSeed` 收口为内置常量,发现自定义配置立即失败关闭。
59
+ - 为 Codex 受控命令绑定一次性委托票据,任务或会话撤销时同步清理。
60
+ - 收紧 EC 字面量解析,按空命令、控制字符、未转义展开、引号错误和命令组合分类失败关闭。
61
+ - Agent 角色目录从不可读 H 类调整为只读 L 类,RoleService 为唯一修改入口。
62
+ - L-class 扩展对 `grep`、`rg`、`find`、`sed` 的安全只读支持。
63
+
64
+ ### 运行可靠性
65
+
66
+ - 守护进程退出时冻结消息队列、关闭任务终态、等待中断屏障并保留可恢复主消息。
67
+ - Trigger 持久化执行检查点与会话绑定,支持重启续跑、期限继承及启动并发去重。
68
+ - 微信渠道区分连接、断开和重新认证状态,令牌刷新后重试待发送消息。
69
+ - API 重试增加单次无事件超时,中断卡住的 runner 后继续退避重试。
70
+ - Session Renew 判定模型改由响应模式桶的 `auxiliaryModel`/`auxiliaryEffort` 决定。
71
+ - 直连推理凭据改为整对解析,`apiKey` 与 `baseUrl` 必须来自同一来源。
72
+ - 入队前写入最终会话 ID 与已解析身份,执行阶段不再重新计算角色。
73
+ - Claude Agent SDK 固定到 `0.3.220`,Codex 自动压缩阈值调整为 185000 token。
74
+
75
+ ### 管理界面
76
+
77
+ - 会话列表新增类型徽标与筛选,消息视图展示 Agent 显示名。
78
+ - 用量视图的模型与粒度筛选改为变更即查询,模型访问明细默认停留在“全部”。
79
+ - 补全联系人及 AUN 群名、渠道色彩、Runner 子任务展示和 Trigger 模型归属提示。
80
+
81
+ ### 修复
82
+
83
+ - 规范 Group Menu 非法请求的协议错误码映射。
84
+ - 保留私聊和群聊 menu 请求的加密状态、话题信息与传输回复上下文。
85
+ - 关联文件统一发送为标准 file 载荷,先返回 `menu.response` 再执行文件发送。
86
+ - 统一 `agent.md` 展示名称解析与缓存,`identity` fragment 仅在名称存在时渲染 `selfName`。
87
+ - 飞书私聊出站补齐 `replyContext.peerId` 与 `routeId` 关联。
88
+
6
89
  ## 0.0.3 (2026-07-25)
7
90
 
8
91
  ### 多渠道交互
package/README.md CHANGED
@@ -43,7 +43,7 @@ EvolCore 是一个轻量级 AI Agent 网关系统。它为 Claude Code / Codex
43
43
  1. **消息渠道层** (`src/channels/`) - Feishu + WeChat + DingTalk + QQBot + WeCom + AUN 网络
44
44
  2. **消息队列层** (`src/core/message/message-queue.ts`) - 会话级串行处理 + 中断支持
45
45
  3. **命令处理层** (`src/core/command/`) - 斜杠命令处理(slash-handler / menu-handler / command-handler)
46
- 4. **消息处理层** (`src/core/message/message-processor.ts`) - 统一事件处理引擎
46
+ 4. **消息处理层** (`src/core/message/response-engine.ts`) - 统一事件处理引擎
47
47
  5. **会话管理层** (`src/core/session/session-manager.ts`) - 多项目会话管理
48
48
  6. **交互路由层** (`src/core/interaction-router.ts`) - 卡片交互回调注册与路由
49
49
  7. **会话存储层** - JSONL 文件(CLI 共用)+ 文件系统(每 chat 一个目录,含 active.json / meta_*.jsonl / messages.jsonl / health.jsonl)
@@ -63,7 +63,7 @@ MessageQueue.enqueue(streamKey, message)
63
63
  ↓ 否 ↓
64
64
  MessageQueue.processNext() ←──────────────┘
65
65
 
66
- MessageProcessor.processMessage()
66
+ ResponseEngine.processMessage()
67
67
 
68
68
  ├─ 解析会话和项目路径
69
69
  ├─ 创建 StreamFlusher(3 秒批量发送)
@@ -78,12 +78,26 @@ MessageProcessor.processMessage()
78
78
  ### 环境要求
79
79
 
80
80
  - **操作系统**:macOS / Linux / Windows
81
- - **Node.js** >= 18
81
+ - **Node.js** >= 22(依赖 `node:sqlite`)
82
82
  - **Claude Code** >= 2.1.32(`npm install -g @anthropic-ai/claude-code`)
83
83
 
84
84
  ### 1. 安装
85
85
 
86
- **npm 全局安装**(推荐):
86
+ **一行安装(macOS / Linux)**:
87
+
88
+ ```bash
89
+ curl -fsSL https://raw.githubusercontent.com/eLeanwang/evolcore/main/scripts/install.sh | bash
90
+ ```
91
+
92
+ **Windows PowerShell**:
93
+
94
+ ```powershell
95
+ irm https://raw.githubusercontent.com/eLeanwang/evolcore/main/scripts/install.ps1 | iex
96
+ ```
97
+
98
+ 安装器会在新实例上自动进入 `ec init` 交互向导,完成后运行 `ec start` 和 `ec status`。已有配置的升级场景会跳过自动初始化,不会停止或重启服务。详细流程见 [一键安装脚本说明](docs/evolcore-install-script.md)。
99
+
100
+ **npm 全局安装**:
87
101
 
88
102
  ```bash
89
103
  npm install -g evolcore
@@ -128,7 +142,7 @@ ec init aun
128
142
 
129
143
  交互式引导完成以下配置:
130
144
  - 环境检查(Node.js >= 22、claude CLI、SDK 版本)
131
- - 渠道选择(飞书/微信)并扫码登录
145
+ - 渠道选择(飞书 / 微信 / 钉钉 / QQ 频道 / 企业微信 / AUN)并扫码登录
132
146
  - 默认项目路径
133
147
  - 模型选择(sonnet/opus/haiku)
134
148
  - owner 配置:默认走 **Evol App 扫码绑定**,若无 Evol App 可手工录入 AID 兜底
@@ -207,7 +221,8 @@ evolcore/
207
221
  │ │ │ └── slash-gate.ts # 权限前置拦截
208
222
  │ │ ├── message/
209
223
  │ │ │ ├── message-bridge.ts # 渠道 ↔ 核心消息桥
210
- │ │ │ ├── message-processor.ts # 统一消息处理引擎
224
+ │ │ │ ├── response-engine.ts # 统一响应处理引擎
225
+ │ │ │ ├── inbound-admission.ts # 私聊入站准入
211
226
  │ │ │ ├── message-queue.ts # 消息队列(串行+中断)
212
227
  │ │ │ ├── message-cache.ts # 消息缓存
213
228
  │ │ │ ├── message-log.ts # 每 chat 的 messages.jsonl
@@ -216,11 +231,11 @@ evolcore/
216
231
  │ │ │ ├── adapters/ # 各后端会话文件适配器
217
232
  │ │ │ ├── session-fs-store.ts # 文件系统存储原语
218
233
  │ │ │ └── session-manager.ts # 会话管理(多项目支持)
219
- │ │ ├── trigger/ # 触发器引擎
234
+ │ │ ├── auth/ # 授权审计与 Trigger 鉴权
220
235
  │ │ ├── evolagent.ts # EvolAgent 实体
221
236
  │ │ ├── evolagent-registry.ts # Agent 注册表(扫描/路由/热重载)
222
237
  │ │ ├── interaction-router.ts # 卡片交互回调路由
223
- │ │ └── permission.ts # 权限网关
238
+ │ │ └── protected-paths.ts # 受保护路径分级
224
239
  │ ├── channels/
225
240
  │ │ ├── feishu.ts # 飞书 WebSocket 渠道
226
241
  │ │ ├── wechat.ts # 微信 ClawBot 渠道
@@ -228,13 +243,18 @@ evolcore/
228
243
  │ │ ├── qqbot.ts # QQ 频道渠道
229
244
  │ │ ├── wecom.ts # 企业微信 AI Bot 渠道
230
245
  │ │ └── aun.ts # AUN Mesh 网络渠道
246
+ │ ├── config/ # 配置分层、访问策略、联系人绑定
247
+ │ ├── eck/ # ECK 上下文组装(manifest / kit 渲染)
248
+ │ ├── response-system/ # 响应模式决策与参数解析
249
+ │ ├── trigger/ # 触发器引擎
250
+ │ ├── stats/ # 用量统计与计费
231
251
  │ ├── cli/ # CLI 命令
232
252
  │ ├── utils/ # 工具函数
233
253
  │ ├── types.ts # 类型定义
234
254
  │ ├── config-store.ts # 配置加载
235
255
  │ ├── paths.ts # 路径解析
236
256
  │ └── index.ts # 主入口
237
- └── kits/ # 共享上下文模板
257
+ └── kits/ # 共享上下文模板(rules / docs / schemas)
238
258
  ```
239
259
 
240
260
  ## 斜杠命令
@@ -252,6 +272,11 @@ evolcore/
252
272
  - `/check` - 系统健康检查(摘要)
253
273
  - `/help` - 显示所有命令
254
274
 
275
+ **身份绑定**:
276
+ - `/bind <6 位绑定码>` - 将当前 IM 账号绑定到已登记的联系人身份(仅私聊)。
277
+ 支持飞书、微信、钉钉、QQ 频道、企业微信。绑定码有效期 10 分钟,可由
278
+ `ec init <channel>` 生成,或在目标 Agent 的 AUN 私聊发送 `/bind <channel>` 自助申请。
279
+
255
280
  ### 管理员级命令(Admin+ 可用)
256
281
 
257
282
  **项目**:
@@ -280,6 +305,16 @@ evolcore/
280
305
  - `/restart` - 重启服务(自愈机制)
281
306
  - `/repair` - 检查并修复会话
282
307
 
308
+ ### 联系人与关系管理(menu 协议)
309
+
310
+ `/connect` 与 `/group` 是 menu 协议操作,通过 Evol App 或 EC Web 调用,不是 IM 里的斜杠命令。
311
+
312
+ - **`/connect`** - 联系人与关系管理:联系人列表、群组列表、角色分配、管理员授权、拉黑状态。
313
+ 准入策略(`connect.access.write`)为 owner 专属,角色分配与 admin 授权委派给 `role` menu 执行,
314
+ 复用其 agent 级互斥锁、CAS revision 与审计链。
315
+ - **`/group`** - 群组管理:群列表、群级 @ 提及策略、群规则加载策略的查询与更新。
316
+ 群操作纳入权限目录与角色能力判定,通过版本比对和原子写入避免并发覆盖。
317
+
283
318
  ### ⚠️ 进程级 menu 操作鉴权(v3.2 Breaking)
284
319
 
285
320
  进程级 menu 操作(`/system restart/upgrade`、`/agent` agent 生命周期管理)的鉴权已迁移到
@@ -309,7 +344,7 @@ v3.2 新增进程级身份标识。启动时自动生成 `ec+5位数字.agentid.
309
344
  ## 技术栈
310
345
 
311
346
  - **运行时**:Node.js >= 22 + TypeScript(ES modules)
312
- - **AI 后端**:@anthropic-ai/claude-agent-sdk >= 0.3.211、Codex CLI app-server、Gemini CLI
347
+ - **AI 后端**:@anthropic-ai/claude-agent-sdk 0.3.220(固定版本)、Codex CLI app-server、Gemini CLI
313
348
  - **消息渠道**:飞书(@larksuiteoapi/node-sdk)、微信(ClawBot ilink API)、钉钉(dingtalk-stream)、QQ频道(pure-qqbot)、企业微信(AI Bot API)、AUN 网络
314
349
  - **数据存储**:文件系统(per-chat 目录) + JSONL(CLI 共用)
315
350
  - **测试框架**:Vitest
@@ -6,6 +6,13 @@ import { spawn } from 'child_process';
6
6
  const MAX_OUTPUT_BYTES = 1024 * 1024;
7
7
  const EC_EXECUTABLES = new Set(['ec']);
8
8
  const HOST_PROCESS_EXECUTABLES = new Set(['ps', 'pgrep', 'pidof', 'kill', 'pkill', 'killall']);
9
+ const HEAD_EXECUTABLE = ['/usr/bin/head', '/bin/head'].find(candidate => {
10
+ try {
11
+ return fs.statSync(candidate).isFile();
12
+ } catch {
13
+ return false;
14
+ }
15
+ });
9
16
 
10
17
  function fail(message) {
11
18
  process.stderr.write(`ec-safe-output: ${message}\n`);
@@ -34,6 +41,33 @@ function validateCommand(payload) {
34
41
  return !payload.argv.some(arg => /\bnode(?:js|\.exe)?\b/i.test(arg));
35
42
  }
36
43
 
44
+ function validateStdinOnlyHeadArgs(args) {
45
+ if (args === undefined) return true;
46
+ if (!Array.isArray(args) || args.some(arg => typeof arg !== 'string')) return false;
47
+ let optionsEnded = false;
48
+ let expectsOptionValue = false;
49
+ for (const arg of args) {
50
+ if (expectsOptionValue) {
51
+ expectsOptionValue = false;
52
+ continue;
53
+ }
54
+ if (optionsEnded) {
55
+ if (arg !== '-') return false;
56
+ continue;
57
+ }
58
+ if (arg === '--') {
59
+ optionsEnded = true;
60
+ continue;
61
+ }
62
+ if (arg === '-') continue;
63
+ if (!arg.startsWith('-')) return false;
64
+ if (arg === '-n' || arg === '-c' || arg === '--lines' || arg === '--bytes') {
65
+ expectsOptionValue = true;
66
+ }
67
+ }
68
+ return true;
69
+ }
70
+
37
71
  function resolveExecutionPaths(payload) {
38
72
  if (typeof payload.workspace !== 'string' || typeof payload.cwd !== 'string') fail('invalid workspace');
39
73
  let workspace;
@@ -87,36 +121,19 @@ function openOutputFile(payload, paths) {
87
121
  const payload = decodePayload(process.argv[2]);
88
122
  if (!payload) fail('invalid payload');
89
123
  if (!validateCommand(payload)) fail('command is not permitted');
90
- if (payload.headLines !== undefined
91
- && (!Number.isSafeInteger(payload.headLines) || payload.headLines < 1 || payload.headLines > 10_000)) {
92
- fail('invalid head line limit');
93
- }
124
+ if (!validateStdinOnlyHeadArgs(payload.headArgs)) fail('head may only consume stdin');
125
+ if (payload.headArgs !== undefined && !HEAD_EXECUTABLE) fail('trusted head executable is unavailable');
94
126
 
95
127
  const paths = resolveExecutionPaths(payload);
96
128
  const output = openOutputFile(payload, paths);
97
129
  let bytesWritten = 0;
98
- let linesWritten = 0;
99
130
  let limitReached = false;
100
131
  let writeFailed = false;
101
132
 
102
133
  function writeLimited(chunk) {
103
134
  if (limitReached || writeFailed) return;
104
135
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
105
- let allowed = Math.min(buffer.length, MAX_OUTPUT_BYTES - bytesWritten);
106
- if (payload.headLines !== undefined) {
107
- let cursor = 0;
108
- while (cursor < allowed) {
109
- const newline = buffer.indexOf(0x0a, cursor);
110
- if (newline < 0 || newline >= allowed) break;
111
- linesWritten++;
112
- cursor = newline + 1;
113
- if (linesWritten >= payload.headLines) {
114
- allowed = cursor;
115
- limitReached = true;
116
- break;
117
- }
118
- }
119
- }
136
+ const allowed = Math.min(buffer.length, MAX_OUTPUT_BYTES - bytesWritten);
120
137
  if (allowed <= 0) {
121
138
  limitReached = true;
122
139
  return;
@@ -132,13 +149,35 @@ function writeLimited(chunk) {
132
149
  }
133
150
  }
134
151
 
152
+ const head = payload.headArgs === undefined
153
+ ? undefined
154
+ : spawn(HEAD_EXECUTABLE, payload.headArgs, {
155
+ cwd: paths.cwd,
156
+ env: process.env,
157
+ stdio: ['pipe', 'pipe', 'inherit'],
158
+ });
159
+ if (head) {
160
+ head.stdout.on('data', writeLimited);
161
+ // Early-exit options such as --help or a small -n/-c close stdin before the
162
+ // EC process finishes. Ignore EPIPE and keep draining EC output below.
163
+ head.stdin.on('error', () => {});
164
+ }
165
+
135
166
  const child = spawn(payload.argv[0], payload.argv.slice(1), {
136
167
  cwd: paths.cwd,
137
168
  env: process.env,
138
169
  stdio: ['ignore', 'pipe', payload.mergeStderr ? 'pipe' : 'inherit'],
139
170
  });
140
- child.stdout.on('data', writeLimited);
141
- if (payload.mergeStderr) child.stderr.on('data', writeLimited);
171
+ const sourceStreams = [child.stdout, ...(payload.mergeStderr ? [child.stderr] : [])];
172
+ if (head) {
173
+ for (const stream of sourceStreams) {
174
+ // The no-op data listener keeps draining the source if head exits early.
175
+ stream.on('data', () => {});
176
+ stream.pipe(head.stdin, { end: false });
177
+ }
178
+ } else {
179
+ for (const stream of sourceStreams) stream.on('data', writeLimited);
180
+ }
142
181
 
143
182
  child.once('error', error => {
144
183
  if (output) {
@@ -148,7 +187,13 @@ child.once('error', error => {
148
187
  fail(`unable to start command: ${error.message}`);
149
188
  });
150
189
 
151
- child.once('close', code => {
190
+ let sourceClosed = false;
191
+ let sourceCode = 1;
192
+ let headClosed = !head;
193
+ let headCode = 1;
194
+
195
+ function finish() {
196
+ if (!sourceClosed || !headClosed) return;
152
197
  if (output) {
153
198
  try { fs.closeSync(output.fd); } catch { writeFailed = true; }
154
199
  }
@@ -157,5 +202,25 @@ child.once('close', code => {
157
202
  fail('failed to write command output');
158
203
  }
159
204
  if (output) process.stderr.write(`Saved output to ${output.target}\n`);
160
- process.exitCode = code ?? 1;
205
+ process.exitCode = head ? headCode : sourceCode;
206
+ }
207
+
208
+ if (head) {
209
+ head.once('error', error => {
210
+ writeFailed = true;
211
+ process.stderr.write(`ec-safe-output: unable to start head: ${error.message}\n`);
212
+ });
213
+ head.once('close', code => {
214
+ headCode = code ?? 1;
215
+ headClosed = true;
216
+ for (const stream of sourceStreams) stream.unpipe(head.stdin);
217
+ finish();
218
+ });
219
+ }
220
+
221
+ child.once('close', code => {
222
+ sourceCode = code ?? 1;
223
+ sourceClosed = true;
224
+ if (head && !head.stdin.destroyed) head.stdin.end();
225
+ finish();
161
226
  });
@@ -32,6 +32,22 @@ export function normalizeBaseagent(input) {
32
32
  const key = String(input || '').trim().toLowerCase().replace(/_/g, '-');
33
33
  return BASEAGENT_ALIASES[key] || { canonical: 'unknown', displayName: input ? String(input) : 'Unknown' };
34
34
  }
35
+ function directInferencePair(apiKey, baseUrl) {
36
+ const normalizedApiKey = directInferenceValue(apiKey);
37
+ const normalizedBaseUrl = directInferenceValue(baseUrl);
38
+ if (!normalizedApiKey || !normalizedBaseUrl)
39
+ return undefined;
40
+ return { apiKey: normalizedApiKey, baseUrl: normalizedBaseUrl };
41
+ }
42
+ function directInferenceValue(...values) {
43
+ for (const value of values) {
44
+ const normalized = value?.trim();
45
+ const lower = normalized?.toLowerCase();
46
+ if (normalized && !lower?.includes('your-') && !lower?.includes('placeholder'))
47
+ return normalized;
48
+ }
49
+ return undefined;
50
+ }
35
51
  function loadClaudeSettings() {
36
52
  try {
37
53
  const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
@@ -42,6 +58,21 @@ function loadClaudeSettings() {
42
58
  catch { }
43
59
  return {};
44
60
  }
61
+ /**
62
+ * Resolve a complete credential/endpoint pair for direct Anthropic HTTP calls.
63
+ * A pair must come from one source so partial configuration cannot be mixed
64
+ * across Agent config, the process environment, and Claude Code settings.
65
+ */
66
+ export function resolveAnthropicDirectConfig(override) {
67
+ const explicit = directInferencePair(override?.apiKey, override?.baseUrl);
68
+ if (explicit)
69
+ return explicit;
70
+ const environment = directInferencePair(directInferenceValue(process.env.ANTHROPIC_AUTH_TOKEN, process.env.ANTHROPIC_API_KEY), process.env.ANTHROPIC_BASE_URL);
71
+ if (environment)
72
+ return environment;
73
+ const settings = loadClaudeSettings();
74
+ return directInferencePair(directInferenceValue(settings.env?.ANTHROPIC_AUTH_TOKEN, settings.env?.ANTHROPIC_API_KEY), settings.env?.ANTHROPIC_BASE_URL);
75
+ }
45
76
  export function resolveAnthropicConfig(config, override) {
46
77
  const settings = loadClaudeSettings();
47
78
  const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
@@ -103,6 +134,21 @@ function loadCodexSettings() {
103
134
  catch { }
104
135
  return {};
105
136
  }
137
+ /**
138
+ * Resolve a complete credential/endpoint pair for direct OpenAI HTTP calls.
139
+ * auth.json and config.toml together form the Codex SDK source; fields are
140
+ * never combined with a partial Agent or process-environment configuration.
141
+ */
142
+ export function resolveOpenaiDirectConfig(override) {
143
+ const explicit = directInferencePair(override?.apiKey, override?.baseUrl);
144
+ if (explicit)
145
+ return explicit;
146
+ const environment = directInferencePair(process.env.OPENAI_API_KEY, process.env.OPENAI_BASE_URL);
147
+ if (environment)
148
+ return environment;
149
+ const settings = loadCodexSettings();
150
+ return directInferencePair(settings.apiKey, settings.baseUrl);
151
+ }
106
152
  export function resolveOpenaiConfig(config, override) {
107
153
  const codexSettings = loadCodexSettings();
108
154
  const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
@@ -855,20 +855,6 @@ export class AgentRunner {
855
855
  const customText = values?.custom_text;
856
856
  finish({ kind: 'answered', value: typeof customText === 'string' && customText.trim() ? customText.trim() : null });
857
857
  }
858
- else if (action === '_show_input') {
859
- if (permCtx?.interceptNextMessage) {
860
- permCtx.interactionRouter?.markWaiting(sessionId);
861
- sendPrompt?.('✏️ 请直接发送你的自定义回复').catch(() => { });
862
- permCtx.interceptNextMessage(sessionId, (msg) => {
863
- permCtx.interactionRouter?.unmarkWaiting(sessionId);
864
- const text = (msg.content || '').trim();
865
- finish({ kind: 'answered', value: text || null });
866
- });
867
- }
868
- else {
869
- finish({ kind: 'cancelled', reason: 'explicit_cancel' });
870
- }
871
- }
872
858
  else if (action === 'submit' && q.multiSelect && values) {
873
859
  const selected = [];
874
860
  q.options.forEach((opt, idx) => {
@@ -900,7 +886,6 @@ export class AgentRunner {
900
886
  });
901
887
  if (settlement.kind === 'cancelled') {
902
888
  try {
903
- permCtx.cancelIntercept?.(sessionId);
904
889
  try {
905
890
  await permCtx.turn?.onInteractionSettled?.(requestId, settlement.reason === 'timeout' ? 'timed_out' : 'cancelled');
906
891
  }
@@ -1133,20 +1118,6 @@ export class AgentRunner {
1133
1118
  const feedback = typeof values?.custom_text === 'string' ? values.custom_text.trim() : '';
1134
1119
  deny(feedback || '用户提交了反馈');
1135
1120
  }
1136
- else if (trimmed === '_show_input') {
1137
- if (permCtx.interceptNextMessage) {
1138
- router.markWaiting(sessionId);
1139
- sendPrompt?.('✏️ 请直接发送你的反馈意见').catch(() => { });
1140
- permCtx.interceptNextMessage(sessionId, msg => {
1141
- router.unmarkWaiting(sessionId);
1142
- const feedback = (msg.content || '').trim();
1143
- deny(feedback || '用户提交了反馈');
1144
- });
1145
- }
1146
- else {
1147
- deny('用户提交了反馈');
1148
- }
1149
- }
1150
1121
  else if (trimmed === '2' || trimmed.toLowerCase() === 'reject' || trimmed === '拒绝') {
1151
1122
  deny('用户拒绝了计划');
1152
1123
  }
@@ -1173,7 +1144,6 @@ export class AgentRunner {
1173
1144
  });
1174
1145
  try {
1175
1146
  if (settlement.kind === 'cancelled') {
1176
- permCtx.cancelIntercept?.(sessionId);
1177
1147
  try {
1178
1148
  await permCtx.turn?.onInteractionSettled?.(requestId, settlement.reason === 'timeout' ? 'timed_out' : 'cancelled');
1179
1149
  }
@@ -1886,7 +1856,7 @@ export class AgentRunner {
1886
1856
  }
1887
1857
  // These tools need the dedicated canUseTool interaction handlers below.
1888
1858
  if (toolName === 'AskUserQuestion' || toolName === 'ExitPlanMode') {
1889
- return hookDecision('defer', undefined, updatedInput);
1859
+ return hookDecision('ask', undefined, updatedInput);
1890
1860
  }
1891
1861
  if (callPermissionMode === 'readonly') {
1892
1862
  const permCtx = this.permissionContexts.get(sessionId);
@@ -167,6 +167,74 @@ export class CodexAppServerClient {
167
167
  await this.request('thread/metadata/update', params);
168
168
  return true;
169
169
  }
170
+ async threadShellCommand(threadId, command, timeoutMs = 10_000) {
171
+ let commandItemId;
172
+ let output = '';
173
+ let settle;
174
+ let fail;
175
+ const completion = new Promise((resolve, reject) => {
176
+ settle = resolve;
177
+ fail = reject;
178
+ });
179
+ const unsubscribe = this.onNotification(notification => {
180
+ const params = (notification.params || {});
181
+ const notificationThreadId = params.threadId ?? params.thread_id;
182
+ if (notificationThreadId !== threadId)
183
+ return;
184
+ const item = params.item && typeof params.item === 'object'
185
+ ? params.item
186
+ : undefined;
187
+ const itemId = typeof item?.id === 'string'
188
+ ? item.id
189
+ : typeof params.itemId === 'string'
190
+ ? params.itemId
191
+ : undefined;
192
+ const source = item?.source;
193
+ const isUserShell = source === 'userShell' || source === 'user_shell';
194
+ if (notification.method === 'item/started'
195
+ && item?.type === 'commandExecution'
196
+ && isUserShell
197
+ && (item.command === command || commandItemId === undefined)) {
198
+ commandItemId = itemId;
199
+ return;
200
+ }
201
+ if (notification.method === 'item/commandExecution/outputDelta'
202
+ && commandItemId
203
+ && itemId === commandItemId
204
+ && typeof params.delta === 'string') {
205
+ output += params.delta;
206
+ return;
207
+ }
208
+ if (notification.method === 'item/completed'
209
+ && item?.type === 'commandExecution'
210
+ && isUserShell
211
+ && (!commandItemId || itemId === commandItemId)) {
212
+ const aggregated = typeof item.aggregatedOutput === 'string'
213
+ ? item.aggregatedOutput
214
+ : typeof item.aggregated_output === 'string'
215
+ ? item.aggregated_output
216
+ : output;
217
+ const exitCode = typeof item.exitCode === 'number'
218
+ ? item.exitCode
219
+ : typeof item.exit_code === 'number'
220
+ ? item.exit_code
221
+ : undefined;
222
+ settle({ output: aggregated, ...(exitCode !== undefined ? { exitCode } : {}) });
223
+ }
224
+ });
225
+ const timer = setTimeout(() => {
226
+ fail(new Error(`thread/shellCommand timed out for thread ${threadId}`));
227
+ }, timeoutMs);
228
+ timer.unref?.();
229
+ try {
230
+ await this.request('thread/shellCommand', { threadId, command });
231
+ return await completion;
232
+ }
233
+ finally {
234
+ clearTimeout(timer);
235
+ unsubscribe();
236
+ }
237
+ }
170
238
  async turnInterrupt(threadId, turnId) {
171
239
  await this.request('turn/interrupt', { threadId, turnId });
172
240
  return true;