evolcore 0.0.12 → 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.
Files changed (116) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/codex-managed-hook.mjs +4 -1
  3. package/bin/ec-safe-output.js +26 -7
  4. package/bin/install-codex-managed-hooks.mjs +201 -0
  5. package/dist/agents/claude-runner.js +105 -11
  6. package/dist/agents/codex-app-server-client.js +176 -7
  7. package/dist/agents/codex-runner.js +248 -84
  8. package/dist/agents/ecagent-runner.js +21 -2
  9. package/dist/agents/gemini-runner.js +9 -4
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/store.js +13 -7
  12. package/dist/aun/group-fs-download.js +56 -0
  13. package/dist/aun/group-identity.js +1 -2
  14. package/dist/aun/msg/group.js +11 -33
  15. package/dist/aun/msg/index.js +1 -2
  16. package/dist/aun/msg/managed-operation.js +176 -55
  17. package/dist/aun/msg/upload.js +16 -1
  18. package/dist/aun/outbox.js +31 -2
  19. package/dist/aun/rpc/client.js +66 -0
  20. package/dist/aun/rpc/index.js +1 -2
  21. package/dist/aun/storage/{upload.js → client.js} +11 -2
  22. package/dist/aun/storage/index.js +1 -2
  23. package/dist/channels/aun.js +514 -150
  24. package/dist/channels/daemon.js +2 -0
  25. package/dist/cli/agent.js +9 -96
  26. package/dist/cli/aun-commands.js +1 -1
  27. package/dist/cli/cli-argv.js +10 -0
  28. package/dist/cli/config.js +9 -17
  29. package/dist/cli/ctl-command.js +1 -1
  30. package/dist/cli/daemon-commands.js +39 -24
  31. package/dist/cli/fs-command.js +65 -19
  32. package/dist/cli/init.js +77 -8
  33. package/dist/cli/queue-command.js +42 -0
  34. package/dist/cli/restart-monitor.js +3 -14
  35. package/dist/cli/task-context.js +172 -0
  36. package/dist/cli/trigger-command.js +7 -0
  37. package/dist/config/access-policy.js +1 -2
  38. package/dist/config/builtin-roles.js +7 -0
  39. package/dist/config/config-manager.js +27 -3
  40. package/dist/config/contact-book-store.js +7 -2
  41. package/dist/config/peer-role-resolver.js +6 -2
  42. package/dist/config/role-migration-startup.js +53 -0
  43. package/dist/config/role-store.js +1 -1
  44. package/dist/core/auth/agent-delegation.js +1 -1
  45. package/dist/core/auth/auth-gateway.js +30 -1
  46. package/dist/core/auth/authorization-audit.js +47 -0
  47. package/dist/core/auth/operation-authorizer.js +1 -0
  48. package/dist/core/auth/operation-catalog.js +10 -1
  49. package/dist/core/bootstrap-messages.js +9 -2
  50. package/dist/core/bootstrap-service.js +8 -1
  51. package/dist/core/command/command-handler.js +201 -51
  52. package/dist/core/command/menu-catalog.js +4 -0
  53. package/dist/core/command/menu-handler.js +85 -18
  54. package/dist/core/command/menu-protocol.js +23 -2
  55. package/dist/core/command/slash-handler.js +58 -10
  56. package/dist/core/event-catalog.js +8 -0
  57. package/dist/core/handoff/runtime.js +12 -1
  58. package/dist/core/handoff/store.js +5 -1
  59. package/dist/core/interaction-router.js +10 -0
  60. package/dist/core/message/im-renderer.js +34 -1
  61. package/dist/core/message/message-bridge.js +68 -13
  62. package/dist/core/message/message-log.js +2 -0
  63. package/dist/core/message/message-queue.js +267 -39
  64. package/dist/core/message/message-utils.js +81 -4
  65. package/dist/core/message/response-engine.js +242 -48
  66. package/dist/core/message/stream-debouncer.js +10 -2
  67. package/dist/core/permission/approval-gateway.js +1 -0
  68. package/dist/core/permission/ec-command-parser.js +649 -18
  69. package/dist/core/permission/index.js +1 -1
  70. package/dist/core/permission/mode.js +15 -0
  71. package/dist/core/permission/readonly-shell-query.js +148 -8
  72. package/dist/core/permission/sandbox-runtime.js +21 -0
  73. package/dist/core/permission/tool-policy.js +266 -56
  74. package/dist/core/protected-paths.js +107 -2
  75. package/dist/core/runtime-lock.js +101 -0
  76. package/dist/core/session/session-manager.js +21 -2
  77. package/dist/core/session/session-mapper.js +20 -1
  78. package/dist/eck/detect.js +18 -1
  79. package/dist/eck/group-rules-sync.js +15 -44
  80. package/dist/index.js +191 -89
  81. package/dist/ipc.js +16 -3
  82. package/dist/paths.js +3 -0
  83. package/dist/response-system/engines/v1/proactive-flow.js +92 -8
  84. package/dist/response-system/modes/single-session/index.js +3 -0
  85. package/dist/trigger/feedback.js +64 -11
  86. package/dist/trigger/history.js +42 -7
  87. package/dist/trigger/manager.js +21 -1
  88. package/dist/trigger/parser.js +14 -2
  89. package/dist/trigger/scheduler.js +1 -0
  90. package/dist/trigger/validation.js +67 -6
  91. package/dist/utils/ecweb-supervisor.js +332 -0
  92. package/dist/utils/error-utils.js +17 -0
  93. package/dist/utils/logger.js +37 -4
  94. package/kits/docs/evolcore/trigger.md +17 -1
  95. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  96. package/kits/schemas/role-config.schema.1.json +1 -0
  97. package/kits/templates/roles/admin.json +7 -0
  98. package/kits/templates/roles/member.json +8 -1
  99. package/kits/templates/roles/visitor.json +2 -0
  100. package/package.json +1 -1
  101. package/dist/aun/msg/payload-type.js +0 -27
  102. package/dist/aun/rpc/caller.js +0 -42
  103. package/dist/aun/rpc/connection.js +0 -25
  104. package/dist/aun/storage/manage.js +0 -10
  105. package/dist/config/access-policy-domain.js +0 -18
  106. package/dist/config/config-batch-get.js +0 -11
  107. package/dist/config/owner-policy.js +0 -4
  108. package/dist/config/role-config-v4-startup.js +0 -32
  109. package/dist/config/role-config-v5-startup.js +0 -27
  110. package/dist/core/auth/trigger-authorization.js +0 -15
  111. package/dist/core/interaction-registration.js +0 -10
  112. package/dist/core/permission/execution-sandbox.js +0 -16
  113. package/dist/core/relation/peer-key.js +0 -1
  114. package/dist/core/session/session-key.js +0 -24
  115. package/dist/eck/baseagent-caps.js +0 -18
  116. package/dist/eck/rules-loader.js +0 -28
