evolcore 0.0.12 → 0.0.13
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 +22 -0
- package/bin/ec-safe-output.js +26 -7
- package/dist/agents/claude-runner.js +53 -7
- package/dist/agents/codex-app-server-client.js +53 -5
- package/dist/agents/codex-runner.js +104 -59
- package/dist/agents/ecagent-runner.js +4 -1
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/store.js +13 -7
- package/dist/aun/group-fs-download.js +56 -0
- package/dist/aun/group-identity.js +1 -2
- package/dist/aun/msg/group.js +11 -33
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +116 -55
- package/dist/aun/msg/upload.js +16 -1
- package/dist/aun/outbox.js +31 -2
- package/dist/aun/rpc/client.js +66 -0
- package/dist/aun/rpc/index.js +1 -2
- package/dist/aun/storage/{upload.js → client.js} +11 -2
- package/dist/aun/storage/index.js +1 -2
- package/dist/channels/aun.js +371 -136
- package/dist/cli/agent.js +9 -96
- package/dist/cli/cli-argv.js +10 -0
- package/dist/cli/config.js +9 -17
- package/dist/cli/ctl-command.js +1 -1
- package/dist/cli/daemon-commands.js +39 -24
- package/dist/cli/fs-command.js +19 -10
- package/dist/cli/init.js +77 -8
- package/dist/cli/queue-command.js +42 -0
- package/dist/cli/restart-monitor.js +3 -14
- package/dist/cli/trigger-command.js +7 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +5 -0
- package/dist/config/config-manager.js +21 -1
- package/dist/config/peer-role-resolver.js +6 -2
- package/dist/config/role-migration-startup.js +53 -0
- package/dist/config/role-store.js +1 -1
- package/dist/core/auth/agent-delegation.js +1 -1
- package/dist/core/auth/auth-gateway.js +29 -1
- package/dist/core/auth/authorization-audit.js +15 -0
- package/dist/core/auth/operation-authorizer.js +1 -0
- package/dist/core/auth/operation-catalog.js +9 -0
- package/dist/core/bootstrap-messages.js +9 -2
- package/dist/core/bootstrap-service.js +1 -0
- package/dist/core/command/command-handler.js +198 -51
- package/dist/core/command/menu-catalog.js +4 -0
- package/dist/core/command/menu-handler.js +85 -18
- package/dist/core/command/menu-protocol.js +23 -2
- package/dist/core/command/slash-handler.js +58 -10
- package/dist/core/event-catalog.js +6 -0
- package/dist/core/handoff/runtime.js +12 -1
- package/dist/core/handoff/store.js +5 -1
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +19 -0
- package/dist/core/message/message-bridge.js +63 -11
- package/dist/core/message/message-log.js +2 -0
- package/dist/core/message/message-queue.js +267 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +107 -41
- package/dist/core/message/stream-debouncer.js +10 -2
- package/dist/core/permission/approval-gateway.js +1 -0
- package/dist/core/permission/ec-command-parser.js +93 -14
- package/dist/core/permission/index.js +1 -1
- package/dist/core/permission/mode.js +15 -0
- package/dist/core/permission/readonly-shell-query.js +148 -8
- package/dist/core/permission/sandbox-runtime.js +21 -0
- package/dist/core/permission/tool-policy.js +249 -27
- package/dist/core/protected-paths.js +107 -2
- package/dist/core/session/session-manager.js +21 -2
- package/dist/core/session/session-mapper.js +20 -1
- package/dist/eck/detect.js +18 -1
- package/dist/eck/group-rules-sync.js +15 -44
- package/dist/index.js +161 -86
- package/dist/ipc.js +16 -3
- package/dist/paths.js +3 -0
- package/dist/trigger/feedback.js +64 -11
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +10 -0
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/templates/roles/admin.json +5 -0
- package/kits/templates/roles/member.json +7 -1
- package/kits/templates/roles/visitor.json +2 -0
- package/package.json +1 -1
- package/dist/aun/msg/payload-type.js +0 -27
- package/dist/aun/rpc/caller.js +0 -42
- package/dist/aun/rpc/connection.js +0 -25
- package/dist/aun/storage/manage.js +0 -10
- package/dist/config/access-policy-domain.js +0 -18
- package/dist/config/config-batch-get.js +0 -11
- package/dist/config/owner-policy.js +0 -4
- package/dist/config/role-config-v4-startup.js +0 -32
- package/dist/config/role-config-v5-startup.js +0 -27
- package/dist/core/auth/trigger-authorization.js +0 -15
- package/dist/core/interaction-registration.js +0 -10
- package/dist/core/permission/execution-sandbox.js +0 -16
- package/dist/core/relation/peer-key.js +0 -1
- package/dist/core/session/session-key.js +0 -24
- package/dist/eck/baseagent-caps.js +0 -18
- package/dist/eck/rules-loader.js +0 -28
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
|
|
4
4
|
[`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
|
|
5
5
|
|
|
6
|
+
## 0.0.13 (2026-08-20)
|
|
7
|
+
|
|
8
|
+
### AUN 消息与文件
|
|
9
|
+
|
|
10
|
+
- 显式区分 AUN 私聊与群聊投递路由,强化入站去重、顺序处理、消息队列持久化和失败恢复。
|
|
11
|
+
- 统一 AUN RPC 与存储客户端,扩展群文件写入与下载能力,收敛文件操作和载荷处理边界。
|
|
12
|
+
- 增加群文件下载票据校验和内容哈希验证,补齐短连接 RPC、存储上传及启动期角色配置迁移能力。
|
|
13
|
+
|
|
14
|
+
### 权限与关系
|
|
15
|
+
|
|
16
|
+
- 收敛角色迁移、权限作用域和最小能力模式,强化 Owner、关系范围与工具执行审计边界。
|
|
17
|
+
- 贯通会话投递元数据和跨会话授权校验,避免托管操作越过当前 Agent、会话或关系范围。
|
|
18
|
+
- 强化受管 `$TMPDIR`、群规则读取和群文件上传的路径隔离,拒绝符号链接、越界目录及不符合权限/所有者要求的路径。
|
|
19
|
+
- 完善菜单授权审计的请求与会话关联信息,扩展只读 Shell 查询、配置批量读取和委派有效期校验。
|
|
20
|
+
|
|
21
|
+
### 会话与运行时
|
|
22
|
+
|
|
23
|
+
- 完善消息撤回隔离、错误回传、菜单交互、重启命令和 Trigger 反馈的上下文校验。
|
|
24
|
+
- 简化 Agent 运行器与启动流程,修正系统重启路径并清理托管会话凭据。
|
|
25
|
+
- 为 Codex app-server 请求增加 request ID 与载荷指纹重放防护,使重复请求复用结果、冲突请求拒绝;会话重试按至多执行一次处理。
|
|
26
|
+
- 统一 EC Web 守护进程的发现、接管、健康检查和退出清理,修正 Linux 进程启动时间计算,并改进 Codex 初始化/升级探测与 Web 消息路由展示。
|
|
27
|
+
|
|
6
28
|
## 0.0.12 (2026-08-19)
|
|
7
29
|
|
|
8
30
|
### AUN 通信
|
package/bin/ec-safe-output.js
CHANGED
|
@@ -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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
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,
|
|
101
|
-
|
|
102
|
-
|
|
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 {
|
|
@@ -20,10 +20,9 @@ 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';
|
|
23
|
+
import { normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
|
|
25
24
|
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';
|
|
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';
|
|
28
27
|
import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
|
|
29
28
|
export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
|
|
@@ -1759,6 +1758,30 @@ export class AgentRunner {
|
|
|
1759
1758
|
else {
|
|
1760
1759
|
logger.error(`[AgentRunner] Subprocess stream failed (session=${sessionId}) with no captured stderr.`);
|
|
1761
1760
|
}
|
|
1761
|
+
if (isSandboxInitializationFailure(err, buf ?? [])) {
|
|
1762
|
+
const detail = createSandboxInitializationError(err, buf ?? []);
|
|
1763
|
+
await this.permissionContexts.get(sessionId)?.recordExecutionAnomaly?.({
|
|
1764
|
+
code: 'capability_unavailable',
|
|
1765
|
+
severity: 'warning',
|
|
1766
|
+
phase: 'execution',
|
|
1767
|
+
occurredAt: Date.now(),
|
|
1768
|
+
toolName: 'Bash',
|
|
1769
|
+
policyCode: SANDBOX_INITIALIZATION_FAILED,
|
|
1770
|
+
decisionSource: 'infrastructure',
|
|
1771
|
+
agentAid: this.permissionContexts.get(sessionId)?.selfAid,
|
|
1772
|
+
summary: detail.message.slice(0, 512),
|
|
1773
|
+
effect: 'operation_skipped',
|
|
1774
|
+
});
|
|
1775
|
+
yield {
|
|
1776
|
+
type: 'complete',
|
|
1777
|
+
isError: true,
|
|
1778
|
+
subtype: 'infrastructure',
|
|
1779
|
+
terminalReason: SANDBOX_INITIALIZATION_FAILED,
|
|
1780
|
+
errors: [detail.message],
|
|
1781
|
+
queryFinal: true,
|
|
1782
|
+
};
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1762
1785
|
throw err;
|
|
1763
1786
|
}
|
|
1764
1787
|
finally {
|
|
@@ -1865,6 +1888,9 @@ export class AgentRunner {
|
|
|
1865
1888
|
toolName,
|
|
1866
1889
|
...(typeof requestId === 'string' && requestId ? { requestId } : {}),
|
|
1867
1890
|
policyCode: code,
|
|
1891
|
+
decisionSource: 'policy',
|
|
1892
|
+
agentAid: this.permissionContexts.get(sessionId)?.selfAid,
|
|
1893
|
+
permissionMode: callPermissionMode,
|
|
1868
1894
|
summary: summarizeToolInputForAudit(toolName, toolInput).slice(0, 512),
|
|
1869
1895
|
effect: 'operation_skipped',
|
|
1870
1896
|
});
|
|
@@ -1932,6 +1958,7 @@ export class AgentRunner {
|
|
|
1932
1958
|
role: permCtx?.role,
|
|
1933
1959
|
permissionMode: callPermissionMode,
|
|
1934
1960
|
allowHostProcessCommands: true,
|
|
1961
|
+
allowProtectedMetadata: false,
|
|
1935
1962
|
safeOutputHelperPath: path.join(getPackageRoot(), 'bin', 'ec-safe-output.js'),
|
|
1936
1963
|
projectPath,
|
|
1937
1964
|
});
|
|
@@ -1963,10 +1990,11 @@ export class AgentRunner {
|
|
|
1963
1990
|
channel: permCtx?.channel,
|
|
1964
1991
|
peerId: permCtx?.userId,
|
|
1965
1992
|
role: permCtx?.role,
|
|
1993
|
+
allowProtectedMetadata: false,
|
|
1966
1994
|
};
|
|
1967
1995
|
const roResult = checkReadonly(toolName, toolInput, projectPath, readonlyContext);
|
|
1968
1996
|
if (roResult.behavior === 'deny') {
|
|
1969
|
-
await recordPreflightDenial(toolName, toolInput, undefined, input.tool_use_id ?? input.toolUseID, 'readonly_mode');
|
|
1997
|
+
await recordPreflightDenial(toolName, toolInput, undefined, input.tool_use_id ?? input.toolUseID, roResult.policyCode ?? 'readonly_mode');
|
|
1970
1998
|
return hookDecision('deny', roResult.message, updatedInput);
|
|
1971
1999
|
}
|
|
1972
2000
|
return hookDecision('allow', undefined, updatedInput);
|
|
@@ -2052,6 +2080,7 @@ export class AgentRunner {
|
|
|
2052
2080
|
role: permCtx?.role,
|
|
2053
2081
|
permissionMode: callPermissionMode,
|
|
2054
2082
|
allowHostProcessCommands: true,
|
|
2083
|
+
allowProtectedMetadata: false,
|
|
2055
2084
|
safeOutputHelperPath: path.join(getPackageRoot(), 'bin', 'ec-safe-output.js'),
|
|
2056
2085
|
projectPath,
|
|
2057
2086
|
});
|
|
@@ -2119,10 +2148,11 @@ export class AgentRunner {
|
|
|
2119
2148
|
channel: permCtx?.channel,
|
|
2120
2149
|
peerId: permCtx?.userId,
|
|
2121
2150
|
role: permCtx?.role,
|
|
2151
|
+
allowProtectedMetadata: false,
|
|
2122
2152
|
};
|
|
2123
2153
|
const roResult = checkReadonly(toolName, input, projectPath, readonlyContext);
|
|
2124
2154
|
if (roResult.behavior === 'deny') {
|
|
2125
|
-
await recordPreflightDenial(toolName, input, undefined, options.toolUseID, 'readonly_mode');
|
|
2155
|
+
await recordPreflightDenial(toolName, input, undefined, options.toolUseID, roResult.policyCode ?? 'readonly_mode');
|
|
2126
2156
|
return { behavior: 'deny', message: roResult.message, decisionClassification: 'user_reject' };
|
|
2127
2157
|
}
|
|
2128
2158
|
return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
|
|
@@ -2373,12 +2403,28 @@ export class AgentRunner {
|
|
|
2373
2403
|
logger.info('[AgentRunner] Creating query with images:', images.length, 'first image size:', images[0]?.data?.length ?? 0);
|
|
2374
2404
|
logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
|
|
2375
2405
|
msgStream.push(prompt, images, inputId);
|
|
2376
|
-
|
|
2406
|
+
try {
|
|
2407
|
+
sdkStream = createQuery(msgStream);
|
|
2408
|
+
}
|
|
2409
|
+
catch (error) {
|
|
2410
|
+
const stderr = this.recentStderr.get(sessionId) ?? [];
|
|
2411
|
+
if (isSandboxInitializationFailure(error, stderr))
|
|
2412
|
+
throw createSandboxInitializationError(error, stderr);
|
|
2413
|
+
throw error;
|
|
2414
|
+
}
|
|
2377
2415
|
}
|
|
2378
2416
|
else {
|
|
2379
2417
|
logger.debug('[AgentRunner] Creating query with text only, agentSessionId:', initialClaudeSessionId);
|
|
2380
2418
|
msgStream.push(prompt, undefined, inputId);
|
|
2381
|
-
|
|
2419
|
+
try {
|
|
2420
|
+
sdkStream = createQuery(msgStream, agentSessionId, resumeAt);
|
|
2421
|
+
}
|
|
2422
|
+
catch (error) {
|
|
2423
|
+
const stderr = this.recentStderr.get(sessionId) ?? [];
|
|
2424
|
+
if (isSandboxInitializationFailure(error, stderr))
|
|
2425
|
+
throw createSandboxInitializationError(error, stderr);
|
|
2426
|
+
throw error;
|
|
2427
|
+
}
|
|
2382
2428
|
}
|
|
2383
2429
|
this.activeMessageStreams.set(sessionId, msgStream);
|
|
2384
2430
|
this.activeQueries.set(sessionId, sdkStream);
|
|
@@ -30,6 +30,22 @@ function runtimeProviderId(baseUrl) {
|
|
|
30
30
|
const digest = createHash('sha256').update(baseUrl || 'default').digest('hex').slice(0, 12);
|
|
31
31
|
return `evolcore_runtime_${digest}`;
|
|
32
32
|
}
|
|
33
|
+
function stableJson(value) {
|
|
34
|
+
if (Array.isArray(value))
|
|
35
|
+
return `[${value.map(stableJson).join(',')}]`;
|
|
36
|
+
if (value && typeof value === 'object') {
|
|
37
|
+
return `{${Object.entries(value)
|
|
38
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
39
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
|
|
40
|
+
.join(',')}}`;
|
|
41
|
+
}
|
|
42
|
+
return JSON.stringify(value) ?? 'undefined';
|
|
43
|
+
}
|
|
44
|
+
function serverRequestFingerprint(request) {
|
|
45
|
+
return createHash('sha256')
|
|
46
|
+
.update(`${request.method}\n${stableJson(request.params)}`)
|
|
47
|
+
.digest('hex');
|
|
48
|
+
}
|
|
33
49
|
function hasPermissionProfile(config) {
|
|
34
50
|
if (!config || typeof config.default_permissions !== 'string' || !config.default_permissions)
|
|
35
51
|
return false;
|
|
@@ -41,6 +57,7 @@ export class CodexAppServerClient {
|
|
|
41
57
|
options;
|
|
42
58
|
proc = null;
|
|
43
59
|
pending = new Map();
|
|
60
|
+
serverRequestReplays = new Map();
|
|
44
61
|
notificationHandlers = new Set();
|
|
45
62
|
nextId = 1;
|
|
46
63
|
initialized = false;
|
|
@@ -305,6 +322,7 @@ export class CodexAppServerClient {
|
|
|
305
322
|
pending.reject(new Error('Codex app-server closed'));
|
|
306
323
|
}
|
|
307
324
|
this.pending.clear();
|
|
325
|
+
this.serverRequestReplays.clear();
|
|
308
326
|
if (!proc) {
|
|
309
327
|
this.cleanupManagedHookRuntime();
|
|
310
328
|
return;
|
|
@@ -627,15 +645,44 @@ export class CodexAppServerClient {
|
|
|
627
645
|
this.respondError(request.id, new Error('Unsupported Codex app-server request: ' + request.method));
|
|
628
646
|
return;
|
|
629
647
|
}
|
|
630
|
-
|
|
648
|
+
const replayKey = String(request.id);
|
|
649
|
+
const fingerprint = serverRequestFingerprint(request);
|
|
650
|
+
const previous = this.serverRequestReplays.get(replayKey);
|
|
651
|
+
if (previous) {
|
|
652
|
+
if (previous.fingerprint !== fingerprint) {
|
|
653
|
+
const conflict = new Error(`Conflicting Codex app-server request payload for id=${request.id}`);
|
|
654
|
+
logger.warn(`[CodexAppServer] server request ID conflict id=${request.id} method=${request.method}`);
|
|
655
|
+
this.respondError(request.id, conflict);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
// A retry must receive a response too, but must never invoke the
|
|
659
|
+
// approval handler a second time.
|
|
660
|
+
this.respondFromPromise(request.id, request.method, previous.response);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
let response;
|
|
664
|
+
try {
|
|
665
|
+
// Invoke synchronously so existing app-server latency stays unchanged,
|
|
666
|
+
// while the explicit catch still turns a synchronous handler throw into
|
|
667
|
+
// the same replayable rejected response as an async failure.
|
|
668
|
+
response = Promise.resolve(handler(request));
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
response = Promise.reject(error);
|
|
672
|
+
}
|
|
673
|
+
this.serverRequestReplays.set(replayKey, { fingerprint, response });
|
|
674
|
+
this.respondFromPromise(request.id, request.method, response);
|
|
675
|
+
}
|
|
676
|
+
respondFromPromise(id, method, response) {
|
|
677
|
+
response
|
|
631
678
|
.then(result => {
|
|
632
|
-
logger.info(`[CodexAppServer] server response id=${
|
|
633
|
-
this.respond(
|
|
679
|
+
logger.info(`[CodexAppServer] server response id=${id} method=${method} result=${JSON.stringify(result ?? null)}`);
|
|
680
|
+
this.respond(id, result ?? null);
|
|
634
681
|
})
|
|
635
682
|
.catch(error => {
|
|
636
683
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
637
|
-
logger.warn(`[CodexAppServer] server request failed id=${
|
|
638
|
-
this.respondError(
|
|
684
|
+
logger.warn(`[CodexAppServer] server request failed id=${id} method=${method}: ${err.message}`);
|
|
685
|
+
this.respondError(id, err);
|
|
639
686
|
});
|
|
640
687
|
}
|
|
641
688
|
emitNotification(notification) {
|
|
@@ -676,6 +723,7 @@ export class CodexAppServerClient {
|
|
|
676
723
|
pending.reject(error);
|
|
677
724
|
}
|
|
678
725
|
this.pending.clear();
|
|
726
|
+
this.serverRequestReplays.clear();
|
|
679
727
|
this.cleanupManagedHookRuntime();
|
|
680
728
|
}
|
|
681
729
|
}
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
|
|
9
9
|
import { checkReadonly, checkDangerousCommand, evaluateToolPreflight } from '../core/permission/tool-policy.js';
|
|
10
|
-
import { normalizePermissionMode } from '../core/permission/mode.js';
|
|
11
|
-
import { resolvePhaseOneExecutionSandbox } from '../core/permission/execution-sandbox.js';
|
|
10
|
+
import { normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
|
|
12
11
|
import { buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtectedCandidate } from '../core/protected-paths.js';
|
|
13
12
|
import { CodexAppServerClient } from './codex-app-server-client.js';
|
|
14
13
|
import { resolveOpenaiConfig } from './baseagent.js';
|
|
@@ -76,6 +75,39 @@ class AsyncEventQueue {
|
|
|
76
75
|
}
|
|
77
76
|
const managedTempDirsForExitCleanup = new Map();
|
|
78
77
|
let managedTempExitCleanupRegistered = false;
|
|
78
|
+
function isTrustedManagedTempDirectory(directory, parent) {
|
|
79
|
+
try {
|
|
80
|
+
const parentPath = path.resolve(parent);
|
|
81
|
+
const directoryPath = path.resolve(directory);
|
|
82
|
+
// The session directory must be a direct child of the private parent.
|
|
83
|
+
// This lexical check is required before canonicalization so a replaced
|
|
84
|
+
// parent symlink cannot turn an old session path into another tree.
|
|
85
|
+
if (path.dirname(directoryPath) !== parentPath)
|
|
86
|
+
return false;
|
|
87
|
+
const trustedDirectory = (candidate) => {
|
|
88
|
+
const stat = fs.lstatSync(candidate);
|
|
89
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
90
|
+
return false;
|
|
91
|
+
if (typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
92
|
+
return false;
|
|
93
|
+
return (stat.mode & 0o077) === 0;
|
|
94
|
+
};
|
|
95
|
+
if (!trustedDirectory(parentPath) || !trustedDirectory(directoryPath))
|
|
96
|
+
return false;
|
|
97
|
+
const canonicalParent = resolveProtectedCandidate(parentPath);
|
|
98
|
+
const canonicalDirectory = resolveProtectedCandidate(directoryPath);
|
|
99
|
+
// Keep the canonical and lexical paths identical after the lstat checks;
|
|
100
|
+
// this rejects symlink replacement in either node or an existing ancestor.
|
|
101
|
+
return canonicalParent === parentPath
|
|
102
|
+
&& canonicalDirectory === directoryPath
|
|
103
|
+
&& canonicalDirectory !== canonicalParent
|
|
104
|
+
&& path.dirname(canonicalDirectory) === canonicalParent
|
|
105
|
+
&& isSameOrDescendant(canonicalDirectory, canonicalParent);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
79
111
|
function registerManagedTempDirForExitCleanup(directory, parent) {
|
|
80
112
|
managedTempDirsForExitCleanup.set(directory, parent);
|
|
81
113
|
if (managedTempExitCleanupRegistered)
|
|
@@ -84,10 +116,8 @@ function registerManagedTempDirForExitCleanup(directory, parent) {
|
|
|
84
116
|
process.once('exit', () => {
|
|
85
117
|
for (const [candidatePath, parentPath] of managedTempDirsForExitCleanup) {
|
|
86
118
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (candidate !== parentCandidate && isSameOrDescendant(candidate, parentCandidate)) {
|
|
90
|
-
fs.rmSync(candidate, { recursive: true, force: true });
|
|
119
|
+
if (isTrustedManagedTempDirectory(candidatePath, parentPath)) {
|
|
120
|
+
fs.rmSync(path.resolve(candidatePath), { recursive: true, force: true });
|
|
91
121
|
}
|
|
92
122
|
}
|
|
93
123
|
catch {
|
|
@@ -121,7 +151,7 @@ const CODEX_DELEGATION_CARRIER_METADATA_KEY = '__codexDelegationCarrierV1';
|
|
|
121
151
|
let codexCatalogCache = null;
|
|
122
152
|
// The permission bridge is version-locked to the audited approval schema and
|
|
123
153
|
// managed PreToolUse hook behavior used by proactive first-tool enforcement.
|
|
124
|
-
export const MIN_CODEX_CLI_VERSION = '0.
|
|
154
|
+
export const MIN_CODEX_CLI_VERSION = '0.145.0';
|
|
125
155
|
export function parseCodexCliVersion(output) {
|
|
126
156
|
const match = output.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/);
|
|
127
157
|
return match?.[1] ?? null;
|
|
@@ -144,19 +174,18 @@ export function getCodexCliVersion() {
|
|
|
144
174
|
}
|
|
145
175
|
export function getCodexAppServerAvailability() {
|
|
146
176
|
if (!resolveCodexCliPath()) {
|
|
147
|
-
|
|
148
|
-
return { available: false, reason: `未检测到可用 Codex CLI。${upgradeHint}` };
|
|
177
|
+
return { available: false, reason: '未检测到 Codex CLI。' };
|
|
149
178
|
}
|
|
150
179
|
const version = getCodexCliVersion();
|
|
151
|
-
const upgradeHint = '请升级 Codex CLI:npm install -g @openai/codex@latest';
|
|
152
180
|
if (!version) {
|
|
153
|
-
return { available: false, reason:
|
|
181
|
+
return { available: false, reason: '无法读取 Codex CLI 版本。' };
|
|
154
182
|
}
|
|
155
183
|
if (!isCodexCliVersionSupported(version)) {
|
|
156
184
|
return {
|
|
157
185
|
available: false,
|
|
158
186
|
version,
|
|
159
|
-
reason: `Codex CLI ${version} 低于最低要求 ${MIN_CODEX_CLI_VERSION}
|
|
187
|
+
reason: `Codex CLI ${version} 低于最低要求 ${MIN_CODEX_CLI_VERSION}。请升级 Codex CLI:npm install -g @openai/codex@latest`,
|
|
188
|
+
requiresUpgrade: true,
|
|
160
189
|
};
|
|
161
190
|
}
|
|
162
191
|
try {
|
|
@@ -168,7 +197,7 @@ export function getCodexAppServerAvailability() {
|
|
|
168
197
|
return { available: true, version };
|
|
169
198
|
}
|
|
170
199
|
catch {
|
|
171
|
-
return { available: false, version, reason: `Codex CLI ${version} 不支持 app-server
|
|
200
|
+
return { available: false, version, reason: `Codex CLI ${version} 不支持 app-server。` };
|
|
172
201
|
}
|
|
173
202
|
}
|
|
174
203
|
export function isCodexAppServerAvailable() {
|
|
@@ -208,6 +237,9 @@ export function getCodexEfforts(model) {
|
|
|
208
237
|
// ── Codex Runner ──
|
|
209
238
|
export class CodexRunner {
|
|
210
239
|
name = 'codex';
|
|
240
|
+
// turn/start persists a new user turn before runQuery resolves. App-server
|
|
241
|
+
// has no retry-in-place API for a completed or failed turn.
|
|
242
|
+
retryInputSemantics = 'at_most_once';
|
|
211
243
|
capabilities;
|
|
212
244
|
model;
|
|
213
245
|
effort;
|
|
@@ -282,16 +314,23 @@ export class CodexRunner {
|
|
|
282
314
|
createDelegationCarrier() {
|
|
283
315
|
return randomBytes(32).toString('base64url');
|
|
284
316
|
}
|
|
285
|
-
approvedLiteralEvolcoreCommandArgv(command) {
|
|
317
|
+
approvedLiteralEvolcoreCommandArgv(command, managedTempDir) {
|
|
286
318
|
const classification = classifyEvolcoreShellCommand(command);
|
|
287
319
|
if (classification.kind === 'bounded-output')
|
|
288
320
|
return classification.command.argv;
|
|
289
321
|
if (classification.kind !== 'literal')
|
|
290
322
|
return undefined;
|
|
291
|
-
|
|
323
|
+
// Keep the approval/delegation parser aligned with the preflight parser:
|
|
324
|
+
// the session-managed `$TMPDIR` token is a permitted EC path reference.
|
|
325
|
+
const argv = parseLiteralShellArgv(command, {
|
|
326
|
+
allowManagedTmpDir: true,
|
|
327
|
+
...(managedTempDir ? { managedTempDir } : {}),
|
|
328
|
+
});
|
|
329
|
+
if (argv?.some(value => value.includes('$TMPDIR')))
|
|
330
|
+
return undefined;
|
|
292
331
|
return argv?.[0] === 'ec' ? argv : undefined;
|
|
293
332
|
}
|
|
294
|
-
approvedEvolcoreCommandArgv(toolInput) {
|
|
333
|
+
approvedEvolcoreCommandArgv(toolInput, managedTempDir) {
|
|
295
334
|
const explicitArgv = Array.isArray(toolInput.commandArgv)
|
|
296
335
|
&& toolInput.commandArgv.every(value => typeof value === 'string')
|
|
297
336
|
? toolInput.commandArgv
|
|
@@ -302,24 +341,27 @@ export class CodexRunner {
|
|
|
302
341
|
? unwrapCodexShellCommandArgv(explicitArgv)
|
|
303
342
|
: undefined;
|
|
304
343
|
if (explicitWrappedCommand !== undefined) {
|
|
305
|
-
return this.approvedLiteralEvolcoreCommandArgv(explicitWrappedCommand);
|
|
344
|
+
return this.approvedLiteralEvolcoreCommandArgv(explicitWrappedCommand, managedTempDir);
|
|
306
345
|
}
|
|
307
346
|
const command = typeof toolInput.command === 'string' ? toolInput.command : '';
|
|
308
|
-
const directArgv = this.approvedLiteralEvolcoreCommandArgv(command);
|
|
347
|
+
const directArgv = this.approvedLiteralEvolcoreCommandArgv(command, managedTempDir);
|
|
309
348
|
if (directArgv)
|
|
310
349
|
return directArgv;
|
|
311
|
-
const wrappedArgv = parseLiteralShellArgv(command
|
|
350
|
+
const wrappedArgv = parseLiteralShellArgv(command, {
|
|
351
|
+
allowManagedTmpDir: true,
|
|
352
|
+
...(managedTempDir ? { managedTempDir } : {}),
|
|
353
|
+
});
|
|
312
354
|
const wrappedCommand = wrappedArgv
|
|
313
355
|
? unwrapCodexShellCommandArgv(wrappedArgv)
|
|
314
356
|
: undefined;
|
|
315
357
|
return wrappedCommand === undefined
|
|
316
358
|
? undefined
|
|
317
|
-
: this.approvedLiteralEvolcoreCommandArgv(wrappedCommand);
|
|
359
|
+
: this.approvedLiteralEvolcoreCommandArgv(wrappedCommand, managedTempDir);
|
|
318
360
|
}
|
|
319
361
|
armApprovedDelegationCommand(sessionId, toolInput) {
|
|
320
362
|
const threadId = this.activeSessions.get(sessionId);
|
|
321
363
|
const carrierToken = threadId ? this.threadDelegationCarriers.get(threadId) : undefined;
|
|
322
|
-
const argv = this.approvedEvolcoreCommandArgv(toolInput);
|
|
364
|
+
const argv = this.approvedEvolcoreCommandArgv(toolInput, this.getManagedTempDir(sessionId));
|
|
323
365
|
if (!carrierToken || !argv)
|
|
324
366
|
return false;
|
|
325
367
|
const commandHash = hashDelegatedCommandArgv(argv);
|
|
@@ -1630,7 +1672,7 @@ export class CodexRunner {
|
|
|
1630
1672
|
const decision = await this.resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath, operationCwd, request.id);
|
|
1631
1673
|
if (decision !== 'deny'
|
|
1632
1674
|
&& (request.method === 'item/commandExecution/requestApproval' || request.method === 'execCommandApproval')
|
|
1633
|
-
&& this.approvedEvolcoreCommandArgv(toolInput)) {
|
|
1675
|
+
&& this.approvedEvolcoreCommandArgv(toolInput, this.getManagedTempDir(sessionKey))) {
|
|
1634
1676
|
if (!this.armApprovedDelegationCommand(sessionKey, toolInput)) {
|
|
1635
1677
|
// Approval policy and delegation authority are separate boundaries.
|
|
1636
1678
|
// If no ticket can be armed, the CLI still fails closed at IPC token
|
|
@@ -2058,10 +2100,15 @@ export class CodexRunner {
|
|
|
2058
2100
|
channel: permCtx?.channel,
|
|
2059
2101
|
peerId: permCtx?.userId,
|
|
2060
2102
|
role: permCtx?.role,
|
|
2103
|
+
allowProtectedMetadata: false,
|
|
2104
|
+
// Revalidate ownership, mode, and parent containment at the final
|
|
2105
|
+
// readonly decision. The map entry alone is stale if the directory was
|
|
2106
|
+
// replaced between preflight and the approval callback.
|
|
2107
|
+
managedTempDir: this.getManagedTempDir(sessionKey),
|
|
2061
2108
|
} : undefined;
|
|
2062
2109
|
if (toolName === 'Bash')
|
|
2063
2110
|
return checkReadonly(toolName, input, projectPath, readonlyContext);
|
|
2064
|
-
return { behavior: 'deny', message: '🔒 只读模式:权限升级和文件写入请求已拒绝' };
|
|
2111
|
+
return { behavior: 'deny', message: '🔒 只读模式:权限升级和文件写入请求已拒绝', policyCode: 'readonly_permission_or_write' };
|
|
2065
2112
|
}
|
|
2066
2113
|
permissionProfiles(input) {
|
|
2067
2114
|
return [input.permissions, input.additionalPermissions]
|
|
@@ -2183,6 +2230,9 @@ export class CodexRunner {
|
|
|
2183
2230
|
toolName,
|
|
2184
2231
|
...(typeof requestId === 'string' || typeof requestId === 'number' ? { requestId: String(requestId) } : {}),
|
|
2185
2232
|
policyCode,
|
|
2233
|
+
decisionSource: 'policy',
|
|
2234
|
+
agentAid: permissionContext?.selfAid,
|
|
2235
|
+
permissionMode: mode,
|
|
2186
2236
|
summary: summarizeToolInputForAudit(toolName, operationInput).slice(0, 512),
|
|
2187
2237
|
effect: 'operation_skipped',
|
|
2188
2238
|
});
|
|
@@ -2193,6 +2243,11 @@ export class CodexRunner {
|
|
|
2193
2243
|
};
|
|
2194
2244
|
const preflight = evaluateToolPreflight(toolName, toolInput, {
|
|
2195
2245
|
sessionId: sessionKey,
|
|
2246
|
+
// An unavailable registered root must fail closed; the policy helper
|
|
2247
|
+
// distinguishes this explicit empty value from runners without a
|
|
2248
|
+
// session-temp capability.
|
|
2249
|
+
managedTempDir: this.getManagedTempDir(sessionKey) ?? '',
|
|
2250
|
+
allowProtectedMetadata: false,
|
|
2196
2251
|
selfAid: permissionContext?.selfAid,
|
|
2197
2252
|
channel: permissionContext?.channel,
|
|
2198
2253
|
userId: permissionContext?.userId,
|
|
@@ -2202,6 +2257,8 @@ export class CodexRunner {
|
|
|
2202
2257
|
workspacePath,
|
|
2203
2258
|
});
|
|
2204
2259
|
if (preflight.behavior === 'deny') {
|
|
2260
|
+
logger.warn(`[CodexRunner] tool preflight denied: session=${sessionKey} tool=${toolName} ` +
|
|
2261
|
+
`policy=${preflight.policyCode ?? 'unknown'} reason=${preflight.message ?? 'policy denied'}`);
|
|
2205
2262
|
if (preflight.policyCode) {
|
|
2206
2263
|
await recordBlockedOperation(preflight.policyCode);
|
|
2207
2264
|
}
|
|
@@ -2231,7 +2288,7 @@ export class CodexRunner {
|
|
|
2231
2288
|
if (mode === 'readonly') {
|
|
2232
2289
|
const readonly = this.checkCodexReadonly(toolName, checkedInput, operationCwd, sessionKey);
|
|
2233
2290
|
if (readonly.behavior === 'deny') {
|
|
2234
|
-
await recordBlockedOperation('readonly_mode', checkedInput);
|
|
2291
|
+
await recordBlockedOperation(readonly.policyCode ?? 'readonly_mode', checkedInput);
|
|
2235
2292
|
return 'deny';
|
|
2236
2293
|
}
|
|
2237
2294
|
return 'allow';
|
|
@@ -2636,19 +2693,18 @@ export class CodexRunner {
|
|
|
2636
2693
|
*reconcileOpenAppServerToolCalls(state, turnId) {
|
|
2637
2694
|
if (state.openToolCalls.size === 0)
|
|
2638
2695
|
return;
|
|
2639
|
-
const
|
|
2696
|
+
const openCalls = [...state.openToolCalls.entries()];
|
|
2697
|
+
const callIds = openCalls.map(([callId]) => callId);
|
|
2640
2698
|
logger.warn(`[CodexRunner] Turn completed with ${callIds.length} open tool call(s): ` +
|
|
2641
2699
|
`thread=${state.threadId} turn=${turnId || state.turnId || 'unknown'} calls=${callIds.join(',')}`);
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
};
|
|
2651
|
-
}
|
|
2700
|
+
yield {
|
|
2701
|
+
type: 'notice',
|
|
2702
|
+
severity: 'warn',
|
|
2703
|
+
subtype: 'missing-item-completed',
|
|
2704
|
+
message: 'Turn completed before tool completion was received',
|
|
2705
|
+
closedToolCallIds: callIds,
|
|
2706
|
+
missingItems: openCalls.map(([itemId, toolName]) => ({ itemId, toolName })),
|
|
2707
|
+
};
|
|
2652
2708
|
state.openToolCalls.clear();
|
|
2653
2709
|
}
|
|
2654
2710
|
*mapAppServerItemStarted(item, state) {
|
|
@@ -3019,39 +3075,28 @@ export class CodexRunner {
|
|
|
3019
3075
|
}
|
|
3020
3076
|
}
|
|
3021
3077
|
isTrustedSessionTempDir(directory) {
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
const stat = fs.lstatSync(candidate);
|
|
3030
|
-
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
3031
|
-
return false;
|
|
3032
|
-
if (typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
3033
|
-
return false;
|
|
3034
|
-
if ((stat.mode & 0o077) !== 0)
|
|
3035
|
-
return false;
|
|
3036
|
-
}
|
|
3037
|
-
return canonical !== parent && isSameOrDescendant(canonical, parent);
|
|
3038
|
-
}
|
|
3039
|
-
catch {
|
|
3040
|
-
return false;
|
|
3041
|
-
}
|
|
3078
|
+
return isTrustedManagedTempDirectory(directory, this.managedTempParent());
|
|
3079
|
+
}
|
|
3080
|
+
getManagedTempDir(sessionId) {
|
|
3081
|
+
const directory = this.sessionTempDirs.get(sessionId);
|
|
3082
|
+
if (!directory || !this.isTrustedSessionTempDir(directory))
|
|
3083
|
+
return undefined;
|
|
3084
|
+
return resolveProtectedCandidate(directory);
|
|
3042
3085
|
}
|
|
3043
3086
|
cleanupSessionTempDir(sessionId) {
|
|
3044
3087
|
const directory = this.sessionTempDirs.get(sessionId);
|
|
3045
3088
|
this.sessionTempDirs.delete(sessionId);
|
|
3046
3089
|
if (!directory)
|
|
3047
3090
|
return;
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3091
|
+
// Reuse the same ownership, mode, lexical-parent, and canonicalization
|
|
3092
|
+
// checks used at execution time. If the path changed, leave it in place
|
|
3093
|
+
// rather than recursively removing an attacker-selected target.
|
|
3094
|
+
if (!this.isTrustedSessionTempDir(directory)) {
|
|
3051
3095
|
managedTempDirsForExitCleanup.delete(directory);
|
|
3052
3096
|
logger.warn(`[CodexRunner] refused to clean unmanaged TMPDIR: session=${sessionId} path=${directory}`);
|
|
3053
3097
|
return;
|
|
3054
3098
|
}
|
|
3099
|
+
const candidate = path.resolve(directory);
|
|
3055
3100
|
try {
|
|
3056
3101
|
fs.rmSync(candidate, { recursive: true, force: true });
|
|
3057
3102
|
managedTempDirsForExitCleanup.delete(directory);
|
|
@@ -3063,7 +3108,7 @@ export class CodexRunner {
|
|
|
3063
3108
|
managedTempInstruction(sessionId) {
|
|
3064
3109
|
if (!this.ensureSessionTempDir(sessionId))
|
|
3065
3110
|
return undefined;
|
|
3066
|
-
return '临时文件必须使用环境变量 `$TMPDIR`。它指向当前 EvolCore 会话的受管临时工作区;不要直接使用 `/tmp
|
|
3111
|
+
return '临时文件必须使用环境变量 `$TMPDIR`。它指向当前 EvolCore 会话的受管临时工作区;不要直接使用 `/tmp`,也不要使用其他会话的临时目录。当前是可持久化 thread 的恢复轮次,早先上下文中的 `/tmp/evolcore-codex-*` 绝对路径可能已经失效;不要重用旧路径。若文件不存在,请先在当前 `$TMPDIR` 重新创建,再继续执行后续 `ec` 命令。';
|
|
3067
3112
|
}
|
|
3068
3113
|
buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrier) {
|
|
3069
3114
|
const managedTempDir = this.ensureSessionTempDir(sessionId);
|
|
@@ -967,6 +967,9 @@ export class EcagentRunner {
|
|
|
967
967
|
occurredAt: Date.now(),
|
|
968
968
|
toolName,
|
|
969
969
|
policyCode,
|
|
970
|
+
decisionSource: 'policy',
|
|
971
|
+
agentAid: permissionContext?.selfAid,
|
|
972
|
+
permissionMode: mode,
|
|
970
973
|
summary: summarizeToolInputForAudit(toolName, operationInput).slice(0, 512),
|
|
971
974
|
effect: 'operation_skipped',
|
|
972
975
|
});
|
|
@@ -1003,7 +1006,7 @@ export class EcagentRunner {
|
|
|
1003
1006
|
sessionId, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
|
|
1004
1007
|
});
|
|
1005
1008
|
if (decision.behavior === 'deny') {
|
|
1006
|
-
await recordBlockedOperation('readonly_mode', checkedInput);
|
|
1009
|
+
await recordBlockedOperation(decision.policyCode ?? 'readonly_mode', checkedInput);
|
|
1007
1010
|
return { block: true, reason: decision.message };
|
|
1008
1011
|
}
|
|
1009
1012
|
return {};
|