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.
Files changed (117) hide show
  1. package/CHANGELOG.md +74 -14
  2. package/README.md +4 -0
  3. package/bin/codex-managed-acl.ps1 +160 -0
  4. package/bin/codex-managed-hook.mjs +127 -10
  5. package/bin/ec-safe-output.js +85 -3
  6. package/bin/install-codex-managed-hooks.mjs +384 -95
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +30 -2
  9. package/dist/agents/claude-runner.js +179 -240
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +369 -0
  14. package/dist/agents/codex-runner.js +839 -192
  15. package/dist/agents/ecagent-runner.js +27 -34
  16. package/dist/agents/gemini-runner.js +62 -8
  17. package/dist/agents/runner-types.js +1 -1
  18. package/dist/aun/aid/managed-operation.js +28 -6
  19. package/dist/aun/aid/store.js +43 -0
  20. package/dist/aun/menu-client.js +272 -0
  21. package/dist/aun/msg/group.js +65 -4
  22. package/dist/aun/msg/index.js +1 -1
  23. package/dist/aun/msg/managed-operation.js +57 -5
  24. package/dist/aun/outbox.js +56 -18
  25. package/dist/aun/rpc/client.js +7 -1
  26. package/dist/channels/aun.js +178 -77
  27. package/dist/channels/daemon.js +7 -1
  28. package/dist/channels/dingtalk.js +1 -0
  29. package/dist/channels/feishu.js +10 -4
  30. package/dist/channels/wechat.js +81 -10
  31. package/dist/channels/wecom.js +2 -1
  32. package/dist/cli/agent-command.js +29 -3
  33. package/dist/cli/agent-create.js +538 -0
  34. package/dist/cli/agent.js +26 -675
  35. package/dist/cli/aun-commands.js +108 -24
  36. package/dist/cli/command-log.js +21 -2
  37. package/dist/cli/ctl-command.js +2 -0
  38. package/dist/cli/daemon-commands.js +289 -61
  39. package/dist/cli/index.js +22 -2
  40. package/dist/cli/init.js +2 -10
  41. package/dist/cli/managed-command-guard.js +3 -0
  42. package/dist/cli/menu-client.js +95 -0
  43. package/dist/cli/model.js +23 -8
  44. package/dist/cli/restart-monitor.js +432 -8
  45. package/dist/cli/stats.js +110 -0
  46. package/dist/cli/task-context.js +12 -2
  47. package/dist/cli/trigger-command.js +9 -1
  48. package/dist/config/builtin-roles.js +2 -0
  49. package/dist/config/schema-migration.js +14 -2
  50. package/dist/core/auth/agent-delegation.js +10 -7
  51. package/dist/core/auth/auth-gateway.js +3 -2
  52. package/dist/core/auth/authorization-audit.js +75 -18
  53. package/dist/core/auth/operation-authorizer.js +63 -3
  54. package/dist/core/auth/operation-catalog.js +14 -2
  55. package/dist/core/auth/remote-access-store.js +136 -0
  56. package/dist/core/baseagent-loader.js +4 -4
  57. package/dist/core/bootstrap-messages.js +39 -18
  58. package/dist/core/bootstrap-service.js +32 -26
  59. package/dist/core/capability/capability-manager.js +8 -4
  60. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  61. package/dist/core/command/agent-control.js +7 -2
  62. package/dist/core/command/command-handler.js +128 -30
  63. package/dist/core/command/daemon-diagnostics.js +448 -0
  64. package/dist/core/command/menu-catalog.js +6 -1
  65. package/dist/core/command/menu-handler.js +410 -98
  66. package/dist/core/command/menu-protocol.js +123 -0
  67. package/dist/core/command/slash-gate.js +4 -6
  68. package/dist/core/command/slash-handler.js +199 -70
  69. package/dist/core/data-migration.js +5 -15
  70. package/dist/core/event-catalog.js +3 -0
  71. package/dist/core/handoff/runtime.js +3 -1
  72. package/dist/core/handoff/store.js +7 -1
  73. package/dist/core/message/im-renderer.js +13 -6
  74. package/dist/core/message/message-bridge.js +5 -36
  75. package/dist/core/message/message-queue.js +20 -15
  76. package/dist/core/message/message-utils.js +6 -3
  77. package/dist/core/message/response-engine.js +147 -94
  78. package/dist/core/model/catalog-service.js +310 -0
  79. package/dist/core/model/model-catalog.js +134 -334
  80. package/dist/core/model/model-diagnostics.js +4 -5
  81. package/dist/core/model/model-fallback.js +9 -16
  82. package/dist/core/model/model-permission.js +5 -2
  83. package/dist/core/permission/approval-gateway.js +183 -32
  84. package/dist/core/permission/protected-paths.js +63 -4
  85. package/dist/core/permission/sandbox-runtime.js +4 -2
  86. package/dist/core/permission/tool-policy.js +257 -31
  87. package/dist/core/runtime-lock.js +13 -8
  88. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  89. package/dist/core/session/session-manager.js +49 -13
  90. package/dist/core/session/session-renew.js +1 -0
  91. package/dist/index.js +214 -57
  92. package/dist/ipc.js +191 -102
  93. package/dist/paths.js +23 -1
  94. package/dist/stats/billing.js +18 -4
  95. package/dist/trigger/manager.js +3 -1
  96. package/dist/trigger/parser.js +77 -13
  97. package/dist/trigger/patch.js +104 -1
  98. package/dist/utils/atomic-write.js +26 -2
  99. package/dist/utils/cross-platform.js +436 -1
  100. package/dist/utils/ecweb-supervisor.js +11 -2
  101. package/dist/utils/ecweb-utils.js +8 -26
  102. package/dist/utils/error-utils.js +16 -2
  103. package/dist/utils/model-prices.jsonl +23 -20
  104. package/dist/utils/npm-ops.js +70 -15
  105. package/dist/utils/windows-autostart.js +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -0
  107. package/kits/docs/evolcore/agent-create.md +13 -0
  108. package/kits/docs/evolcore/group-collaboration.md +7 -2
  109. package/kits/docs/evolcore/menu-client.md +34 -0
  110. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  111. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  112. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  113. package/kits/templates/roles/admin.json +2 -0
  114. package/kits/templates/roles/visitor.json +1 -0
  115. package/kits/templates/system-fragments/bootstrap.md +8 -1
  116. package/package.json +16 -4
  117. 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.22 (2026-09-05)
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) finish(null);
94
+ if (!connected) {
95
+ diagnostic('connect_timeout');
96
+ finish(null);
97
+ }
39
98
  }, connectTimeoutMs);
40
99
  timer.unref?.();
41
- socket.once('error', () => finish(null));
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 { finish(JSON.parse(buffer.slice(0, index))); } catch { finish(null); }
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
- deny('Invalid Codex PreToolUse payload.');
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
- if (result.ok === true && result.applicable === true && result.decision === 'allow') return;
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();
@@ -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 HEAD_EXECUTABLE = ['/usr/bin/head', '/bin/head'].find(candidate => {
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(payload.argv[0], payload.argv.slice(1), {
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) {