evolcore 0.0.26 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/codex-managed-acl.ps1 +43 -0
  3. package/dist/agents/baseagent-availability.js +68 -0
  4. package/dist/agents/claude-runner.js +123 -22
  5. package/dist/agents/codex/app-server-client.js +63 -26
  6. package/dist/agents/codex-runner.js +316 -411
  7. package/dist/agents/ecagent-runner.js +170 -25
  8. package/dist/agents/gemini-runner.js +6 -0
  9. package/dist/agents/runner-types.js +50 -2
  10. package/dist/aun/aid/agentmd.js +73 -31
  11. package/dist/aun/aid/client.js +1 -1
  12. package/dist/aun/aid/control-aid.js +180 -1
  13. package/dist/aun/aid/identity.js +3 -3
  14. package/dist/aun/group/history.js +303 -0
  15. package/dist/aun/group/identity.js +43 -0
  16. package/dist/aun/group/index.js +5 -0
  17. package/dist/aun/{msg/group.js → group/operations.js} +161 -38
  18. package/dist/aun/{menu-client.js → menu/client.js} +12 -7
  19. package/dist/aun/menu/index.js +2 -0
  20. package/dist/aun/menu/transport.js +49 -0
  21. package/dist/aun/msg/index.js +2 -1
  22. package/dist/aun/msg/managed-operation.js +84 -18
  23. package/dist/aun/msg/mention-schema.js +51 -0
  24. package/dist/aun/msg/p2p.js +13 -8
  25. package/dist/aun/msg/upload.js +43 -88
  26. package/dist/aun/outbox.js +29 -7
  27. package/dist/aun/rpc/client.js +1 -0
  28. package/dist/aun/storage/client.js +39 -50
  29. package/dist/channels/aun.js +522 -233
  30. package/dist/channels/daemon.js +10 -6
  31. package/dist/cli/agent-command.js +108 -6
  32. package/dist/cli/agent-create.js +515 -2
  33. package/dist/cli/agent.js +109 -8
  34. package/dist/cli/aun-commands.js +191 -42
  35. package/dist/cli/config.js +8 -5
  36. package/dist/cli/contact.js +2 -2
  37. package/dist/cli/ctl-command.js +2 -2
  38. package/dist/cli/daemon-commands.js +35 -23
  39. package/dist/cli/diag.js +2 -2
  40. package/dist/cli/fs-command.js +3 -3
  41. package/dist/cli/handoff-command.js +6 -6
  42. package/dist/cli/index.js +8 -2
  43. package/dist/cli/init.js +34 -1
  44. package/dist/cli/managed-command-guard.js +2 -1
  45. package/dist/cli/menu-client.js +9 -5
  46. package/dist/cli/model.js +3 -3
  47. package/dist/cli/queue-command.js +2 -2
  48. package/dist/cli/response.js +2 -2
  49. package/dist/cli/restart-monitor.js +16 -2
  50. package/dist/cli/task-command.js +59 -0
  51. package/dist/cli/task-context.js +97 -49
  52. package/dist/cli/trigger-command.js +20 -10
  53. package/dist/config/builtin-roles.js +10 -0
  54. package/dist/config/config-field-policy.js +11 -0
  55. package/dist/config/config-manager.js +32 -4
  56. package/dist/config/config-operation-service.js +20 -5
  57. package/dist/config/contact-book-store.js +1 -1
  58. package/dist/config/contact-book.js +1 -1
  59. package/dist/config/contact-operation-service.js +1 -1
  60. package/dist/config/peer-role-resolver.js +1 -1
  61. package/dist/config/resolved-config-op.js +3 -0
  62. package/dist/config-store.js +1 -1
  63. package/dist/core/agent-application-service.js +5 -0
  64. package/dist/core/audit/event-key.js +17 -2
  65. package/dist/core/audit/log-integrity.js +34 -8
  66. package/dist/core/auth/agent-delegation.js +48 -14
  67. package/dist/core/auth/auth-gateway.js +2 -0
  68. package/dist/core/auth/authorization-audit.js +34 -38
  69. package/dist/core/auth/operation-authorizer.js +67 -5
  70. package/dist/core/auth/operation-catalog.js +110 -0
  71. package/dist/core/auth/remote-access-store.js +115 -9
  72. package/dist/core/bootstrap-messages.js +66 -7
  73. package/dist/core/bootstrap-service.js +110 -19
  74. package/dist/core/command/agent-control.js +88 -7
  75. package/dist/core/command/agent-storage-menu.js +443 -0
  76. package/dist/core/command/cli-intent-parser.js +19 -0
  77. package/dist/core/command/command-handler.js +76 -39
  78. package/dist/core/command/data-root-resolver.js +273 -0
  79. package/dist/core/command/group-menu.js +7 -2
  80. package/dist/core/command/menu-catalog.js +34 -4
  81. package/dist/core/command/menu-handler.js +348 -77
  82. package/dist/core/command/menu-protocol.js +40 -6
  83. package/dist/core/command/role-menu.js +2 -2
  84. package/dist/core/command/slash-handler.js +37 -22
  85. package/dist/core/event-catalog.js +4 -0
  86. package/dist/core/evolagent-registry.js +28 -2
  87. package/dist/core/group/invite-service.js +924 -0
  88. package/dist/core/interaction-router.js +22 -8
  89. package/dist/core/message/im-renderer.js +76 -15
  90. package/dist/core/message/message-bridge.js +94 -11
  91. package/dist/core/message/message-log.js +5 -3
  92. package/dist/core/message/message-queue.js +73 -41
  93. package/dist/core/message/response-engine.js +709 -117
  94. package/dist/core/permission/approval-gateway.js +36 -3
  95. package/dist/core/permission/ec-command-parser.js +61 -29
  96. package/dist/core/permission/protected-paths.js +134 -16
  97. package/dist/core/permission/sandbox-runtime.js +72 -3
  98. package/dist/core/permission/tool-error-code.js +15 -0
  99. package/dist/core/permission/tool-policy.js +7 -7
  100. package/dist/core/relation/peer-identity.js +57 -6
  101. package/dist/core/relation/shared-data.js +106 -0
  102. package/dist/core/role/runtime-policy.js +64 -0
  103. package/dist/core/runtime-lock.js +10 -26
  104. package/dist/core/session/runner-task-history.js +170 -0
  105. package/dist/core/session/session-manager.js +34 -6
  106. package/dist/core/session/session-mapper.js +41 -0
  107. package/dist/core/session/session-turn-coordinator.js +37 -7
  108. package/dist/core/session/session-turns.js +48 -7
  109. package/dist/core/task/task-recovery-service.js +176 -0
  110. package/dist/core/task/task-resume-operation.js +32 -0
  111. package/dist/eck/group-rules-sync.js +5 -3
  112. package/dist/eck/kit-renderer.js +1 -0
  113. package/dist/index.js +308 -54
  114. package/dist/ipc.js +141 -15
  115. package/dist/paths.js +17 -4
  116. package/dist/product.js +2 -0
  117. package/dist/trigger/event-source.js +19 -0
  118. package/dist/trigger/feedback.js +233 -40
  119. package/dist/trigger/parser.js +55 -5
  120. package/dist/trigger/patch.js +9 -0
  121. package/dist/trigger/scheduler.js +30 -4
  122. package/dist/trigger/validation.js +20 -3
  123. package/dist/utils/ecweb-utils.js +1 -0
  124. package/dist/utils/error-utils.js +40 -2
  125. package/dist/utils/logger.js +23 -13
  126. package/dist/utils/tool-summary.js +59 -3
  127. package/dist/utils/welcome.js +174 -1
  128. package/ecagent/dist/harness/agent-harness.js +3 -0
  129. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  130. package/kits/docs/INDEX.md +0 -6
  131. package/kits/docs/channels/aun.md +15 -2
  132. package/kits/docs/channels/feishu.md +1 -1
  133. package/kits/docs/context-assembly.md +29 -17
  134. package/kits/docs/evolcore/INDEX.md +8 -3
  135. package/kits/docs/evolcore/agent-create.md +39 -7
  136. package/kits/docs/evolcore/agent.md +33 -1
  137. package/kits/docs/evolcore/aid.md +9 -1
  138. package/kits/docs/evolcore/config.md +40 -1
  139. package/kits/docs/evolcore/group-collaboration.md +1 -1
  140. package/kits/docs/evolcore/group.md +117 -2
  141. package/kits/docs/evolcore/menu-client.md +16 -0
  142. package/kits/docs/evolcore/msg.md +3 -0
  143. package/kits/docs/evolcore/storage.md +3 -0
  144. package/kits/docs/evolcore/task.md +14 -0
  145. package/kits/docs/evolcore/trigger.md +51 -3
  146. package/kits/docs/venues/group.md +5 -0
  147. package/kits/eck_manifest.json +27 -0
  148. package/kits/rules/01-overview.md +11 -7
  149. package/kits/rules/04-relation.md +2 -1
  150. package/kits/rules/05-venue.md +1 -1
  151. package/kits/rules/06-channel.md +6 -4
  152. package/kits/schemas/agent-config.schema.12.json +25 -1
  153. package/kits/schemas/daemon.schema.6.json +2 -2
  154. package/kits/schemas/defaults.schema.5.json +1 -1
  155. package/kits/schemas/relation-config.schema.1.json +1 -1
  156. package/kits/schemas/relation-config.schema.5.json +1 -1
  157. package/kits/schemas/role-config.schema.1.json +14 -0
  158. package/kits/templates/roles/member.json +5 -0
  159. package/kits/templates/roles/visitor.json +5 -0
  160. package/kits/templates/system-fragments/baseagent.md +1 -1
  161. package/kits/templates/system-fragments/bootstrap.md +26 -9
  162. package/kits/templates/system-fragments/channel.md +8 -44
  163. package/kits/templates/system-fragments/commands.md +10 -2
  164. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  165. package/kits/templates/system-fragments/reply.md +56 -0
  166. package/package.json +7 -3
  167. package/dist/aun/group-identity.js +0 -9
  168. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  169. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,27 @@
