evolcore 0.0.15 → 0.0.16

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,23 @@
3
3
  本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
4
4
  [`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
5
5
 
6
+ ## 0.0.16 (2026-08-25)
7
+
8
+ ### 任务与消息可靠性
9
+
10
+ - 统一任务总执行时限与 AUN 附件投递重试边界,补充可恢复失败的退避与错误上下文,避免无配置时误设总超时。
11
+ - 优化会话响应、事件总线和守护进程生命周期,完善任务结束、重试和清理时的状态收敛。
12
+
13
+ ### Windows 与运行兼容性
14
+
15
+ - 强化 Windows 进程识别、登录自启状态探测和命令输出解码,避免误识别其它 Node 服务并改善失败诊断。
16
+ - 修正 Windows Claude 沙箱 settings 的受管临时文件生命周期,收紧运行目录校验,并为过长的 Unix socket 路径提供隔离的短路径回退。
17
+
18
+ ### 审计与权限边界
19
+
20
+ - 增强结构化日志完整性检查、生命周期关联和重复记录识别,统一工具错误分类与审计上下文。
21
+ - 收紧只读诊断、受保护路径和运行时锁访问策略,仅允许显式授权的有界源码诊断。
22
+
6
23
  ## 0.0.15 (2026-08-25)
7
24
 
8
25
  ### 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 秒
@@ -25,6 +25,7 @@ import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClass
25
25
  import { buildHClassGuardCommand, createSandboxInitializationError, isSandboxInitializationFailure, SANDBOX_INITIALIZATION_FAILED, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, shouldFailIfClaudeSandboxUnavailable, } from '../core/permission/sandbox-runtime.js';
26
26
  import { buildClaudeUnixSocketAllowlist } from '../core/permission/unix-socket-policy.js';
27
27
  import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
28
+ import { isManagedSessionRuntimeDir } from '../cli/task-context.js';
28
29
  import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
29
30
  export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
30
31
  // Built-in tools execute inside the Claude runtime and are covered by the
@@ -243,6 +244,17 @@ function trustedRuntimeWritePaths(runtimeEnv) {
243
244
  }
244
245
  return [...new Set(paths)];
245
246
  }
247
+ function resolveClaudeSettingsDirectory(runtimeEnv) {
248
+ const configured = runtimeEnv?.TMPDIR?.trim();
249
+ if (!configured || !path.isAbsolute(configured)) {
250
+ throw new Error('[ClaudeSandbox] managed TMPDIR is unavailable');
251
+ }
252
+ const directory = path.resolve(configured);
253
+ if (!isManagedSessionRuntimeDir(directory, configured)) {
254
+ throw new Error('[ClaudeSandbox] settings file directory is outside the managed TMPDIR');
255
+ }
256
+ return directory;
257
+ }
246
258
  async function assertClaudeSettingSourcesHaveLiteralSandboxPaths(cwd, settingSources, managedSettings) {
247
259
  if (process.platform !== 'linux' || settingSources.length === 0)
248
260
  return;
@@ -601,6 +613,7 @@ export class AgentRunner {
601
613
  activeMessageStreams = new Map();
602
614
  interruptFns = new Map();
603
615
  activeQueries = new Map();
616
+ sandboxSettingsFiles = new Map();
604
617
  streamDone = new Map();
605
618
  streamDoneResolvers = new Map();
606
619
  onSessionIdUpdate;
@@ -612,6 +625,33 @@ export class AgentRunner {
612
625
  /** 每个 session 最近的子进程 stderr 行(环形缓冲),用于子进程崩溃时还原真正原因 */
613
626
  recentStderr = new Map();
614
627
  static STDERR_BUFFER_MAX = 80;
628
+ trackSandboxSettingsFile(sessionId, filePath) {
629
+ const files = this.sandboxSettingsFiles.get(sessionId) ?? new Set();
630
+ files.add(filePath);
631
+ this.sandboxSettingsFiles.set(sessionId, files);
632
+ }
633
+ cleanupSandboxSettingsFile(sessionId, filePath) {
634
+ if (!filePath)
635
+ return;
636
+ try {
637
+ fs.rmSync(filePath, { force: true });
638
+ }
639
+ catch (error) {
640
+ const code = error && typeof error === 'object' && 'code' in error
641
+ ? String(error.code)
642
+ : 'unknown';
643
+ logger.warn(`[ClaudeSandbox] failed to remove transient settings file code=${code}`);
644
+ }
645
+ const files = this.sandboxSettingsFiles.get(sessionId);
646
+ files?.delete(filePath);
647
+ if (files?.size === 0)
648
+ this.sandboxSettingsFiles.delete(sessionId);
649
+ }
650
+ cleanupSandboxSettingsFiles(sessionId) {
651
+ for (const filePath of [...(this.sandboxSettingsFiles.get(sessionId) ?? [])]) {
652
+ this.cleanupSandboxSettingsFile(sessionId, filePath);
653
+ }
654
+ }
615
655
  constructor(apiKey, model, onSessionIdUpdate, baseUrl, config) {
616
656
  this.apiKey = apiKey;
617
657
  this.model = model || 'sonnet';
@@ -1366,7 +1406,7 @@ export class AgentRunner {
1366
1406
  * SDK 原始事件 → 标准 AgentEvent 转换
1367
1407
  * 所有 SDK 特有的事件类型引用封装在此方法内
1368
1408
  */
1369
- async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false) {
1409
+ async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile) {
1370
1410
  let lastSessionId;
1371
1411
  let hasTurnActivity = false;
1372
1412
  let ignoredPreTurnResult = false;
@@ -1813,6 +1853,7 @@ export class AgentRunner {
1813
1853
  }
1814
1854
  finally {
1815
1855
  this.recentStderr.delete(sessionId);
1856
+ this.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
1816
1857
  }
1817
1858
  }
1818
1859
  async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
@@ -2001,6 +2042,7 @@ export class AgentRunner {
2001
2042
  userId: permCtx?.userId,
2002
2043
  role: permCtx?.role,
2003
2044
  permissionMode: callPermissionMode,
2045
+ allowReadonlySourceDiagnostics: permCtx?.allowReadonlySourceDiagnostics === true,
2004
2046
  allowHostProcessCommands: true,
2005
2047
  allowProtectedMetadata: false,
2006
2048
  safeOutputHelperPath: path.join(getPackageRoot(), 'bin', 'ec-safe-output.js'),
@@ -2035,6 +2077,7 @@ export class AgentRunner {
2035
2077
  peerId: permCtx?.userId,
2036
2078
  role: permCtx?.role,
2037
2079
  allowProtectedMetadata: false,
2080
+ allowReadonlySourceDiagnostics: permCtx?.allowReadonlySourceDiagnostics === true,
2038
2081
  };
2039
2082
  const roResult = checkReadonly(toolName, toolInput, projectPath, readonlyContext);
2040
2083
  if (roResult.behavior === 'deny') {
@@ -2123,6 +2166,7 @@ export class AgentRunner {
2123
2166
  userId: permCtx?.userId,
2124
2167
  role: permCtx?.role,
2125
2168
  permissionMode: callPermissionMode,
2169
+ allowReadonlySourceDiagnostics: permCtx?.allowReadonlySourceDiagnostics === true,
2126
2170
  allowHostProcessCommands: true,
2127
2171
  allowProtectedMetadata: false,
2128
2172
  safeOutputHelperPath: path.join(getPackageRoot(), 'bin', 'ec-safe-output.js'),
@@ -2193,6 +2237,7 @@ export class AgentRunner {
2193
2237
  peerId: permCtx?.userId,
2194
2238
  role: permCtx?.role,
2195
2239
  allowProtectedMetadata: false,
2240
+ allowReadonlySourceDiagnostics: permCtx?.allowReadonlySourceDiagnostics === true,
2196
2241
  };
2197
2242
  const roResult = checkReadonly(toolName, input, projectPath, readonlyContext);
2198
2243
  if (roResult.behavior === 'deny') {
@@ -2362,23 +2407,54 @@ export class AgentRunner {
2362
2407
  stderr: handleClaudeStderr,
2363
2408
  env: this.getAgentEnv(runtimeEnv, sessionId, callPermissionMode)
2364
2409
  };
2410
+ // Native Windows has a small CreateProcess command-line limit. The SDK
2411
+ // normally serializes `sandbox` into inline `--settings` JSON, so preserve
2412
+ // the same flag-settings semantics while passing a short file path.
2413
+ let sandboxSettingsFile;
2414
+ const shouldUseSandboxSettingsFile = process.platform === 'win32' && sandboxOptions && sandboxOptions.enabled !== false;
2415
+ if (shouldUseSandboxSettingsFile) {
2416
+ try {
2417
+ const managedTmpDir = resolveClaudeSettingsDirectory(runtimeEnv);
2418
+ sandboxSettingsFile = path.join(managedTmpDir, `.claude-sandbox-${crypto.randomUUID()}.json`);
2419
+ const capabilitySettings = capabilityOptions.settings;
2420
+ const settingsContent = JSON.stringify({
2421
+ ...(capabilitySettings && typeof capabilitySettings === 'object' && !Array.isArray(capabilitySettings)
2422
+ ? capabilitySettings
2423
+ : {}),
2424
+ sandbox: sandboxOptions,
2425
+ });
2426
+ fs.writeFileSync(sandboxSettingsFile, settingsContent, { flag: 'wx', mode: 0o600 });
2427
+ this.trackSandboxSettingsFile(sessionId, sandboxSettingsFile);
2428
+ logger.info(`[ClaudeSandbox] Windows settings externalized settingsChars=${settingsContent.length} ` +
2429
+ `settingsPathChars=${sandboxSettingsFile.length}`);
2430
+ }
2431
+ catch (error) {
2432
+ this.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
2433
+ throw new Error(`[ClaudeSandbox] failed to externalize Windows settings: ${error instanceof Error ? error.message : String(error)}`);
2434
+ }
2435
+ }
2365
2436
  const createQuery = (promptInput, resumeSessionId, resumeAt) => {
2366
2437
  if (useSettingSources) {
2438
+ const queryOptions = {
2439
+ ...commonOptions,
2440
+ ...(!resumeSessionId && callSessionTitle ? { title: callSessionTitle } : {}),
2441
+ settingSources: [...settingSources],
2442
+ systemPrompt: {
2443
+ type: 'preset',
2444
+ preset: 'claude_code',
2445
+ ...(excludeDynamic ? { excludeDynamicSections: true } : {}),
2446
+ ...(systemPromptAppend ? { append: systemPromptAppend } : {})
2447
+ },
2448
+ ...(resumeSessionId ? { resume: resumeSessionId } : {}),
2449
+ ...(resumeAt ? { resumeSessionAt: resumeAt } : {}),
2450
+ };
2451
+ if (sandboxSettingsFile) {
2452
+ delete queryOptions.sandbox;
2453
+ queryOptions.settings = sandboxSettingsFile;
2454
+ }
2367
2455
  return query({
2368
2456
  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
- }
2457
+ options: queryOptions
2382
2458
  });
2383
2459
  }
2384
2460
  else {
@@ -2408,22 +2484,27 @@ export class AgentRunner {
2408
2484
  globalClaudeMd,
2409
2485
  systemPromptAppend,
2410
2486
  ].filter(Boolean).join('\n\n');
2487
+ const queryOptions = {
2488
+ ...commonOptions,
2489
+ ...(!resumeSessionId && callSessionTitle ? { title: callSessionTitle } : {}),
2490
+ settingSources: [],
2491
+ ...(resumeSessionId ? { resume: resumeSessionId } : {}),
2492
+ ...(resumeAt ? { resumeSessionAt: resumeAt } : {}),
2493
+ ...(fullAppend ? {
2494
+ systemPrompt: {
2495
+ type: 'preset',
2496
+ preset: 'claude_code',
2497
+ append: fullAppend
2498
+ }
2499
+ } : {}),
2500
+ };
2501
+ if (sandboxSettingsFile) {
2502
+ delete queryOptions.sandbox;
2503
+ queryOptions.settings = sandboxSettingsFile;
2504
+ }
2411
2505
  return query({
2412
2506
  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
- }
2507
+ options: queryOptions
2427
2508
  });
2428
2509
  }
2429
2510
  };
@@ -2455,6 +2536,7 @@ export class AgentRunner {
2455
2536
  sdkStream = createQuery(msgStream);
2456
2537
  }
2457
2538
  catch (error) {
2539
+ this.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
2458
2540
  const stderr = this.recentStderr.get(sessionId) ?? [];
2459
2541
  if (isSandboxInitializationFailure(error, stderr))
2460
2542
  throw createSandboxInitializationError(error, stderr);
@@ -2468,6 +2550,7 @@ export class AgentRunner {
2468
2550
  sdkStream = createQuery(msgStream, agentSessionId, resumeAt);
2469
2551
  }
2470
2552
  catch (error) {
2553
+ this.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
2471
2554
  const stderr = this.recentStderr.get(sessionId) ?? [];
2472
2555
  if (isSandboxInitializationFailure(error, stderr))
2473
2556
  throw createSandboxInitializationError(error, stderr);
@@ -2483,13 +2566,14 @@ export class AgentRunner {
2483
2566
  this.interruptFns.set(sessionId, () => sdkStream.interrupt());
2484
2567
  }
2485
2568
  // 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
2486
- const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId);
2569
+ const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile);
2487
2570
  const self = this;
2488
2571
  return (async function* () {
2489
2572
  try {
2490
2573
  yield* transformed;
2491
2574
  }
2492
2575
  finally {
2576
+ self.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
2493
2577
  self.streamDoneResolvers.get(sessionId)?.();
2494
2578
  self.streamDoneResolvers.delete(sessionId);
2495
2579
  self.streamDone.delete(sessionId);
@@ -2536,6 +2620,7 @@ export class AgentRunner {
2536
2620
  }
2537
2621
  this.interruptFns.delete(sessionId);
2538
2622
  this.activeStreams.delete(sessionId);
2623
+ this.cleanupSandboxSettingsFiles(sessionId);
2539
2624
  return { stillQueued, cancelledInputIds, closed: true };
2540
2625
  }
2541
2626
  hasActiveStream(sessionId) {
@@ -2553,6 +2638,7 @@ export class AgentRunner {
2553
2638
  this.activeStreams.delete(sessionId);
2554
2639
  this.interruptFns.delete(sessionId);
2555
2640
  this.activeQueries.delete(sessionId);
2641
+ this.cleanupSandboxSettingsFiles(sessionId);
2556
2642
  this.recentStderr.delete(sessionId);
2557
2643
  }
2558
2644
  injectUserMessage(sessionId, text) {
@@ -2657,6 +2743,7 @@ export class AgentRunner {
2657
2743
  this.interruptFns.delete(sessionId);
2658
2744
  this.activeQueries.delete(sessionId);
2659
2745
  this.permissionContexts.delete(sessionId);
2746
+ this.cleanupSandboxSettingsFiles(sessionId);
2660
2747
  }
2661
2748
  async dispose() {
2662
2749
  const interrupts = [...this.interruptFns.values()].map(async (interrupt) => {
@@ -2686,6 +2773,9 @@ export class AgentRunner {
2686
2773
  this.streamDoneResolvers.clear();
2687
2774
  this.permissionContexts.clear();
2688
2775
  this.recentStderr.clear();
2776
+ for (const sessionId of [...this.sandboxSettingsFiles.keys()]) {
2777
+ this.cleanupSandboxSettingsFiles(sessionId);
2778
+ }
2689
2779
  }
2690
2780
  resolveSessionFile(agentSessionId, projectPath) {
2691
2781
  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);
@@ -11,20 +11,20 @@ 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')
@@ -41,6 +41,41 @@ export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
41
41
  const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
42
42
  const AUN_INBOUND_DEDUP_TTL_MS = 7 * 24 * 60 * 60 * 1000;
43
43
  const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
44
+ const AUN_ATTACHMENT_DOWNLOAD_ATTEMPTS = 3;
45
+ const AUN_ATTACHMENT_RETRY_DELAYS_MS = [250, 750];
46
+ function attachmentDownloadHost(url) {
47
+ try {
48
+ return new URL(url).host || '<unknown>';
49
+ }
50
+ catch {
51
+ return '<invalid-url>';
52
+ }
53
+ }
54
+ function attachmentDownloadErrorDetails(error) {
55
+ const value = error && typeof error === 'object' ? error : {};
56
+ const cause = value.cause && typeof value.cause === 'object'
57
+ ? value.cause
58
+ : {};
59
+ const status = typeof value.status === 'number'
60
+ ? value.status
61
+ : typeof cause.status === 'number' ? cause.status : undefined;
62
+ const code = typeof cause.code === 'string'
63
+ ? cause.code
64
+ : typeof value.code === 'string' ? value.code : undefined;
65
+ const message = typeof value.message === 'string'
66
+ ? value.message
67
+ : String(error);
68
+ const messageStatus = status === undefined
69
+ ? message.match(/\bHTTP\s+(\d{3})\b|\bDownload failed:\s*(\d{3})\b/i)
70
+ : null;
71
+ const parsedStatus = messageStatus
72
+ ? Number(messageStatus[1] ?? messageStatus[2])
73
+ : undefined;
74
+ return { status: status ?? (Number.isFinite(parsedStatus) ? parsedStatus : undefined), code, message };
75
+ }
76
+ function isRetryableAttachmentStatus(status) {
77
+ return status === undefined || status === 408 || status === 429 || status >= 500;
78
+ }
44
79
  // AUN limits the complete encrypted thought envelope to 8192 bytes. The
45
80
  // encrypted group envelope grows with the number of recipient devices, so a
46
81
  // conservative plaintext budget avoids rejecting otherwise valid thoughts.
@@ -1449,6 +1484,31 @@ export class AUNChannel {
1449
1484
  logger.info(`${this.logPrefix()} [attachments] count=${rawAttachments.length} images=${images.length} files=${fileParts.length}`);
1450
1485
  return { finalText, images };
1451
1486
  }
1487
+ async downloadAttachmentWithRetry(url, filename, source, download) {
1488
+ const host = attachmentDownloadHost(url);
1489
+ for (let attempt = 1; attempt <= AUN_ATTACHMENT_DOWNLOAD_ATTEMPTS; attempt++) {
1490
+ try {
1491
+ const buffer = await download();
1492
+ if (attempt > 1) {
1493
+ logger.info(`${this.logPrefix()} ${source} attachment download recovered for ${filename}: attempt=${attempt} host=${host}`);
1494
+ }
1495
+ return buffer;
1496
+ }
1497
+ catch (error) {
1498
+ const details = attachmentDownloadErrorDetails(error);
1499
+ const status = details.status === undefined ? '-' : String(details.status);
1500
+ const code = details.code ?? '-';
1501
+ logger.warn(`${this.logPrefix()} ${source} attachment download failed for ${filename}: `
1502
+ + `attempt=${attempt}/${AUN_ATTACHMENT_DOWNLOAD_ATTEMPTS} host=${host} `
1503
+ + `status=${status} code=${code} error=${details.message}`);
1504
+ if (attempt >= AUN_ATTACHMENT_DOWNLOAD_ATTEMPTS || !isRetryableAttachmentStatus(details.status))
1505
+ break;
1506
+ const delayMs = AUN_ATTACHMENT_RETRY_DELAYS_MS[attempt - 1] ?? AUN_ATTACHMENT_RETRY_DELAYS_MS.at(-1) ?? 0;
1507
+ await new Promise(resolve => setTimeout(resolve, delayMs));
1508
+ }
1509
+ }
1510
+ return null;
1511
+ }
1452
1512
  async downloadAttachment(att, channelId, delivery) {
1453
1513
  const ownerAid = att.owner_aid
1454
1514
  || (delivery?.chatType === 'private' ? channelId : '')
@@ -1491,34 +1551,27 @@ export class AUNChannel {
1491
1551
  logger.warn(`${this.logPrefix()} create_download_ticket failed for ${filename}: ${e}`);
1492
1552
  }
1493
1553
  }
1494
- let buffer;
1554
+ let buffer = null;
1495
1555
  if (downloadUrl) {
1496
- try {
1556
+ buffer = await this.downloadAttachmentWithRetry(downloadUrl, filename, 'ticket', async () => {
1497
1557
  const res = await fetch(downloadUrl);
1498
1558
  if (!res.ok) {
1499
- logger.warn(`${this.logPrefix()} Download failed for ${filename}: HTTP ${res.status}`);
1500
- return null;
1559
+ const error = new Error(`HTTP ${res.status}`);
1560
+ error.status = res.status;
1561
+ throw error;
1501
1562
  }
1502
- buffer = Buffer.from(await res.arrayBuffer());
1503
- }
1504
- catch (e) {
1505
- logger.warn(`${this.logPrefix()} Download error for ${filename}: ${e}`);
1506
- return null;
1507
- }
1563
+ return Buffer.from(await res.arrayBuffer());
1564
+ });
1508
1565
  }
1509
- else {
1510
- if (!fallbackUrl)
1511
- return null;
1512
- try {
1513
- const host = new URL(fallbackUrl).hostname;
1514
- buffer = await safeFetch(fallbackUrl, { allowedHosts: new Set([host]) });
1566
+ if (!buffer && fallbackUrl && fallbackUrl !== downloadUrl) {
1567
+ const host = new URL(fallbackUrl).hostname;
1568
+ buffer = await this.downloadAttachmentWithRetry(fallbackUrl, filename, 'payload', () => safeFetch(fallbackUrl, { allowedHosts: new Set([host]) }));
1569
+ if (buffer) {
1515
1570
  logger.info(`${this.logPrefix()} Downloaded attachment via payload URL fallback: ${filename}`);
1516
1571
  }
1517
- catch (e) {
1518
- logger.warn(`${this.logPrefix()} Payload URL fallback failed for ${filename}: ${e}`);
1519
- return null;
1520
- }
1521
1572
  }
1573
+ if (!buffer)
1574
+ return null;
1522
1575
  if (att.sha256) {
1523
1576
  const { createHash } = await import('node:crypto');
1524
1577
  const actual = createHash('sha256').update(buffer).digest('hex');