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.
- package/CHANGELOG.md +25 -0
- package/bin/codex-managed-acl.ps1 +43 -0
- package/dist/agents/baseagent-availability.js +68 -0
- package/dist/agents/claude-runner.js +123 -22
- package/dist/agents/codex/app-server-client.js +63 -26
- package/dist/agents/codex-runner.js +316 -411
- package/dist/agents/ecagent-runner.js +170 -25
- package/dist/agents/gemini-runner.js +6 -0
- package/dist/agents/runner-types.js +50 -2
- package/dist/aun/aid/agentmd.js +73 -31
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/control-aid.js +180 -1
- package/dist/aun/aid/identity.js +3 -3
- package/dist/aun/group/history.js +303 -0
- package/dist/aun/group/identity.js +43 -0
- package/dist/aun/group/index.js +5 -0
- package/dist/aun/{msg/group.js → group/operations.js} +161 -38
- package/dist/aun/{menu-client.js → menu/client.js} +12 -7
- package/dist/aun/menu/index.js +2 -0
- package/dist/aun/menu/transport.js +49 -0
- package/dist/aun/msg/index.js +2 -1
- package/dist/aun/msg/managed-operation.js +84 -18
- package/dist/aun/msg/mention-schema.js +51 -0
- package/dist/aun/msg/p2p.js +13 -8
- package/dist/aun/msg/upload.js +43 -88
- package/dist/aun/outbox.js +29 -7
- package/dist/aun/rpc/client.js +1 -0
- package/dist/aun/storage/client.js +39 -50
- package/dist/channels/aun.js +522 -233
- package/dist/channels/daemon.js +10 -6
- package/dist/cli/agent-command.js +108 -6
- package/dist/cli/agent-create.js +515 -2
- package/dist/cli/agent.js +109 -8
- package/dist/cli/aun-commands.js +191 -42
- package/dist/cli/config.js +8 -5
- package/dist/cli/contact.js +2 -2
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +35 -23
- package/dist/cli/diag.js +2 -2
- package/dist/cli/fs-command.js +3 -3
- package/dist/cli/handoff-command.js +6 -6
- package/dist/cli/index.js +8 -2
- package/dist/cli/init.js +34 -1
- package/dist/cli/managed-command-guard.js +2 -1
- package/dist/cli/menu-client.js +9 -5
- package/dist/cli/model.js +3 -3
- package/dist/cli/queue-command.js +2 -2
- package/dist/cli/response.js +2 -2
- package/dist/cli/restart-monitor.js +16 -2
- package/dist/cli/task-command.js +59 -0
- package/dist/cli/task-context.js +97 -49
- package/dist/cli/trigger-command.js +20 -10
- package/dist/config/builtin-roles.js +10 -0
- package/dist/config/config-field-policy.js +11 -0
- package/dist/config/config-manager.js +32 -4
- package/dist/config/config-operation-service.js +20 -5
- package/dist/config/contact-book-store.js +1 -1
- package/dist/config/contact-book.js +1 -1
- package/dist/config/contact-operation-service.js +1 -1
- package/dist/config/peer-role-resolver.js +1 -1
- package/dist/config/resolved-config-op.js +3 -0
- package/dist/config-store.js +1 -1
- package/dist/core/agent-application-service.js +5 -0
- package/dist/core/audit/event-key.js +17 -2
- package/dist/core/audit/log-integrity.js +34 -8
- package/dist/core/auth/agent-delegation.js +48 -14
- package/dist/core/auth/auth-gateway.js +2 -0
- package/dist/core/auth/authorization-audit.js +34 -38
- package/dist/core/auth/operation-authorizer.js +67 -5
- package/dist/core/auth/operation-catalog.js +110 -0
- package/dist/core/auth/remote-access-store.js +115 -9
- package/dist/core/bootstrap-messages.js +66 -7
- package/dist/core/bootstrap-service.js +110 -19
- package/dist/core/command/agent-control.js +88 -7
- package/dist/core/command/agent-storage-menu.js +443 -0
- package/dist/core/command/cli-intent-parser.js +19 -0
- package/dist/core/command/command-handler.js +76 -39
- package/dist/core/command/data-root-resolver.js +273 -0
- package/dist/core/command/group-menu.js +7 -2
- package/dist/core/command/menu-catalog.js +34 -4
- package/dist/core/command/menu-handler.js +348 -77
- package/dist/core/command/menu-protocol.js +40 -6
- package/dist/core/command/role-menu.js +2 -2
- package/dist/core/command/slash-handler.js +37 -22
- package/dist/core/event-catalog.js +4 -0
- package/dist/core/evolagent-registry.js +28 -2
- package/dist/core/group/invite-service.js +924 -0
- package/dist/core/interaction-router.js +22 -8
- package/dist/core/message/im-renderer.js +76 -15
- package/dist/core/message/message-bridge.js +94 -11
- package/dist/core/message/message-log.js +5 -3
- package/dist/core/message/message-queue.js +73 -41
- package/dist/core/message/response-engine.js +709 -117
- package/dist/core/permission/approval-gateway.js +36 -3
- package/dist/core/permission/ec-command-parser.js +61 -29
- package/dist/core/permission/protected-paths.js +134 -16
- package/dist/core/permission/sandbox-runtime.js +72 -3
- package/dist/core/permission/tool-error-code.js +15 -0
- package/dist/core/permission/tool-policy.js +7 -7
- package/dist/core/relation/peer-identity.js +57 -6
- package/dist/core/relation/shared-data.js +106 -0
- package/dist/core/role/runtime-policy.js +64 -0
- package/dist/core/runtime-lock.js +10 -26
- package/dist/core/session/runner-task-history.js +170 -0
- package/dist/core/session/session-manager.js +34 -6
- package/dist/core/session/session-mapper.js +41 -0
- package/dist/core/session/session-turn-coordinator.js +37 -7
- package/dist/core/session/session-turns.js +48 -7
- package/dist/core/task/task-recovery-service.js +176 -0
- package/dist/core/task/task-resume-operation.js +32 -0
- package/dist/eck/group-rules-sync.js +5 -3
- package/dist/eck/kit-renderer.js +1 -0
- package/dist/index.js +308 -54
- package/dist/ipc.js +141 -15
- package/dist/paths.js +17 -4
- package/dist/product.js +2 -0
- package/dist/trigger/event-source.js +19 -0
- package/dist/trigger/feedback.js +233 -40
- package/dist/trigger/parser.js +55 -5
- package/dist/trigger/patch.js +9 -0
- package/dist/trigger/scheduler.js +30 -4
- package/dist/trigger/validation.js +20 -3
- package/dist/utils/ecweb-utils.js +1 -0
- package/dist/utils/error-utils.js +40 -2
- package/dist/utils/logger.js +23 -13
- package/dist/utils/tool-summary.js +59 -3
- package/dist/utils/welcome.js +174 -1
- package/ecagent/dist/harness/agent-harness.js +3 -0
- package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
- package/kits/docs/INDEX.md +0 -6
- package/kits/docs/channels/aun.md +15 -2
- package/kits/docs/channels/feishu.md +1 -1
- package/kits/docs/context-assembly.md +29 -17
- package/kits/docs/evolcore/INDEX.md +8 -3
- package/kits/docs/evolcore/agent-create.md +39 -7
- package/kits/docs/evolcore/agent.md +33 -1
- package/kits/docs/evolcore/aid.md +9 -1
- package/kits/docs/evolcore/config.md +40 -1
- package/kits/docs/evolcore/group-collaboration.md +1 -1
- package/kits/docs/evolcore/group.md +117 -2
- package/kits/docs/evolcore/menu-client.md +16 -0
- package/kits/docs/evolcore/msg.md +3 -0
- package/kits/docs/evolcore/storage.md +3 -0
- package/kits/docs/evolcore/task.md +14 -0
- package/kits/docs/evolcore/trigger.md +51 -3
- package/kits/docs/venues/group.md +5 -0
- package/kits/eck_manifest.json +27 -0
- package/kits/rules/01-overview.md +11 -7
- package/kits/rules/04-relation.md +2 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/rules/06-channel.md +6 -4
- package/kits/schemas/agent-config.schema.12.json +25 -1
- package/kits/schemas/daemon.schema.6.json +2 -2
- package/kits/schemas/defaults.schema.5.json +1 -1
- package/kits/schemas/relation-config.schema.1.json +1 -1
- package/kits/schemas/relation-config.schema.5.json +1 -1
- package/kits/schemas/role-config.schema.1.json +14 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/baseagent.md +1 -1
- package/kits/templates/system-fragments/bootstrap.md +26 -9
- package/kits/templates/system-fragments/channel.md +8 -44
- package/kits/templates/system-fragments/commands.md +10 -2
- package/kits/templates/system-fragments/group-unread-history.md +7 -0
- package/kits/templates/system-fragments/reply.md +56 -0
- package/package.json +7 -3
- package/dist/aun/group-identity.js +0 -9
- /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
- /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
|
|
286
|
-
if (!
|
|
286
|
+
const sessionDir = runtimeEnv?.EVOLCORE_SESSION_RUNTIME_DIR?.trim();
|
|
287
|
+
if (!sessionDir || !isManagedSessionRuntimeDir(sessionDir))
|
|
287
288
|
return [];
|
|
288
|
-
const root = resolveProtectedCandidate(
|
|
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
|
|
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
|
-
|
|
2875
|
+
const cancellations = await Promise.all(stillQueued.map(async (inputId) => {
|
|
2821
2876
|
try {
|
|
2822
|
-
|
|
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
|
|
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([
|
|
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.
|
|
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
|
-
|
|
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, '
|
|
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
|
-
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
this.
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
}
|