3
3
  本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
4
4
  [`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
5
5
 
6
+ ## Unreleased
7
+
8
+ ## 0.0.27 (2026-09-24)
9
+
10
+ - 持久化 AUN 群邀请的 Owner 审核流程,并增加 Base Agent 清单查询入口。
11
+ - 支持 EC Agent 的角色级模型与推理强度策略;为 Claude、Codex、EC Agent runner 会话增加任务历史聚合,并在未确认中断后设置会话重用栅栏。
12
+ - 模型类 Trigger 支持单次执行时限;目标会话失联时明确记录为孤儿执行失败。
13
+ - 修复 AUN 撤回事件导致的私聊游标跳跃;手动停止的 Agent reload 后恢复渠道连接。
14
+ - 规范 Shell 工具活动中的命令展示;本地缺少证书时跳过控制 AID 名片发布。
15
+ - 收紧 Codex 分支会话的权限上下文,避免从旧会话缓存继承过期授权。
16
+ - 移除 CodeBuddy、QwenWork 非标准 Base Agent 的运行时自动管理路径;EC Agent 默认配置恢复显式凭证要求。
17
+ - 移除运行时命令索引中尚未实现的组织管理、任务、资源和工作流命令说明。
18
+ - 修复 AUN 出站 payload 截断按原始字符串大小判断的问题;现在按最终 JSON/UTF-8 大小压缩嵌套日志和转义输出,避免可截断负载被误判为仍超限。
19
+ - 升级 `@agentunion/fastaun` 至 `0.5.17`,同步运行时最低版本要求;包含 Storage / Group FS 上传 Content-Type 一致性修复,避免 OSS 预签名校验不一致导致的 403。
20
+ - 附件 MIME 采用 SDK 字节识别及存储结果;头像与 Storage 上传、删除复用 SDK 文件系统,公开文件返回稳定 AID URL。
21
+ - 订阅 `agentmd.changed` 失效 Core 身份缓存,按需刷新并保留失败重试,避免旧查询覆盖更新。
22
+ - 对齐无持久序号消息的 Pull 类型,按实际返回透传 volatile 元数据,并修正 SDK 未返回 count 时的消息数量。
23
+ - 群命令与托管群文件操作支持 `{base}.{issuer}` 群 AID;不再接受或转换 `group.{issuer}/{base}`,保留当前群权限检查。
24
+ - 默认建群名称改为 `g-` 加五位随机数字(10000–99999),明确身份占用时最多尝试 5 次;自定义名称完整保留 `g-`,并提示实际创建身份与请求不一致的情况。
25
+ - 群参数格式校验与主体类型提示分离;关系角色查询优先采用已存储的关系类型和已验签身份信息。
26
+
6
27
  ## 0.0.26 (2026-09-18)
7
28
 
8
29
  ### AUN、联系人与远程诊断
@@ -21,6 +42,10 @@
21
42
 
22
43
  ### 权限与兼容性
23
44
 
45
+ - 修复 App 管理页读取指定群关系配置时被当前关系限制拒绝:owner/admin 可读取当前 Agent 的目标关系字段,
46
+ 保留 admin 字段权限、普通成员和跨关系写入限制。
47
+ - 批量配置读取为当前版本未支持的 runner 字段返回逐字段 `UNSUPPORTED_FIELD`,
48
+ 保留有效字段并以成功状态返回兼容缺项;真实权限与执行错误仍按失败处理。
24
49
  - npm 包整体排除本机 `skills/` 目录,避免本地 skill 文件随包发布。
25
50
  - 新建 agent.md 默认使用 `agent` 类型,并按白名单归一化类型;
26
51
  对齐 SDK 的可选 description、Unicode 字数、avatar 长度及 4 KB 文档上限校验。
@@ -28,6 +28,48 @@ function Add-Grant($Acl, [string]$Sid, [Security.AccessControl.FileSystemRights]
28
28
  $Acl.AddAccessRule($rule)
29
29
  }
30
30
 
31
+ function Grant-PipeAccess([string]$Target) {
32
+ $prefix = '\\.\pipe\'
33
+ if (-not $Target.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) {
34
+ throw 'Only a local named pipe can receive Codex sandbox access.'
35
+ }
36
+ $pipeName = $Target.Substring($prefix.Length)
37
+ if (-not $pipeName -or $pipeName.Contains('\') -or $pipeName.Contains('/')) {
38
+ throw 'Invalid local named pipe name.'
39
+ }
40
+ $accounts = @()
41
+ foreach ($name in @('CodexSandboxOffline', 'CodexSandboxOnline')) {
42
+ try {
43
+ $account = New-Object Security.Principal.NTAccount([Environment]::MachineName, $name)
44
+ $sid = $account.Translate([Security.Principal.SecurityIdentifier])
45
+ $accounts += @{ name = $name; sid = $sid }
46
+ } catch [Security.Principal.IdentityNotMappedException] {
47
+ # Codex creates these local users during sandbox provisioning. Do not
48
+ # replace a missing user with Everyone, Authenticated Users, or a group.
49
+ }
50
+ }
51
+ if ($accounts.Count -eq 0) { return @{ granted = @() } }
52
+ $pipe = New-Object System.IO.Pipes.NamedPipeClientStream('.', $pipeName,
53
+ [System.IO.Pipes.PipeAccessRights]'ReadWrite,ReadPermissions,ChangePermissions',
54
+ [System.IO.Pipes.PipeOptions]::Asynchronous,
55
+ [Security.Principal.TokenImpersonationLevel]::Identification,
56
+ [System.IO.HandleInheritability]::None)
57
+ try {
58
+ $pipe.Connect(5000)
59
+ $acl = $pipe.GetAccessControl()
60
+ $rights = [System.IO.Pipes.PipeAccessRights]'ReadWrite,Synchronize'
61
+ foreach ($account in $accounts) {
62
+ # GenericWrite/FullControl also grant CreateNewInstance on named pipes.
63
+ # Grant only data I/O to the actual restricted user; the sandbox group
64
+ # alone does not satisfy Windows restricted-token access checks.
65
+ $acl.AddAccessRule((New-Object System.IO.Pipes.PipeAccessRule(
66
+ $account.sid, $rights, [Security.AccessControl.AccessControlType]::Allow)))
67
+ }
68
+ $pipe.SetAccessControl($acl)
69
+ return @{ granted = @($accounts | ForEach-Object { $_.name }) }
70
+ } finally { $pipe.Dispose() }
71
+ }
72
+
31
73
  function Initialize-AccessProbe {
32
74
  # An elevated process reading the file is not evidence that a normal process
33
75
  # can read it. Evaluate the DACL with its UAC linked token when available.
@@ -122,6 +164,7 @@ function Verify-Access($Entry) {
122
164
  try {
123
165
  $payload = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Request)) | ConvertFrom-Json
124
166
  $result = switch ($payload.action) {
167
+ 'pipe' { Grant-PipeAccess ([string]$payload.path) }
125
168
  'preflight' {
126
169
  $principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
127
170
  if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
@@ -1,3 +1,4 @@
1
+ import { normalizeBaseagent } from './baseagent.js';
1
2
  import { commandExists } from '../utils/cross-platform.js';
2
3
  import { getCodexAppServerAvailability } from './codex-runner.js';
3
4
  export const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini', 'ecagent'];
@@ -46,3 +47,70 @@ export function detectBaseagentDetails(codexAvailability = getCodexAppServerAvai
46
47
  },
47
48
  ];
48
49
  }
50
+ /** Only loaded runners can be switched to; never infer this from PATH/config. */
51
+ export function loadedBaseagentIds(keys, owner) {
52
+ const result = new Set();
53
+ for (const key of keys) {
54
+ if (owner !== undefined && !key.startsWith(`${owner}::`))
55
+ continue;
56
+ const separator = key.indexOf('::');
57
+ result.add(separator < 0 ? key : key.slice(separator + 2));
58
+ }
59
+ return [...result];
60
+ }
61
+ function canonical(value) {
62
+ if (typeof value !== 'string')
63
+ return null;
64
+ const id = normalizeBaseagent(value).canonical;
65
+ return id === 'unknown' ? null : id;
66
+ }
67
+ function record(value) {
68
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
69
+ }
70
+ function text(value) {
71
+ return typeof value === 'string' && value.trim() ? value : null;
72
+ }
73
+ /** Safe public projection: credentials and arbitrary backend settings never leave this boundary. */
74
+ export function buildBaseagentSnapshot(config, detections, loaded) {
75
+ const blocks = new Map();
76
+ for (const [name, value] of Object.entries(record(config.baseagents))) {
77
+ const id = canonical(name);
78
+ if (id)
79
+ blocks.set(id, record(value));
80
+ }
81
+ const active = canonical(config.active_baseagent);
82
+ const probes = new Map((detections ?? []).map(item => [String(item.id), item]));
83
+ const loadedIds = loaded === undefined ? null : new Set(loaded);
84
+ const ids = new Set([
85
+ ...blocks.keys(),
86
+ ...(detections ?? []).filter(item => item.detected).map(item => item.id),
87
+ ...(loaded ?? []),
88
+ ...(active ? [active] : []),
89
+ ]);
90
+ return {
91
+ active,
92
+ baseagents: [...ids].map(id => ({
93
+ id,
94
+ label: normalizeBaseagent(id).displayName,
95
+ configured: blocks.has(id),
96
+ detected: detections === null ? null : probes.get(id)?.detected === true,
97
+ loaded: loadedIds?.has(id) ?? null,
98
+ active: id === active,
99
+ model: text(blocks.get(id)?.model),
100
+ effort: text(blocks.get(id)?.effort ?? blocks.get(id)?.reasoning),
101
+ version: probes.get(id)?.version ?? null,
102
+ })),
103
+ };
104
+ }
105
+ /** Shared defaults/Agent inventory for menu, slash and local history readers. */
106
+ export async function getBaseagentSnapshot(options = {}) {
107
+ let config = options.config;
108
+ if (config === undefined) {
109
+ const { read, ConfigTarget } = await import('../config/config-manager.js');
110
+ config = read(ConfigTarget.Defaults, undefined, { cache: false, expand: false }) ?? {};
111
+ }
112
+ const detections = options.probe === false
113
+ ? null
114
+ : (await import('./baseagent-availability.js')).detectBaseagentDetails();
115
+ return buildBaseagentSnapshot(config ?? {}, detections, options.loaded);
116
+ }
@@ -25,14 +25,15 @@ import { sanitizeSessionTitle } from '../core/session/session-title.js';
25
25
  import { resolveClaudeCapabilityRunOptionsForProject } from '../core/capability/capability-manager.js';
26
26
  import { normalizeExecutionPermissionMode, normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
27
27
  import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClassReference, isHClassPath, isLClassPath, isSameOrDescendant, resolveProtectedCandidate, } from '../core/permission/protected-paths.js';
28
- import { buildHClassGuardCommand, collectSandboxGuardDiagnostics, createSandboxInitializationError, getActiveSandboxInitializationFailure, isSandboxInitializationFailure, isSandboxInitializationToolResult, recordSandboxInitializationFailure, SANDBOX_INITIALIZATION_COOLDOWN_MS, SANDBOX_INITIALIZATION_FAILED, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, summarizeBubblewrapArgsForLog, summarizeSandboxGuardDiagnostics, shouldFailIfClaudeSandboxUnavailable, shouldLogSandboxCircuitNotice, } from '../core/permission/sandbox-runtime.js';
28
+ import { buildHClassGuardCommand, collectSandboxGuardDiagnostics, createSandboxInitializationError, getActiveSandboxInitializationFailure, isSandboxInitializationFailure, isSandboxInitializationToolResult, recordSandboxInitializationFailure, getSandboxInitializationFailureCounters, SANDBOX_INITIALIZATION_COOLDOWN_MS, SANDBOX_INITIALIZATION_FAILED, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, summarizeBubblewrapArgsForLog, summarizeSandboxGuardDiagnostics, shouldEnableClaudeWeakerNestedSandbox, shouldFailIfClaudeSandboxUnavailable, shouldLogSandboxCircuitNotice, } from '../core/permission/sandbox-runtime.js';
29
29
  import { buildClaudeUnixSocketAllowlist } from '../core/permission/unix-socket-policy.js';
30
30
  import { auditToolInfrastructureFailure, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
31
31
  import { getManagedTaskTempDir, isManagedSessionRuntimeDir } from '../cli/task-context.js';
32
- import { hasTrustedFullAccessContext } from './runner-types.js';
32
+ import { unsettledInterruptError, withInterruptDeadline, hasTrustedFullAccessContext } from './runner-types.js';
33
33
  import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel, sdkAutoCompactWindowForModel } from './runner-types.js';
34
34
  import { hasContextWindowSuffix, stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
35
35
  const resolveModule = createRequire(import.meta.url);
36
+ const CLAUDE_INTERRUPT_TIMEOUT_MS = 5_000;
36
37
  /**
37
38
  * Resolve the platform package explicitly instead of relying on the SDK's
38
39
  * internal optional-dependency lookup. The latter is sensitive to the module
@@ -282,16 +283,17 @@ function buildClaudeManagedLockdownSettings(capabilityOptions) {
282
283
  }
283
284
  const CLAUDE_SANDBOX_GLOB_MAGIC = /[*?[\]]/;
284
285
  function trustedRuntimeWritePaths(runtimeEnv) {
285
- const tmp = process.env.TMPDIR?.trim();
286
- if (!tmp || !path.isAbsolute(tmp))
286
+ const sessionDir = runtimeEnv?.EVOLCORE_SESSION_RUNTIME_DIR?.trim();
287
+ if (!sessionDir || !isManagedSessionRuntimeDir(sessionDir))
287
288
  return [];
288
- const root = resolveProtectedCandidate(tmp);
289
+ const root = resolveProtectedCandidate(sessionDir);
289
290
  const candidates = [runtimeEnv?.EVOLCORE_SESSION_RUNTIME_DIR, runtimeEnv?.EVOLCORE_RUNTIME_LOCK_DIR]
290
291
  .filter((value) => typeof value === 'string' && path.isAbsolute(value));
291
292
  const paths = [];
292
293
  for (const candidate of candidates) {
293
294
  const resolved = resolveProtectedCandidate(candidate);
294
- if (!isSameOrDescendant(resolved, root))
295
+ if (!isSameOrDescendant(resolved, root)
296
+ || (resolved !== root && !(path.dirname(resolved) === root && path.basename(resolved) === 'locks')))
295
297
  continue;
296
298
  try {
297
299
  const stat = fs.lstatSync(resolved);
@@ -480,7 +482,7 @@ class MessageStream {
480
482
  }
481
483
  export class AgentRunner {
482
484
  name = 'claude';
483
- capabilities = { fullaccess: true, clear: true, compact: true, fork: true, forkAtTurn: true, askUserQuestion: true, planApproval: true, fileRewind: 'checkpoint' };
485
+ capabilities = { taskRecovery: true, fullaccess: true, clear: true, compact: true, fork: true, forkAtTurn: true, askUserQuestion: true, planApproval: true, fileRewind: 'checkpoint' };
484
486
  apiKey;
485
487
  model;
486
488
  effort;
@@ -499,6 +501,7 @@ export class AgentRunner {
499
501
  sandboxSettingsFiles = new Map();
500
502
  streamDone = new Map();
501
503
  streamDoneResolvers = new Map();
504
+ unconfirmedLifecycleFences = new Map();
502
505
  onSessionIdUpdate;
503
506
  onCompactStart;
504
507
  permissionGateway;
@@ -524,6 +527,11 @@ export class AgentRunner {
524
527
  callId: inputId,
525
528
  taskId: context?.taskId,
526
529
  occurrences: circuit.occurrences,
530
+ sandboxFailure: {
531
+ kind: circuit.kind,
532
+ observation: 'initialization_failed',
533
+ counters: getSandboxInitializationFailureCounters(),
534
+ },
527
535
  });
528
536
  await context?.recordExecutionAnomaly?.({
529
537
  code: 'capability_unavailable',
@@ -1337,7 +1345,7 @@ export class AgentRunner {
1337
1345
  * SDK 原始事件 → 标准 AgentEvent 转换
1338
1346
  * 所有 SDK 特有的事件类型引用封装在此方法内
1339
1347
  */
1340
- async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile, permissionMode, turn, projectPath = process.cwd(), topicBinding = false) {
1348
+ async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile, permissionMode, turn, projectPath = process.cwd(), topicBinding = false, recoverySessionId) {
1341
1349
  let lastSessionId;
1342
1350
  let pendingSessionId;
1343
1351
  let hasTurnActivity = false;
@@ -1364,6 +1372,9 @@ export class AgentRunner {
1364
1372
  const candidate = pendingSessionId;
1365
1373
  if (!candidate)
1366
1374
  return undefined;
1375
+ if (recoverySessionId && candidate !== recoverySessionId) {
1376
+ throw new Error('Task recovery cannot replace the original Claude session');
1377
+ }
1367
1378
  if (!isResuming && topicBinding) {
1368
1379
  const ready = waitForFile
1369
1380
  ? await waitForClaudeSessionFile(projectPath, candidate)
@@ -1490,6 +1501,12 @@ export class AgentRunner {
1490
1501
  if (event.type === 'stream_event') {
1491
1502
  const streamEvent = event.event;
1492
1503
  if (streamEvent?.type === 'content_block_start'
1504
+ && streamEvent.content_block?.type === 'text') {
1505
+ hasTurnActivity = true;
1506
+ const contentIndex = typeof streamEvent.index === 'number' ? streamEvent.index : 0;
1507
+ yield { type: 'text_start', contentIndex, turn: turnCount || undefined };
1508
+ }
1509
+ else if (streamEvent?.type === 'content_block_start'
1493
1510
  && (streamEvent.content_block?.type === 'thinking' || streamEvent.content_block?.type === 'redacted_thinking')) {
1494
1511
  hasTurnActivity = true;
1495
1512
  yield { type: 'heartbeat', source: 'reasoning', turn: turnCount || undefined };
@@ -1936,6 +1953,17 @@ export class AgentRunner {
1936
1953
  // Validate the daemon-injected session namespace before the SDK inherits
1937
1954
  // the process environment for any temporary state.
1938
1955
  getManagedTaskTempDir(runtimeEnv);
1956
+ this.assertLifecycleQuiescent(sessionId);
1957
+ const staleLifecycle = this.activeStreams.has(sessionId)
1958
+ || this.activeQueries.has(sessionId)
1959
+ || this.activeMessageStreams.has(sessionId)
1960
+ || this.interruptFns.has(sessionId);
1961
+ if (staleLifecycle) {
1962
+ logger.warn(`[AgentRunner] Stale query lifecycle before runQuery; forcing cleanup: session=${sessionId}`);
1963
+ await this.interrupt(sessionId).catch(error => {
1964
+ logger.warn(`[AgentRunner] Stale query interrupt failed before runQuery: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
1965
+ });
1966
+ }
1939
1967
  // 记录当前 evolcore session ID,用于 Agent ctl 环境变量注入
