evolcore 0.0.15 → 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,37 @@
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
+
20
+ ## 0.0.16 (2026-08-25)
21
+
22
+ ### 任务与消息可靠性
23
+
24
+ - 统一任务总执行时限与 AUN 附件投递重试边界,补充可恢复失败的退避与错误上下文,避免无配置时误设总超时。
25
+ - 优化会话响应、事件总线和守护进程生命周期,完善任务结束、重试和清理时的状态收敛。
26
+
27
+ ### Windows 与运行兼容性
28
+
29
+ - 强化 Windows 进程识别、登录自启状态探测和命令输出解码,避免误识别其它 Node 服务并改善失败诊断。
30
+ - 修正 Windows Claude 沙箱 settings 的受管临时文件生命周期,收紧运行目录校验,并为过长的 Unix socket 路径提供隔离的短路径回退。
31
+
32
+ ### 审计与权限边界
33
+
34
+ - 增强结构化日志完整性检查、生命周期关联和重复记录识别,统一工具错误分类与审计上下文。
35
+ - 收紧只读诊断、受保护路径和运行时锁访问策略,仅允许显式授权的有界源码诊断。
36
+
6
37
  ## 0.0.15 (2026-08-25)
7
38
 
8
39
  ### Codex 文件授权
package/README.md CHANGED
@@ -162,6 +162,7 @@ ec init aun
162
162
  "idleMonitor": {
163
163
  "enabled": true, // 任务超时监控开关
164
164
  "timeout": 120, // 超时阈值(秒),默认 120 秒
165
+ "maxExecutionTime": 7200, // 可选:任务总执行时限(秒);未配置、非数字或 <=0 时不限制
165
166
  "safeModeThreshold": 3 // 连续超时 N 次后进入安全模式(设为 0 禁用安全模式)
166
167
  },
167
168
  "flushDelay": 4 // 工具活动消息聚合发送间隔(秒),默认 4 秒
@@ -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';
@@ -25,6 +26,7 @@ import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClass
25
26
  import { buildHClassGuardCommand, createSandboxInitializationError, isSandboxInitializationFailure, SANDBOX_INITIALIZATION_FAILED, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, shouldFailIfClaudeSandboxUnavailable, } from '../core/permission/sandbox-runtime.js';
26
27
  import { buildClaudeUnixSocketAllowlist } from '../core/permission/unix-socket-policy.js';
27
28
  import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
29
+ import { isManagedSessionRuntimeDir } from '../cli/task-context.js';
28
30
  import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
29
31
  export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
30
32
  // Built-in tools execute inside the Claude runtime and are covered by the
@@ -243,6 +245,17 @@ function trustedRuntimeWritePaths(runtimeEnv) {
243
245
  }
244
246
  return [...new Set(paths)];
245
247
  }
