evolcore 0.0.22 → 0.0.24
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 +74 -14
- package/README.md +4 -0
- package/bin/codex-managed-acl.ps1 +160 -0
- package/bin/codex-managed-hook.mjs +127 -10
- package/bin/ec-safe-output.js +85 -3
- package/bin/install-codex-managed-hooks.mjs +384 -95
- package/dist/agents/baseagent-availability.js +48 -0
- package/dist/agents/baseagent.js +30 -2
- package/dist/agents/claude-runner.js +179 -240
- package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
- package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
- package/dist/agents/codex/catalog.js +70 -0
- package/dist/agents/codex/diagnostics.js +369 -0
- package/dist/agents/codex-runner.js +839 -192
- package/dist/agents/ecagent-runner.js +27 -34
- package/dist/agents/gemini-runner.js +62 -8
- package/dist/agents/runner-types.js +1 -1
- package/dist/aun/aid/managed-operation.js +28 -6
- package/dist/aun/aid/store.js +43 -0
- package/dist/aun/menu-client.js +272 -0
- package/dist/aun/msg/group.js +65 -4
- package/dist/aun/msg/index.js +1 -1
- package/dist/aun/msg/managed-operation.js +57 -5
- package/dist/aun/outbox.js +56 -18
- package/dist/aun/rpc/client.js +7 -1
- package/dist/channels/aun.js +178 -77
- package/dist/channels/daemon.js +7 -1
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +10 -4
- package/dist/channels/wechat.js +81 -10
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent-command.js +29 -3
- package/dist/cli/agent-create.js +538 -0
- package/dist/cli/agent.js +26 -675
- package/dist/cli/aun-commands.js +108 -24
- package/dist/cli/command-log.js +21 -2
- package/dist/cli/ctl-command.js +2 -0
- package/dist/cli/daemon-commands.js +289 -61
- package/dist/cli/index.js +22 -2
- package/dist/cli/init.js +2 -10
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/menu-client.js +95 -0
- package/dist/cli/model.js +23 -8
- package/dist/cli/restart-monitor.js +432 -8
- package/dist/cli/stats.js +110 -0
- package/dist/cli/task-context.js +12 -2
- package/dist/cli/trigger-command.js +9 -1
- package/dist/config/builtin-roles.js +2 -0
- package/dist/config/schema-migration.js +14 -2
- package/dist/core/auth/agent-delegation.js +10 -7
- package/dist/core/auth/auth-gateway.js +3 -2
- package/dist/core/auth/authorization-audit.js +75 -18
- package/dist/core/auth/operation-authorizer.js +63 -3
- package/dist/core/auth/operation-catalog.js +14 -2
- package/dist/core/auth/remote-access-store.js +136 -0
- package/dist/core/baseagent-loader.js +4 -4
- package/dist/core/bootstrap-messages.js +39 -18
- package/dist/core/bootstrap-service.js +32 -26
- package/dist/core/capability/capability-manager.js +8 -4
- package/dist/core/capability/providers/codex-capability-provider.js +1 -1
- package/dist/core/command/agent-control.js +7 -2
- package/dist/core/command/command-handler.js +128 -30
- package/dist/core/command/daemon-diagnostics.js +448 -0
- package/dist/core/command/menu-catalog.js +6 -1
- package/dist/core/command/menu-handler.js +410 -98
- package/dist/core/command/menu-protocol.js +123 -0
- package/dist/core/command/slash-gate.js +4 -6
- package/dist/core/command/slash-handler.js +199 -70
- package/dist/core/data-migration.js +5 -15
- package/dist/core/event-catalog.js +3 -0
- package/dist/core/handoff/runtime.js +3 -1
- package/dist/core/handoff/store.js +7 -1
- package/dist/core/message/im-renderer.js +13 -6
- package/dist/core/message/message-bridge.js +5 -36
- package/dist/core/message/message-queue.js +20 -15
- package/dist/core/message/message-utils.js +6 -3
- package/dist/core/message/response-engine.js +147 -94
- package/dist/core/model/catalog-service.js +310 -0
- package/dist/core/model/model-catalog.js +134 -334
- package/dist/core/model/model-diagnostics.js +4 -5
- package/dist/core/model/model-fallback.js +9 -16
- package/dist/core/model/model-permission.js +5 -2
- package/dist/core/permission/approval-gateway.js +183 -32
- package/dist/core/permission/protected-paths.js +63 -4
- package/dist/core/permission/sandbox-runtime.js +4 -2
- package/dist/core/permission/tool-policy.js +257 -31
- package/dist/core/runtime-lock.js +13 -8
- package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
- package/dist/core/session/session-manager.js +49 -13
- package/dist/core/session/session-renew.js +1 -0
- package/dist/index.js +214 -57
- package/dist/ipc.js +191 -102
- package/dist/paths.js +23 -1
- package/dist/stats/billing.js +18 -4
- package/dist/trigger/manager.js +3 -1
- package/dist/trigger/parser.js +77 -13
- package/dist/trigger/patch.js +104 -1
- package/dist/utils/atomic-write.js +26 -2
- package/dist/utils/cross-platform.js +436 -1
- package/dist/utils/ecweb-supervisor.js +11 -2
- package/dist/utils/ecweb-utils.js +8 -26
- package/dist/utils/error-utils.js +16 -2
- package/dist/utils/model-prices.jsonl +23 -20
- package/dist/utils/npm-ops.js +70 -15
- package/dist/utils/windows-autostart.js +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -0
- package/kits/docs/evolcore/agent-create.md +13 -0
- package/kits/docs/evolcore/group-collaboration.md +7 -2
- package/kits/docs/evolcore/menu-client.md +34 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
- package/kits/migrations/migrate-role-config-v4.mjs +11 -5
- package/kits/migrations/migrate-role-config-v5.mjs +8 -2
- package/kits/templates/roles/admin.json +2 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +8 -1
- package/package.json +16 -4
- package/dist/utils/codex-cli.js +0 -43
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,80 @@
|
|
|
3
3
|
本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
|
|
4
4
|
[`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
|
|
5
5
|
|
|
6
|
-
## 0.0.
|
|
6
|
+
## 0.0.24 (2026-09-14)
|
|
7
|
+
|
|
8
|
+
### Windows 与 Codex 运行时
|
|
9
|
+
|
|
10
|
+
- 启动、重启前统一检查 Codex managed hook;
|
|
11
|
+
缺失时自动安装并验证,异常时保持失败关闭。
|
|
12
|
+
- managed hook 按 thread 凭据分流;
|
|
13
|
+
非托管 Codex 不再受机器级策略误伤。
|
|
14
|
+
- 凭据可跨 daemon 重启恢复;
|
|
15
|
+
失效或伪造凭据继续保持失败关闭。
|
|
16
|
+
- 修复 Windows ACL、npm 子进程和安全输出解析;
|
|
17
|
+
兼容 cmd.exe 包装与跨平台可执行文件查找。
|
|
18
|
+
- 修复 Windows Codex 沙箱配置;
|
|
19
|
+
提升自启动任务的运行权限。
|
|
20
|
+
- 将 Windows Hook 启动探针并入 `ec diagnose --codex-hook`;
|
|
21
|
+
不再增加 Hook 热路径入口。
|
|
22
|
+
- 恢复 Codex 会话共享策略并保留 Agent 描述;
|
|
23
|
+
修复单 runner 与直接 backend key 的解析。
|
|
24
|
+
- Codex 首轮会话在 rollout 可恢复后绑定;
|
|
25
|
+
审批请求、响应和终态统一记录生命周期审计。
|
|
26
|
+
|
|
27
|
+
### Agent、AID 与生命周期
|
|
28
|
+
|
|
29
|
+
- 新增受权限约束的 AID 查询、展示和管理能力。
|
|
30
|
+
- 新增 Agent 创建、委托创建和创建进度管理;
|
|
31
|
+
统一创建参数与 Base Agent 探测。
|
|
32
|
+
- 完善 Agent 生命周期、Bootstrap、Owner Welcome;
|
|
33
|
+
收敛会话交接与主会话切换行为。
|
|
34
|
+
- Claude 会话恢复增加 transcript 就绪校验;
|
|
35
|
+
失效话题提供 `/renew` 恢复提示。
|
|
36
|
+
- 统一 Agent outbox 的 canonical 路径;
|
|
37
|
+
旧布局首次读写时执行去重导入。
|
|
38
|
+
- 推理心跳与空闲通知状态隔离;
|
|
39
|
+
修复话题会话命令的回复路由。
|
|
40
|
+
- `ec stats` 支持托管任务和关系角色范围;
|
|
41
|
+
`ec msg history` 支持管理角色跨对端查询。
|
|
42
|
+
- 自动安装插件,并修复 Evol App 的 Menu 兼容访问。
|
|
43
|
+
|
|
44
|
+
### AUN、权限与模型
|
|
45
|
+
|
|
46
|
+
- 增强 AUN 消息可靠投递、去重恢复和群消息处理。
|
|
47
|
+
- 统一远程配对、token 鉴权和 Menu 命令权限边界。
|
|
48
|
+
- 主机支持查询 Base Agent 模型列表;
|
|
49
|
+
统一模型目录与推理强度回退。
|
|
50
|
+
- 合并模型价格与能力参数表;
|
|
51
|
+
统一维护价格、上下文窗口和模型能力。
|
|
52
|
+
- `max_tokens` 改用模型真实 `context_window`;
|
|
53
|
+
未知模型继续使用兼容性默认值。
|
|
54
|
+
- 重启与升级流程改为事务化;
|
|
55
|
+
编译失败时回滚并保持服务运行。
|
|
56
|
+
## 0.0.23 (2026-09-08)
|
|
57
|
+
|
|
58
|
+
### 会话、Trigger 与 Codex 运行时
|
|
59
|
+
|
|
60
|
+
- 完善 Codex 子代理的上下文继承、权限传递和任务生命周期收敛,并贯通工具调用标识与授权审计。
|
|
61
|
+
- 为受信任的 root Fullaccess Claude 会话显式设置沙箱标记,并限制复杂 PowerShell carrier 的摘要解包范围。
|
|
62
|
+
- 确保 daemon Trigger 在错误已经渲染后仍发送明确终态,并传播中断生命周期元数据、统一中断结果分类。
|
|
63
|
+
- 完善话题续接、会话后端身份校验和跨客户端线程释放,降低重启、续接与并发操作造成的旧任务串入风险。
|
|
64
|
+
|
|
65
|
+
### Windows、AUN 与配置安全
|
|
66
|
+
|
|
67
|
+
- 统一 Windows IPC 端点路径规范和跨进程命名管道连接,修复实例 socket 路径在不同运行入口间不一致的问题。
|
|
68
|
+
- 修复 Windows 配置与联系人数据的锁竞争,支持安全识别并回收损坏或遗留锁文件。
|
|
69
|
+
- 强化 Windows Shell 签名信任校验,在允许受信任命令执行的同时收紧受管命令边界。
|
|
70
|
+
- 增加受保护的 AUN Menu 客户端和 daemon 诊断入口,补充 E2EE 请求校验、EC Web token 授权与日志查询边界。
|
|
71
|
+
- 校验并恢复 WeChat 增量同步游标,遇到无效游标时隔离本地状态并自动重试。
|
|
72
|
+
- 升级 `@agentunion/fastaun` 至 `0.5.13`;`ec group invite` 改用定向邀请生命周期,并新增邀请类型与接受/拒绝/撤回命令。
|
|
73
|
+
|
|
74
|
+
### EC Web 与监控
|
|
75
|
+
|
|
76
|
+
- 完善 EC Web 认证恢复、Trigger 管理和操作授权审计上下文。
|
|
77
|
+
- EC Web Monitor 按客户端订阅生命周期启停进程树采样,避免无订阅时持续采集。
|
|
78
|
+
|
|
79
|
+
## 0.0.22 (2026-09-04)
|
|
7
80
|
|
|
8
81
|
### 话题续接与触发任务
|
|
9
82
|
|
|
@@ -32,19 +105,6 @@
|
|
|
32
105
|
- 修复 Windows Codex managed hook 的动态 IPC 校验与安装配置,统一 proactive hook 可用性检查并保持 H-class/L-class 保护边界。
|
|
33
106
|
- 修正 Codex Windows 文件系统 glob 规则,改善受管临时目录和锁目录的跨平台匹配。
|
|
34
107
|
- 统一 BaseAgent 请求地址和身份请求头传递,完善 Codex 会话启动、重连与错误诊断。
|
|
35
|
-
- 完善 Codex 子代理的上下文继承、权限传递和任务生命周期收敛,并贯通工具调用标识与授权审计。
|
|
36
|
-
- 为受信任的 root Fullaccess Claude 会话显式设置沙箱标记,并限制复杂 PowerShell carrier 的摘要解包范围。
|
|
37
|
-
|
|
38
|
-
### Windows IPC 与运行稳定性
|
|
39
|
-
|
|
40
|
-
- 统一 Windows IPC 端点路径规范和跨进程命名管道连接,修复实例 socket 路径在不同运行入口间不一致的问题。
|
|
41
|
-
- 修复 Windows 配置与联系人数据的锁竞争,支持安全识别并回收损坏或遗留锁文件。
|
|
42
|
-
- 强化 Windows Shell 签名信任校验,在允许受信任命令执行的同时收紧受管命令边界。
|
|
43
|
-
|
|
44
|
-
### Trigger 与监控收敛
|
|
45
|
-
|
|
46
|
-
- 确保 daemon Trigger 在错误已经渲染后仍发送明确终态,并传播中断生命周期元数据、统一中断结果分类。
|
|
47
|
-
- EC Web Monitor 按订阅生命周期启停进程树采样,避免无订阅时持续采集。
|
|
48
108
|
|
|
49
109
|
## 0.0.21 (2026-09-03)
|
|
50
110
|
|
package/README.md
CHANGED
|
@@ -105,6 +105,10 @@ npm install -g evolcore
|
|
|
105
105
|
|
|
106
106
|
Linux 全局安装会在 `postinstall` 阶段准备 Codex 受管运行所需的真实目录 `/etc/codex`。如果当前 npm 全局目录属于普通用户且该系统目录尚不存在,请先执行 `sudo install -d -m 0755 /etc/codex`,再重新安装。
|
|
107
107
|
|
|
108
|
+
Windows 的 npm 安装和升级不会直接写入机器级 Codex 配置。配置使用 Codex 后,执行 `ec start` 或 `ec restart` 时会自动检测托管 Hook;缺失时自动请求 Windows 管理员授权(UAC),安装并复检成功后继续启动。已安装时直接继续,无需单独执行安装命令,详见 [Windows Codex 托管 Hook](docs/codex-windows-managed-hook.md)。
|
|
109
|
+
|
|
110
|
+
如果取消 UAC 或安装失败,本次启动或重启会停止;重启检查发生在停止旧服务之前。已有策略异常或安装位置变化需要管理员核对,自动安装不会覆盖这些状态。
|
|
111
|
+
|
|
108
112
|
> **Windows 用户**:首次运行前可能需要执行 `Set-ExecutionPolicy RemoteSigned -Scope CurrentUser`
|
|
109
113
|
|
|
110
114
|
**从源码安装**:
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
param([Parameter(Mandatory = $true)][string]$Request)
|
|
2
|
+
|
|
3
|
+
# Called by the Node installer. Paths and security descriptors are data, never
|
|
4
|
+
# interpolated into PowerShell source. Use Windows PowerShell's built-in .NET ACL API.
|
|
5
|
+
$ErrorActionPreference = 'Stop'
|
|
6
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
7
|
+
# A parent pwsh session can export its incompatible PowerShell 7 modules.
|
|
8
|
+
# This helper always runs in Windows PowerShell 5.1.
|
|
9
|
+
$env:PSModulePath = Join-Path $PSHOME 'Modules'
|
|
10
|
+
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
11
|
+
|
|
12
|
+
function Get-Descriptor([string]$Target) {
|
|
13
|
+
return (Get-Acl -LiteralPath $Target).GetSecurityDescriptorSddlForm(
|
|
14
|
+
[Security.AccessControl.AccessControlSections]'Access,Owner,Group')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function Set-Descriptor([string]$Target, [string]$Sddl) {
|
|
18
|
+
$acl = Get-Acl -LiteralPath $Target
|
|
19
|
+
$acl.SetSecurityDescriptorSddlForm($Sddl, [Security.AccessControl.AccessControlSections]'Access,Owner,Group')
|
|
20
|
+
Set-Acl -LiteralPath $Target -AclObject $acl
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function Add-Grant($Acl, [string]$Sid, [Security.AccessControl.FileSystemRights]$Rights, [bool]$Directory) {
|
|
24
|
+
$inheritance = if ($Directory) { [Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit' } else { [Security.AccessControl.InheritanceFlags]::None }
|
|
25
|
+
$rule = New-Object Security.AccessControl.FileSystemAccessRule(
|
|
26
|
+
(New-Object Security.Principal.SecurityIdentifier($Sid)), $Rights, $inheritance,
|
|
27
|
+
[Security.AccessControl.PropagationFlags]::None, [Security.AccessControl.AccessControlType]::Allow)
|
|
28
|
+
$Acl.AddAccessRule($rule)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function Initialize-AccessProbe {
|
|
32
|
+
# An elevated process reading the file is not evidence that a normal process
|
|
33
|
+
# can read it. Evaluate the DACL with its UAC linked token when available.
|
|
34
|
+
Add-Type -TypeDefinition @'
|
|
35
|
+
using System;
|
|
36
|
+
using System.Runtime.InteropServices;
|
|
37
|
+
using System.Security.Principal;
|
|
38
|
+
public static class EvolCoreAclProbe {
|
|
39
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
40
|
+
struct Mapping { public uint Read, Write, Execute, All; }
|
|
41
|
+
[DllImport("advapi32.dll", SetLastError=true)]
|
|
42
|
+
static extern bool GetTokenInformation(IntPtr token, int type, out IntPtr linked, int size, out int returned);
|
|
43
|
+
[DllImport("advapi32.dll", SetLastError=true)]
|
|
44
|
+
static extern bool DuplicateTokenEx(IntPtr token, uint desiredAccess, IntPtr tokenAttributes,
|
|
45
|
+
int impersonationLevel, int tokenType, out IntPtr duplicate);
|
|
46
|
+
[DllImport("advapi32.dll", SetLastError=true)]
|
|
47
|
+
static extern bool AccessCheck(byte[] descriptor, IntPtr token, uint desired,
|
|
48
|
+
ref Mapping mapping, byte[] privileges, ref uint length, out uint granted, out bool allowed);
|
|
49
|
+
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
|
|
50
|
+
public static string TokenKind;
|
|
51
|
+
public static long Granted(byte[] descriptor) {
|
|
52
|
+
using (var identity = WindowsIdentity.GetCurrent()) {
|
|
53
|
+
var principal = new WindowsPrincipal(identity);
|
|
54
|
+
IntPtr token = identity.Token, linked = IntPtr.Zero, duplicate = IntPtr.Zero;
|
|
55
|
+
try {
|
|
56
|
+
if (identity.IsSystem || principal.IsInRole(WindowsBuiltInRole.Administrator)) {
|
|
57
|
+
int size;
|
|
58
|
+
if (!GetTokenInformation(token, 19, out linked, IntPtr.Size, out size)) {
|
|
59
|
+
TokenKind = "unavailable";
|
|
60
|
+
return -1;
|
|
61
|
+
}
|
|
62
|
+
token = linked;
|
|
63
|
+
TokenKind = "linked-token";
|
|
64
|
+
} else { TokenKind = "current-token"; }
|
|
65
|
+
// AccessCheck only needs SecurityIdentification (1). A UAC linked
|
|
66
|
+
// token can be limited to that level, so requesting the higher
|
|
67
|
+
// SecurityImpersonation level fails with ERROR_BAD_IMPERSONATION_LEVEL.
|
|
68
|
+
if (!DuplicateTokenEx(token, 0x0000000A, IntPtr.Zero, 1, 2, out duplicate))
|
|
69
|
+
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
|
|
70
|
+
var mapping = new Mapping { Read=0x120089, Write=0x120116, Execute=0x1200a0, All=0x1f01ff };
|
|
71
|
+
uint length=1024, granted;
|
|
72
|
+
bool allowed;
|
|
73
|
+
if (!AccessCheck(descriptor, duplicate, 0x02000000, ref mapping,
|
|
74
|
+
new byte[length], ref length, out granted, out allowed))
|
|
75
|
+
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
|
|
76
|
+
return allowed ? granted : 0;
|
|
77
|
+
} finally {
|
|
78
|
+
if (duplicate != IntPtr.Zero) CloseHandle(duplicate);
|
|
79
|
+
if (linked != IntPtr.Zero) CloseHandle(linked);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
'@
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function Verify-Access($Entry) {
|
|
88
|
+
$target = [string]$Entry.path
|
|
89
|
+
$acl = Get-Acl -LiteralPath $target
|
|
90
|
+
$read = [long][Security.AccessControl.FileSystemRights]::ReadAndExecute
|
|
91
|
+
$write = [long][Security.AccessControl.FileSystemRights]'Write,Delete,DeleteSubdirectoriesAndFiles,ChangePermissions,TakeOwnership'
|
|
92
|
+
if ($Entry.protected) {
|
|
93
|
+
$grants = @{}
|
|
94
|
+
foreach ($rule in $acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])) {
|
|
95
|
+
if (($rule.PropagationFlags -band [Security.AccessControl.PropagationFlags]::InheritOnly) -ne 0) { continue }
|
|
96
|
+
$sid = $rule.IdentityReference.Value
|
|
97
|
+
$rights = [long]$rule.FileSystemRights
|
|
98
|
+
if ($sid -in @('S-1-1-0','S-1-5-11','S-1-5-32-545')) {
|
|
99
|
+
if ($rule.AccessControlType -eq 'Allow' -and ($rights -band $write) -ne 0) {
|
|
100
|
+
throw "Unsafe write permission on ${target}: $sid has $($rule.FileSystemRights). Existing policy was not removed."
|
|
101
|
+
}
|
|
102
|
+
if ($rule.AccessControlType -eq 'Deny' -and ($rights -band $read) -ne 0) {
|
|
103
|
+
throw "Read permission denied on ${target}: $sid. Existing policy was not removed."
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if ($rule.AccessControlType -eq 'Allow') { $grants[$sid] = ([long]$grants[$sid]) -bor $rights }
|
|
107
|
+
}
|
|
108
|
+
foreach ($sid in @('S-1-5-18','S-1-5-32-544','S-1-5-32-545')) {
|
|
109
|
+
$required = if ($sid -eq 'S-1-5-32-545') { $read } else { [long][Security.AccessControl.FileSystemRights]::FullControl }
|
|
110
|
+
if ((([long]$grants[$sid]) -band $required) -ne $required) { throw "Missing required access on ${target}: $sid" }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
$granted = [EvolCoreAclProbe]::Granted($acl.GetSecurityDescriptorBinaryForm())
|
|
114
|
+
if ($granted -ge 0) {
|
|
115
|
+
$required = if ($Entry.execute) { $read } else { [long][Security.AccessControl.FileSystemRights]::Read }
|
|
116
|
+
if (($granted -band $required) -ne $required) { throw "Normal-token read/execute access denied: $target" }
|
|
117
|
+
if ($Entry.protected -and ($granted -band $write) -ne 0) { throw "Normal token can modify the protected file or its ACL: $target" }
|
|
118
|
+
}
|
|
119
|
+
return @{ path = $target; userProbe = [EvolCoreAclProbe]::TokenKind }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
$payload = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Request)) | ConvertFrom-Json
|
|
124
|
+
$result = switch ($payload.action) {
|
|
125
|
+
'preflight' {
|
|
126
|
+
$principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
|
|
127
|
+
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
128
|
+
throw 'Machine-wide Codex hook provisioning requires an elevated Windows installer. npm installation does not require this step.'
|
|
129
|
+
}
|
|
130
|
+
@{ elevated = $true }
|
|
131
|
+
}
|
|
132
|
+
'snapshot' { @{ sddl = (Get-Descriptor $payload.path) } }
|
|
133
|
+
'prepare' {
|
|
134
|
+
$target = [string]$payload.path
|
|
135
|
+
if ($payload.reference) {
|
|
136
|
+
# Same-directory staging preserves the shared configuration's owner,
|
|
137
|
+
# inherited policy and custom administrator ACEs. Never reset its ACL.
|
|
138
|
+
Set-Descriptor $target (Get-Descriptor $payload.reference)
|
|
139
|
+
} else {
|
|
140
|
+
$flags = if ($payload.directory) { 'OICI' } else { '' }
|
|
141
|
+
Set-Descriptor $target "O:BAG:BAD:P(A;${flags};FA;;;SY)(A;${flags};FA;;;BA)(A;${flags};FRFX;;;BU)"
|
|
142
|
+
}
|
|
143
|
+
$acl = Get-Acl -LiteralPath $target
|
|
144
|
+
Add-Grant $acl 'S-1-5-18' 'FullControl' ([bool]$payload.directory)
|
|
145
|
+
Add-Grant $acl 'S-1-5-32-544' 'FullControl' ([bool]$payload.directory)
|
|
146
|
+
Add-Grant $acl 'S-1-5-32-545' 'ReadAndExecute' ([bool]$payload.directory)
|
|
147
|
+
Set-Acl -LiteralPath $target -AclObject $acl
|
|
148
|
+
@{ prepared = $true }
|
|
149
|
+
}
|
|
150
|
+
'verify' {
|
|
151
|
+
Initialize-AccessProbe
|
|
152
|
+
@($payload.entries | ForEach-Object { Verify-Access $_ })
|
|
153
|
+
}
|
|
154
|
+
default { throw "Unknown ACL operation: $($payload.action)" }
|
|
155
|
+
}
|
|
156
|
+
ConvertTo-Json -InputObject $result -Depth 8 -Compress
|
|
157
|
+
} catch {
|
|
158
|
+
[Console]::Error.WriteLine("[evolcore] ACL $($payload.action) failed for $($payload.path): $($_.Exception.Message)")
|
|
159
|
+
exit 1
|
|
160
|
+
}
|
|
@@ -1,12 +1,67 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import net from 'node:net';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
const startedAt = Date.now();
|
|
8
|
+
const runId = randomUUID();
|
|
9
|
+
let logPath;
|
|
10
|
+
let logFailed = false;
|
|
11
|
+
let context = {};
|
|
12
|
+
|
|
13
|
+
// This file is also copied into the Linux runtime; keep diagnostics self-contained.
|
|
14
|
+
function diagnostic(event, details = {}) {
|
|
15
|
+
if (process.env.EVOLCORE_CODEX_HOOK_LOG === '0' || logFailed) return;
|
|
16
|
+
try {
|
|
17
|
+
if (!logPath) {
|
|
18
|
+
logPath = path.resolve(process.env.EVOLCORE_CODEX_HOOK_LOG
|
|
19
|
+
|| path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'log', 'evolcore-hook.jsonl'));
|
|
20
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true, mode: 0o700 });
|
|
21
|
+
// Rotation is best-effort when concurrent tool calls start together.
|
|
22
|
+
try {
|
|
23
|
+
if (fs.statSync(logPath).size >= 2 * 1024 * 1024) fs.renameSync(logPath, `${logPath}.1`);
|
|
24
|
+
} catch {}
|
|
25
|
+
}
|
|
26
|
+
fs.appendFileSync(logPath, JSON.stringify({
|
|
27
|
+
time: new Date().toISOString(), runId, pid: process.pid,
|
|
28
|
+
elapsedMs: Date.now() - startedAt, event, ...context, ...details,
|
|
29
|
+
}) + '\n', { mode: 0o600 });
|
|
30
|
+
} catch (error) {
|
|
31
|
+
logFailed = true;
|
|
32
|
+
process.stderr.write(`[evolcore] Hook diagnostic log unavailable (${error?.code || 'unknown'}): ${logPath || '(unresolved)'}\n`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function errorDetails(error) {
|
|
37
|
+
// JSON parser messages and policy reasons can contain tool input. Omit them.
|
|
38
|
+
return {
|
|
39
|
+
errorName: error?.name,
|
|
40
|
+
errorCode: error?.code,
|
|
41
|
+
syscall: error?.syscall,
|
|
42
|
+
frames: typeof error?.stack === 'string'
|
|
43
|
+
? error.stack.split('\n').filter(line => /^\s+at /.test(line)).slice(0, 6) : undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
process.on('uncaughtExceptionMonitor', error => diagnostic('uncaught_exception', errorDetails(error)));
|
|
48
|
+
process.on('exit', code => diagnostic('exit', { exitCode: code }));
|
|
49
|
+
diagnostic('start', {
|
|
50
|
+
nodeVersion: process.version, execPath: process.execPath, entrypoint: process.argv[1],
|
|
51
|
+
argumentCount: Math.max(0, process.argv.length - 2),
|
|
52
|
+
socketArgumentPresent: Boolean(process.argv[2]),
|
|
53
|
+
socketEnvironmentPresent: Boolean(process.env.EVOLCORE_INSTANCE_SOCKET),
|
|
54
|
+
socketArgumentUnexpanded: /^%EVOLCORE_INSTANCE_SOCKET%$/i.test(process.argv[2] || ''),
|
|
55
|
+
socketSource: process.argv[2] ? 'argument' : process.env.EVOLCORE_INSTANCE_SOCKET ? 'environment' : 'missing',
|
|
56
|
+
});
|
|
3
57
|
|
|
4
58
|
// Only bound socket establishment. Once connected, the official Codex hook
|
|
5
59
|
// timeout owns the lifetime of this process; an application timeout here would
|
|
6
60
|
// turn /pause into a denial/allow race.
|
|
7
61
|
const connectTimeoutMs = 1500;
|
|
8
62
|
|
|
9
|
-
function deny(reason) {
|
|
63
|
+
function deny(reason, category) {
|
|
64
|
+
diagnostic('decision', { decision: 'deny', category });
|
|
10
65
|
process.stdout.write(JSON.stringify({
|
|
11
66
|
hookSpecificOutput: {
|
|
12
67
|
hookEventName: 'PreToolUse',
|
|
@@ -21,6 +76,7 @@ function deny(reason) {
|
|
|
21
76
|
}
|
|
22
77
|
|
|
23
78
|
function queryDaemon(socketPath, payload) {
|
|
79
|
+
diagnostic('connect_start', { timeoutMs: connectTimeoutMs });
|
|
24
80
|
return new Promise(resolve => {
|
|
25
81
|
const socket = net.createConnection(socketPath);
|
|
26
82
|
let buffer = '';
|
|
@@ -35,24 +91,53 @@ function queryDaemon(socketPath, payload) {
|
|
|
35
91
|
resolve(value);
|
|
36
92
|
};
|
|
37
93
|
timer = setTimeout(() => {
|
|
38
|
-
if (!connected)
|
|
94
|
+
if (!connected) {
|
|
95
|
+
diagnostic('connect_timeout');
|
|
96
|
+
finish(null);
|
|
97
|
+
}
|
|
39
98
|
}, connectTimeoutMs);
|
|
40
99
|
timer.unref?.();
|
|
41
|
-
socket.once('error',
|
|
100
|
+
socket.once('error', error => {
|
|
101
|
+
diagnostic('socket_error', { connected, ...errorDetails(error) });
|
|
102
|
+
finish(null);
|
|
103
|
+
});
|
|
104
|
+
socket.once('close', () => {
|
|
105
|
+
if (!settled) diagnostic('socket_closed_without_response', { connected, responseBytes: Buffer.byteLength(buffer) });
|
|
106
|
+
});
|
|
42
107
|
socket.once('connect', () => {
|
|
43
108
|
connected = true;
|
|
44
109
|
clearTimeout(timer);
|
|
110
|
+
diagnostic('connected');
|
|
45
111
|
socket.write(JSON.stringify(payload) + '\n');
|
|
46
112
|
});
|
|
47
113
|
socket.on('data', chunk => {
|
|
48
114
|
buffer += String(chunk);
|
|
49
115
|
const index = buffer.indexOf('\n');
|
|
50
116
|
if (index < 0) return;
|
|
51
|
-
try {
|
|
117
|
+
try {
|
|
118
|
+
const result = JSON.parse(buffer.slice(0, index));
|
|
119
|
+
diagnostic('response', {
|
|
120
|
+
ok: result?.ok === true, applicable: result?.applicable === true,
|
|
121
|
+
decision: ['allow', 'deny'].includes(result?.decision) ? result.decision : 'other',
|
|
122
|
+
reasonPresent: typeof result?.reason === 'string',
|
|
123
|
+
});
|
|
124
|
+
finish(result);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
diagnostic('invalid_response', { responseBytes: Buffer.byteLength(buffer), ...errorDetails(error) });
|
|
127
|
+
finish(null);
|
|
128
|
+
}
|
|
52
129
|
});
|
|
53
130
|
});
|
|
54
131
|
}
|
|
55
132
|
|
|
133
|
+
// The Windows managed requirements are machine-wide, so unmanaged Codex
|
|
134
|
+
// processes also reach this hook. EvolCore-owned threads carry a per-thread
|
|
135
|
+
// credential (EVOLCORE_CODEX_SESSION_TOKEN, injected through the thread's
|
|
136
|
+
// shell environment policy); unmanaged ones do not. Unmanaged calls are
|
|
137
|
+
// allowed outright and never contact the daemon — EvolCore being stopped must
|
|
138
|
+
// not break a standalone Codex. Credentialed calls always require an explicit
|
|
139
|
+
// daemon allow: a credential without its daemon is an orphaned managed thread
|
|
140
|
+
// and fails closed.
|
|
56
141
|
async function main() {
|
|
57
142
|
// Windows managed requirements use one machine-wide command. Resolve the
|
|
58
143
|
// per-daemon named pipe from the inherited environment so multiple EvolCore
|
|
@@ -61,17 +146,45 @@ async function main() {
|
|
|
61
146
|
let input;
|
|
62
147
|
try {
|
|
63
148
|
input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
64
|
-
} catch {
|
|
65
|
-
|
|
149
|
+
} catch (error) {
|
|
150
|
+
diagnostic('invalid_input', errorDetails(error));
|
|
151
|
+
deny('Invalid Codex PreToolUse payload.', 'invalid_input');
|
|
66
152
|
return;
|
|
67
153
|
}
|
|
154
|
+
context = {
|
|
155
|
+
sessionId: typeof input?.session_id === 'string' ? input.session_id.slice(0, 128) : undefined,
|
|
156
|
+
toolName: typeof input?.tool_name === 'string' ? input.tool_name.slice(0, 128) : undefined,
|
|
157
|
+
callId: typeof (input?.tool_use_id ?? input?.toolUseId ?? input?.item_id ?? input?.itemId) === 'string'
|
|
158
|
+
? (input.tool_use_id ?? input.toolUseId ?? input.item_id ?? input.itemId).slice(0, 128) : undefined,
|
|
159
|
+
};
|
|
160
|
+
diagnostic('input', {
|
|
161
|
+
preToolUse: input?.hook_event_name === 'PreToolUse',
|
|
162
|
+
sessionIdPresent: typeof input?.session_id === 'string' && Boolean(input.session_id),
|
|
163
|
+
toolNamePresent: typeof input?.tool_name === 'string' && Boolean(input.tool_name),
|
|
164
|
+
toolInputPresent: input?.tool_input !== undefined,
|
|
165
|
+
});
|
|
68
166
|
if (!socketPath
|
|
69
167
|
|| input?.hook_event_name !== 'PreToolUse'
|
|
70
168
|
|| typeof input?.session_id !== 'string'
|
|
71
169
|
|| !input.session_id
|
|
72
170
|
|| typeof input?.tool_name !== 'string'
|
|
73
171
|
|| !input.tool_name) {
|
|
74
|
-
deny('Incomplete Codex PreToolUse payload.');
|
|
172
|
+
deny('Incomplete Codex PreToolUse payload.', 'incomplete_input');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const sessionToken = typeof process.env.EVOLCORE_CODEX_SESSION_TOKEN === 'string'
|
|
177
|
+
&& process.env.EVOLCORE_CODEX_SESSION_TOKEN
|
|
178
|
+
? process.env.EVOLCORE_CODEX_SESSION_TOKEN
|
|
179
|
+
: undefined;
|
|
180
|
+
|
|
181
|
+
if (!sessionToken) {
|
|
182
|
+
// Unmanaged process: nothing to enforce, and no reason to pay the daemon
|
|
183
|
+
// round-trip (or its outage) for a call we would allow anyway.
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (!socketPath) {
|
|
187
|
+
deny('EvolCore session credential is present but the policy service address is missing.');
|
|
75
188
|
return;
|
|
76
189
|
}
|
|
77
190
|
|
|
@@ -80,16 +193,20 @@ async function main() {
|
|
|
80
193
|
threadId: input.session_id,
|
|
81
194
|
toolName: input.tool_name,
|
|
82
195
|
toolInput: input.tool_input ?? null,
|
|
196
|
+
sessionToken,
|
|
83
197
|
...(typeof (input.tool_use_id ?? input.toolUseId ?? input.item_id ?? input.itemId) === 'string'
|
|
84
198
|
? { callId: input.tool_use_id ?? input.toolUseId ?? input.item_id ?? input.itemId }
|
|
85
199
|
: {}),
|
|
86
200
|
});
|
|
87
201
|
if (!result) {
|
|
88
|
-
deny('EvolCore policy service is unavailable.');
|
|
202
|
+
deny('EvolCore policy service is unavailable.', 'service_unavailable');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (result.ok === true && result.applicable === true && result.decision === 'allow') {
|
|
206
|
+
diagnostic('decision', { decision: 'allow' });
|
|
89
207
|
return;
|
|
90
208
|
}
|
|
91
|
-
|
|
92
|
-
deny(result.reason || 'EvolCore policy did not explicitly allow this tool call.');
|
|
209
|
+
deny(result.reason || 'EvolCore policy did not explicitly allow this tool call.', 'policy_denied');
|
|
93
210
|
}
|
|
94
211
|
|
|
95
212
|
void main();
|
package/bin/ec-safe-output.js
CHANGED
|
@@ -2,17 +2,95 @@
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { spawn } from 'child_process';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
5
6
|
|
|
6
7
|
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
7
8
|
const EC_EXECUTABLES = new Set(['ec']);
|
|
8
9
|
const HOST_PROCESS_EXECUTABLES = new Set(['ps', 'pgrep', 'pidof', 'kill', 'pkill', 'killall']);
|
|
9
|
-
const
|
|
10
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
11
|
+
const HELPER_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
// A rooted POSIX path is not absolute on Windows: Node resolves `/usr/bin/head`
|
|
13
|
+
// onto the current drive as `C:\usr\bin\head`. Keep these fixed candidates as
|
|
14
|
+
// the trusted fast path where they are genuinely absolute, and discover the
|
|
15
|
+
// executable through PATH everywhere else.
|
|
16
|
+
const POSIX_HEAD_CANDIDATES = ['/usr/bin/head', '/bin/head'];
|
|
17
|
+
|
|
18
|
+
function isRegularFile(candidate) {
|
|
10
19
|
try {
|
|
11
20
|
return fs.statSync(candidate).isFile();
|
|
12
21
|
} catch {
|
|
13
22
|
return false;
|
|
14
23
|
}
|
|
15
|
-
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pathExtensionCandidates() {
|
|
27
|
+
if (!IS_WINDOWS) return [''];
|
|
28
|
+
const configured = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
29
|
+
.split(';')
|
|
30
|
+
.map(value => value.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
// The extensionless name is tried first so an explicitly named file wins over
|
|
33
|
+
// a same-named shim; npm publishes both `ec` (sh) and `ec.cmd` on Windows.
|
|
34
|
+
return ['', ...configured];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve one executable to an absolute path without invoking a shell.
|
|
39
|
+
* `spawn` performs no PATH or PATHEXT lookup on Windows, which is why a bare
|
|
40
|
+
* command name has to be materialized here before the child is started.
|
|
41
|
+
*/
|
|
42
|
+
function resolveExecutablePath(executable) {
|
|
43
|
+
if (executable.includes('/') || executable.includes('\\')) {
|
|
44
|
+
return isRegularFile(executable) ? path.resolve(executable) : undefined;
|
|
45
|
+
}
|
|
46
|
+
const entries = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
for (const extension of pathExtensionCandidates()) {
|
|
49
|
+
const candidate = path.join(entry, `${executable}${extension}`);
|
|
50
|
+
if (isRegularFile(candidate)) return path.resolve(candidate);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveHeadExecutable() {
|
|
57
|
+
if (!IS_WINDOWS) {
|
|
58
|
+
const fixed = POSIX_HEAD_CANDIDATES.find(isRegularFile);
|
|
59
|
+
if (fixed) return fixed;
|
|
60
|
+
}
|
|
61
|
+
return resolveExecutablePath('head');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const HEAD_EXECUTABLE = resolveHeadExecutable();
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the concrete process for a validated payload.
|
|
68
|
+
*
|
|
69
|
+
* For `ec`, prefer this package's own Node entrypoint next to the helper. It is
|
|
70
|
+
* the same installation that published this file, needs no PATH lookup, and
|
|
71
|
+
* avoids the Windows batch shim entirely — `CreateProcess` cannot execute a
|
|
72
|
+
* `.cmd` wrapper, so `spawn('ec')` fails with ENOENT there.
|
|
73
|
+
*/
|
|
74
|
+
function resolveChildProcessCommand(payload) {
|
|
75
|
+
if (payload.kind === 'ec') {
|
|
76
|
+
const nodeEntrypoint = path.join(HELPER_DIR, 'ec.js');
|
|
77
|
+
// On Windows CreateProcess cannot execute the npm `.cmd` shim reliably,
|
|
78
|
+
// so prefer this package's Node entrypoint. POSIX hosts can resolve the
|
|
79
|
+
// trusted `ec` executable normally; this also preserves caller-provided
|
|
80
|
+
// PATH shims used by isolated runtimes and tests.
|
|
81
|
+
if (IS_WINDOWS && isRegularFile(nodeEntrypoint)) {
|
|
82
|
+
return { file: process.execPath, args: [nodeEntrypoint, ...payload.argv.slice(1)] };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const resolved = resolveExecutablePath(payload.argv[0]);
|
|
86
|
+
if (!resolved) fail(`unable to resolve executable: ${payload.argv[0]}`);
|
|
87
|
+
// Quoting rules for batch files differ from CreateProcess argument parsing,
|
|
88
|
+
// so a `.cmd`/`.bat` target could execute something other than this argv.
|
|
89
|
+
if (IS_WINDOWS && /\.(?:cmd|bat)$/i.test(resolved)) {
|
|
90
|
+
fail(`refusing to run a batch wrapper: ${resolved}`);
|
|
91
|
+
}
|
|
92
|
+
return { file: resolved, args: payload.argv.slice(1) };
|
|
93
|
+
}
|
|
16
94
|
|
|
17
95
|
function fail(message) {
|
|
18
96
|
process.stderr.write(`ec-safe-output: ${message}\n`);
|
|
@@ -143,6 +221,9 @@ if (!validateCommand(payload)) fail('command is not permitted');
|
|
|
143
221
|
if (!validateStdinOnlyHeadArgs(payload.headArgs)) fail('head may only consume stdin');
|
|
144
222
|
if (payload.headArgs !== undefined && !HEAD_EXECUTABLE) fail('trusted head executable is unavailable');
|
|
145
223
|
|
|
224
|
+
// Resolve the child executable before anything with a side effect: an
|
|
225
|
+
// unresolvable command must not leave an empty create-only output file behind.
|
|
226
|
+
const childCommand = resolveChildProcessCommand(payload);
|
|
146
227
|
const paths = resolveExecutionPaths(payload);
|
|
147
228
|
const output = openOutputFile(payload, paths);
|
|
148
229
|
let bytesWritten = 0;
|
|
@@ -182,10 +263,11 @@ if (head) {
|
|
|
182
263
|
head.stdin.on('error', () => {});
|
|
183
264
|
}
|
|
184
265
|
|
|
185
|
-
const child = spawn(
|
|
266
|
+
const child = spawn(childCommand.file, childCommand.args, {
|
|
186
267
|
cwd: paths.cwd,
|
|
187
268
|
env: process.env,
|
|
188
269
|
stdio: ['ignore', 'pipe', payload.mergeStderr ? 'pipe' : 'inherit'],
|
|
270
|
+
windowsHide: IS_WINDOWS,
|
|
189
271
|
});
|
|
190
272
|
const sourceStreams = [child.stdout, ...(payload.mergeStderr ? [child.stderr] : [])];
|
|
191
273
|
if (head) {
|