1940
1968
  // 同步用户级配置到内存
1941
1969
  this.syncFromUserSettings();
@@ -1966,6 +1994,10 @@ export class AgentRunner {
1966
1994
  if (topicBinding && topicAgentSessionId !== agentSessionId) {
1967
1995
  agentSessionId = topicAgentSessionId || undefined;
1968
1996
  }
1997
+ const recoverySessionId = modelOverride?.recovery?.agentSessionId;
1998
+ if (modelOverride?.recovery && (!recoverySessionId || agentSessionId !== recoverySessionId)) {
1999
+ throw new Error('Task recovery requires the original Claude session binding');
2000
+ }
1969
2001
  // 验证会话文件是否存在且有效(仅在有 agentSessionId 时)
1970
2002
  if (agentSessionId) {
1971
2003
  const homeDir = os.homedir();
@@ -1980,7 +2012,7 @@ export class AgentRunner {
1980
2012
  for (const line of lines) {
1981
2013
  try {
1982
2014
  const data = JSON.parse(line);
1983
- if (data.sessionId && data.version) {
2015
+ if (data.sessionId && data.version && (!recoverySessionId || data.sessionId === recoverySessionId)) {
1984
2016
  isValid = true;
1985
2017
  break;
1986
2018
  }
@@ -1996,6 +2028,9 @@ export class AgentRunner {
1996
2028
  }
1997
2029
  }
1998
2030
  if (!isValid) {
2031
+ if (recoverySessionId) {
2032
+ throw new Error(`Task recovery backend is unavailable: ${recoverySessionId}`);
2033
+ }
1999
2034
  if (topicBinding) {
2000
2035
  const error = new Error(`bound topic backend is unavailable: ${agentSessionId}`);
2001
2036
  error.code = 'TOPIC_BACKEND_UNAVAILABLE';
@@ -2047,6 +2082,11 @@ export class AgentRunner {
2047
2082
  callId: inputId,
2048
2083
  taskId: context?.taskId,
2049
2084
  occurrences: activeSandboxFailure.occurrences,
2085
+ sandboxFailure: {
2086
+ kind: activeSandboxFailure.kind,
2087
+ observation: 'circuit_open',
2088
+ counters: getSandboxInitializationFailureCounters(),
2089
+ },
2050
2090
  });
2051
2091
  await context?.recordExecutionAnomaly?.({
2052
2092
  code: 'capability_unavailable',
@@ -2517,6 +2557,7 @@ export class AgentRunner {
2517
2557
  : await (async () => {
2518
2558
  const sandboxProjectionStartedAt = Date.now();
2519
2559
  ensureClaudeGitWorktreeConfig(projectPath);
2560
+ const enableWeakerNestedSandbox = shouldEnableClaudeWeakerNestedSandbox();
2520
2561
  const sandboxFilesystem = buildClaudeProtectedFilesystem(resolvePaths().root, {
2521
2562
  denyLClassRead: callPermissionMode === 'readonly',
2522
2563
  });
@@ -2537,10 +2578,12 @@ export class AgentRunner {
2537
2578
  throw new Error(`[ClaudeSandbox] protected filesystem projection exceeded 2000ms: ${sandboxProjectionDurationMs}ms`);
2538
2579
  }
2539
2580
  logger.info(`[ClaudeSandbox] projection ready denyRead=${sandboxFilesystem.denyRead.length} ` +
2540
- `denyWrite=${sandboxFilesystem.denyWrite.length} durationMs=${sandboxProjectionDurationMs}`);
2581
+ `denyWrite=${sandboxFilesystem.denyWrite.length} durationMs=${sandboxProjectionDurationMs} ` +
2582
+ `weakerNestedSandbox=${enableWeakerNestedSandbox}`);
2541
2583
  return {
2542
2584
  enabled: true,
2543
2585
  failIfUnavailable: shouldFailIfClaudeSandboxUnavailable(),
2586
+ enableWeakerNestedSandbox,
2544
2587
  allowUnsandboxedCommands: true,
2545
2588
  network: {
2546
2589
  allowUnixSockets: sandboxUnixSockets,
@@ -2740,7 +2783,7 @@ export class AgentRunner {
2740
2783
  // Topic bindings are authoritative for every input shape. Starting
2741
2784
  // an unresumed Claude query for a bound topic would discover a second
2742
2785
  // backend and then fail activation with a binding conflict.
2743
- sdkStream = topicBinding
2786
+ sdkStream = topicBinding || recoverySessionId
2744
2787
  ? createQuery(msgStream, agentSessionId, resumeAt)
2745
2788
  : createQuery(msgStream);
2746
2789
  }
@@ -2785,7 +2828,7 @@ export class AgentRunner {
2785
2828
  this.interruptFns.set(sessionId, () => sdkStream.interrupt());
2786
2829
  }
2787
2830
  // 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
2788
- const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile, callPermissionMode, modelOverride?.turn, projectPath, topicBinding);
2831
+ const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile, callPermissionMode, modelOverride?.turn, projectPath, topicBinding, recoverySessionId);
2789
2832
  const self = this;
2790
2833
  return (async function* () {
2791
2834
  try {
@@ -2808,43 +2851,92 @@ export class AgentRunner {
2808
2851
  async interrupt(sessionId) {
2809
2852
  this.permissionContexts.get(sessionId)?.pauseController?.cancel();
2810
2853
  const streamRegistered = this.activeStreams.has(sessionId);
2854
+ const registeredStream = this.activeStreams.get(sessionId);
2811
2855
  const fn = this.interruptFns.get(sessionId);
2812
2856
  const query = this.activeQueries.get(sessionId);
2857
+ const messageStream = this.activeMessageStreams.get(sessionId);
2813
2858
  const cancelledInputIds = [];
2814
2859
  let stillQueued;
2860
+ let interruptConfirmed = false;
2861
+ let queuedCancellationComplete = true;
2815
2862
  if (fn) {
2863
+ let interruptTimer;
2816
2864
  try {
2817
- const receipt = await fn();
2865
+ const receipt = await Promise.race([
2866
+ fn(),
2867
+ new Promise((_, reject) => {
2868
+ interruptTimer = setTimeout(() => reject(new Error('CLAUDE_INTERRUPT_TIMEOUT')), CLAUDE_INTERRUPT_TIMEOUT_MS);
2869
+ interruptTimer.unref?.();
2870
+ }),
2871
+ ]);
2872
+ interruptConfirmed = true;
2818
2873
  stillQueued = Array.isArray(receipt?.still_queued) ? receipt.still_queued.filter((id) => typeof id === 'string') : undefined;
2819
2874
  if (stillQueued && query && typeof query.cancelAsyncMessage === 'function') {
2820
- for (const inputId of stillQueued) {
2875
+ const cancellations = await Promise.all(stillQueued.map(async (inputId) => {
2821
2876
  try {
2822
- if (await query.cancelAsyncMessage(inputId))
2877
+ const cancelled = await withInterruptDeadline(query.cancelAsyncMessage(inputId), 'Claude cancel queued input', CLAUDE_INTERRUPT_TIMEOUT_MS);
2878
+ if (cancelled)
2823
2879
  cancelledInputIds.push(inputId);
2880
+ return cancelled;
2824
2881
  }
2825
2882
  catch (error) {
2826
2883
  logger.debug(`[AgentRunner] Failed to cancel queued input ${inputId}: ${error}`);
2884
+ return false;
2827
2885
  }
2828
- }
2886
+ }));
2887
+ queuedCancellationComplete = cancellations.every(Boolean);
2829
2888
  }
2830
2889
  logger.info(`[AgentRunner] Interrupted session: ${sessionId}`);
2831
2890
  }
2832
2891
  catch (error) {
2833
- logger.warn(`[AgentRunner] Interrupt failed (transport closed): ${sessionId}`);
2892
+ logger.warn(`[AgentRunner] Interrupt failed: ${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
2893
+ }
2894
+ finally {
2895
+ if (interruptTimer)
2896
+ clearTimeout(interruptTimer);
2834
2897
  }
2835
2898
  }
2836
2899
  this.activeMessageStreams.get(sessionId)?.end();
2837
2900
  if (query && typeof query.close === 'function')
2838
2901
  query.close();
2839
2902
  const done = this.streamDone.get(sessionId);
2903
+ let streamDoneSettled = false;
2840
2904
  // A stream interrupted after runQuery resolves but before registerStream has
2841
2905
  // no consumer that can enter the wrapper generator's finally block. Waiting
2842
2906
  // for streamDone here would always burn the full timeout.
2843
2907
  if (done && streamRegistered) {
2844
- await Promise.race([done, new Promise(resolve => setTimeout(resolve, 5_000))]);
2908
+ await Promise.race([
2909
+ done.then(() => { streamDoneSettled = queuedCancellationComplete; }),
2910
+ new Promise(resolve => setTimeout(resolve, CLAUDE_INTERRUPT_TIMEOUT_MS)),
2911
+ ]);
2912
+ }
2913
+ else if (done) {
2914
+ // A runQuery result can exist before ResponseEngine registers a consumer.
2915
+ // In that state there is no wrapper-finally callback to observe. Trust an
2916
+ // acknowledged SDK interrupt; otherwise keep the session fenced.
2917
+ streamDoneSettled = (!query || interruptConfirmed) && queuedCancellationComplete;
2918
+ }
2919
+ // Local queue cleanup cannot authorize a second SDK writer while the old
2920
+ // query remains unconfirmed.
2921
+ // Do not let a late interrupt from an older turn delete a lifecycle that
2922
+ // a newer turn installed while the old SDK call was unwinding.
2923
+ if (this.activeStreams.get(sessionId) === registeredStream
2924
+ && this.interruptFns.get(sessionId) === fn
2925
+ && this.activeQueries.get(sessionId) === query
2926
+ && this.activeMessageStreams.get(sessionId) === messageStream) {
2927
+ if ((done && !streamDoneSettled) || !queuedCancellationComplete) {
2928
+ const fence = done ?? new Promise(() => { });
2929
+ this.unconfirmedLifecycleFences.set(sessionId, fence);
2930
+ if (queuedCancellationComplete) {
2931
+ void fence.then(() => {
2932
+ if (this.unconfirmedLifecycleFences.get(sessionId) === fence) {
2933
+ this.unconfirmedLifecycleFences.delete(sessionId);
2934
+ }
2935
+ });
2936
+ }
2937
+ }
2938
+ this.cleanupStreamLifecycle(sessionId, false, streamDoneSettled);
2845
2939
  }
2846
- this.interruptFns.delete(sessionId);
2847
- this.activeStreams.delete(sessionId);
2848
2940
  this.cleanupSandboxSettingsFiles(sessionId);
2849
2941
  return { stillQueued, cancelledInputIds, closed: true };
2850
2942
  }
@@ -2855,9 +2947,18 @@ export class AgentRunner {
2855
2947
  this.activeStreams.set(key, stream);
2856
2948
  }
2857
2949
  cleanupStream(sessionId) {
2858
- this.activeMessageStreams.get(sessionId)?.end();
2950
+ this.cleanupStreamLifecycle(sessionId, true, true);
2951
+ }
2952
+ assertLifecycleQuiescent(sessionId) {
2953
+ if (this.unconfirmedLifecycleFences.has(sessionId))
2954
+ throw unsettledInterruptError('Claude');
2955
+ }
2956
+ cleanupStreamLifecycle(sessionId, endMessageStream, resolveDone) {
2957
+ if (endMessageStream)
2958
+ this.activeMessageStreams.get(sessionId)?.end();
2859
2959
  this.activeMessageStreams.delete(sessionId);
2860
- this.streamDoneResolvers.get(sessionId)?.();
2960
+ if (resolveDone)
2961
+ this.streamDoneResolvers.get(sessionId)?.();
2861
2962
  this.streamDoneResolvers.delete(sessionId);
2862
2963
  this.streamDone.delete(sessionId);
2863
2964
  this.activeStreams.delete(sessionId);
@@ -210,8 +210,8 @@ export class CodexAppServerClient {
210
210
  }
211
211
  return undefined;
212
212
  }
213
- async threadRead(threadId, includeTurns = true) {
214
- return this.request('thread/read', { threadId, includeTurns });
213
+ async threadRead(threadId, includeTurns = true, timeoutMs) {
214
+ return this.request('thread/read', { threadId, includeTurns }, timeoutMs);
215
215
  }
216
216
  async threadRollback(threadId, numTurns) {
217
217
  return this.request('thread/rollback', { threadId, numTurns });
@@ -622,7 +622,7 @@ export class CodexAppServerClient {
622
622
  env.TMPDIR = managedTmpDir;
623
623
  delete env.EVOLCORE_RUNTIME_LOCK_DIR;
624
624
  if (managedTmpDir) {
625
- const runtimeLockDir = path.join(managedTmpDir, 'evolcore-locks');
625
+ const runtimeLockDir = path.join(managedTmpDir, 'locks');
626
626
  try {
627
627
  // The parent is the already-validated process root, so only create
628
628
  // and validate the final component. Never follow a pre-existing
@@ -768,30 +768,63 @@ export class CodexAppServerClient {
768
768
  }
769
769
  return args;
770
770
  }
771
- async request(method, params) {
772
- await this.ensureReadyFor(method);
773
- const proc = this.proc;
774
- if (!proc)
775
- throw new Error('Codex app-server is not running');
776
- const id = String(this.nextId++);
777
- const payload = { id, method };
778
- if (params !== undefined && params !== null)
779
- payload.params = params;
780
- return new Promise((resolve, reject) => {
781
- this.pending.set(id, {
782
- resolve,
783
- reject,
784
- method,
785
- params,
786
- stderrStartSequence: this.stderrSequence,
787
- });
788
- proc.stdin.write(`${JSON.stringify(payload)}\n`, error => {
789
- if (!error)
790
- return;
791
- this.pending.delete(id);
792
- reject(error);
771
+ async request(method, params, timeoutMs) {
772
+ // Only lifecycle RPCs get short deadlines. Model work has its own policy.
773
+ const deadline = timeoutMs ?? (method === 'turn/interrupt' ? 5_000 : undefined);
774
+ let timer;
775
+ let requestId;
776
+ let expired = false;
777
+ const operation = (async () => {
778
+ await this.ensureReadyFor(method);
779
+ if (expired)
780
+ throw new Error(`Codex ${method} expired before dispatch`);
781
+ const proc = this.proc;
782
+ if (!proc)
783
+ throw new Error('Codex app-server is not running');
784
+ const id = String(this.nextId++);
785
+ requestId = id;
786
+ const payload = { id, method };
787
+ if (params !== undefined && params !== null)
788
+ payload.params = params;
789
+ return new Promise((resolve, reject) => {
790
+ this.pending.set(id, {
791
+ resolve,
792
+ reject,
793
+ method,
794
+ params,
795
+ stderrStartSequence: this.stderrSequence,
796
+ });
797
+ proc.stdin.write(`${JSON.stringify(payload)}\n`, error => {
798
+ if (!error)
799
+ return;
800
+ this.pending.delete(id);
801
+ reject(error);
802
+ });
793
803
  });
794
- });
804
+ })();
805
+ if (deadline === undefined)
806
+ return operation;
807
+ try {
808
+ return await Promise.race([
809
+ operation,
810
+ new Promise((_, reject) => {
811
+ timer = setTimeout(() => {
812
+ expired = true;
813
+ reject(new Error(`Codex ${method} deadline exceeded`));
814
+ }, deadline);
815
+ timer.unref?.();
816
+ }),
817
+ ]);
818
+ }
819
+ finally {
820
+ if (timer)
821
+ clearTimeout(timer);
822
+ if (requestId && expired) {
823
+ const pending = this.pending.get(requestId);
824
+ this.pending.delete(requestId);
825
+ pending?.reject(new Error(`Codex ${method} deadline exceeded`));
826
+ }
827
+ }
795
828
  }
796
829
  respond(id, result) {
797
830
  if (!this.proc)
@@ -870,6 +903,10 @@ export class CodexAppServerClient {
870
903
  else if (/already has an active writer/i.test(detail)) {
871
904
  error.code = CODEX_THREAD_ACTIVE_WRITER;
872
905
  }
906
+ else if (pending.method === 'turn/steer' && /\bno active turn\b/i.test(detail)) {
907
+ // Explicit server rejection proves the input was not accepted. Transport errors do not.
908
+ error.code = 'CODEX_NO_ACTIVE_TURN';
909
+ }
873
910
  pending.reject(error);
874
911
  return;
875
912
  }