evolcore 0.0.26 → 0.0.28

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 (177) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/bin/codex-managed-acl.ps1 +43 -0
  3. package/dist/agents/baseagent-availability.js +68 -0
  4. package/dist/agents/baseagent.js +10 -5
  5. package/dist/agents/claude-runner.js +123 -22
  6. package/dist/agents/codex/app-server-client.js +70 -26
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex-runner.js +363 -421
  9. package/dist/agents/ecagent-runner.js +170 -25
  10. package/dist/agents/gemini-runner.js +6 -0
  11. package/dist/agents/runner-types.js +50 -2
  12. package/dist/aun/aid/agentmd.js +73 -31
  13. package/dist/aun/aid/client.js +1 -1
  14. package/dist/aun/aid/control-aid.js +180 -1
  15. package/dist/aun/aid/identity.js +3 -3
  16. package/dist/aun/group/history.js +303 -0
  17. package/dist/aun/group/identity.js +43 -0
  18. package/dist/aun/group/index.js +5 -0
  19. package/dist/aun/{msg/group.js → group/operations.js} +161 -38
  20. package/dist/aun/{menu-client.js → menu/client.js} +13 -7
  21. package/dist/aun/menu/index.js +2 -0
  22. package/dist/aun/menu/transport.js +49 -0
  23. package/dist/aun/msg/index.js +2 -1
  24. package/dist/aun/msg/managed-operation.js +84 -18
  25. package/dist/aun/msg/mention-schema.js +51 -0
  26. package/dist/aun/msg/p2p.js +13 -8
  27. package/dist/aun/msg/upload.js +43 -88
  28. package/dist/aun/outbox.js +29 -7
  29. package/dist/aun/rpc/client.js +1 -0
  30. package/dist/aun/storage/client.js +39 -50
  31. package/dist/channels/aun.js +544 -239
  32. package/dist/channels/daemon.js +10 -6
  33. package/dist/cli/agent-command.js +108 -6
  34. package/dist/cli/agent-create.js +515 -2
  35. package/dist/cli/agent.js +109 -8
  36. package/dist/cli/aun-commands.js +191 -42
  37. package/dist/cli/config.js +8 -5
  38. package/dist/cli/contact.js +2 -2
  39. package/dist/cli/ctl-command.js +2 -2
  40. package/dist/cli/daemon-commands.js +111 -162
  41. package/dist/cli/diag.js +3 -3
  42. package/dist/cli/fs-command.js +3 -3
  43. package/dist/cli/handoff-command.js +6 -6
  44. package/dist/cli/index.js +8 -2
  45. package/dist/cli/init.js +34 -1
  46. package/dist/cli/managed-command-guard.js +2 -1
  47. package/dist/cli/menu-client.js +13 -5
  48. package/dist/cli/model.js +3 -3
  49. package/dist/cli/queue-command.js +2 -2
  50. package/dist/cli/response.js +2 -2
  51. package/dist/cli/restart-monitor.js +81 -19
  52. package/dist/cli/task-command.js +59 -0
  53. package/dist/cli/task-context.js +97 -49
  54. package/dist/cli/trigger-command.js +20 -10
  55. package/dist/config/builtin-roles.js +10 -0
  56. package/dist/config/config-field-policy.js +11 -0
  57. package/dist/config/config-manager.js +32 -4
  58. package/dist/config/config-operation-service.js +20 -5
  59. package/dist/config/contact-book-store.js +1 -1
  60. package/dist/config/contact-book.js +1 -1
  61. package/dist/config/contact-operation-service.js +1 -1
  62. package/dist/config/peer-role-resolver.js +1 -1
  63. package/dist/config/resolved-config-op.js +3 -0
  64. package/dist/config-store.js +1 -1
  65. package/dist/core/agent-application-service.js +5 -0
  66. package/dist/core/audit/event-key.js +17 -2
  67. package/dist/core/audit/log-integrity.js +34 -8
  68. package/dist/core/auth/agent-delegation.js +48 -14
  69. package/dist/core/auth/auth-gateway.js +2 -0
  70. package/dist/core/auth/authorization-audit.js +34 -38
  71. package/dist/core/auth/operation-authorizer.js +67 -5
  72. package/dist/core/auth/operation-catalog.js +110 -0
  73. package/dist/core/auth/remote-access-store.js +115 -9
  74. package/dist/core/bootstrap-messages.js +66 -7
  75. package/dist/core/bootstrap-service.js +110 -19
  76. package/dist/core/command/agent-control.js +88 -7
  77. package/dist/core/command/agent-storage-menu.js +443 -0
  78. package/dist/core/command/cli-intent-parser.js +19 -0
  79. package/dist/core/command/command-handler.js +105 -40
  80. package/dist/core/command/data-root-resolver.js +273 -0
  81. package/dist/core/command/diag-service.js +292 -27
  82. package/dist/core/command/group-menu.js +7 -2
  83. package/dist/core/command/menu-catalog.js +34 -4
  84. package/dist/core/command/menu-handler.js +364 -96
  85. package/dist/core/command/menu-protocol.js +43 -6
  86. package/dist/core/command/role-menu.js +2 -2
  87. package/dist/core/command/slash-handler.js +61 -37
  88. package/dist/core/event-catalog.js +4 -0
  89. package/dist/core/evolagent-registry.js +28 -2
  90. package/dist/core/group/invite-service.js +924 -0
  91. package/dist/core/interaction-router.js +22 -8
  92. package/dist/core/message/im-renderer.js +94 -16
  93. package/dist/core/message/message-bridge.js +140 -13
  94. package/dist/core/message/message-log.js +5 -3
  95. package/dist/core/message/message-queue.js +73 -41
  96. package/dist/core/message/response-engine.js +729 -118
  97. package/dist/core/permission/approval-gateway.js +36 -3
  98. package/dist/core/permission/ec-command-parser.js +76 -36
  99. package/dist/core/permission/protected-paths.js +134 -16
  100. package/dist/core/permission/sandbox-runtime.js +72 -3
  101. package/dist/core/permission/tool-error-code.js +15 -0
  102. package/dist/core/permission/tool-policy.js +8 -8
  103. package/dist/core/relation/peer-identity.js +57 -6
  104. package/dist/core/relation/shared-data.js +106 -0
  105. package/dist/core/role/runtime-policy.js +64 -0
  106. package/dist/core/runtime-lock.js +10 -26
  107. package/dist/core/session/runner-task-history.js +170 -0
  108. package/dist/core/session/session-manager.js +38 -6
  109. package/dist/core/session/session-mapper.js +41 -0
  110. package/dist/core/session/session-turn-coordinator.js +37 -7
  111. package/dist/core/session/session-turns.js +48 -7
  112. package/dist/core/task/task-recovery-service.js +176 -0
  113. package/dist/core/task/task-resume-operation.js +32 -0
  114. package/dist/eck/group-rules-sync.js +5 -3
  115. package/dist/eck/kit-renderer.js +1 -0
  116. package/dist/index.js +322 -207
  117. package/dist/ipc.js +141 -15
  118. package/dist/paths.js +17 -4
  119. package/dist/product.js +3 -0
  120. package/dist/trigger/event-source.js +19 -0
  121. package/dist/trigger/feedback.js +233 -40
  122. package/dist/trigger/parser.js +55 -5
  123. package/dist/trigger/patch.js +9 -0
  124. package/dist/trigger/scheduler.js +30 -4
  125. package/dist/trigger/validation.js +20 -3
  126. package/dist/utils/atomic-write.js +98 -0
  127. package/dist/utils/cross-platform.js +87 -16
  128. package/dist/utils/ecweb-utils.js +1 -0
  129. package/dist/utils/error-utils.js +40 -2
  130. package/dist/utils/logger.js +23 -13
  131. package/dist/utils/npm-ops.js +119 -4
  132. package/dist/utils/package-version-info.js +244 -0
  133. package/dist/utils/tool-summary.js +59 -3
  134. package/dist/utils/welcome.js +174 -1
  135. package/dist/utils/windows-shell-trust.js +37 -0
  136. package/ecagent/dist/harness/agent-harness.js +3 -0
  137. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  138. package/kits/docs/INDEX.md +0 -6
  139. package/kits/docs/channels/aun.md +23 -2
  140. package/kits/docs/channels/feishu.md +1 -1
  141. package/kits/docs/context-assembly.md +29 -17
  142. package/kits/docs/evolcore/INDEX.md +8 -3
  143. package/kits/docs/evolcore/agent-create.md +39 -7
  144. package/kits/docs/evolcore/agent.md +33 -1
  145. package/kits/docs/evolcore/aid.md +9 -1
  146. package/kits/docs/evolcore/config.md +40 -1
  147. package/kits/docs/evolcore/group-collaboration.md +1 -1
  148. package/kits/docs/evolcore/group.md +117 -2
  149. package/kits/docs/evolcore/menu-client.md +16 -0
  150. package/kits/docs/evolcore/msg.md +3 -0
  151. package/kits/docs/evolcore/storage.md +3 -0
  152. package/kits/docs/evolcore/task.md +14 -0
  153. package/kits/docs/evolcore/trigger.md +51 -3
  154. package/kits/docs/venues/group.md +5 -0
  155. package/kits/eck_manifest.json +27 -0
  156. package/kits/rules/01-overview.md +11 -7
  157. package/kits/rules/04-relation.md +2 -1
  158. package/kits/rules/05-venue.md +1 -1
  159. package/kits/rules/06-channel.md +6 -4
  160. package/kits/schemas/agent-config.schema.12.json +25 -1
  161. package/kits/schemas/daemon.schema.6.json +2 -2
  162. package/kits/schemas/defaults.schema.5.json +1 -1
  163. package/kits/schemas/relation-config.schema.1.json +1 -1
  164. package/kits/schemas/relation-config.schema.5.json +1 -1
  165. package/kits/schemas/role-config.schema.1.json +14 -0
  166. package/kits/templates/roles/member.json +5 -0
  167. package/kits/templates/roles/visitor.json +5 -0
  168. package/kits/templates/system-fragments/baseagent.md +1 -1
  169. package/kits/templates/system-fragments/bootstrap.md +26 -9
  170. package/kits/templates/system-fragments/channel.md +8 -44
  171. package/kits/templates/system-fragments/commands.md +10 -2
  172. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  173. package/kits/templates/system-fragments/reply.md +56 -0
  174. package/package.json +7 -3
  175. package/dist/aun/group-identity.js +0 -9
  176. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  177. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,38 @@
