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.
- package/CHANGELOG.md +12 -0
- package/bin/ec-safe-output.js +270 -54
- package/dist/agents/baseagent-availability.js +37 -1
- package/dist/agents/baseagent.js +19 -9
- package/dist/agents/claude-runner.js +171 -13
- package/dist/agents/codex/app-server-client.js +6 -2
- package/dist/agents/codex/diagnostics.js +4 -3
- package/dist/agents/codex-runner.js +396 -95
- package/dist/agents/gemini-runner.js +35 -17
- package/dist/aun/group/history.js +130 -90
- package/dist/aun/group/operations.js +1 -3
- package/dist/aun/menu/transport.js +5 -3
- package/dist/aun/msg/managed-operation.js +70 -0
- package/dist/aun/msg/p2p.js +1 -3
- package/dist/aun/msg/upload.js +2 -2
- package/dist/aun/outbox.js +1 -37
- package/dist/channels/aun.js +249 -89
- package/dist/channels/daemon.js +18 -1
- package/dist/channels/feishu.js +1 -2
- package/dist/cli/agent-command.js +23 -3
- package/dist/cli/agent-create.js +68 -2
- package/dist/cli/agent.js +12 -2
- package/dist/cli/aun-commands.js +87 -13
- package/dist/cli/cli-argv.js +26 -0
- package/dist/cli/daemon-commands.js +40 -10
- package/dist/cli/data-command.js +4 -4
- package/dist/cli/handoff-command.js +2 -0
- package/dist/cli/index.js +18 -1
- package/dist/cli/managed-command-guard.js +10 -0
- package/dist/cli/memory-command.js +441 -0
- package/dist/cli/restart-monitor.js +62 -6
- package/dist/cli/skill-command.js +41 -0
- package/dist/cli/task-context.js +2 -23
- package/dist/cli/trigger-command.js +9 -1
- package/dist/cli/watch-msg-diag.js +209 -0
- package/dist/config/config-field-policy.js +52 -3
- package/dist/config/config-manager.js +17 -4
- package/dist/config/contact-book-store.js +2 -2
- package/dist/config/mention-mode.js +7 -2
- package/dist/config/merge.js +28 -9
- package/dist/config/resolved-config-op.js +1 -1
- package/dist/config/runtime-env.js +137 -0
- package/dist/config/schema-registry.js +11 -1
- package/dist/config-store.js +10 -10
- package/dist/core/audit/event-key.js +3 -1
- package/dist/core/audit/log-integrity.js +4 -0
- package/dist/core/auth/agent-delegation.js +2 -1
- package/dist/core/auth/auth-gateway.js +13 -6
- package/dist/core/auth/authorization-audit.js +54 -19
- package/dist/core/auth/operation-authorizer.js +17 -0
- package/dist/core/auth/operation-catalog.js +9 -0
- package/dist/core/causation/context.js +5 -1
- package/dist/core/command/agent-control.js +1 -2
- package/dist/core/command/command-handler.js +51 -23
- package/dist/core/command/memory-slash.js +270 -0
- package/dist/core/command/menu-catalog.js +1 -0
- package/dist/core/command/menu-handler.js +315 -60
- package/dist/core/command/menu-protocol.js +42 -4
- package/dist/core/command/slash-gate.js +10 -10
- package/dist/core/command/slash-handler.js +206 -64
- package/dist/core/data-migration.js +282 -505
- package/dist/core/event-catalog.js +26 -0
- package/dist/core/evolagent-registry.js +6 -0
- package/dist/core/evolagent.js +0 -10
- package/dist/core/handoff/store.js +1 -20
- package/dist/core/inference/text-inference.js +57 -1
- package/dist/core/memory/consolidation-context.js +505 -0
- package/dist/core/memory/consolidation-ops-records.js +124 -0
- package/dist/core/memory/consolidation-trigger.js +394 -0
- package/dist/core/memory/index-manager.js +308 -0
- package/dist/core/memory/launch-consolidation.js +395 -0
- package/dist/core/memory/memory-admin.js +234 -0
- package/dist/core/memory/memory-document.js +100 -0
- package/dist/core/memory/memory-session-policy.js +41 -0
- package/dist/core/memory/memory-signature.js +257 -0
- package/dist/core/memory/memory-writer.js +432 -0
- package/dist/core/memory/staleness.js +117 -0
- package/dist/core/message/auto-mention-gate.js +454 -0
- package/dist/core/message/im-renderer.js +69 -16
- package/dist/core/message/message-bridge.js +287 -21
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +114 -27
- package/dist/core/message/pending-hints.js +329 -161
- package/dist/core/message/response-engine.js +567 -80
- package/dist/core/permission/ec-command-parser.js +1029 -80
- package/dist/core/permission/protected-paths.js +376 -126
- package/dist/core/permission/sandbox-runtime.js +40 -12
- package/dist/core/permission/tool-error-code.js +96 -13
- package/dist/core/permission/tool-policy.js +276 -23
- package/dist/core/role/runtime-policy.js +3 -7
- package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
- package/dist/core/session/session-fs-store.js +0 -76
- package/dist/core/session/session-idle-monitor.js +355 -0
- package/dist/core/session/session-manager.js +478 -17
- package/dist/core/session/session-mapper.js +33 -16
- package/dist/core/session/session-renew.js +217 -155
- package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
- package/dist/core/skill/global-watcher.js +282 -0
- package/dist/core/skill/index-generator.js +84 -0
- package/dist/core/skill/startup.js +73 -0
- package/dist/core/skill/switch-resolver.js +115 -0
- package/dist/core/task/task-activity-store.js +219 -0
- package/dist/eck/kit-renderer.js +30 -3
- package/dist/eck/manifest-engine.js +26 -4
- package/dist/index.js +708 -18
- package/dist/ipc.js +564 -2
- package/dist/paths.js +30 -9
- package/dist/response-system/engines/v1/proactive-flow.js +116 -15
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/feedback.js +1 -0
- package/dist/trigger/manager.js +0 -10
- package/dist/trigger/scheduler.js +11 -0
- package/dist/trigger/validation.js +22 -0
- package/dist/utils/avatar-upload.js +59 -18
- package/dist/utils/error-dict.json +6 -3
- package/dist/utils/error-utils.js +27 -0
- package/dist/utils/logger.js +32 -10
- package/dist/utils/media-cache.js +84 -0
- package/dist/utils/msg-diag.js +97 -0
- package/dist/utils/welcome.js +37 -8
- package/kits/docs/AUTHORING.md +314 -0
- package/kits/docs/GUIDE.md +2 -0
- package/kits/docs/INDEX.md +2 -0
- package/kits/docs/aun/CHEATSHEET.md +4 -0
- package/kits/docs/aun/SDK.md +60 -0
- package/kits/docs/context-assembly.md +10 -6
- package/kits/docs/evolcore/INDEX.md +17 -3
- package/kits/docs/evolcore/agent-create.md +24 -0
- package/kits/docs/evolcore/agent.md +5 -2
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/ec-capabilities.md +364 -0
- package/kits/docs/evolcore/group.md +34 -18
- package/kits/docs/evolcore/memory-architecture.md +78 -0
- package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
- package/kits/docs/evolcore/memory-consolidation.md +263 -0
- package/kits/docs/evolcore/memory-tags.md +63 -0
- package/kits/docs/evolcore/memory.md +110 -0
- package/kits/docs/evolcore/menu-client.md +27 -11
- package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
- package/kits/docs/evolcore/menu-protocol.md +213 -0
- package/kits/docs/evolcore/msg.md +2 -0
- package/kits/docs/evolcore/trigger.md +7 -5
- package/kits/docs/path-registry.md +6 -8
- package/kits/docs/prompt-loading-architecture.md +5 -3
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_manifest.json +294 -88
- package/kits/eck_manifest.memory.json +257 -0
- package/kits/rules/03-identity.md +8 -8
- package/kits/rules/05-venue.md +4 -2
- package/kits/rules/06-channel.md +1 -1
- package/kits/schemas/_meta.json +276 -47
- package/kits/schemas/agent-config.schema.12.json +38 -2
- package/kits/schemas/agent-config.schema.13.json +591 -0
- package/kits/schemas/daemon.schema.6.json +9 -9
- package/kits/schemas/defaults.schema.5.json +29 -0
- package/kits/schemas/relation-config.schema.8.json +14 -1
- package/kits/schemas/relation-config.schema.9.json +129 -0
- package/kits/schemas/role-config.schema.1.json +3 -3
- package/kits/skills/.switches.json +3 -0
- package/kits/skills/ec-skill/SKILL.md +217 -0
- package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
- package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
- package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
- package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
- package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
- package/kits/templates/decision/auto-mention.md +25 -0
- package/kits/templates/decision/session-renew.md +22 -0
- package/kits/templates/memory-consolidation/forget.md +104 -0
- package/kits/templates/memory-consolidation/generalize.md +76 -0
- package/kits/templates/memory-consolidation/notes.md +133 -0
- package/kits/templates/memory-consolidation/profile.md +58 -0
- package/kits/templates/memory-consolidation/promote.md +72 -0
- package/kits/templates/memory-consolidation/workflow.md +75 -0
- package/kits/templates/system-fragments/bootstrap.md +14 -5
- package/kits/templates/system-fragments/commands.md +1 -0
- package/kits/templates/system-fragments/group-history.md +6 -0
- package/kits/templates/system-fragments/identity.md +0 -3
- package/kits/templates/system-fragments/memory.md +79 -0
- package/kits/templates/system-fragments/reply.md +1 -0
- package/kits/templates/system-fragments/venue.md +1 -1
- package/package.json +4 -6
- package/dist/trigger/legacy-session-history.js +0 -19
- 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 版本缓存,改进升级状态展示及启动、重启流程。
|
package/bin/ec-safe-output.js
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
147
|
+
function resolveFilterExecutable(name) {
|
|
57
148
|
if (!IS_WINDOWS) {
|
|
58
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 (
|
|
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 !== '-')
|
|
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('-'))
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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
|
-
|
|
222
|
-
if (
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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.
|
|
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
|
|
294
|
-
let
|
|
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 || !
|
|
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
|
-
|
|
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 (
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
{
|
package/dist/agents/baseagent.js
CHANGED
|
@@ -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(
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
174
|
-
const
|
|
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
|
-
||
|
|
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');
|