evolcore 0.0.16 → 0.0.17

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 CHANGED
@@ -3,6 +3,20 @@
3
3
  本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
4
4
  [`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
5
5
 
6
+ ## 0.0.17 (2026-08-25)
7
+
8
+ ### Claude 与任务边界
9
+
10
+ - 统一识别 Claude 启动参数过长错误,记录结构化能力不可用审计并以明确终态结束任务,避免无效重试和敏感启动参数泄露。
11
+
12
+ ### Codex 与 AUN 消息兼容
13
+
14
+ - 将运行时 BaseAgent 信息传入任务上下文,仅对 Codex AUN 私聊/群聊文本载荷解码一层 `\\n`/`\\r`/`\\t` 转义,保持其它 Runner 和原始换行不变。
15
+
16
+ ### 临时目录与跨平台稳定性
17
+
18
+ - 改进受管临时目录创建根路径解析,在拒绝不可信 TMPDIR 时使用固定系统回退并兼容 macOS `/var` 实路径,强化跨平台私有目录校验。
19
+
6
20
  ## 0.0.16 (2026-08-25)
7
21
 
8
22
  ### 任务与消息可靠性
@@ -11,6 +11,7 @@ import fs from 'fs';
11
11
  import os from 'os';
12
12
  import crypto from 'crypto';
13
13
  import { logger } from '../utils/logger.js';
14
+ import { createClaudeStartupArgumentsTooLongError, isClaudeStartupArgumentsTooLong, } from '../utils/error-utils.js';
14
15
  import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
15
16
  import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
16
17
  import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
@@ -1825,6 +1826,30 @@ export class AgentRunner {
1825
1826
  else {
1826
1827
  logger.error(`[AgentRunner] Subprocess stream failed (session=${sessionId}) with no captured stderr.`);
1827
1828
  }
1829
+ if (isClaudeStartupArgumentsTooLong(err)) {
1830
+ const detail = createClaudeStartupArgumentsTooLongError(err);
1831
+ await this.permissionContexts.get(sessionId)?.recordExecutionAnomaly?.({
1832
+ code: 'capability_unavailable',
1833
+ severity: 'warning',
1834
+ phase: 'execution',
1835
+ occurredAt: Date.now(),
1836
+ toolName: 'Bash',
1837
+ policyCode: 'claude_startup_arguments_too_long',
1838
+ decisionSource: 'infrastructure',
1839
+ agentAid: this.permissionContexts.get(sessionId)?.selfAid,
1840
+ summary: detail.message.slice(0, 512),
1841
+ effect: 'operation_skipped',
1842
+ });
1843
+ yield {
1844
+ type: 'complete',
1845
+ isError: true,
1846
+ subtype: 'infrastructure',
1847
+ terminalReason: 'claude_startup_arguments_too_long',
1848
+ errors: [detail.message],
1849
+ queryFinal: true,
1850
+ };
1851
+ return;
1852
+ }
1828
1853
  if (isSandboxInitializationFailure(err, buf ?? [])) {
1829
1854
  const detail = createSandboxInitializationError(err, buf ?? []);
1830
1855
  await this.permissionContexts.get(sessionId)?.recordExecutionAnomaly?.({
@@ -2538,6 +2563,8 @@ export class AgentRunner {
2538
2563
  catch (error) {
2539
2564
  this.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
2540
2565
  const stderr = this.recentStderr.get(sessionId) ?? [];
2566
+ if (isClaudeStartupArgumentsTooLong(error))
2567
+ throw createClaudeStartupArgumentsTooLongError(error);
2541
2568
  if (isSandboxInitializationFailure(error, stderr))
2542
2569
  throw createSandboxInitializationError(error, stderr);
2543
2570
  throw error;
@@ -2552,6 +2579,8 @@ export class AgentRunner {
2552
2579
  catch (error) {
2553
2580
  this.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
2554
2581
  const stderr = this.recentStderr.get(sessionId) ?? [];
2582
+ if (isClaudeStartupArgumentsTooLong(error))
2583
+ throw createClaudeStartupArgumentsTooLongError(error);
2555
2584
  if (isSandboxInitializationFailure(error, stderr))
2556
2585
  throw createSandboxInitializationError(error, stderr);
2557
2586
  throw error;
@@ -9,7 +9,7 @@ import { daemonTaskSendIpcTimeoutMs, resolveAunMessageEncrypt } from './p2p.js';
9
9
  import { checkGroupIndex, getGroupIndex } from './group-index.js';
10
10
  import { appendMessageLog, buildOutboundEntry, classifyAunPayloadForLog } from '../../core/message/message-log.js';
11
11
  import { chatDirPath } from '../../core/session/session-fs-store.js';
12
- import { readBestTaskRuntimeContext } from '../../cli/task-context.js';
12
+ import { normalizeCodexTextPayload, readBestTaskRuntimeContext } from '../../cli/task-context.js';
13
13
  import { ipcQuery } from '../../ipc.js';
14
14
  import { resolvePaths } from '../../paths.js';
15
15
  import { AGENT_DELEGATION_TOKEN_ENV } from '../../core/auth/agent-delegation.js';
@@ -151,6 +151,7 @@ export async function groupSend(args) {
151
151
  const runtimeContext = await readBestTaskRuntimeContext();
152
152
  if (args.body.mode !== 'file') {
153
153
  const payload = buildGroupPayload(args.body);
154
+ normalizeCodexTextPayload(payload, runtimeContext);
154
155
  applyGroupRoutingPayloadFields(payload, args, runtimeContext);
155
156
  const daemonResult = await tryDaemonGroupSend(args, payload, runtimeContext);
156
157
  if (daemonResult)
@@ -192,6 +193,7 @@ export async function groupSend(args) {
192
193
  break;
193
194
  }
194
195
  }
196
+ normalizeCodexTextPayload(payload, runtimeContext);
195
197
  const payloadMentions = Object.prototype.hasOwnProperty.call(payload, 'mentions')
196
198
  ? normalizeAunMentionEntries(payload.mentions)
197
199
  : undefined;
@@ -4,7 +4,7 @@ import { createShortConnection } from '../rpc/index.js';
4
4
  import { getAidStore, SLOT } from '../aid/store.js';
5
5
  import { uploadFileAndBuildPayload } from './upload.js';
6
6
  import { appendMessageLog, buildOutboundEntry, classifyAunPayloadForLog } from '../../core/message/message-log.js';
7
- import { readBestTaskRuntimeContext, runtimeRefMessageIdForMsgSend } from '../../cli/task-context.js';
7
+ import { normalizeCodexTextPayload, readBestTaskRuntimeContext, runtimeRefMessageIdForMsgSend } from '../../cli/task-context.js';
8
8
  import { chatDirPath } from '../../core/session/session-fs-store.js';
9
9
  import { resolvePaths } from '../../paths.js';
10
10
  import { ipcQuery } from '../../ipc.js';
@@ -207,6 +207,7 @@ export async function msgSend(args) {
207
207
  let payload;
208
208
  if (args.body.mode !== 'file') {
209
209
  payload = buildSimplePayload(args.body);
210
+ normalizeCodexTextPayload(payload, runtimeContext);
210
211
  if (Object.prototype.hasOwnProperty.call(payload, 'mentions')) {
211
212
  payload.mentions = normalizeAunMentionEntries(payload.mentions);
212
213
  }
@@ -263,6 +264,7 @@ export async function msgSend(args) {
263
264
  // 3. 构建 payload
264
265
  if (!payload) {
265
266
  payload = buildSimplePayload(args.body);
267
+ normalizeCodexTextPayload(payload, runtimeContext);
266
268
  }
267
269
  // 4. 写入 payload.chatmode
268
270
  payload.chatmode = chatmode;
@@ -23,6 +23,7 @@ function normalizeTaskRuntimeContext(value) {
23
23
  taskId: optionalString(value.taskId),
24
24
  sessionId: optionalString(value.sessionId),
25
25
  messageId: optionalString(value.messageId),
26
+ baseagent: optionalString(value.baseagent),
26
27
  channel: optionalString(value.channel),
27
28
  channelId: optionalString(value.channelId),
28
29
  chatType: optionalString(value.chatType),
@@ -38,6 +39,29 @@ function normalizeTaskRuntimeContext(value) {
38
39
  causation: normalizeCausation(value.causation),
39
40
  };
40
41
  }
42
+ /**
43
+ * Codex's shell carrier can leave one JSON-style escape layer in a message
44
+ * argument (for example, the two characters `\\` and `n`). Decode only that
45
+ * layer, and only for a task that is explicitly running the Codex backend.
46
+ * Actual line breaks and all non-Codex CLI input remain unchanged.
47
+ */
48
+ export function decodeCodexMessageText(text, runtime) {
49
+ if (runtime?.baseagent?.trim().toLowerCase() !== 'codex')
50
+ return text;
51
+ return text.replace(/\\([nrt])/g, (_match, escape) => {
52
+ if (escape === 'n')
53
+ return '\n';
54
+ if (escape === 'r')
55
+ return '\r';
56
+ return '\t';
57
+ });
58
+ }
59
+ /** Apply the Codex-only normalization to a text message payload. */
60
+ export function normalizeCodexTextPayload(payload, runtime) {
61
+ if (payload.type !== 'text' || typeof payload.text !== 'string')
62
+ return;
63
+ payload.text = decodeCodexMessageText(payload.text, runtime);
64
+ }
41
65
  function optionalAbsolutePath(value) {
42
66
  if (typeof value !== 'string' || !path.isAbsolute(value))
43
67
  return undefined;
@@ -75,6 +99,27 @@ function isPrivateDirectory(directory) {
75
99
  return false;
76
100
  }
77
101
  }
102
+ /**
103
+ * Resolve the platform temp root before creating a managed directory. macOS
104
+ * exposes its per-user temp directory through `/var`, which is a symlink to
105
+ * `/private/var`; walking that lexical path would otherwise reject a freshly
106
+ * created directory even though its real path is private. When an inherited
107
+ * TMPDIR was rejected, use a fixed system fallback instead of following that
108
+ * untrusted path back into the fallback.
109
+ */
110
+ function managedTempCreationRoot(configured) {
111
+ const candidate = configured
112
+ ? process.platform === 'win32'
113
+ ? path.join(process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows', 'Temp')
114
+ : '/tmp'
115
+ : os.tmpdir();
116
+ try {
117
+ return fs.realpathSync.native(candidate);
118
+ }
119
+ catch {
120
+ return path.resolve(candidate);
121
+ }
122
+ }
78
123
  /**
79
124
  * Ensure the daemon has a private process-wide temporary root.
80
125
  *
@@ -95,7 +140,7 @@ export function ensureProcessManagedTempDir() {
95
140
  if (isPrivateDirectory(resolved))
96
141
  return resolved;
97
142
  }
98
- const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'evolcore-managed-tmp-'));
143
+ const directory = fs.mkdtempSync(path.join(managedTempCreationRoot(configured), 'evolcore-managed-tmp-'));
99
144
  try {
100
145
  fs.chmodSync(directory, 0o700);
101
146
  if (!isPrivateDirectory(directory)) {
@@ -2775,6 +2775,7 @@ export class ResponseEngine {
2775
2775
  taskId,
2776
2776
  sessionId: session.id,
2777
2777
  messageId: message.messageId,
2778
+ baseagent: normalizedBaseagent.canonical,
2778
2779
  channel: configChannelType,
2779
2780
  channelId: message.channelId,
2780
2781
  chatType: configChatType,
@@ -3,6 +3,32 @@ import path from 'path';
3
3
  import { getPackageRoot, resolvePaths } from '../paths.js';
4
4
  import { logger } from './logger.js';
5
5
  import { isSandboxInitializationFailure } from '../core/permission/sandbox-runtime.js';
6
+ export const CLAUDE_STARTUP_ARGUMENTS_TOO_LONG = 'claude_startup_arguments_too_long';
7
+ /** Identify a Claude process-start failure without treating ordinary tool errors as startup failures. */
8
+ export function isClaudeStartupArgumentsTooLong(error) {
9
+ const code = error && typeof error === 'object' && 'code' in error
10
+ ? String(error.code ?? '')
11
+ : '';
12
+ const message = error && typeof error === 'object' && 'message' in error
13
+ ? String(error.message ?? '')
14
+ : String(error ?? '');
15
+ const text = `${code}\n${message}`.toLowerCase();
16
+ return text.includes(CLAUDE_STARTUP_ARGUMENTS_TOO_LONG)
17
+ || text.includes('enametoolong')
18
+ || text.includes('e2big')
19
+ || text.includes('argument list too long')
20
+ || text.includes('command line too long');
21
+ }
22
+ export function createClaudeStartupArgumentsTooLongError(error) {
23
+ const wrapped = new Error(`${CLAUDE_STARTUP_ARGUMENTS_TOO_LONG}: Claude 启动参数过长,任务未执行`);
24
+ wrapped.name = 'ClaudeStartupArgumentsTooLongError';
25
+ wrapped.code = CLAUDE_STARTUP_ARGUMENTS_TOO_LONG;
26
+ // Keep the original error available to diagnostics without echoing argv,
27
+ // settings contents, or absolute paths to users.
28
+ if (error !== undefined)
29
+ wrapped.cause = error;
30
+ return wrapped;
31
+ }
6
32
  export var ErrorType;
7
33
  (function (ErrorType) {
8
34
  ErrorType["SDK_TIMEOUT"] = "sdk_timeout";
@@ -13,6 +39,7 @@ export var ErrorType;
13
39
  ErrorType["CONTEXT_TOO_LONG"] = "context_too_long";
14
40
  ErrorType["MODEL_UNAVAILABLE"] = "model_unavailable";
15
41
  ErrorType["SANDBOX_INITIALIZATION_FAILED"] = "sandbox_initialization_failed";
42
+ ErrorType["CLAUDE_STARTUP_ARGUMENTS_TOO_LONG"] = "claude_startup_arguments_too_long";
16
43
  ErrorType["UNKNOWN"] = "unknown";
17
44
  })(ErrorType || (ErrorType = {}));
18
45
  /**
@@ -251,6 +278,11 @@ function hasRetryableHttpStatus(text) {
251
278
  }
252
279
  export function classifyError(error) {
253
280
  const msg = (error?.message || '').toLowerCase();
281
+ // Startup-size failures are deterministic local failures and must win over
282
+ // dictionary rules that would otherwise turn them into generic retries.
283
+ if (isClaudeStartupArgumentsTooLong(error)) {
284
+ return ErrorType.CLAUDE_STARTUP_ARGUMENTS_TOO_LONG;
285
+ }
254
286
  // 字典优先 — 命中则直接返回
255
287
  const rule = matchErrorRule(msg);
256
288
  if (rule) {
@@ -314,6 +346,10 @@ export function isRetryableError(error) {
314
346
  return false;
315
347
  }
316
348
  export function getErrorMessage(error, terminalReason, includeEmoji = true) {
349
+ if (isClaudeStartupArgumentsTooLong(error)) {
350
+ const prefix = includeEmoji ? '⚠️ ' : '';
351
+ return `${prefix}当前 Claude 启动参数过长,任务未执行`;
352
+ }
317
353
  // terminalReason 提供更精确的错误提示(SDK 0.2.100+)
318
354
  if (terminalReason) {
319
355
  const prefix = includeEmoji ? '❌ ' : '';
@@ -335,6 +371,8 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
335
371
  return `${prefix}权限被拒绝,操作已取消`;
336
372
  case 'sandbox_initialization_failed':
337
373
  return `${warnPrefix}只读执行沙箱启动失败(sandbox_initialization_failed),当前任务未执行;请检查 user namespace/seccomp 配置后重试`;
374
+ case 'claude_startup_arguments_too_long':
375
+ return `${warnPrefix}当前 Claude 启动参数过长,任务未执行`;
338
376
  case 'aborted_streaming':
339
377
  case 'aborted_tools':
340
378
  return `${prefix}任务已中断`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolcore",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "AI Agent gateway connecting Claude, Codex, Gemini, and the bundled ecagent runner to messaging channels with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",