3
3
  本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
4
4
  [`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
5
5
 
6
+ ## Unreleased
7
+
8
+ ## 0.0.28 (2026-09-24)
9
+
10
+ - 增加 npm 版本缓存,改进升级状态展示及启动、重启流程。
11
+ - 改进 AUN 回复投递与消息展示。
12
+ - 加固 Windows CLI 启动和 PowerShell 命令解析,完善 Codex CLI 诊断。
13
+ - 强化 Menu 请求校验与错误处理。
14
+
15
+ ## 0.0.27 (2026-09-24)
16
+
17
+ ### AUN、群聊与消息
18
+
19
+ - 持久化 AUN 群邀请的 Owner 审核流程,并增加 Base Agent 清单查询入口。
20
+ - 支持 EC Agent 的角色级模型与推理强度策略;完善 Claude、Codex、EC Agent 会话的任务历史和中断恢复。
21
+ - 模型类 Trigger 支持单次执行时限;目标会话失联时明确记录为失败。
22
+ - 修复 AUN 撤回消息造成的私聊游标跳跃;手动停止的 Agent 重新加载后可恢复渠道连接。
23
+ - 规范工具活动中的 Shell 命令展示,并在缺少本地证书时跳过控制 AID 名片发布。
24
+ - 优化消息大小处理,提升长消息和复杂内容的发送稳定性。
25
+ - 升级 AUN SDK 至 `0.5.17`,改进消息、群文件和存储上传的兼容性。
26
+ - 改进附件、头像和文件存储处理,提升文件类型识别、上传和公开访问的稳定性。
27
+ - 改进身份信息缓存刷新,确保对端资料更新后及时生效。
28
+ - 支持无连续编号消息的完整拉取信息,并修正消息数量统计。
29
+ - 群命令和托管群文件操作支持标准群 AID 格式。
30
+ - 优化群名称生成和冲突处理,自定义名称保持原样。
31
+ - 改进群组参数校验和成员身份识别,提升群聊操作准确性。
32
+
33
+ ### Agent、权限与运行稳定性
34
+
35
+ - 收紧 Codex 分支会话的权限上下文,避免旧会话继承过期授权。
36
+ - 完善 Agent 初始化、会话恢复、权限边界和运行状态处理。
37
+
6
38
  ## 0.0.26 (2026-09-18)
7
39
 
8
40
  ### AUN、联系人与远程诊断
@@ -21,6 +53,10 @@
21
53
 
22
54
  ### 权限与兼容性
23
55
 
56
+ - 修复 App 管理页读取指定群关系配置时被当前关系限制拒绝:owner/admin 可读取当前 Agent 的目标关系字段,
57
+ 保留 admin 字段权限、普通成员和跨关系写入限制。
58
+ - 批量配置读取为当前版本未支持的 runner 字段返回逐字段 `UNSUPPORTED_FIELD`,
59
+ 保留有效字段并以成功状态返回兼容缺项;真实权限与执行错误仍按失败处理。
24
60
  - npm 包整体排除本机 `skills/` 目录,避免本地 skill 文件随包发布。
25
61
  - 新建 agent.md 默认使用 `agent` 类型,并按白名单归一化类型;
26
62
  对齐 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
+ }
@@ -12,22 +12,26 @@
12
12
  import fs from 'fs';
13
13
  import path from 'path';
14
14
  import os from 'os';
15
- import { commandExists, isWindows, resolveProbePath } from '../utils/cross-platform.js';
15
+ import { buildCliLaunchCommand, commandExists, isWindows, resolvePowerShellShimPath, resolveProbePath, } from '../utils/cross-platform.js';
16
16
  import { execFileSync } from 'child_process';
17
17
  import { parse as parseToml } from 'smol-toml';
18
18
  /** Resolve the installed Codex CLI entrypoint, including npm shims on Windows. */
19
19
  export function resolveCodexCliPath() {
20
20
  return resolveProbePath('codex');
21
21
  }
22
+ /** Return a PS1-only Codex shim for installation diagnostics, never execution. */
23
+ export function resolveCodexPowerShellShimPath() {
24
+ return resolvePowerShellShimPath('codex');
25
+ }
22
26
  export function resolveCodexLaunchCommand(args, options = {}) {
23
27
  const windows = options.isWindows ?? isWindows;
24
28
  const commandPath = options.commandPath !== undefined ? options.commandPath : resolveCodexCliPath();
25
29
  if (!commandPath)
26
30
  return null;
27
- if (windows && /\.(cmd|bat)$/i.test(commandPath)) {
28
- return { command: options.comSpec ?? process.env.ComSpec ?? 'cmd.exe', args: ['/d', '/s', '/c', commandPath, ...args] };
29
- }
30
- return { command: commandPath, args };
31
+ return buildCliLaunchCommand(commandPath, args, {
32
+ isWindows: windows,
33
+ comSpec: options.comSpec,
34
+ });
31
35
  }
32
36
  /** Execute a Codex CLI command using the resolved npm shim/path. */
33
37
  export function execCodexCliSync(args, options) {
@@ -40,6 +44,7 @@ export function execCodexCliSync(args, options) {
40
44
  return execFileSync(launchCommand.command, launchCommand.args, {
41
45
  ...(options ?? {}),
42
46
  windowsHide: isWindows,
47
+ windowsVerbatimArguments: launchCommand.windowsVerbatimArguments,
43
48
  });
44
49
  }
45
50
  const BASEAGENT_ALIASES = {
@@ -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);