evolcore 0.0.13 → 0.0.14
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 +15 -0
- package/bin/codex-managed-hook.mjs +4 -1
- package/bin/install-codex-managed-hooks.mjs +201 -0
- package/dist/agents/claude-runner.js +53 -5
- package/dist/agents/codex-app-server-client.js +123 -2
- package/dist/agents/codex-runner.js +149 -30
- package/dist/agents/ecagent-runner.js +17 -1
- package/dist/agents/gemini-runner.js +9 -4
- package/dist/aun/msg/managed-operation.js +63 -3
- package/dist/channels/aun.js +144 -15
- package/dist/channels/daemon.js +2 -0
- package/dist/cli/aun-commands.js +1 -1
- package/dist/cli/fs-command.js +46 -9
- package/dist/cli/task-context.js +172 -0
- package/dist/config/builtin-roles.js +2 -0
- package/dist/config/config-manager.js +6 -2
- package/dist/config/contact-book-store.js +7 -2
- package/dist/core/auth/auth-gateway.js +1 -0
- package/dist/core/auth/authorization-audit.js +32 -0
- package/dist/core/auth/operation-catalog.js +3 -3
- package/dist/core/bootstrap-service.js +7 -1
- package/dist/core/command/command-handler.js +3 -0
- package/dist/core/event-catalog.js +2 -0
- package/dist/core/message/im-renderer.js +15 -1
- package/dist/core/message/message-bridge.js +5 -2
- package/dist/core/message/response-engine.js +138 -10
- package/dist/core/permission/ec-command-parser.js +556 -4
- package/dist/core/permission/tool-policy.js +17 -29
- package/dist/core/runtime-lock.js +101 -0
- package/dist/index.js +30 -3
- package/dist/response-system/engines/v1/proactive-flow.js +92 -8
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/history.js +42 -7
- package/dist/utils/error-utils.js +7 -0
- package/dist/utils/logger.js +37 -4
- package/kits/templates/roles/admin.json +2 -0
- package/kits/templates/roles/member.json +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
|
|
4
4
|
[`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
|
|
5
5
|
|
|
6
|
+
## 0.0.14 (2026-08-24)
|
|
7
|
+
|
|
8
|
+
### Codex 受管运行与安全
|
|
9
|
+
|
|
10
|
+
- 强化 Codex proactive 模式的受管 PreToolUse 校验,Linux 使用隔离运行时,Windows 支持受管 Hook 安装、加载检查和 fail-closed 启动。
|
|
11
|
+
- 隔离会话临时目录并统一运行时锁管理,收紧 Shell、文件访问和受控 EC 命令解析,扩展群文件 ACL 操作及权限校验。
|
|
12
|
+
- 完善工具审批、委派票据和工具结果关联审计,避免未成功发送或未完成授权的操作推进主动模式状态。
|
|
13
|
+
- 为 Claude、Codex、ecagent 的工具预检拒绝补充统一结构化授权审计,记录策略码、请求/任务及会话上下文,便于跨 Runner 追踪拦截原因。
|
|
14
|
+
- 规范 Codex EC 审批命令与委派校验,拒绝无法规范化的命令、复合命令和空字节输入;允许触发器正文安全使用受管临时目录标记。
|
|
15
|
+
|
|
16
|
+
### AUN 消息与授权
|
|
17
|
+
|
|
18
|
+
- 贯通 AUN 消息路由、会话上下文和授权审计关联信息,补齐请求、工具调用和结果之间的 correlation ID。
|
|
19
|
+
- 统一结构化日志字段,改进异常、策略拦截和拒绝结果的可追踪性。
|
|
20
|
+
|
|
6
21
|
## 0.0.13 (2026-08-20)
|
|
7
22
|
|
|
8
23
|
### AUN 消息与文件
|
|
@@ -45,7 +45,10 @@ function queryDaemon(socketPath, payload) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
async function main() {
|
|
48
|
-
|
|
48
|
+
// Windows managed requirements use one machine-wide command. Resolve the
|
|
49
|
+
// per-daemon named pipe from the inherited environment so multiple EvolCore
|
|
50
|
+
// homes can share the installed hook without rewriting requirements.toml.
|
|
51
|
+
const socketPath = process.argv[2] || process.env.EVOLCORE_INSTANCE_SOCKET;
|
|
49
52
|
let input;
|
|
50
53
|
try {
|
|
51
54
|
input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const markerStart = '# BEGIN EVOLCORE MANAGED CODEX HOOK';
|
|
9
|
+
const markerEnd = '# END EVOLCORE MANAGED CODEX HOOK';
|
|
10
|
+
|
|
11
|
+
function argument(name) {
|
|
12
|
+
const index = process.argv.indexOf(name);
|
|
13
|
+
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fail(message) {
|
|
17
|
+
process.stderr.write(`[evolcore] ${message}\n`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function tomlString(value) {
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function windowsCommandArg(value) {
|
|
26
|
+
// Keep filesystem separators intact; tomlString() performs the required
|
|
27
|
+
// TOML escaping after this Windows command-line quoting step.
|
|
28
|
+
return `"${value.replaceAll('"', '\\"')}"`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function removeManagedBlock(source) {
|
|
32
|
+
const expression = new RegExp(`\\n?${markerStart}[\\s\\S]*?${markerEnd}\\n?`, 'g');
|
|
33
|
+
return source.replace(expression, '\n').replace(/\n{3,}/g, '\n\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function tableBounds(lines, table) {
|
|
37
|
+
const header = table ? `[${table}]` : null;
|
|
38
|
+
const start = header
|
|
39
|
+
? lines.findIndex(line => line.trim() === header)
|
|
40
|
+
: -1;
|
|
41
|
+
if (table && start < 0) return null;
|
|
42
|
+
const from = table ? start + 1 : 0;
|
|
43
|
+
let to = lines.length;
|
|
44
|
+
for (let index = from; index < lines.length; index += 1) {
|
|
45
|
+
if (/^\s*\[\[?[^\]]+\]\]?\s*(?:#.*)?$/.test(lines[index])) {
|
|
46
|
+
to = index;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { start, from, to };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function setTomlKey(source, table, key, value) {
|
|
54
|
+
const lines = source.split('\n');
|
|
55
|
+
let bounds = tableBounds(lines, table);
|
|
56
|
+
if (!bounds) {
|
|
57
|
+
if (table) {
|
|
58
|
+
if (lines.length && lines.at(-1) !== '') lines.push('');
|
|
59
|
+
lines.push(`[${table}]`, `${key} = ${value}`);
|
|
60
|
+
} else {
|
|
61
|
+
let insertAt = lines.findIndex(line => /^\s*\[/.test(line));
|
|
62
|
+
if (insertAt < 0) insertAt = lines.length;
|
|
63
|
+
lines.splice(insertAt, 0, `${key} = ${value}`);
|
|
64
|
+
}
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|
|
67
|
+
const keyExpression = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\s*=`);
|
|
68
|
+
const existing = lines.slice(bounds.from, bounds.to).findIndex(line => keyExpression.test(line));
|
|
69
|
+
if (existing >= 0) {
|
|
70
|
+
lines[bounds.from + existing] = `${key} = ${value}`;
|
|
71
|
+
} else {
|
|
72
|
+
lines.splice(bounds.from, 0, `${key} = ${value}`);
|
|
73
|
+
}
|
|
74
|
+
return lines.join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function atomicReplace(file, temporary) {
|
|
78
|
+
try {
|
|
79
|
+
fs.renameSync(temporary, file);
|
|
80
|
+
return;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error?.code !== 'EEXIST' && error?.code !== 'EPERM') throw error;
|
|
83
|
+
}
|
|
84
|
+
const backup = `${file}.evolcore-backup-${process.pid}-${Date.now()}`;
|
|
85
|
+
fs.renameSync(file, backup);
|
|
86
|
+
try {
|
|
87
|
+
fs.renameSync(temporary, file);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
try { fs.renameSync(backup, file); } catch {}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
try { fs.rmSync(backup, { force: true }); } catch {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function atomicWrite(file, content) {
|
|
96
|
+
const temporary = `${file}.evolcore-${process.pid}-${Date.now()}.tmp`;
|
|
97
|
+
try {
|
|
98
|
+
fs.writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o440, flag: 'wx' });
|
|
99
|
+
atomicReplace(file, temporary);
|
|
100
|
+
} finally {
|
|
101
|
+
try { fs.rmSync(temporary, { force: true }); } catch {}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function restrictWindowsAcl(target) {
|
|
106
|
+
if (process.platform !== 'win32') return;
|
|
107
|
+
// Keep the hook and requirements administrator-owned while allowing the
|
|
108
|
+
// daemon's normal user token to read and execute them.
|
|
109
|
+
execFileSync('icacls.exe', [
|
|
110
|
+
target,
|
|
111
|
+
'/inheritance:r',
|
|
112
|
+
'/grant:r',
|
|
113
|
+
'*S-1-5-18:(OI)(CI)F',
|
|
114
|
+
'*S-1-5-32-544:(OI)(CI)F',
|
|
115
|
+
'*S-1-5-32-545:(OI)(CI)RX',
|
|
116
|
+
], { stdio: 'ignore' });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function managedHookLauncher(packageRoot) {
|
|
120
|
+
// Keep the machine-wide entrypoint stable across npm upgrades. The app
|
|
121
|
+
// npm upgrades replace the package contents at this stable global path, so
|
|
122
|
+
// this file never needs to be rewritten just because the hook changed.
|
|
123
|
+
const trustedPackageRoot = path.resolve(packageRoot);
|
|
124
|
+
return `#!/usr/bin/env node
|
|
125
|
+
import path from 'node:path';
|
|
126
|
+
import { pathToFileURL } from 'node:url';
|
|
127
|
+
|
|
128
|
+
// EVOLCORE_MANAGED_HOOK_LAUNCHER
|
|
129
|
+
const packageRoot = ${JSON.stringify(trustedPackageRoot)};
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
await import(pathToFileURL(path.join(packageRoot, 'bin', 'codex-managed-hook.mjs')).href);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
process.stderr.write('[evolcore] failed to load the current managed hook: '
|
|
135
|
+
+ (error instanceof Error ? error.message : String(error)) + '\\n');
|
|
136
|
+
process.exit(2);
|
|
137
|
+
}
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function main() {
|
|
142
|
+
if (process.platform !== 'win32' && !argument('--requirements')) {
|
|
143
|
+
fail('Codex managed hook provisioning is only supported on Windows.');
|
|
144
|
+
}
|
|
145
|
+
const packageRoot = argument('--package-root');
|
|
146
|
+
const nodePath = argument('--node-path') || process.execPath;
|
|
147
|
+
const requirements = argument('--requirements')
|
|
148
|
+
|| path.join(process.env.ProgramData || path.join(os.homedir(), 'ProgramData'), 'OpenAI', 'Codex', 'requirements.toml');
|
|
149
|
+
if (!packageRoot) fail('--package-root is required.');
|
|
150
|
+
const existingRequirements = fs.existsSync(requirements) ? fs.readFileSync(requirements, 'utf8') : '';
|
|
151
|
+
const targetDirectory = argument('--managed-dir')
|
|
152
|
+
|| path.join(path.dirname(requirements), 'EvolCore', 'hooks');
|
|
153
|
+
const sourceHook = path.join(packageRoot, 'bin', 'codex-managed-hook.mjs');
|
|
154
|
+
if (!fs.statSync(sourceHook).isFile()) fail(`Managed hook source was not found: ${sourceHook}`);
|
|
155
|
+
|
|
156
|
+
fs.mkdirSync(targetDirectory, { recursive: true, mode: 0o700 });
|
|
157
|
+
const hookPath = path.join(targetDirectory, 'pre-tool-use.mjs');
|
|
158
|
+
const temporaryHook = `${hookPath}.evolcore-${process.pid}-${Date.now()}.tmp`;
|
|
159
|
+
try {
|
|
160
|
+
fs.writeFileSync(temporaryHook, managedHookLauncher(packageRoot), { encoding: 'utf8', mode: 0o500, flag: 'wx' });
|
|
161
|
+
fs.chmodSync(temporaryHook, 0o500);
|
|
162
|
+
atomicReplace(hookPath, temporaryHook);
|
|
163
|
+
} finally {
|
|
164
|
+
try { fs.rmSync(temporaryHook, { force: true }); } catch {}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let content = existingRequirements;
|
|
168
|
+
content = removeManagedBlock(content);
|
|
169
|
+
content = setTomlKey(content, null, 'allow_managed_hooks_only', 'true');
|
|
170
|
+
content = setTomlKey(content, 'features', 'hooks', 'true');
|
|
171
|
+
content = setTomlKey(content, 'hooks', 'windows_managed_dir', tomlString(path.resolve(targetDirectory)));
|
|
172
|
+
if (!content.endsWith('\n')) content += '\n';
|
|
173
|
+
const command = `${windowsCommandArg(nodePath)} ${windowsCommandArg(hookPath)}`;
|
|
174
|
+
content += [
|
|
175
|
+
markerStart,
|
|
176
|
+
'[[hooks.PreToolUse]]',
|
|
177
|
+
'matcher = "*"',
|
|
178
|
+
'',
|
|
179
|
+
'[[hooks.PreToolUse.hooks]]',
|
|
180
|
+
'type = "command"',
|
|
181
|
+
`command = ${tomlString(command)}`,
|
|
182
|
+
`command_windows = ${tomlString(command)}`,
|
|
183
|
+
'timeout = 3',
|
|
184
|
+
'statusMessage = "Checking EvolCore session policy"',
|
|
185
|
+
markerEnd,
|
|
186
|
+
'',
|
|
187
|
+
].join('\n');
|
|
188
|
+
fs.mkdirSync(path.dirname(requirements), { recursive: true });
|
|
189
|
+
atomicWrite(requirements, content);
|
|
190
|
+
try { fs.chmodSync(requirements, 0o440); } catch {}
|
|
191
|
+
restrictWindowsAcl(targetDirectory);
|
|
192
|
+
restrictWindowsAcl(hookPath);
|
|
193
|
+
restrictWindowsAcl(requirements);
|
|
194
|
+
process.stdout.write(`${JSON.stringify({ requirements, managedDir: path.resolve(targetDirectory), hookPath })}\n`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
main();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
201
|
+
}
|
|
@@ -21,9 +21,10 @@ import { resolveEffective } from '../config/config-manager.js';
|
|
|
21
21
|
import { sanitizeSessionTitle } from '../core/session/session-title.js';
|
|
22
22
|
import { resolveClaudeCapabilityRunOptionsForProject } from '../core/capability/capability-manager.js';
|
|
23
23
|
import { normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
|
|
24
|
-
import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClassReference, isHClassPath, isLClassPath, resolveProtectedCandidate, } from '../core/protected-paths.js';
|
|
24
|
+
import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClassReference, isHClassPath, isLClassPath, isSameOrDescendant, resolveProtectedCandidate, } from '../core/protected-paths.js';
|
|
25
25
|
import { buildHClassGuardCommand, createSandboxInitializationError, isSandboxInitializationFailure, SANDBOX_INITIALIZATION_FAILED, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, shouldFailIfClaudeSandboxUnavailable, } from '../core/permission/sandbox-runtime.js';
|
|
26
26
|
import { buildClaudeUnixSocketAllowlist } from '../core/permission/unix-socket-policy.js';
|
|
27
|
+
import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
|
|
27
28
|
import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
|
|
28
29
|
export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
|
|
29
30
|
// Built-in tools execute inside the Claude runtime and are covered by the
|
|
@@ -216,6 +217,32 @@ function buildClaudeManagedLockdownSettings(capabilityOptions) {
|
|
|
216
217
|
};
|
|
217
218
|
}
|
|
218
219
|
const CLAUDE_SANDBOX_GLOB_MAGIC = /[*?[\]]/;
|
|
220
|
+
function trustedRuntimeWritePaths(runtimeEnv) {
|
|
221
|
+
const tmp = process.env.TMPDIR?.trim();
|
|
222
|
+
if (!tmp || !path.isAbsolute(tmp))
|
|
223
|
+
return [];
|
|
224
|
+
const root = resolveProtectedCandidate(tmp);
|
|
225
|
+
const candidates = [runtimeEnv?.EVOLCORE_SESSION_RUNTIME_DIR, runtimeEnv?.EVOLCORE_RUNTIME_LOCK_DIR]
|
|
226
|
+
.filter((value) => typeof value === 'string' && path.isAbsolute(value));
|
|
227
|
+
const paths = [];
|
|
228
|
+
for (const candidate of candidates) {
|
|
229
|
+
const resolved = resolveProtectedCandidate(candidate);
|
|
230
|
+
if (!isSameOrDescendant(resolved, root))
|
|
231
|
+
continue;
|
|
232
|
+
try {
|
|
233
|
+
const stat = fs.lstatSync(resolved);
|
|
234
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
235
|
+
continue;
|
|
236
|
+
if (typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
paths.push(resolved, path.join(resolved, '**'));
|
|
243
|
+
}
|
|
244
|
+
return [...new Set(paths)];
|
|
245
|
+
}
|
|
219
246
|
async function assertClaudeSettingSourcesHaveLiteralSandboxPaths(cwd, settingSources, managedSettings) {
|
|
220
247
|
if (process.platform !== 'linux' || settingSources.length === 0)
|
|
221
248
|
return;
|
|
@@ -1879,8 +1906,25 @@ export class AgentRunner {
|
|
|
1879
1906
|
const code = policyCode ?? (preflight?.behavior === 'deny' ? preflight.policyCode : undefined);
|
|
1880
1907
|
if (!code)
|
|
1881
1908
|
return;
|
|
1909
|
+
const ctx = this.permissionContexts.get(sessionId);
|
|
1910
|
+
const summary = summarizeToolInputForAudit(toolName, toolInput).slice(0, 512);
|
|
1911
|
+
auditToolPreflightDenial({
|
|
1912
|
+
toolName,
|
|
1913
|
+
policyCode: code,
|
|
1914
|
+
reason: preflight?.behavior === 'deny' ? (preflight.message ?? 'policy denied') : 'policy denied',
|
|
1915
|
+
summary,
|
|
1916
|
+
sessionId,
|
|
1917
|
+
agentAid: ctx?.selfAid,
|
|
1918
|
+
permissionMode: callPermissionMode,
|
|
1919
|
+
channel: ctx?.channel,
|
|
1920
|
+
actorId: ctx?.userId,
|
|
1921
|
+
role: ctx?.role,
|
|
1922
|
+
selfAid: ctx?.selfAid,
|
|
1923
|
+
requestId: typeof requestId === 'string' ? requestId : undefined,
|
|
1924
|
+
taskId: ctx?.taskId,
|
|
1925
|
+
});
|
|
1882
1926
|
try {
|
|
1883
|
-
await
|
|
1927
|
+
await ctx?.recordExecutionAnomaly?.({
|
|
1884
1928
|
code: 'operation_blocked',
|
|
1885
1929
|
severity: 'warning',
|
|
1886
1930
|
phase: 'execution',
|
|
@@ -1889,9 +1933,9 @@ export class AgentRunner {
|
|
|
1889
1933
|
...(typeof requestId === 'string' && requestId ? { requestId } : {}),
|
|
1890
1934
|
policyCode: code,
|
|
1891
1935
|
decisionSource: 'policy',
|
|
1892
|
-
agentAid:
|
|
1936
|
+
agentAid: ctx?.selfAid,
|
|
1893
1937
|
permissionMode: callPermissionMode,
|
|
1894
|
-
summary
|
|
1938
|
+
summary,
|
|
1895
1939
|
effect: 'operation_skipped',
|
|
1896
1940
|
});
|
|
1897
1941
|
}
|
|
@@ -2243,6 +2287,7 @@ export class AgentRunner {
|
|
|
2243
2287
|
const sandboxUnixSockets = buildClaudeUnixSocketAllowlist(projectPath, {
|
|
2244
2288
|
role: runPermissionContext?.role,
|
|
2245
2289
|
});
|
|
2290
|
+
const runtimeWritePaths = trustedRuntimeWritePaths(runtimeEnv);
|
|
2246
2291
|
await assertClaudeSettingSourcesHaveLiteralSandboxPaths(projectPath, [...settingSources], managedSettings);
|
|
2247
2292
|
const sandboxProjectionDurationMs = Date.now() - sandboxProjectionStartedAt;
|
|
2248
2293
|
if (sandboxProjectionDurationMs > 2_000) {
|
|
@@ -2259,7 +2304,10 @@ export class AgentRunner {
|
|
|
2259
2304
|
allowAllUnixSockets: false,
|
|
2260
2305
|
allowLocalBinding: false,
|
|
2261
2306
|
},
|
|
2262
|
-
filesystem:
|
|
2307
|
+
filesystem: {
|
|
2308
|
+
...sandboxFilesystem,
|
|
2309
|
+
...(runtimeWritePaths.length > 0 ? { allowWrite: runtimeWritePaths } : {}),
|
|
2310
|
+
},
|
|
2263
2311
|
};
|
|
2264
2312
|
})();
|
|
2265
2313
|
if (executionSandbox.state === 'off') {
|
|
@@ -6,8 +6,8 @@ import { resolveCodexLaunchCommand } from '../utils/codex-cli.js';
|
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
7
|
import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
|
|
8
8
|
import fs from 'fs';
|
|
9
|
-
import os from 'os';
|
|
10
9
|
import path from 'path';
|
|
10
|
+
import { ensureProcessManagedTempDir } from '../cli/task-context.js';
|
|
11
11
|
import { getPackageRoot, resolvePaths } from '../paths.js';
|
|
12
12
|
import { registerCodexAppServerProcess, unregisterCodexAppServerProcess, } from '../utils/codex-app-server-registry.js';
|
|
13
13
|
const BUILT_IN_CODEX_PROVIDER_IDS = new Set(['openai']);
|
|
@@ -298,6 +298,9 @@ export class CodexAppServerClient {
|
|
|
298
298
|
includeLayers: false,
|
|
299
299
|
});
|
|
300
300
|
}
|
|
301
|
+
async configRequirementsRead() {
|
|
302
|
+
return this.request('configRequirements/read');
|
|
303
|
+
}
|
|
301
304
|
async pluginInstalled(cwd) {
|
|
302
305
|
return this.request('plugin/installed', {
|
|
303
306
|
...(cwd ? { cwds: [cwd] } : {}),
|
|
@@ -379,7 +382,25 @@ export class CodexAppServerClient {
|
|
|
379
382
|
this.initializing = null;
|
|
380
383
|
}
|
|
381
384
|
}
|
|
385
|
+
async assertManagedHooksAvailable() {
|
|
386
|
+
await this.ensureStarted();
|
|
387
|
+
if (process.platform !== 'win32')
|
|
388
|
+
return;
|
|
389
|
+
try {
|
|
390
|
+
await this.verifyWindowsManagedHooks();
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
// Do not leave an unverified app-server alive after a fail-closed check.
|
|
394
|
+
await this.close();
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
382
398
|
startProcess() {
|
|
399
|
+
// Linux managed hooks need a private runtime even when the daemon was
|
|
400
|
+
// launched by a service manager that did not export TMPDIR.
|
|
401
|
+
if (process.platform === 'linux' && !process.env.TMPDIR?.trim()) {
|
|
402
|
+
ensureProcessManagedTempDir();
|
|
403
|
+
}
|
|
383
404
|
const env = this.buildProcessEnvironment();
|
|
384
405
|
const args = this.buildProcessArgs();
|
|
385
406
|
const launchCommand = resolveCodexLaunchCommand(args);
|
|
@@ -448,7 +469,8 @@ export class CodexAppServerClient {
|
|
|
448
469
|
if (this.managedHookRuntimeDir) {
|
|
449
470
|
return path.join(this.managedHookRuntimeDir, 'requirements.toml');
|
|
450
471
|
}
|
|
451
|
-
const
|
|
472
|
+
const tmpDir = ensureProcessManagedTempDir();
|
|
473
|
+
const directory = fs.mkdtempSync(path.join(tmpDir, 'evolcore-codex-hooks-'));
|
|
452
474
|
try {
|
|
453
475
|
fs.chmodSync(directory, 0o700);
|
|
454
476
|
const hookPath = path.join(directory, 'pre-tool-use.mjs');
|
|
@@ -505,7 +527,39 @@ export class CodexAppServerClient {
|
|
|
505
527
|
...process.env,
|
|
506
528
|
...this.options.env,
|
|
507
529
|
});
|
|
530
|
+
// The app-server is shared by multiple EvolCore sessions. Its process
|
|
531
|
+
// environment must therefore use the daemon's trusted private root;
|
|
532
|
+
// per-thread session roots are injected later through thread config.
|
|
533
|
+
const managedTmpDir = ensureProcessManagedTempDir();
|
|
534
|
+
env.TMPDIR = managedTmpDir;
|
|
535
|
+
delete env.EVOLCORE_RUNTIME_LOCK_DIR;
|
|
536
|
+
if (managedTmpDir) {
|
|
537
|
+
const runtimeLockDir = path.join(managedTmpDir, 'evolcore-locks');
|
|
538
|
+
try {
|
|
539
|
+
// The parent is the already-validated process root, so only create
|
|
540
|
+
// and validate the final component. Never follow a pre-existing
|
|
541
|
+
// symlink supplied through the inherited environment.
|
|
542
|
+
try {
|
|
543
|
+
fs.mkdirSync(runtimeLockDir, { mode: 0o700 });
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
if (error?.code !== 'EEXIST')
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
549
|
+
const stat = fs.lstatSync(runtimeLockDir);
|
|
550
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
551
|
+
|| (typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
552
|
+
|| (stat.mode & 0o077) !== 0) {
|
|
553
|
+
throw new Error('runtime lock directory is not private');
|
|
554
|
+
}
|
|
555
|
+
env.EVOLCORE_RUNTIME_LOCK_DIR = runtimeLockDir;
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
logger.warn(`[CodexAppServer] managed runtime lock directory unavailable: ${runtimeLockDir}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
508
561
|
env.CODEX_API_KEY = this.options.apiKey;
|
|
562
|
+
env.EVOLCORE_INSTANCE_SOCKET = resolvePaths().instanceSocket;
|
|
509
563
|
for (const [name, value] of Object.entries(this.options.headers ?? {})) {
|
|
510
564
|
env[codexHeaderEnvName(name)] = value;
|
|
511
565
|
}
|
|
@@ -514,6 +568,73 @@ export class CodexAppServerClient {
|
|
|
514
568
|
}
|
|
515
569
|
return env;
|
|
516
570
|
}
|
|
571
|
+
async verifyWindowsManagedHooks() {
|
|
572
|
+
const response = await this.configRequirementsRead();
|
|
573
|
+
const requirements = response.requirements;
|
|
574
|
+
const hooks = requirements?.hooks;
|
|
575
|
+
const featureRequirements = requirements?.featureRequirements ?? requirements?.feature_requirements;
|
|
576
|
+
const managedDir = hooks?.windowsManagedDir ?? hooks?.windows_managed_dir;
|
|
577
|
+
const preToolUse = hooks?.PreToolUse;
|
|
578
|
+
const expectedHookPath = typeof managedDir === 'string'
|
|
579
|
+
? path.join(managedDir, 'pre-tool-use.mjs')
|
|
580
|
+
: undefined;
|
|
581
|
+
const currentHookLauncher = !!expectedHookPath && (() => {
|
|
582
|
+
try {
|
|
583
|
+
const source = fs.readFileSync(expectedHookPath, 'utf8');
|
|
584
|
+
const expectedPackageRoot = JSON.stringify(path.resolve(getPackageRoot()));
|
|
585
|
+
return source.includes('EVOLCORE_MANAGED_HOOK_LAUNCHER')
|
|
586
|
+
&& (source.includes(expectedPackageRoot)
|
|
587
|
+
|| (process.platform === 'win32' && source.toLowerCase().includes(expectedPackageRoot.toLowerCase())));
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
})();
|
|
593
|
+
const normalizeManagedPath = (value) => path.normalize(value).toLowerCase();
|
|
594
|
+
const commandTargetsHook = (value) => {
|
|
595
|
+
const command = normalizeManagedPath(value).trim();
|
|
596
|
+
const hook = normalizeManagedPath(expectedHookPath ?? '');
|
|
597
|
+
// The managed command is intentionally a single executable invocation.
|
|
598
|
+
// Reject shell operators and trailing arguments so a modified
|
|
599
|
+
// requirements file cannot retain the expected path while running a
|
|
600
|
+
// second command or changing the hook's behavior.
|
|
601
|
+
if (!command || !hook || /[\r\n;&|<>]/.test(command))
|
|
602
|
+
return false;
|
|
603
|
+
return command.replace(/["']+$/, '').endsWith(hook);
|
|
604
|
+
};
|
|
605
|
+
const managedHookLoaded = Array.isArray(preToolUse)
|
|
606
|
+
&& preToolUse.some(group => {
|
|
607
|
+
if (!group || typeof group !== 'object' || Array.isArray(group))
|
|
608
|
+
return false;
|
|
609
|
+
if (group.matcher !== '*')
|
|
610
|
+
return false;
|
|
611
|
+
const handlers = group.hooks;
|
|
612
|
+
return Array.isArray(handlers) && handlers.some(handler => {
|
|
613
|
+
if (!handler || typeof handler !== 'object' || Array.isArray(handler))
|
|
614
|
+
return false;
|
|
615
|
+
const entry = handler;
|
|
616
|
+
const command = entry.commandWindows ?? entry.command_windows;
|
|
617
|
+
return entry.type === 'command'
|
|
618
|
+
&& entry.async !== true
|
|
619
|
+
&& typeof entry.command === 'string'
|
|
620
|
+
&& entry.command.length > 0
|
|
621
|
+
&& typeof command === 'string'
|
|
622
|
+
&& command.length > 0
|
|
623
|
+
&& !!expectedHookPath
|
|
624
|
+
&& fs.existsSync(expectedHookPath)
|
|
625
|
+
&& fs.statSync(expectedHookPath).isFile()
|
|
626
|
+
&& currentHookLauncher
|
|
627
|
+
&& commandTargetsHook(command);
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
const lockdownEnabled = (requirements?.allowManagedHooksOnly ?? requirements?.allow_managed_hooks_only) === true;
|
|
631
|
+
const hooksEnabled = featureRequirements?.hooks === true;
|
|
632
|
+
if (typeof managedDir !== 'string' || !path.isAbsolute(managedDir)
|
|
633
|
+
|| !fs.existsSync(managedDir) || !managedHookLoaded || !lockdownEnabled || !hooksEnabled) {
|
|
634
|
+
throw new Error('Codex Windows proactive requires an installed and loaded EvolCore managed PreToolUse hook. '
|
|
635
|
+
+ 'Run the elevated Windows installer again to provision %ProgramData%\\OpenAI\\Codex\\requirements.toml.');
|
|
636
|
+
}
|
|
637
|
+
}
|
|
517
638
|
buildProcessArgs() {
|
|
518
639
|
const args = ['app-server', '--listen', 'stdio://'];
|
|
519
640
|
if (this.options.enableRequestUserInput !== false) {
|