package/CHANGELOG.md CHANGED
@@ -3,6 +3,43 @@
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
+
21
+ ## 0.0.13 (2026-08-20)
22
+
23
+ ### AUN 消息与文件
24
+
25
+ - 显式区分 AUN 私聊与群聊投递路由,强化入站去重、顺序处理、消息队列持久化和失败恢复。
26
+ - 统一 AUN RPC 与存储客户端,扩展群文件写入与下载能力,收敛文件操作和载荷处理边界。
27
+ - 增加群文件下载票据校验和内容哈希验证,补齐短连接 RPC、存储上传及启动期角色配置迁移能力。
28
+
29
+ ### 权限与关系
30
+
31
+ - 收敛角色迁移、权限作用域和最小能力模式,强化 Owner、关系范围与工具执行审计边界。
32
+ - 贯通会话投递元数据和跨会话授权校验,避免托管操作越过当前 Agent、会话或关系范围。
33
+ - 强化受管 `$TMPDIR`、群规则读取和群文件上传的路径隔离,拒绝符号链接、越界目录及不符合权限/所有者要求的路径。
34
+ - 完善菜单授权审计的请求与会话关联信息,扩展只读 Shell 查询、配置批量读取和委派有效期校验。
35
+
36
+ ### 会话与运行时
37
+
38
+ - 完善消息撤回隔离、错误回传、菜单交互、重启命令和 Trigger 反馈的上下文校验。
39
+ - 简化 Agent 运行器与启动流程,修正系统重启路径并清理托管会话凭据。
40
+ - 为 Codex app-server 请求增加 request ID 与载荷指纹重放防护,使重复请求复用结果、冲突请求拒绝;会话重试按至多执行一次处理。
41
+ - 统一 EC Web 守护进程的发现、接管、健康检查和退出清理,修正 Linux 进程启动时间计算,并改进 Codex 初始化/升级探测与 Web 消息路由展示。
42
+
6
43
  ## 0.0.12 (2026-08-19)
