evolcore 0.0.17 → 0.0.18
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 +32 -0
- package/bin/codex-managed-hook.mjs +16 -7
- package/bin/install-codex-managed-hooks.mjs +4 -2
- package/dist/agents/claude-runner.js +113 -24
- package/dist/agents/codex-app-server-client.js +6 -1
- package/dist/agents/codex-runner.js +21 -6
- package/dist/agents/ecagent-runner.js +39 -10
- package/dist/agents/gemini-runner.js +90 -19
- package/dist/aun/aid/store.js +36 -0
- package/dist/aun/msg/p2p.js +20 -8
- package/dist/channels/aun.js +159 -21
- package/dist/cli/agent-command.js +67 -6
- package/dist/cli/agent.js +26 -0
- package/dist/cli/command-log.js +23 -4
- package/dist/cli/daemon-commands.js +53 -12
- package/dist/cli/init.js +21 -5
- package/dist/cli/restart-monitor.js +13 -6
- package/dist/cli/watch-logs.js +2 -2
- package/dist/config/builtin-roles.js +5 -1
- package/dist/config/role-ranks.js +4 -0
- package/dist/core/audit/event-key.js +29 -0
- package/dist/core/audit/log-integrity.js +13 -3
- package/dist/core/auth/auth-gateway.js +14 -18
- package/dist/core/auth/authorization-audit.js +110 -3
- package/dist/core/auth/authorization-denial.js +17 -0
- package/dist/core/auth/operation-authorizer.js +143 -18
- package/dist/core/auth/operation-catalog.js +21 -5
- package/dist/core/bootstrap-messages.js +11 -6
- package/dist/core/bootstrap-service.js +26 -4
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +25 -16
- package/dist/core/command/command-handler.js +50 -4
- package/dist/core/command/group-menu.js +1 -1
- package/dist/core/command/menu-catalog.js +32 -7
- package/dist/core/command/menu-handler.js +59 -23
- package/dist/core/command/menu-protocol.js +196 -0
- package/dist/core/command/slash-gate.js +14 -5
- package/dist/core/command/slash-handler.js +81 -99
- package/dist/core/event-catalog.js +18 -0
- package/dist/core/message/message-bridge.js +72 -9
- package/dist/core/message/pause-controller.js +53 -0
- package/dist/core/message/response-engine.js +97 -11
- package/dist/core/permission/sandbox-runtime.js +79 -13
- package/dist/core/permission/tool-policy.js +1 -1
- package/dist/index.js +357 -48
- package/dist/ipc.js +75 -4
- package/dist/utils/atomic-write.js +45 -11
- package/dist/utils/logger.js +27 -0
- package/dist/utils/windows-autostart.js +740 -83
- package/ecagent/dist/harness/agent-harness.d.ts +1 -1
- package/ecagent/dist/harness/agent-harness.js +6 -4
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/group-rules.md +2 -1
- package/kits/docs/identity/ROLE_DETAIL.md +3 -1
- package/kits/eck_manifest.json +25 -16
- package/kits/rules/01-overview.md +5 -5
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/04-relation.md +4 -4
- package/kits/rules/05-venue.md +5 -5
- package/kits/templates/bootstrap-welcome.md +3 -1
- package/kits/templates/system-fragments/bootstrap.md +17 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,38 @@
|
|
|
3
3
|
本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
|
|
4
4
|
[`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
|
|
5
5
|
|
|
6
|
+
## 0.0.18 (2026-08-27)
|
|
7
|
+
|
|
8
|
+
### DaemonOwner 与受管 Agent 操作
|
|
9
|
+
|
|
10
|
+
- 新增 `daemon-owner` 进程角色,仅由 `daemon.json.owners` 的控制 AID 解析。
|
|
11
|
+
- 进程操作、跨 Agent Trigger 与内部 Trigger 目录统一要求 DaemonOwner。
|
|
12
|
+
- Agent channel 保持仅能操作自身;拒绝结果附带原因码与下一步指引。
|
|
13
|
+
- 受管任务通过委派 IPC 执行授权的 `ec agent` 查询、非交互创建和生命周期操作。
|
|
14
|
+
|
|
15
|
+
### 任务暂停与 Bootstrap 门禁
|
|
16
|
+
|
|
17
|
+
- 新增 `/pause`、`/resume`,在下一次工具调用前暂停或恢复当前任务。
|
|
18
|
+
- Codex、Claude 与 ecagent 的工具边界均接入暂停门禁;任务结束自动解除。
|
|
19
|
+
- Agent 仅在 `active`、`bootstrapping` 生命周期运行,不再回退默认 Runner。
|
|
20
|
+
- Bootstrap 隔离普通任务与上下文,仅允许受限的 `agent.md` 确认、发布和 ready。
|
|
21
|
+
- 首次欢迎消息按实际私聊或群聊路由持久化投递。
|
|
22
|
+
|
|
23
|
+
### AUN 投递与连接可靠性
|
|
24
|
+
|
|
25
|
+
- 群消息恢复支持有界多页补拉,避免尾部同步后遗留大积压。
|
|
26
|
+
- 适配新版 AUN 连接状态事件,隔离旧客户端事件并记录传输/入站时间。
|
|
27
|
+
- 受管沙箱发送 AUN 消息改经配置存储读取超时和默认加密设置。
|
|
28
|
+
|
|
29
|
+
### 审计、安全与跨平台运行
|
|
30
|
+
|
|
31
|
+
- 菜单响应新增 `processing_ms` 与独立的协议业务流日志。
|
|
32
|
+
- 工具与授权审计新增生命周期键、执行状态和敏感参数脱敏。
|
|
33
|
+
- 统一 Codex 受管 `PreToolUse` Hook,强化 Bubblewrap 挂载与失败熔断诊断。
|
|
34
|
+
- 原子写入保留正式文件并刷新热备,避免受保护状态挂载竞态。
|
|
35
|
+
- `ec stop`、`ec restart` 优先经 IPC 请求守护进程优雅退出并校验 PID。
|
|
36
|
+
- Windows 自启增加计划任务校验、用户级 Run 回退和非 EvolCore 配置保护。
|
|
37
|
+
|
|
6
38
|
## 0.0.17 (2026-08-25)
|
|
7
39
|
|
|
8
40
|
### Claude 与任务边界
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import net from 'node:net';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Only bound socket establishment. Once connected, the official Codex hook
|
|
5
|
+
// timeout owns the lifetime of this process; an application timeout here would
|
|
6
|
+
// turn /pause into a denial/allow race.
|
|
7
|
+
const connectTimeoutMs = 1500;
|
|
5
8
|
|
|
6
9
|
function deny(reason) {
|
|
7
10
|
process.stdout.write(JSON.stringify({
|
|
@@ -11,9 +14,8 @@ function deny(reason) {
|
|
|
11
14
|
permissionDecisionReason: reason || 'EvolCore policy denied this tool call.',
|
|
12
15
|
},
|
|
13
16
|
}));
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// blocking transport; retain the JSON shape for compatible hook consumers.
|
|
17
|
+
// Exit 2 is the documented blocking transport. Keep the structured JSON as
|
|
18
|
+
// well so Codex can surface the policy reason through the PreToolUse result.
|
|
17
19
|
process.stderr.write(`${reason || 'EvolCore policy denied this tool call.'}\n`);
|
|
18
20
|
process.exitCode = 2;
|
|
19
21
|
}
|
|
@@ -24,6 +26,7 @@ function queryDaemon(socketPath, payload) {
|
|
|
24
26
|
let buffer = '';
|
|
25
27
|
let settled = false;
|
|
26
28
|
let timer;
|
|
29
|
+
let connected = false;
|
|
27
30
|
const finish = value => {
|
|
28
31
|
if (settled) return;
|
|
29
32
|
settled = true;
|
|
@@ -31,10 +34,16 @@ function queryDaemon(socketPath, payload) {
|
|
|
31
34
|
socket.destroy();
|
|
32
35
|
resolve(value);
|
|
33
36
|
};
|
|
34
|
-
timer = setTimeout(() =>
|
|
37
|
+
timer = setTimeout(() => {
|
|
38
|
+
if (!connected) finish(null);
|
|
39
|
+
}, connectTimeoutMs);
|
|
35
40
|
timer.unref?.();
|
|
36
41
|
socket.once('error', () => finish(null));
|
|
37
|
-
socket.once('connect', () =>
|
|
42
|
+
socket.once('connect', () => {
|
|
43
|
+
connected = true;
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
socket.write(JSON.stringify(payload) + '\n');
|
|
46
|
+
});
|
|
38
47
|
socket.on('data', chunk => {
|
|
39
48
|
buffer += String(chunk);
|
|
40
49
|
const index = buffer.indexOf('\n');
|
|
@@ -69,7 +78,7 @@ async function main() {
|
|
|
69
78
|
const result = await queryDaemon(socketPath, {
|
|
70
79
|
type: 'codex-pre-tool-use',
|
|
71
80
|
threadId: input.session_id,
|
|
72
|
-
toolName:
|
|
81
|
+
toolName: input.tool_name,
|
|
73
82
|
toolInput: input.tool_input ?? null,
|
|
74
83
|
});
|
|
75
84
|
if (!result) {
|
|
@@ -151,7 +151,7 @@ function main() {
|
|
|
151
151
|
const targetDirectory = argument('--managed-dir')
|
|
152
152
|
|| path.join(path.dirname(requirements), 'EvolCore', 'hooks');
|
|
153
153
|
const sourceHook = path.join(packageRoot, 'bin', 'codex-managed-hook.mjs');
|
|
154
|
-
if (!fs.statSync(sourceHook).isFile()) fail(`
|
|
154
|
+
if (!fs.existsSync(sourceHook) || !fs.statSync(sourceHook).isFile()) fail(`Codex PreToolUse hook source was not found: ${sourceHook}`);
|
|
155
155
|
|
|
156
156
|
fs.mkdirSync(targetDirectory, { recursive: true, mode: 0o700 });
|
|
157
157
|
const hookPath = path.join(targetDirectory, 'pre-tool-use.mjs');
|
|
@@ -180,7 +180,9 @@ function main() {
|
|
|
180
180
|
'type = "command"',
|
|
181
181
|
`command = ${tomlString(command)}`,
|
|
182
182
|
`command_windows = ${tomlString(command)}`,
|
|
183
|
-
|
|
183
|
+
// Intentionally omit timeout: Codex applies its documented 600-second
|
|
184
|
+
// default for synchronous PreToolUse hooks. The application pause gate
|
|
185
|
+
// itself has no timeout; Codex remains the outer infrastructure ceiling.
|
|
184
186
|
'statusMessage = "Checking EvolCore session policy"',
|
|
185
187
|
markerEnd,
|
|
186
188
|
'',
|
|
@@ -23,9 +23,9 @@ import { sanitizeSessionTitle } from '../core/session/session-title.js';
|
|
|
23
23
|
import { resolveClaudeCapabilityRunOptionsForProject } from '../core/capability/capability-manager.js';
|
|
24
24
|
import { normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
|
|
25
25
|
import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClassReference, isHClassPath, isLClassPath, isSameOrDescendant, resolveProtectedCandidate, } from '../core/protected-paths.js';
|
|
26
|
-
import { buildHClassGuardCommand, createSandboxInitializationError, isSandboxInitializationFailure, SANDBOX_INITIALIZATION_FAILED, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, shouldFailIfClaudeSandboxUnavailable, } from '../core/permission/sandbox-runtime.js';
|
|
26
|
+
import { buildHClassGuardCommand, createSandboxInitializationError, getActiveSandboxInitializationFailure, isSandboxInitializationFailure, recordSandboxInitializationFailure, SANDBOX_INITIALIZATION_COOLDOWN_MS, SANDBOX_INITIALIZATION_FAILED, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, shouldFailIfClaudeSandboxUnavailable, shouldLogSandboxCircuitNotice, } from '../core/permission/sandbox-runtime.js';
|
|
27
27
|
import { buildClaudeUnixSocketAllowlist } from '../core/permission/unix-socket-policy.js';
|
|
28
|
-
import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
|
|
28
|
+
import { auditToolInfrastructureFailure, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
|
|
29
29
|
import { isManagedSessionRuntimeDir } from '../cli/task-context.js';
|
|
30
30
|
import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
|
|
31
31
|
export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
|
|
@@ -626,6 +626,40 @@ export class AgentRunner {
|
|
|
626
626
|
/** 每个 session 最近的子进程 stderr 行(环形缓冲),用于子进程崩溃时还原真正原因 */
|
|
627
627
|
recentStderr = new Map();
|
|
628
628
|
static STDERR_BUFFER_MAX = 80;
|
|
629
|
+
async recordSandboxInfrastructureFailure(sessionId, inputId, permissionMode, error, stderr = []) {
|
|
630
|
+
const detail = createSandboxInitializationError(error, stderr);
|
|
631
|
+
const circuit = recordSandboxInitializationFailure(detail, stderr);
|
|
632
|
+
const context = this.permissionContexts.get(sessionId);
|
|
633
|
+
auditToolInfrastructureFailure({
|
|
634
|
+
toolName: 'Bash',
|
|
635
|
+
policyCode: SANDBOX_INITIALIZATION_FAILED,
|
|
636
|
+
reason: detail.message.slice(0, 512),
|
|
637
|
+
sessionId,
|
|
638
|
+
agentAid: context?.selfAid,
|
|
639
|
+
permissionMode,
|
|
640
|
+
role: context?.role,
|
|
641
|
+
callId: inputId,
|
|
642
|
+
taskId: context?.taskId,
|
|
643
|
+
occurrences: circuit.occurrences,
|
|
644
|
+
});
|
|
645
|
+
await context?.recordExecutionAnomaly?.({
|
|
646
|
+
code: 'capability_unavailable',
|
|
647
|
+
severity: 'warning',
|
|
648
|
+
phase: 'execution',
|
|
649
|
+
occurredAt: Date.now(),
|
|
650
|
+
correlationId: inputId,
|
|
651
|
+
requestId: inputId,
|
|
652
|
+
sessionId,
|
|
653
|
+
toolName: 'Bash',
|
|
654
|
+
policyCode: SANDBOX_INITIALIZATION_FAILED,
|
|
655
|
+
decisionSource: 'infrastructure',
|
|
656
|
+
agentAid: context?.selfAid,
|
|
657
|
+
permissionMode: permissionMode,
|
|
658
|
+
summary: detail.message.slice(0, 512),
|
|
659
|
+
effect: 'operation_skipped',
|
|
660
|
+
});
|
|
661
|
+
return { detail, occurrences: circuit.occurrences };
|
|
662
|
+
}
|
|
629
663
|
trackSandboxSettingsFile(sessionId, filePath) {
|
|
630
664
|
const files = this.sandboxSettingsFiles.get(sessionId) ?? new Set();
|
|
631
665
|
files.add(filePath);
|
|
@@ -1407,7 +1441,7 @@ export class AgentRunner {
|
|
|
1407
1441
|
* SDK 原始事件 → 标准 AgentEvent 转换
|
|
1408
1442
|
* 所有 SDK 特有的事件类型引用封装在此方法内
|
|
1409
1443
|
*/
|
|
1410
|
-
async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile) {
|
|
1444
|
+
async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile, permissionMode) {
|
|
1411
1445
|
let lastSessionId;
|
|
1412
1446
|
let hasTurnActivity = false;
|
|
1413
1447
|
let ignoredPreTurnResult = false;
|
|
@@ -1851,19 +1885,7 @@ export class AgentRunner {
|
|
|
1851
1885
|
return;
|
|
1852
1886
|
}
|
|
1853
1887
|
if (isSandboxInitializationFailure(err, buf ?? [])) {
|
|
1854
|
-
const detail =
|
|
1855
|
-
await this.permissionContexts.get(sessionId)?.recordExecutionAnomaly?.({
|
|
1856
|
-
code: 'capability_unavailable',
|
|
1857
|
-
severity: 'warning',
|
|
1858
|
-
phase: 'execution',
|
|
1859
|
-
occurredAt: Date.now(),
|
|
1860
|
-
toolName: 'Bash',
|
|
1861
|
-
policyCode: SANDBOX_INITIALIZATION_FAILED,
|
|
1862
|
-
decisionSource: 'infrastructure',
|
|
1863
|
-
agentAid: this.permissionContexts.get(sessionId)?.selfAid,
|
|
1864
|
-
summary: detail.message.slice(0, 512),
|
|
1865
|
-
effect: 'operation_skipped',
|
|
1866
|
-
});
|
|
1888
|
+
const { detail } = await this.recordSandboxInfrastructureFailure(sessionId, inputId, permissionMode, err, buf ?? []);
|
|
1867
1889
|
yield {
|
|
1868
1890
|
type: 'complete',
|
|
1869
1891
|
isError: true,
|
|
@@ -1955,6 +1977,57 @@ export class AgentRunner {
|
|
|
1955
1977
|
const callPermissionMode = normalizedPermission.mode;
|
|
1956
1978
|
const runPermissionContext = this.permissionContexts.get(sessionId);
|
|
1957
1979
|
const executionSandbox = resolvePhaseOneExecutionSandbox(callPermissionMode, runPermissionContext?.role);
|
|
1980
|
+
const inputId = modelOverride?.turn?.inputId ?? crypto.randomUUID();
|
|
1981
|
+
if (executionSandbox.state !== 'off') {
|
|
1982
|
+
const activeSandboxFailure = getActiveSandboxInitializationFailure();
|
|
1983
|
+
if (activeSandboxFailure) {
|
|
1984
|
+
const detail = createSandboxInitializationError(new Error(`sandbox bootstrap circuit open; firstObservedAt=${activeSandboxFailure.openedAt} ` +
|
|
1985
|
+
`occurrences=${activeSandboxFailure.occurrences}; ${activeSandboxFailure.detail}`));
|
|
1986
|
+
const context = this.permissionContexts.get(sessionId);
|
|
1987
|
+
auditToolInfrastructureFailure({
|
|
1988
|
+
toolName: 'Bash',
|
|
1989
|
+
policyCode: SANDBOX_INITIALIZATION_FAILED,
|
|
1990
|
+
reason: detail.message.slice(0, 512),
|
|
1991
|
+
sessionId,
|
|
1992
|
+
agentAid: context?.selfAid,
|
|
1993
|
+
permissionMode: callPermissionMode,
|
|
1994
|
+
role: context?.role,
|
|
1995
|
+
callId: inputId,
|
|
1996
|
+
taskId: context?.taskId,
|
|
1997
|
+
occurrences: activeSandboxFailure.occurrences,
|
|
1998
|
+
});
|
|
1999
|
+
await context?.recordExecutionAnomaly?.({
|
|
2000
|
+
code: 'capability_unavailable',
|
|
2001
|
+
severity: 'warning',
|
|
2002
|
+
phase: 'execution',
|
|
2003
|
+
occurredAt: Date.now(),
|
|
2004
|
+
correlationId: inputId,
|
|
2005
|
+
requestId: inputId,
|
|
2006
|
+
sessionId,
|
|
2007
|
+
toolName: 'Bash',
|
|
2008
|
+
policyCode: SANDBOX_INITIALIZATION_FAILED,
|
|
2009
|
+
decisionSource: 'infrastructure',
|
|
2010
|
+
agentAid: context?.selfAid,
|
|
2011
|
+
permissionMode: callPermissionMode,
|
|
2012
|
+
summary: detail.message.slice(0, 512),
|
|
2013
|
+
effect: 'operation_skipped',
|
|
2014
|
+
});
|
|
2015
|
+
if (shouldLogSandboxCircuitNotice()) {
|
|
2016
|
+
logger.warn(`[ClaudeSandbox] bootstrap circuit open; skipping subprocess spawn ` +
|
|
2017
|
+
`cooldownMs=${SANDBOX_INITIALIZATION_COOLDOWN_MS} occurrences=${activeSandboxFailure.occurrences}`);
|
|
2018
|
+
}
|
|
2019
|
+
return (async function* () {
|
|
2020
|
+
yield {
|
|
2021
|
+
type: 'complete',
|
|
2022
|
+
isError: true,
|
|
2023
|
+
subtype: 'infrastructure',
|
|
2024
|
+
terminalReason: SANDBOX_INITIALIZATION_FAILED,
|
|
2025
|
+
errors: [detail.message],
|
|
2026
|
+
queryFinal: true,
|
|
2027
|
+
};
|
|
2028
|
+
})();
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
1958
2031
|
const permissionPrompt = runPermissionContext?.sendPrompt ?? this.sendPromptFn;
|
|
1959
2032
|
const permissionContext = (signal) => {
|
|
1960
2033
|
const context = this.permissionContexts.get(sessionId);
|
|
@@ -2013,7 +2086,14 @@ export class AgentRunner {
|
|
|
2013
2086
|
// random one-shot payload from a validated output wrapper.
|
|
2014
2087
|
const trustedSafeOutputCommands = new Set();
|
|
2015
2088
|
// PreToolUse Hook - 黑名单检查 + input 修正(不可绕过,所有模式都走)
|
|
2016
|
-
const preToolUseHook = async (input) => {
|
|
2089
|
+
const preToolUseHook = async (input, _toolUseId, hookOptions) => {
|
|
2090
|
+
const pauseController = this.permissionContexts.get(sessionId)?.pauseController;
|
|
2091
|
+
if (pauseController) {
|
|
2092
|
+
const pauseResult = await pauseController.waitAtToolBoundary(hookOptions?.signal);
|
|
2093
|
+
if (pauseResult === 'cancelled') {
|
|
2094
|
+
return hookDecision('deny', '工具调用已取消');
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2017
2097
|
const toolName = input.tool_name;
|
|
2018
2098
|
const originalInput = (input.tool_input || {});
|
|
2019
2099
|
let toolInput = originalInput;
|
|
@@ -2358,6 +2438,10 @@ export class AgentRunner {
|
|
|
2358
2438
|
role: runPermissionContext?.role,
|
|
2359
2439
|
});
|
|
2360
2440
|
const runtimeWritePaths = trustedRuntimeWritePaths(runtimeEnv);
|
|
2441
|
+
const bootstrapWritePaths = modelOverride?.bootstrapAgentMdPath
|
|
2442
|
+
? [path.resolve(modelOverride.bootstrapAgentMdPath)]
|
|
2443
|
+
: [];
|
|
2444
|
+
const allowedWritePaths = [...new Set([...runtimeWritePaths, ...bootstrapWritePaths])];
|
|
2361
2445
|
await assertClaudeSettingSourcesHaveLiteralSandboxPaths(projectPath, [...settingSources], managedSettings);
|
|
2362
2446
|
const sandboxProjectionDurationMs = Date.now() - sandboxProjectionStartedAt;
|
|
2363
2447
|
if (sandboxProjectionDurationMs > 2_000) {
|
|
@@ -2376,7 +2460,7 @@ export class AgentRunner {
|
|
|
2376
2460
|
},
|
|
2377
2461
|
filesystem: {
|
|
2378
2462
|
...sandboxFilesystem,
|
|
2379
|
-
...(
|
|
2463
|
+
...(allowedWritePaths.length > 0 ? { allowWrite: allowedWritePaths } : {}),
|
|
2380
2464
|
},
|
|
2381
2465
|
};
|
|
2382
2466
|
})();
|
|
@@ -2552,7 +2636,6 @@ export class AgentRunner {
|
|
|
2552
2636
|
}
|
|
2553
2637
|
let sdkStream;
|
|
2554
2638
|
const msgStream = new MessageStream();
|
|
2555
|
-
const inputId = modelOverride?.turn?.inputId ?? crypto.randomUUID();
|
|
2556
2639
|
if (images && images.length > 0) {
|
|
2557
2640
|
logger.info('[AgentRunner] Creating query with images:', images.length, 'first image size:', images[0]?.data?.length ?? 0);
|
|
2558
2641
|
logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
|
|
@@ -2565,8 +2648,10 @@ export class AgentRunner {
|
|
|
2565
2648
|
const stderr = this.recentStderr.get(sessionId) ?? [];
|
|
2566
2649
|
if (isClaudeStartupArgumentsTooLong(error))
|
|
2567
2650
|
throw createClaudeStartupArgumentsTooLongError(error);
|
|
2568
|
-
if (isSandboxInitializationFailure(error, stderr))
|
|
2569
|
-
|
|
2651
|
+
if (isSandboxInitializationFailure(error, stderr)) {
|
|
2652
|
+
const { detail } = await this.recordSandboxInfrastructureFailure(sessionId, inputId, callPermissionMode, error, stderr);
|
|
2653
|
+
throw detail;
|
|
2654
|
+
}
|
|
2570
2655
|
throw error;
|
|
2571
2656
|
}
|
|
2572
2657
|
}
|
|
@@ -2581,8 +2666,10 @@ export class AgentRunner {
|
|
|
2581
2666
|
const stderr = this.recentStderr.get(sessionId) ?? [];
|
|
2582
2667
|
if (isClaudeStartupArgumentsTooLong(error))
|
|
2583
2668
|
throw createClaudeStartupArgumentsTooLongError(error);
|
|
2584
|
-
if (isSandboxInitializationFailure(error, stderr))
|
|
2585
|
-
|
|
2669
|
+
if (isSandboxInitializationFailure(error, stderr)) {
|
|
2670
|
+
const { detail } = await this.recordSandboxInfrastructureFailure(sessionId, inputId, callPermissionMode, error, stderr);
|
|
2671
|
+
throw detail;
|
|
2672
|
+
}
|
|
2586
2673
|
throw error;
|
|
2587
2674
|
}
|
|
2588
2675
|
}
|
|
@@ -2595,7 +2682,7 @@ export class AgentRunner {
|
|
|
2595
2682
|
this.interruptFns.set(sessionId, () => sdkStream.interrupt());
|
|
2596
2683
|
}
|
|
2597
2684
|
// 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
|
|
2598
|
-
const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile);
|
|
2685
|
+
const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile, callPermissionMode);
|
|
2599
2686
|
const self = this;
|
|
2600
2687
|
return (async function* () {
|
|
2601
2688
|
try {
|
|
@@ -2611,6 +2698,7 @@ export class AgentRunner {
|
|
|
2611
2698
|
})();
|
|
2612
2699
|
}
|
|
2613
2700
|
async interrupt(sessionId) {
|
|
2701
|
+
this.permissionContexts.get(sessionId)?.pauseController?.cancel();
|
|
2614
2702
|
const streamRegistered = this.activeStreams.has(sessionId);
|
|
2615
2703
|
const fn = this.interruptFns.get(sessionId);
|
|
2616
2704
|
const query = this.activeQueries.get(sessionId);
|
|
@@ -2764,6 +2852,7 @@ export class AgentRunner {
|
|
|
2764
2852
|
}
|
|
2765
2853
|
}
|
|
2766
2854
|
async closeSession(sessionId) {
|
|
2855
|
+
this.permissionContexts.get(sessionId)?.pauseController?.cancel();
|
|
2767
2856
|
const query = this.activeQueries.get(sessionId);
|
|
2768
2857
|
if (query && typeof query.close === 'function')
|
|
2769
2858
|
query.close();
|
|
@@ -475,6 +475,8 @@ export class CodexAppServerClient {
|
|
|
475
475
|
fs.chmodSync(directory, 0o700);
|
|
476
476
|
const hookPath = path.join(directory, 'pre-tool-use.mjs');
|
|
477
477
|
const sourcePath = path.join(getPackageRoot(), 'bin', 'codex-managed-hook.mjs');
|
|
478
|
+
if (!fs.existsSync(sourcePath))
|
|
479
|
+
throw new Error('Codex PreToolUse hook source is unavailable');
|
|
478
480
|
fs.copyFileSync(sourcePath, hookPath, fs.constants.COPYFILE_EXCL);
|
|
479
481
|
fs.chmodSync(hookPath, 0o500);
|
|
480
482
|
const command = `${this.shellQuote(process.execPath)} ${this.shellQuote(hookPath)} ${this.shellQuote(resolvePaths().instanceSocket)}`;
|
|
@@ -494,7 +496,10 @@ export class CodexAppServerClient {
|
|
|
494
496
|
'[[hooks.PreToolUse.hooks]]',
|
|
495
497
|
'type = "command"',
|
|
496
498
|
`command = ${JSON.stringify(command)}`,
|
|
497
|
-
|
|
499
|
+
// Intentionally omit timeout: Codex applies its documented 600-second
|
|
500
|
+
// default for synchronous PreToolUse hooks. The application pause
|
|
501
|
+
// gate itself has no timeout; Codex remains the outer infrastructure
|
|
502
|
+
// ceiling.
|
|
498
503
|
'statusMessage = "Checking EvolCore session policy"',
|
|
499
504
|
'',
|
|
500
505
|
].join('\n'), { mode: 0o400, flag: 'wx' });
|
|
@@ -795,7 +795,7 @@ export class CodexRunner {
|
|
|
795
795
|
}
|
|
796
796
|
return Object.fromEntries([...paths].map(candidate => [candidate, 'write']));
|
|
797
797
|
}
|
|
798
|
-
buildPermissionProfileConfig(sessionId, mode, projectPath) {
|
|
798
|
+
buildPermissionProfileConfig(sessionId, mode, projectPath, bootstrapAgentMdPath) {
|
|
799
799
|
const profileName = this.permissionProfileName(sessionId);
|
|
800
800
|
const managedTempDir = this.ensureSessionTempDir(sessionId);
|
|
801
801
|
const network = {
|
|
@@ -812,6 +812,9 @@ export class CodexRunner {
|
|
|
812
812
|
denyLClassRead: mode === 'readonly',
|
|
813
813
|
}),
|
|
814
814
|
...this.resolveGitMetadataWriteRules(projectPath, mode),
|
|
815
|
+
...(bootstrapAgentMdPath ? {
|
|
816
|
+
[path.resolve(bootstrapAgentMdPath)]: mode === 'readonly' ? 'read' : 'write',
|
|
817
|
+
} : {}),
|
|
815
818
|
...(mode !== 'readonly' && managedTempDir ? {
|
|
816
819
|
[managedTempDir]: 'write',
|
|
817
820
|
[path.join(managedTempDir, '**')]: 'write',
|
|
@@ -824,12 +827,12 @@ export class CodexRunner {
|
|
|
824
827
|
},
|
|
825
828
|
};
|
|
826
829
|
}
|
|
827
|
-
buildExecutionSandboxOptions(sessionId, mode, projectPath) {
|
|
830
|
+
buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath) {
|
|
828
831
|
const decision = resolvePhaseOneExecutionSandbox(mode, this.permissionContexts.get(sessionId)?.role);
|
|
829
832
|
return {
|
|
830
833
|
decision,
|
|
831
834
|
permissionProfileConfig: decision.state === 'active'
|
|
832
|
-
? this.buildPermissionProfileConfig(sessionId, decision.permissionMode, projectPath)
|
|
835
|
+
? this.buildPermissionProfileConfig(sessionId, decision.permissionMode, projectPath, bootstrapAgentMdPath)
|
|
833
836
|
: undefined,
|
|
834
837
|
sandbox: decision.state === 'off' ? 'danger-full-access' : undefined,
|
|
835
838
|
};
|
|
@@ -837,7 +840,11 @@ export class CodexRunner {
|
|
|
837
840
|
buildLifecycleLockdownConfig() {
|
|
838
841
|
return {
|
|
839
842
|
features: {
|
|
840
|
-
|
|
843
|
+
// The app-server is launched with allow_managed_hooks_only=true, so
|
|
844
|
+
// enabling the feature exposes only EvolCore's requirements-managed
|
|
845
|
+
// PreToolUse hook. Disabling it here suppresses that managed hook too
|
|
846
|
+
// and bypasses the pause/proactive policy boundary entirely.
|
|
847
|
+
hooks: true,
|
|
841
848
|
},
|
|
842
849
|
};
|
|
843
850
|
}
|
|
@@ -867,7 +874,7 @@ export class CodexRunner {
|
|
|
867
874
|
throw new Error('Codex proactive managed PreToolUse enforcement requires Linux or an installed Windows managed hook');
|
|
868
875
|
}
|
|
869
876
|
}
|
|
870
|
-
async evaluatePreToolUse(threadId, toolName, toolInput) {
|
|
877
|
+
async evaluatePreToolUse(threadId, toolName, toolInput, signal) {
|
|
871
878
|
const sessionKey = this.findSessionKeyByThread(threadId);
|
|
872
879
|
const activeThread = this.activeSessions.get(sessionKey);
|
|
873
880
|
const context = this.permissionContexts.get(sessionKey);
|
|
@@ -878,6 +885,12 @@ export class CodexRunner {
|
|
|
878
885
|
if (!context || (proactiveSession && !context.preToolUsePolicyHook)) {
|
|
879
886
|
return { ok: false, applicable: true, decision: 'deny', reason: 'EvolCore proactive session policy context is unavailable' };
|
|
880
887
|
}
|
|
888
|
+
if (context.pauseController) {
|
|
889
|
+
const pauseResult = await context.pauseController.waitAtToolBoundary(signal);
|
|
890
|
+
if (pauseResult === 'cancelled') {
|
|
891
|
+
return { ok: true, applicable: true, decision: 'deny', reason: '工具调用已取消' };
|
|
892
|
+
}
|
|
893
|
+
}
|
|
881
894
|
// A tracked interactive thread still needs an authenticated, explicit
|
|
882
895
|
// allow so the managed hook can fail closed when no runner matches.
|
|
883
896
|
if (!context.preToolUsePolicyHook)
|
|
@@ -1044,7 +1057,7 @@ export class CodexRunner {
|
|
|
1044
1057
|
const callMode = normalizedPermission.mode;
|
|
1045
1058
|
this.chatModes.set(sessionId, callMode);
|
|
1046
1059
|
const callApprovalPolicy = this.toApprovalPolicy(callMode);
|
|
1047
|
-
const executionSandbox = this.buildExecutionSandboxOptions(sessionId, callMode, projectPath);
|
|
1060
|
+
const executionSandbox = this.buildExecutionSandboxOptions(sessionId, callMode, projectPath, modelOverride?.bootstrapAgentMdPath);
|
|
1048
1061
|
const appServer = this.getAppServerClient();
|
|
1049
1062
|
this.dropExpiredPendingInterrupt(sessionId);
|
|
1050
1063
|
const effectiveApprovalPolicy = callApprovalPolicy;
|
|
@@ -1203,6 +1216,7 @@ export class CodexRunner {
|
|
|
1203
1216
|
}
|
|
1204
1217
|
// ── Interrupt ──
|
|
1205
1218
|
async interrupt(sessionKey) {
|
|
1219
|
+
this.permissionContexts.get(sessionKey)?.pauseController?.cancel();
|
|
1206
1220
|
const controller = this.activeAbortControllers.get(sessionKey);
|
|
1207
1221
|
const activeTurn = this.activeTurns.get(sessionKey);
|
|
1208
1222
|
const hadActiveState = !!controller || !!activeTurn || this.activeStreams.has(sessionKey);
|
|
@@ -1265,6 +1279,7 @@ export class CodexRunner {
|
|
|
1265
1279
|
this.onSessionIdUpdate?.(sessionId, agentSessionId);
|
|
1266
1280
|
}
|
|
1267
1281
|
async closeSession(sessionId) {
|
|
1282
|
+
this.permissionContexts.get(sessionId)?.pauseController?.cancel();
|
|
1268
1283
|
const threadId = this.activeSessions.get(sessionId);
|
|
1269
1284
|
this.activeSessions.delete(sessionId);
|
|
1270
1285
|
this.activeStreams.delete(sessionId);
|
|
@@ -459,11 +459,30 @@ function isWithin(root, candidate) {
|
|
|
459
459
|
const relative = path.relative(root, candidate);
|
|
460
460
|
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
461
461
|
}
|
|
462
|
-
function
|
|
462
|
+
export function resolveEcagentToolPath(projectPath, candidate, forWrite = false, bootstrapAgentMdPath) {
|
|
463
463
|
const root = fs.realpathSync(projectPath);
|
|
464
464
|
const resolved = path.resolve(root, candidate || '.');
|
|
465
|
-
|
|
465
|
+
const allowedBootstrapFile = bootstrapAgentMdPath
|
|
466
|
+
? path.resolve(bootstrapAgentMdPath)
|
|
467
|
+
: undefined;
|
|
468
|
+
const isBootstrapAgentMd = allowedBootstrapFile === resolved;
|
|
469
|
+
if (!isWithin(root, resolved) && !isBootstrapAgentMd) {
|
|
466
470
|
throw new Error(`Path is outside the project: ${candidate}`);
|
|
471
|
+
}
|
|
472
|
+
if (isBootstrapAgentMd) {
|
|
473
|
+
const parent = path.dirname(resolved);
|
|
474
|
+
const canonicalParent = fs.realpathSync(parent);
|
|
475
|
+
if (!isWithin(parent, canonicalParent) || !isWithin(canonicalParent, parent)) {
|
|
476
|
+
throw new Error(`Bootstrap agent.md parent resolves outside its declared path: ${candidate}`);
|
|
477
|
+
}
|
|
478
|
+
if (fs.existsSync(resolved)) {
|
|
479
|
+
const canonicalFile = fs.realpathSync(resolved);
|
|
480
|
+
if (!isWithin(resolved, canonicalFile) || !isWithin(canonicalFile, resolved)) {
|
|
481
|
+
throw new Error(`Bootstrap agent.md resolves outside its declared path: ${candidate}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return resolved;
|
|
485
|
+
}
|
|
467
486
|
const existing = forWrite ? path.dirname(resolved) : resolved;
|
|
468
487
|
let cursor = existing;
|
|
469
488
|
while (!fs.existsSync(cursor) && cursor !== path.dirname(cursor))
|
|
@@ -522,7 +541,7 @@ async function walkFiles(root, signal) {
|
|
|
522
541
|
function textResult(text, details = {}) {
|
|
523
542
|
return { content: [{ type: 'text', text: truncate(text) }], details };
|
|
524
543
|
}
|
|
525
|
-
function createTools(env, projectPath, runtimeEnv) {
|
|
544
|
+
function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath) {
|
|
526
545
|
const read = {
|
|
527
546
|
name: 'Read', label: 'Read', description: 'Read a UTF-8 text file from the project.',
|
|
528
547
|
parameters: Type.Object({
|
|
@@ -531,7 +550,7 @@ function createTools(env, projectPath, runtimeEnv) {
|
|
|
531
550
|
limit: Type.Optional(Type.Number({ minimum: 1 })),
|
|
532
551
|
}),
|
|
533
552
|
async execute(_id, params, signal) {
|
|
534
|
-
const file =
|
|
553
|
+
const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath);
|
|
535
554
|
const stat = await fs.promises.stat(file);
|
|
536
555
|
if (stat.size > MAX_READ_BYTES)
|
|
537
556
|
throw new Error(`File exceeds ${MAX_READ_BYTES} byte read limit`);
|
|
@@ -548,7 +567,7 @@ function createTools(env, projectPath, runtimeEnv) {
|
|
|
548
567
|
name: 'Glob', label: 'Glob', description: 'Find project files matching a glob pattern.',
|
|
549
568
|
parameters: Type.Object({ pattern: Type.String(), path: Type.Optional(Type.String()) }),
|
|
550
569
|
async execute(_id, params, signal) {
|
|
551
|
-
const root =
|
|
570
|
+
const root = resolveEcagentToolPath(projectPath, params.path ?? '.');
|
|
552
571
|
const matcher = globRegex(params.pattern.replaceAll('\\', '/'));
|
|
553
572
|
const files = (await walkFiles(root, signal))
|
|
554
573
|
.map(file => path.relative(root, file).replaceAll(path.sep, '/'))
|
|
@@ -564,7 +583,7 @@ function createTools(env, projectPath, runtimeEnv) {
|
|
|
564
583
|
case_insensitive: Type.Optional(Type.Boolean()), literal: Type.Optional(Type.Boolean()),
|
|
565
584
|
}),
|
|
566
585
|
async execute(_id, params, signal) {
|
|
567
|
-
const root =
|
|
586
|
+
const root = resolveEcagentToolPath(projectPath, params.path ?? '.');
|
|
568
587
|
const files = (await fs.promises.stat(root)).isFile() ? [root] : await walkFiles(root, signal);
|
|
569
588
|
const fileMatcher = params.glob ? globRegex(params.glob.replaceAll('\\', '/')) : undefined;
|
|
570
589
|
const flags = params.case_insensitive ? 'i' : '';
|
|
@@ -597,7 +616,7 @@ function createTools(env, projectPath, runtimeEnv) {
|
|
|
597
616
|
name: 'Write', label: 'Write', description: 'Create or completely overwrite a project file.',
|
|
598
617
|
parameters: Type.Object({ file_path: Type.String(), content: Type.String() }), executionMode: 'sequential',
|
|
599
618
|
async execute(_id, params, signal) {
|
|
600
|
-
const file =
|
|
619
|
+
const file = resolveEcagentToolPath(projectPath, params.file_path, true, bootstrapAgentMdPath);
|
|
601
620
|
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
602
621
|
await fs.promises.writeFile(file, params.content, { encoding: 'utf8', signal });
|
|
603
622
|
return textResult(`Wrote ${Buffer.byteLength(params.content)} bytes to ${path.relative(projectPath, file)}`, { file });
|
|
@@ -610,7 +629,7 @@ function createTools(env, projectPath, runtimeEnv) {
|
|
|
610
629
|
replace_all: Type.Optional(Type.Boolean()),
|
|
611
630
|
}), executionMode: 'sequential',
|
|
612
631
|
async execute(_id, params, signal) {
|
|
613
|
-
const file =
|
|
632
|
+
const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath);
|
|
614
633
|
const value = await fs.promises.readFile(file, { encoding: 'utf8', signal });
|
|
615
634
|
if (!params.old_string)
|
|
616
635
|
throw new Error('old_string must not be empty');
|
|
@@ -809,7 +828,7 @@ export class EcagentRunner {
|
|
|
809
828
|
models,
|
|
810
829
|
model,
|
|
811
830
|
thinkingLevel: normalizeThinkingLevel(effort),
|
|
812
|
-
tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv),
|
|
831
|
+
tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv, request.modelOverride?.bootstrapAgentMdPath),
|
|
813
832
|
systemPrompt: [DEFAULT_SYSTEM_PROMPT, request.systemPromptAppend].filter(Boolean).join('\n\n'),
|
|
814
833
|
streamOptions: {
|
|
815
834
|
timeoutMs: this.config.timeoutMs,
|
|
@@ -823,7 +842,15 @@ export class EcagentRunner {
|
|
|
823
842
|
runtime.harness = undefined;
|
|
824
843
|
throw new Error('ecagent run interrupted');
|
|
825
844
|
}
|
|
826
|
-
harness.on('tool_call',
|
|
845
|
+
harness.on('tool_call', async (event, signal) => {
|
|
846
|
+
const pauseController = this.permissionContexts.get(request.sessionId)?.pauseController;
|
|
847
|
+
if (pauseController) {
|
|
848
|
+
const pauseResult = await pauseController.waitAtToolBoundary(signal);
|
|
849
|
+
if (pauseResult === 'cancelled')
|
|
850
|
+
return { block: true, reason: '工具调用已取消' };
|
|
851
|
+
}
|
|
852
|
+
return this.authorizeTool(request.sessionId, request.projectPath, permissionMode, event.toolName, event.input);
|
|
853
|
+
});
|
|
827
854
|
const unsubscribe = harness.subscribe(event => this.mapHarnessEvent(queue, event, stats, model, effort));
|
|
828
855
|
queue.push({ type: 'state_changed', state: 'running' });
|
|
829
856
|
queue.push({ type: 'status', subtype: 'started', message: `ecagent is using ${modelId}` });
|
|
@@ -1043,6 +1070,7 @@ export class EcagentRunner {
|
|
|
1043
1070
|
return decision.matched && decision.decision === 'deny' ? { block: true, reason: '危险操作未获人工批准' } : {};
|
|
1044
1071
|
}
|
|
1045
1072
|
async interrupt(sessionKey) {
|
|
1073
|
+
this.permissionContexts.get(sessionKey)?.pauseController?.cancel();
|
|
1046
1074
|
this.permissionGateway?.cancelAll(sessionKey, 'interrupted');
|
|
1047
1075
|
const harness = this.runtimes.get(sessionKey)?.harness;
|
|
1048
1076
|
if (harness) {
|
|
@@ -1058,6 +1086,7 @@ export class EcagentRunner {
|
|
|
1058
1086
|
void harness.steer(text).catch(error => logger.warn('[EcagentRunner] Failed to steer active run:', error));
|
|
1059
1087
|
}
|
|
1060
1088
|
async closeSession(sessionId) {
|
|
1089
|
+
this.permissionContexts.get(sessionId)?.pauseController?.cancel();
|
|
1061
1090
|
await this.interrupt(sessionId).catch(() => { });
|
|
1062
1091
|
this.runtimes.delete(sessionId);
|
|
1063
1092
|
this.activeStreams.delete(sessionId);
|