248
+ function resolveClaudeSettingsDirectory(runtimeEnv) {
249
+ const configured = runtimeEnv?.TMPDIR?.trim();
250
+ if (!configured || !path.isAbsolute(configured)) {
251
+ throw new Error('[ClaudeSandbox] managed TMPDIR is unavailable');
252
+ }
253
+ const directory = path.resolve(configured);
254
+ if (!isManagedSessionRuntimeDir(directory, configured)) {
255
+ throw new Error('[ClaudeSandbox] settings file directory is outside the managed TMPDIR');
256
+ }
257
+ return directory;
258
+ }
246
259
  async function assertClaudeSettingSourcesHaveLiteralSandboxPaths(cwd, settingSources, managedSettings) {
247
260
  if (process.platform !== 'linux' || settingSources.length === 0)
248
261
  return;
@@ -601,6 +614,7 @@ export class AgentRunner {
601
614
  activeMessageStreams = new Map();
602
615
  interruptFns = new Map();
603
616
  activeQueries = new Map();
617
+ sandboxSettingsFiles = new Map();
604
618
  streamDone = new Map();
605
619
  streamDoneResolvers = new Map();
606
620
  onSessionIdUpdate;
@@ -612,6 +626,33 @@ export class AgentRunner {
612
626
  /** 每个 session 最近的子进程 stderr 行(环形缓冲),用于子进程崩溃时还原真正原因 */
613
627
  recentStderr = new Map();
614
628
  static STDERR_BUFFER_MAX = 80;
629
+ trackSandboxSettingsFile(sessionId, filePath) {
630
+ const files = this.sandboxSettingsFiles.get(sessionId) ?? new Set();
631
+ files.add(filePath);
632
+ this.sandboxSettingsFiles.set(sessionId, files);
633
+ }
634
+ cleanupSandboxSettingsFile(sessionId, filePath) {
635
+ if (!filePath)
636
+ return;
637
+ try {
638
+ fs.rmSync(filePath, { force: true });
639
+ }
640
+ catch (error) {
641
+ const code = error && typeof error === 'object' && 'code' in error
642
+ ? String(error.code)
643
+ : 'unknown';
644
+ logger.warn(`[ClaudeSandbox] failed to remove transient settings file code=${code}`);
645
+ }
646
+ const files = this.sandboxSettingsFiles.get(sessionId);
647
+ files?.delete(filePath);
648
+ if (files?.size === 0)
649
+ this.sandboxSettingsFiles.delete(sessionId);
650
+ }
651
+ cleanupSandboxSettingsFiles(sessionId) {
652
+ for (const filePath of [...(this.sandboxSettingsFiles.get(sessionId) ?? [])]) {
653
+ this.cleanupSandboxSettingsFile(sessionId, filePath);
654
+ }
655
+ }
615
656
  constructor(apiKey, model, onSessionIdUpdate, baseUrl, config) {
616
657
  this.apiKey = apiKey;
617
658
  this.model = model || 'sonnet';
@@ -1366,7 +1407,7 @@ export class AgentRunner {
1366
1407
  * SDK 原始事件 → 标准 AgentEvent 转换
1367
1408
  * 所有 SDK 特有的事件类型引用封装在此方法内
1368
1409
  */
1369
- async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false) {
1410
+ async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile) {
1370
1411
  let lastSessionId;
1371
1412
  let hasTurnActivity = false;
1372
1413
  let ignoredPreTurnResult = false;
@@ -1785,6 +1826,30 @@ export class AgentRunner {
1785
1826
  else {
1786
1827
  logger.error(`[AgentRunner] Subprocess stream failed (session=${sessionId}) with no captured stderr.`);
1787
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
+ }
1788
1853
  if (isSandboxInitializationFailure(err, buf ?? [])) {
1789
1854
  const detail = createSandboxInitializationError(err, buf ?? []);
1790
1855
  await this.permissionContexts.get(sessionId)?.recordExecutionAnomaly?.({
@@ -1813,6 +1878,7 @@ export class AgentRunner {
1813
1878
  }
1814
1879
  finally {
1815
1880
  this.recentStderr.delete(sessionId);
1881
+ this.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
1816
1882
  }
1817
1883
  }
1818
1884
  async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
@@ -2001,6 +2067,7 @@ export class AgentRunner {
2001
2067
  userId: permCtx?.userId,
2002
2068
  role: permCtx?.role,
2003
2069
  permissionMode: callPermissionMode,
2070
+ allowReadonlySourceDiagnostics: permCtx?.allowReadonlySourceDiagnostics === true,
2004
2071
  allowHostProcessCommands: true,
2005
2072
  allowProtectedMetadata: false,
2006
2073
  safeOutputHelperPath: path.join(getPackageRoot(), 'bin', 'ec-safe-output.js'),
@@ -2035,6 +2102,7 @@ export class AgentRunner {
2035
2102
  peerId: permCtx?.userId,
2036
2103
  role: permCtx?.role,
2037
2104
  allowProtectedMetadata: false,
2105
+ allowReadonlySourceDiagnostics: permCtx?.allowReadonlySourceDiagnostics === true,
2038
2106
  };
2039
2107
  const roResult = checkReadonly(toolName, toolInput, projectPath, readonlyContext);
2040
2108
  if (roResult.behavior === 'deny') {
@@ -2123,6 +2191,7 @@ export class AgentRunner {
2123
2191
  userId: permCtx?.userId,
2124
2192
  role: permCtx?.role,
2125
2193
  permissionMode: callPermissionMode,
2194
+ allowReadonlySourceDiagnostics: permCtx?.allowReadonlySourceDiagnostics === true,
2126
2195
  allowHostProcessCommands: true,
2127
2196
  allowProtectedMetadata: false,
2128
2197
  safeOutputHelperPath: path.join(getPackageRoot(), 'bin', 'ec-safe-output.js'),
@@ -2193,6 +2262,7 @@ export class AgentRunner {
2193
2262
  peerId: permCtx?.userId,
2194
2263
  role: permCtx?.role,
2195
2264
  allowProtectedMetadata: false,
2265
+ allowReadonlySourceDiagnostics: permCtx?.allowReadonlySourceDiagnostics === true,
2196
2266
  };
2197
2267
  const roResult = checkReadonly(toolName, input, projectPath, readonlyContext);
2198
2268
  if (roResult.behavior === 'deny') {
@@ -2362,23 +2432,54 @@ export class AgentRunner {
2362
2432
  stderr: handleClaudeStderr,
2363
2433
  env: this.getAgentEnv(runtimeEnv, sessionId, callPermissionMode)
2364
2434
  };
2435
+ // Native Windows has a small CreateProcess command-line limit. The SDK
2436
+ // normally serializes `sandbox` into inline `--settings` JSON, so preserve
2437
+ // the same flag-settings semantics while passing a short file path.
2438
+ let sandboxSettingsFile;
2439
+ const shouldUseSandboxSettingsFile = process.platform === 'win32' && sandboxOptions && sandboxOptions.enabled !== false;
2440
+ if (shouldUseSandboxSettingsFile) {
2441
+ try {
2442
+ const managedTmpDir = resolveClaudeSettingsDirectory(runtimeEnv);
2443
+ sandboxSettingsFile = path.join(managedTmpDir, `.claude-sandbox-${crypto.randomUUID()}.json`);
2444
+ const capabilitySettings = capabilityOptions.settings;
2445
+ const settingsContent = JSON.stringify({
2446
+ ...(capabilitySettings && typeof capabilitySettings === 'object' && !Array.isArray(capabilitySettings)
2447
+ ? capabilitySettings
2448
+ : {}),
2449
+ sandbox: sandboxOptions,
2450
+ });
2451
+ fs.writeFileSync(sandboxSettingsFile, settingsContent, { flag: 'wx', mode: 0o600 });
2452
+ this.trackSandboxSettingsFile(sessionId, sandboxSettingsFile);
2453
+ logger.info(`[ClaudeSandbox] Windows settings externalized settingsChars=${settingsContent.length} ` +
2454
+ `settingsPathChars=${sandboxSettingsFile.length}`);
2455
+ }
2456
+ catch (error) {
2457
+ this.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
2458
+ throw new Error(`[ClaudeSandbox] failed to externalize Windows settings: ${error instanceof Error ? error.message : String(error)}`);
2459
+ }
2460
+ }
2365
2461
  const createQuery = (promptInput, resumeSessionId, resumeAt) => {
2366
2462
  if (useSettingSources) {
2463
+ const queryOptions = {
2464
+ ...commonOptions,
2465
+ ...(!resumeSessionId && callSessionTitle ? { title: callSessionTitle } : {}),
2466
+ settingSources: [...settingSources],
2467
+ systemPrompt: {
2468
+ type: 'preset',
2469
+ preset: 'claude_code',
2470
+ ...(excludeDynamic ? { excludeDynamicSections: true } : {}),
2471
+ ...(systemPromptAppend ? { append: systemPromptAppend } : {})
2472
+ },
2473
+ ...(resumeSessionId ? { resume: resumeSessionId } : {}),
2474
+ ...(resumeAt ? { resumeSessionAt: resumeAt } : {}),
2475
+ };
2476
+ if (sandboxSettingsFile) {
2477
+ delete queryOptions.sandbox;
2478
+ queryOptions.settings = sandboxSettingsFile;
2479
+ }
2367
2480
  return query({
2368
2481
  prompt: promptInput,
2369
- options: {
2370
- ...commonOptions,
2371
- ...(!resumeSessionId && callSessionTitle ? { title: callSessionTitle } : {}),
2372
- settingSources: [...settingSources],
2373
- systemPrompt: {
2374
- type: 'preset',
2375
- preset: 'claude_code',
2376
- ...(excludeDynamic ? { excludeDynamicSections: true } : {}),
2377
- ...(systemPromptAppend ? { append: systemPromptAppend } : {})
2378
- },
2379
- ...(resumeSessionId ? { resume: resumeSessionId } : {}),
2380
- ...(resumeAt ? { resumeSessionAt: resumeAt } : {}),
2381
- }
2482
+ options: queryOptions
2382
2483
  });
2383
2484
  }
2384
2485
  else {
@@ -2408,22 +2509,27 @@ export class AgentRunner {
2408
2509
  globalClaudeMd,
2409
2510
  systemPromptAppend,
2410
2511
  ].filter(Boolean).join('\n\n');
2512
+ const queryOptions = {
2513
+ ...commonOptions,
2514
+ ...(!resumeSessionId && callSessionTitle ? { title: callSessionTitle } : {}),
2515
+ settingSources: [],
2516
+ ...(resumeSessionId ? { resume: resumeSessionId } : {}),
2517
+ ...(resumeAt ? { resumeSessionAt: resumeAt } : {}),
2518
+ ...(fullAppend ? {
2519
+ systemPrompt: {
2520
+ type: 'preset',
2521
+ preset: 'claude_code',
2522
+ append: fullAppend
2523
+ }
2524
+ } : {}),
2525
+ };
2526
+ if (sandboxSettingsFile) {
2527
+ delete queryOptions.sandbox;
2528
+ queryOptions.settings = sandboxSettingsFile;
2529
+ }
2411
2530
  return query({
2412
2531
  prompt: promptInput,
2413
- options: {
2414
- ...commonOptions,
2415
- ...(!resumeSessionId && callSessionTitle ? { title: callSessionTitle } : {}),
2416
- settingSources: [],
2417
- ...(resumeSessionId ? { resume: resumeSessionId } : {}),
2418
- ...(resumeAt ? { resumeSessionAt: resumeAt } : {}),
2419
- ...(fullAppend ? {
2420
- systemPrompt: {
2421
- type: 'preset',
2422
- preset: 'claude_code',
2423
- append: fullAppend
2424
- }
2425
- } : {}),
2426
- }
2532
+ options: queryOptions
2427
2533
  });
2428
2534
  }
2429
2535
  };
@@ -2455,7 +2561,10 @@ export class AgentRunner {
2455
2561
  sdkStream = createQuery(msgStream);
2456
2562
  }
2457
2563
  catch (error) {
2564
+ this.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
2458
2565
  const stderr = this.recentStderr.get(sessionId) ?? [];
2566
+ if (isClaudeStartupArgumentsTooLong(error))
2567
+ throw createClaudeStartupArgumentsTooLongError(error);
2459
2568
  if (isSandboxInitializationFailure(error, stderr))
2460
2569
  throw createSandboxInitializationError(error, stderr);
2461
2570
  throw error;
@@ -2468,7 +2577,10 @@ export class AgentRunner {
2468
2577
  sdkStream = createQuery(msgStream, agentSessionId, resumeAt);
2469
2578
  }
2470
2579
  catch (error) {
2580
+ this.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
2471
2581
  const stderr = this.recentStderr.get(sessionId) ?? [];
2582
+ if (isClaudeStartupArgumentsTooLong(error))
2583
+ throw createClaudeStartupArgumentsTooLongError(error);
2472
2584
  if (isSandboxInitializationFailure(error, stderr))
2473
2585
  throw createSandboxInitializationError(error, stderr);
2474
2586
  throw error;
@@ -2483,13 +2595,14 @@ export class AgentRunner {
2483
2595
  this.interruptFns.set(sessionId, () => sdkStream.interrupt());
2484
2596
  }
2485
2597
  // 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
2486
- const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId);
2598
+ const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile);
2487
2599
  const self = this;
2488
2600
  return (async function* () {
2489
2601
  try {
2490
2602
  yield* transformed;
2491
2603
  }
2492
2604
  finally {
2605
+ self.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
2493
2606
  self.streamDoneResolvers.get(sessionId)?.();
2494
2607
  self.streamDoneResolvers.delete(sessionId);
2495
2608
  self.streamDone.delete(sessionId);
@@ -2536,6 +2649,7 @@ export class AgentRunner {
2536
2649
  }
2537
2650
  this.interruptFns.delete(sessionId);
2538
2651
  this.activeStreams.delete(sessionId);
2652
+ this.cleanupSandboxSettingsFiles(sessionId);
2539
2653
  return { stillQueued, cancelledInputIds, closed: true };
2540
2654
  }
2541
2655
  hasActiveStream(sessionId) {
@@ -2553,6 +2667,7 @@ export class AgentRunner {
2553
2667
  this.activeStreams.delete(sessionId);
2554
2668
  this.interruptFns.delete(sessionId);
2555
2669
  this.activeQueries.delete(sessionId);
2670
+ this.cleanupSandboxSettingsFiles(sessionId);
2556
2671
  this.recentStderr.delete(sessionId);
2557
2672
  }
2558
2673
  injectUserMessage(sessionId, text) {
@@ -2657,6 +2772,7 @@ export class AgentRunner {
2657
2772
  this.interruptFns.delete(sessionId);
2658
2773
  this.activeQueries.delete(sessionId);
2659
2774
  this.permissionContexts.delete(sessionId);
2775
+ this.cleanupSandboxSettingsFiles(sessionId);
2660
2776
  }
2661
2777
  async dispose() {
2662
2778
  const interrupts = [...this.interruptFns.values()].map(async (interrupt) => {
@@ -2686,6 +2802,9 @@ export class AgentRunner {
2686
2802
  this.streamDoneResolvers.clear();
2687
2803
  this.permissionContexts.clear();
2688
2804
  this.recentStderr.clear();
2805
+ for (const sessionId of [...this.sandboxSettingsFiles.keys()]) {
2806
+ this.cleanupSandboxSettingsFiles(sessionId);
2807
+ }
2689
2808
  }
2690
2809
  resolveSessionFile(agentSessionId, projectPath) {
2691
2810
  const encodedProjectPath = encodePath(projectPath);
@@ -13,7 +13,7 @@ import { CodexAppServerClient } from './codex-app-server-client.js';
13
13
  import { resolveOpenaiConfig } from './baseagent.js';
14
14
  import { logger } from '../utils/logger.js';
15
15
  import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
16
- import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
16
+ import { auditCodexApprovalDecision, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
17
17
  import { isRetryableError } from '../utils/error-utils.js';
18
18
  import { renderActionAsText } from '../core/interaction-router.js';
19
19
  import { buildEnvelope, sendInteractionPayload } from '../core/message/message-utils.js';
@@ -1694,7 +1694,7 @@ export class CodexRunner {
1694
1694
  logger.warn(`[CodexRunner] failed to record policy-hook denial: session=${sessionKey} tool=${toolName} error=${error instanceof Error ? error.message : String(error)}`);
1695
1695
  }
1696
1696
  const response = this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
1697
- this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'policy', policyResult.policyCode ?? 'session_policy_hook');
1697
+ this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'policy', policyResult.policyCode ?? 'session_policy_hook', policyResult.reason ?? 'session policy denied approval');
1698
1698
  return response;
1699
1699
  }
1700
1700
  const summary = this.summarizeAppServerRequest(request.method, params);
@@ -1703,7 +1703,7 @@ export class CodexRunner {
1703
1703
  if (!workspacePath) {
1704
1704
  logger.warn(`[CodexRunner] approval denied because thread workspace is unknown: method=${request.method} thread=${params.threadId ?? params.conversationId ?? '<missing>'}`);
1705
1705
  const response = this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
1706
- this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'infrastructure', 'approval_workspace_unknown');
1706
+ this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'infrastructure', 'approval_workspace_unknown', 'thread workspace is unknown');
1707
1707
  return response;
1708
1708
  }
1709
1709
  const operationCwd = this.resolvePermissionOperationCwd(params, workspacePath, toolName);
@@ -1738,7 +1738,7 @@ export class CodexRunner {
1738
1738
  logger.warn(`[CodexRunner] failed to record infrastructure approval denial: session=${sessionKey} code=${policyCode} error=${error instanceof Error ? error.message : String(error)}`);
1739
1739
  }
1740
1740
  const response = this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
1741
- this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'infrastructure', policyCode);
1741
+ this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'infrastructure', policyCode, message);
1742
1742
  return response;
1743
1743
  };
1744
1744
  if (managedEcIntent && !approvedCommand) {
@@ -1763,7 +1763,13 @@ export class CodexRunner {
1763
1763
  }
1764
1764
  }
1765
1765
  const response = this.toAppServerApprovalResponse(request.method, decision, toolInput, unattended);
1766
- this.logAppServerApprovalAudit(request, sessionKey, toolName, decision === 'deny' ? 'deny' : 'allow', decision === 'deny' ? (sessionMode === 'auto' || sessionMode === 'readonly' ? 'policy' : 'approval') : 'approval');
1766
+ this.logAppServerApprovalAudit(request, sessionKey, toolName, decision === 'deny' ? 'deny' : 'allow', decision === 'deny' ? (sessionMode === 'auto' || sessionMode === 'readonly' ? 'policy' : 'approval') : 'approval', decision === 'deny' && (sessionMode === 'auto' || sessionMode === 'readonly')
1767
+ ? 'permission_mode_denied'
1768
+ : undefined, decision === 'deny'
1769
+ ? (reason || (sessionMode === 'auto' || sessionMode === 'readonly'
1770
+ ? `permission mode ${sessionMode} denied approval`
1771
+ : 'user approval denied'))
1772
+ : 'approval accepted');
1767
1773
  logger.info(`[CodexRunner] app-server approval response id=${request.id} method=${request.method} decision=${decision} response=${JSON.stringify(response)}`);
1768
1774
  return response;
1769
1775
  }
@@ -1773,7 +1779,21 @@ export class CodexRunner {
1773
1779
  throw error;
1774
1780
  }
1775
1781
  }
1776
- logAppServerApprovalAudit(request, sessionKey, toolName, decision, decisionSource, policyCode) {
1782
+ logAppServerApprovalAudit(request, sessionKey, toolName, decision, decisionSource, policyCode, reason) {
1783
+ const context = this.permissionContexts.get(sessionKey);
1784
+ auditCodexApprovalDecision({
1785
+ requestId: request.id !== undefined ? String(request.id) : undefined,
1786
+ method: request.method,
1787
+ sessionId: sessionKey,
1788
+ agentAid: context?.selfAid,
1789
+ toolName,
1790
+ decision,
1791
+ decisionSource,
1792
+ policyCode,
1793
+ permissionMode: normalizePermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode).mode,
1794
+ role: context?.role,
1795
+ reason: reason ?? policyCode ?? (decision === 'allow' ? 'approval accepted' : 'approval denied'),
1796
+ });
1777
1797
  logger.info(`[CodexRunner] app-server approval audit ${JSON.stringify({
1778
1798
  requestId: request.id !== undefined ? String(request.id) : undefined,
1779
1799
  method: request.method,
@@ -1782,6 +1802,7 @@ export class CodexRunner {
1782
1802
  decision,
1783
1803
  decisionSource,
1784
1804
  ...(policyCode ? { policyCode } : {}),
1805
+ reason: reason ?? policyCode ?? (decision === 'allow' ? 'approval accepted' : 'approval denied'),
1785
1806
  executed: false,
1786
1807
  })}`);
1787
1808
  }
@@ -2200,6 +2221,7 @@ export class CodexRunner {
2200
2221
  // readonly decision. The map entry alone is stale if the directory was
2201
2222
  // replaced between preflight and the approval callback.
2202
2223
  managedTempDir: this.getManagedTempDir(sessionKey),
2224
+ allowReadonlySourceDiagnostics: permCtx?.allowReadonlySourceDiagnostics === true,
2203
2225
  } : undefined;
2204
2226
  if (toolName === 'Bash')
2205
2227
  return checkReadonly(toolName, input, projectPath, readonlyContext);
@@ -2315,12 +2337,12 @@ export class CodexRunner {
2315
2337
  // per-session 权限模式(runQuery 写入);缺省回落实例级 currentMode(兼容无 runQuery 上下文的调用)
2316
2338
  const rawMode = this.chatModes.get(sessionKey) ?? this.currentMode;
2317
2339
  const mode = normalizePermissionMode(rawMode).mode;
2318
- const recordBlockedOperation = async (policyCode, operationInput = toolInput) => {
2340
+ const recordBlockedOperation = async (policyCode, operationInput = toolInput, denialReason = 'policy denied') => {
2319
2341
  const summary = summarizeToolInputForAudit(toolName, operationInput).slice(0, 512);
2320
2342
  auditToolPreflightDenial({
2321
2343
  toolName,
2322
2344
  policyCode,
2323
- reason: 'policy denied',
2345
+ reason: denialReason,
2324
2346
  summary,
2325
2347
  sessionId: sessionKey,
2326
2348
  agentAid: permissionContext?.selfAid,
@@ -2364,6 +2386,7 @@ export class CodexRunner {
2364
2386
  userId: permissionContext?.userId,
2365
2387
  role: permissionContext?.role,
2366
2388
  permissionMode: mode,
2389
+ allowReadonlySourceDiagnostics: permissionContext?.allowReadonlySourceDiagnostics === true,
2367
2390
  projectPath: operationCwd,
2368
2391
  workspacePath,
2369
2392
  });
@@ -2371,7 +2394,7 @@ export class CodexRunner {
2371
2394
  logger.warn(`[CodexRunner] tool preflight denied: session=${sessionKey} tool=${toolName} ` +
2372
2395
  `policy=${preflight.policyCode ?? 'unknown'} reason=${preflight.message ?? 'policy denied'}`);
2373
2396
  if (preflight.policyCode) {
2374
- await recordBlockedOperation(preflight.policyCode);
2397
+ await recordBlockedOperation(preflight.policyCode, toolInput, preflight.message ?? 'tool preflight denied');
2375
2398
  }
2376
2399
  return 'deny';
2377
2400
  }
@@ -2399,7 +2422,7 @@ export class CodexRunner {
2399
2422
  if (mode === 'readonly') {
2400
2423
  const readonly = this.checkCodexReadonly(toolName, checkedInput, operationCwd, sessionKey);
2401
2424
  if (readonly.behavior === 'deny') {
2402
- await recordBlockedOperation(readonly.policyCode ?? 'readonly_mode', checkedInput);
2425
+ await recordBlockedOperation(readonly.policyCode ?? 'readonly_mode', checkedInput, readonly.message);
2403
2426
  return 'deny';
2404
2427
  }
2405
2428
  return 'allow';
@@ -2928,6 +2951,7 @@ export class CodexRunner {
2928
2951
  name: 'Shell',
2929
2952
  result: item.aggregatedOutput ?? '',
2930
2953
  isError: item.exitCode !== null && item.exitCode !== undefined ? item.exitCode !== 0 : item.status === 'failed',
2954
+ ...(item.error?.code || item.error?.errorCode ? { errorCode: String(item.error.code ?? item.error.errorCode) } : {}),
2931
2955
  callId: item.id,
2932
2956
  durationMs: typeof item.durationMs === 'number' ? item.durationMs : undefined,
2933
2957
  };
@@ -2939,6 +2963,7 @@ export class CodexRunner {
2939
2963
  result: item.result,
2940
2964
  isError: item.status === 'failed',
2941
2965
  error: item.error?.message,
2966
+ ...(item.error?.code || item.error?.errorCode ? { errorCode: String(item.error.code ?? item.error.errorCode) } : {}),
2942
2967
  callId: item.id,
2943
2968
  durationMs: typeof item.durationMs === 'number' ? item.durationMs : undefined,
2944
2969
  };
@@ -2949,6 +2974,7 @@ export class CodexRunner {
2949
2974
  name: item.namespace ? `${item.namespace}:${item.tool}` : item.tool,
2950
2975
  result: item.contentItems,
2951
2976
  isError: item.success === false || item.status === 'failed',
2977
+ ...(item.error?.code || item.error?.errorCode ? { errorCode: String(item.error.code ?? item.error.errorCode) } : {}),
2952
2978
  callId: item.id,
2953
2979
  durationMs: typeof item.durationMs === 'number' ? item.durationMs : undefined,
2954
2980
  };
@@ -924,7 +924,8 @@ export class EcagentRunner {
924
924
  if (event.type === 'tool_execution_end') {
925
925
  queue.push({
926
926
  type: 'tool_result', name: event.toolName, result: extractToolResult(event.result),
927
- isError: event.isError, error: event.isError ? extractToolResult(event.result) : undefined, callId: event.toolCallId,
927
+ isError: event.isError, error: event.isError ? extractToolResult(event.result) : undefined,
928
+ callId: event.toolCallId,
928
929
  });
929
930
  return;
930
931
  }
@@ -1006,6 +1007,7 @@ export class EcagentRunner {
1006
1007
  userId: permissionContext?.userId,
1007
1008
  role: permissionContext?.role,
1008
1009
  permissionMode: mode,
1010
+ allowReadonlySourceDiagnostics: permissionContext?.allowReadonlySourceDiagnostics === true,
1009
1011
  projectPath,
1010
1012
  });
1011
1013
  if (preflight.behavior === 'deny') {
@@ -1020,6 +1022,7 @@ export class EcagentRunner {
1020
1022
  if (mode === 'readonly') {
1021
1023
  const decision = checkReadonly(toolName, checkedInput, projectPath, {
1022
1024
  sessionId, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
1025
+ allowReadonlySourceDiagnostics: permissionContext?.allowReadonlySourceDiagnostics === true,
1023
1026
  });
1024
1027
  if (decision.behavior === 'deny') {
1025
1028
  await recordBlockedOperation(decision.policyCode ?? 'readonly_mode', checkedInput);
@@ -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,27 +4,27 @@ 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';
11
11
  import { AGENT_DELEGATION_TOKEN_ENV } from '../../core/auth/agent-delegation.js';
12
12
  import { loadDaemonConfig } from '../../config-store.js';
13
13
  import { normalizeAunMentionEntries } from './mention-schema.js';
14
- const DEFAULT_TASK_EXECUTION_MS = 60 * 60 * 1000;
15
14
  const DAEMON_SEND_TIMEOUT_GRACE_MS = 10_000;
15
+ const DEFAULT_IDLE_TIMEOUT_MS = 120_000;
16
16
  const MAX_TIMER_MS = 2_147_483_647;
17
17
  export function daemonTaskSendIpcTimeoutMs(file = false) {
18
- let executionMs = DEFAULT_TASK_EXECUTION_MS;
18
+ let idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS;
19
19
  try {
20
20
  const config = JSON.parse(fs.readFileSync(resolvePaths().daemonConfig, 'utf8'));
21
- const seconds = config.idleMonitor?.maxExecutionTime;
21
+ const seconds = config.idleMonitor?.timeout;
22
22
  if (typeof seconds === 'number' && Number.isFinite(seconds) && seconds > 0) {
23
- executionMs = seconds * 1000;
23
+ idleTimeoutMs = seconds * 1000;
24
24
  }
25
25
  }
26
26
  catch { }
27
- return Math.min(MAX_TIMER_MS, Math.max(file ? 120_000 : 5_000, executionMs + DAEMON_SEND_TIMEOUT_GRACE_MS));
27
+ return Math.min(MAX_TIMER_MS, Math.max(file ? 120_000 : 5_000, idleTimeoutMs + DAEMON_SEND_TIMEOUT_GRACE_MS));
28
28
  }
29
29
  export function resolveAunMessageEncrypt(explicit) {
30
30
  if (typeof explicit === 'boolean')
@@ -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;