7
44
 
8
45
  ### AUN 通信
@@ -45,7 +45,10 @@ function queryDaemon(socketPath, payload) {
45
45
  }
46
46
 
47
47
  async function main() {
48
- const socketPath = process.argv[2];
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'));
@@ -82,12 +82,27 @@ function resolveExecutionPaths(payload) {
82
82
  return { workspace, cwd };
83
83
  }
84
84
 
85
+ function resolveManagedTempRoot() {
86
+ const raw = typeof process.env.TMPDIR === 'string' ? process.env.TMPDIR.trim() : '';
87
+ if (!raw || !path.isAbsolute(raw)) fail('managed TMPDIR is unavailable');
88
+ try {
89
+ const root = fs.realpathSync(raw);
90
+ if (!fs.statSync(root).isDirectory()) fail('managed TMPDIR is not a directory');
91
+ return root;
92
+ } catch {
93
+ fail('managed TMPDIR is unavailable');
94
+ }
95
+ }
96
+
85
97
  function openOutputFile(payload, paths) {
86
98
  if (payload.outputPath === undefined) return undefined;
87
- if (typeof payload.outputPath !== 'string' || !payload.outputPath || path.isAbsolute(payload.outputPath)) {
88
- fail('output path must be relative');
89
- }
90
- const target = path.resolve(paths.cwd, payload.outputPath);
99
+ if (typeof payload.outputPath !== 'string' || !payload.outputPath) fail('output path is invalid');
100
+
101
+ const isManagedTempOutput = path.isAbsolute(payload.outputPath);
102
+ const outputRoot = isManagedTempOutput ? resolveManagedTempRoot() : paths.workspace;
103
+ const target = isManagedTempOutput
104
+ ? path.resolve(payload.outputPath)
105
+ : path.resolve(paths.cwd, payload.outputPath);
91
106
  const parent = path.dirname(target);
92
107
  let realParent;
93
108
  try {
@@ -97,9 +112,13 @@ function openOutputFile(payload, paths) {
97
112
  fail('output parent does not exist');
98
113
  }
99
114
  const canonicalTarget = path.join(realParent, path.basename(target));
100
- if (!sameOrDescendant(canonicalTarget, paths.workspace)) fail('output path escapes workspace');
101
- const relative = path.relative(paths.workspace, canonicalTarget);
102
- if (relative === '.git' || relative.startsWith(`.git${path.sep}`)) fail('output path may not target Git metadata');
115
+ if (!sameOrDescendant(canonicalTarget, outputRoot)) {
116
+ fail(isManagedTempOutput ? 'output path escapes managed TMPDIR' : 'output path escapes workspace');
117
+ }
118
+ if (!isManagedTempOutput) {
119
+ const relative = path.relative(paths.workspace, canonicalTarget);
120
+ if (relative === '.git' || relative.startsWith(`.git${path.sep}`)) fail('output path may not target Git metadata');
121
+ }
103
122
 
104
123
  const noFollow = fs.constants.O_NOFOLLOW ?? 0;
105
124
  try {
@@ -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
+ }
@@ -20,11 +20,11 @@ import { getPackageRoot, resolvePaths } from '../paths.js';
20
20
  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
- import { normalizePermissionMode } from '../core/permission/mode.js';
24
- import { resolvePhaseOneExecutionSandbox } from '../core/permission/execution-sandbox.js';
25
- import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClassReference, isHClassPath, isLClassPath, resolveProtectedCandidate, } from '../core/protected-paths.js';
26
- import { buildHClassGuardCommand, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, shouldFailIfClaudeSandboxUnavailable, } from '../core/permission/sandbox-runtime.js';
23
+ import { normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
24
+ import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClassReference, isHClassPath, isLClassPath, isSameOrDescendant, resolveProtectedCandidate, } from '../core/protected-paths.js';
25
+ import { buildHClassGuardCommand, createSandboxInitializationError, isSandboxInitializationFailure, SANDBOX_INITIALIZATION_FAILED, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, shouldFailIfClaudeSandboxUnavailable, } from '../core/permission/sandbox-runtime.js';
27
26
  import { buildClaudeUnixSocketAllowlist } from '../core/permission/unix-socket-policy.js';
27
+ import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
28
28
  import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
29
29
  export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
30
30
  // Built-in tools execute inside the Claude runtime and are covered by the
@@ -217,6 +217,32 @@ function buildClaudeManagedLockdownSettings(capabilityOptions) {
217
217
  };
218
218
  }
219
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
+ }
220
246
  async function assertClaudeSettingSourcesHaveLiteralSandboxPaths(cwd, settingSources, managedSettings) {
221
247
  if (process.platform !== 'linux' || settingSources.length === 0)
222
248
  return;
@@ -1759,6 +1785,30 @@ export class AgentRunner {
1759
1785
  else {
1760
1786
  logger.error(`[AgentRunner] Subprocess stream failed (session=${sessionId}) with no captured stderr.`);
1761
1787
  }
1788
+ if (isSandboxInitializationFailure(err, buf ?? [])) {
1789
+ const detail = createSandboxInitializationError(err, buf ?? []);
1790
+ await this.permissionContexts.get(sessionId)?.recordExecutionAnomaly?.({
1791
+ code: 'capability_unavailable',
1792
+ severity: 'warning',
1793
+ phase: 'execution',
1794
+ occurredAt: Date.now(),
1795
+ toolName: 'Bash',
1796
+ policyCode: SANDBOX_INITIALIZATION_FAILED,
1797
+ decisionSource: 'infrastructure',
1798
+ agentAid: this.permissionContexts.get(sessionId)?.selfAid,
1799
+ summary: detail.message.slice(0, 512),
1800
+ effect: 'operation_skipped',
1801
+ });
1802
+ yield {
1803
+ type: 'complete',
1804
+ isError: true,
1805
+ subtype: 'infrastructure',
1806
+ terminalReason: SANDBOX_INITIALIZATION_FAILED,
1807
+ errors: [detail.message],
1808
+ queryFinal: true,
1809
+ };
1810
+ return;
1811
+ }
1762
1812
  throw err;
1763
1813
  }
1764
1814
  finally {
@@ -1856,8 +1906,25 @@ export class AgentRunner {
1856
1906
  const code = policyCode ?? (preflight?.behavior === 'deny' ? preflight.policyCode : undefined);
1857
1907
  if (!code)
1858
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
+ });
1859
1926
  try {
1860
- await this.permissionContexts.get(sessionId)?.recordExecutionAnomaly?.({
1927
+ await ctx?.recordExecutionAnomaly?.({
1861
1928
  code: 'operation_blocked',
1862
1929
  severity: 'warning',
1863
1930
  phase: 'execution',
@@ -1865,7 +1932,10 @@ export class AgentRunner {
1865
1932
  toolName,
1866
1933
  ...(typeof requestId === 'string' && requestId ? { requestId } : {}),
1867
1934
  policyCode: code,
1868
- summary: summarizeToolInputForAudit(toolName, toolInput).slice(0, 512),
1935
+ decisionSource: 'policy',
1936
+ agentAid: ctx?.selfAid,
1937
+ permissionMode: callPermissionMode,
1938
+ summary,
1869
1939
  effect: 'operation_skipped',
1870
1940
  });
1871
1941
  }
@@ -1932,6 +2002,7 @@ export class AgentRunner {
1932
2002
  role: permCtx?.role,
1933
2003
  permissionMode: callPermissionMode,
1934
2004
  allowHostProcessCommands: true,
2005
+ allowProtectedMetadata: false,
1935
2006
  safeOutputHelperPath: path.join(getPackageRoot(), 'bin', 'ec-safe-output.js'),
1936
2007
  projectPath,
1937
2008
  });
@@ -1963,10 +2034,11 @@ export class AgentRunner {
1963
2034
  channel: permCtx?.channel,
1964
2035
  peerId: permCtx?.userId,
1965
2036
  role: permCtx?.role,
2037
+ allowProtectedMetadata: false,
1966
2038
  };
1967
2039
  const roResult = checkReadonly(toolName, toolInput, projectPath, readonlyContext);
1968
2040
  if (roResult.behavior === 'deny') {
1969
- await recordPreflightDenial(toolName, toolInput, undefined, input.tool_use_id ?? input.toolUseID, 'readonly_mode');
2041
+ await recordPreflightDenial(toolName, toolInput, undefined, input.tool_use_id ?? input.toolUseID, roResult.policyCode ?? 'readonly_mode');
1970
2042
  return hookDecision('deny', roResult.message, updatedInput);
1971
2043
  }
1972
2044
  return hookDecision('allow', undefined, updatedInput);
@@ -2052,6 +2124,7 @@ export class AgentRunner {
2052
2124
  role: permCtx?.role,
2053
2125
  permissionMode: callPermissionMode,
2054
2126
  allowHostProcessCommands: true,
2127
+ allowProtectedMetadata: false,
2055
2128
  safeOutputHelperPath: path.join(getPackageRoot(), 'bin', 'ec-safe-output.js'),
2056
2129
  projectPath,
2057
2130
  });
@@ -2119,10 +2192,11 @@ export class AgentRunner {
2119
2192
  channel: permCtx?.channel,
2120
2193
  peerId: permCtx?.userId,
2121
2194
  role: permCtx?.role,
2195
+ allowProtectedMetadata: false,
2122
2196
  };
2123
2197
  const roResult = checkReadonly(toolName, input, projectPath, readonlyContext);
2124
2198
  if (roResult.behavior === 'deny') {
2125
- await recordPreflightDenial(toolName, input, undefined, options.toolUseID, 'readonly_mode');
2199
+ await recordPreflightDenial(toolName, input, undefined, options.toolUseID, roResult.policyCode ?? 'readonly_mode');
2126
2200
  return { behavior: 'deny', message: roResult.message, decisionClassification: 'user_reject' };
2127
2201
  }
2128
2202
  return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
@@ -2213,6 +2287,7 @@ export class AgentRunner {
2213
2287
  const sandboxUnixSockets = buildClaudeUnixSocketAllowlist(projectPath, {
2214
2288
  role: runPermissionContext?.role,
2215
2289
  });
2290
+ const runtimeWritePaths = trustedRuntimeWritePaths(runtimeEnv);
2216
2291
  await assertClaudeSettingSourcesHaveLiteralSandboxPaths(projectPath, [...settingSources], managedSettings);
2217
2292
  const sandboxProjectionDurationMs = Date.now() - sandboxProjectionStartedAt;
2218
2293
  if (sandboxProjectionDurationMs > 2_000) {
@@ -2229,7 +2304,10 @@ export class AgentRunner {
2229
2304
  allowAllUnixSockets: false,
2230
2305
  allowLocalBinding: false,
2231
2306
  },
2232
- filesystem: sandboxFilesystem,
2307
+ filesystem: {
2308
+ ...sandboxFilesystem,
2309
+ ...(runtimeWritePaths.length > 0 ? { allowWrite: runtimeWritePaths } : {}),
2310
+ },
2233
2311
  };
2234
2312
  })();
2235
2313
  if (executionSandbox.state === 'off') {
@@ -2373,12 +2451,28 @@ export class AgentRunner {
2373
2451
  logger.info('[AgentRunner] Creating query with images:', images.length, 'first image size:', images[0]?.data?.length ?? 0);
2374
2452
  logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
2375
2453
  msgStream.push(prompt, images, inputId);
2376
- sdkStream = createQuery(msgStream);
2454
+ try {
2455
+ sdkStream = createQuery(msgStream);
2456
+ }
2457
+ catch (error) {
2458
+ const stderr = this.recentStderr.get(sessionId) ?? [];
2459
+ if (isSandboxInitializationFailure(error, stderr))
2460
+ throw createSandboxInitializationError(error, stderr);
2461
+ throw error;
2462
+ }
2377
2463
  }
2378
2464
  else {
2379
2465
  logger.debug('[AgentRunner] Creating query with text only, agentSessionId:', initialClaudeSessionId);
2380
2466
  msgStream.push(prompt, undefined, inputId);
2381
- sdkStream = createQuery(msgStream, agentSessionId, resumeAt);
2467
+ try {
2468
+ sdkStream = createQuery(msgStream, agentSessionId, resumeAt);
2469
+ }
2470
+ catch (error) {
2471
+ const stderr = this.recentStderr.get(sessionId) ?? [];
2472
+ if (isSandboxInitializationFailure(error, stderr))
2473
+ throw createSandboxInitializationError(error, stderr);
2474
+ throw error;
2475
+ }
2382
2476
  }
2383
2477
  this.activeMessageStreams.set(sessionId, msgStream);
2384
2478
  this.activeQueries.set(sessionId, sdkStream);