evolcore 0.0.11 → 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 +44 -0
- package/bin/codex-managed-hook.mjs +80 -0
- package/bin/ec-safe-output.js +26 -7
- package/dist/agents/claude-runner.js +86 -7
- package/dist/agents/codex-app-server-client.js +174 -20
- package/dist/agents/codex-runner.js +375 -92
- package/dist/agents/ecagent-runner.js +39 -4
- package/dist/agents/gemini-runner.js +1 -0
- package/dist/aun/aid/client.js +11 -21
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/managed-operation.js +107 -0
- 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 +91 -44
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +865 -0
- package/dist/aun/msg/mention-schema.js +20 -0
- package/dist/aun/msg/p2p.js +7 -0
- 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 +655 -240
- package/dist/channels/daemon.js +13 -0
- package/dist/cli/agent-command.js +56 -8
- package/dist/cli/agent.js +108 -31
- package/dist/cli/aun-commands.js +270 -69
- package/dist/cli/bench.js +12 -3
- 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 +70 -55
- package/dist/cli/fs-command.js +79 -10
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +30 -2
- package/dist/cli/init.js +77 -8
- package/dist/cli/managed-command-guard.js +41 -0
- package/dist/cli/model.js +11 -4
- package/dist/cli/queue-command.js +53 -1
- package/dist/cli/response.js +71 -0
- package/dist/cli/restart-monitor.js +7 -32
- package/dist/cli/trigger-command.js +11 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +9 -0
- package/dist/config/config-manager.js +21 -1
- package/dist/config/contact-book-store.js +11 -3
- package/dist/config/contact-request-service.js +161 -15
- package/dist/config/mention-mode.js +8 -24
- 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 -2
- 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 +31 -7
- package/dist/core/auth/operation-catalog.js +196 -25
- package/dist/core/bootstrap-messages.js +25 -0
- package/dist/core/bootstrap-service.js +64 -4
- package/dist/core/capability/providers/claude-capability-provider.js +9 -31
- package/dist/core/capability/providers/codex-capability-provider.js +4 -15
- package/dist/core/capability/skill-discovery.js +55 -0
- package/dist/core/channel-loader.js +1 -1
- package/dist/core/command/cli-intent-parser.js +9 -0
- package/dist/core/command/command-handler.js +650 -42
- package/dist/core/command/connect-menu.js +15 -0
- package/dist/core/command/group-menu.js +4 -4
- package/dist/core/command/menu-catalog.js +370 -0
- package/dist/core/command/menu-handler.js +230 -51
- package/dist/core/command/menu-protocol.js +94 -2
- package/dist/core/command/slash-gate.js +5 -2
- package/dist/core/command/slash-handler.js +99 -31
- package/dist/core/daemon-file-cache.js +2 -1
- package/dist/core/data-migration.js +9 -2
- package/dist/core/event-catalog.js +6 -0
- package/dist/core/evolagent.js +4 -2
- package/dist/core/handoff/dispatcher.js +5 -2
- package/dist/core/handoff/runtime.js +157 -22
- package/dist/core/handoff/store.js +29 -1
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +19 -0
- package/dist/core/message/logical-queue-bridge.js +1 -0
- package/dist/core/message/mention-schema.js +126 -0
- package/dist/core/message/message-bridge.js +298 -88
- package/dist/core/message/message-log.js +3 -0
- package/dist/core/message/message-queue.js +268 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +161 -77
- 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 +243 -18
- 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 +43 -1
- package/dist/core/permission/tool-policy.js +322 -57
- package/dist/core/protected-paths.js +114 -4
- package/dist/core/session/session-manager.js +42 -4
- 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/eck/manifest-engine.js +39 -13
- package/dist/index.js +541 -180
- package/dist/ipc.js +232 -10
- package/dist/paths.js +3 -0
- package/dist/response-system/coordinator.js +6 -4
- package/dist/response-system/engines/v1/proactive-flow.js +26 -8
- package/dist/response-system/modes/single-session/index.js +14 -1
- package/dist/response-system/selector.js +6 -5
- package/dist/trigger/anomaly-store.js +4 -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/script-executor.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/codex-app-server-registry.js +90 -0
- package/dist/utils/codex-cli.js +5 -1
- package/dist/utils/cross-platform.js +15 -0
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +10 -0
- package/dist/utils/npm-ops.js +5 -12
- package/dist/utils/tool-summary.js +11 -0
- package/kits/docs/channels/aun.md +1 -1
- package/kits/docs/context-assembly.md +2 -2
- package/kits/docs/evolcore/agent.md +11 -6
- package/kits/docs/evolcore/aid.md +14 -0
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/docs/prompt-loading-architecture.md +1 -2
- package/kits/docs/venues/group.md +1 -1
- package/kits/eck_manifest.json +0 -12
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/05-venue.md +2 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/single-session.schema.1.json +3 -3
- package/kits/schemas/single-session.schema.2.json +3 -3
- package/kits/templates/roles/admin.json +27 -0
- package/kits/templates/roles/member.json +34 -1
- package/kits/templates/roles/visitor.json +35 -3
- package/kits/templates/system-fragments/bootstrap.md +1 -1
- package/kits/templates/system-fragments/channel.md +1 -1
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +2 -2
- 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/command/evol-menu-version-gate.js +0 -39
- 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
|
@@ -7,19 +7,19 @@
|
|
|
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';
|
|
15
14
|
import { logger } from '../utils/logger.js';
|
|
15
|
+
import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
|
|
16
16
|
import { isRetryableError } from '../utils/error-utils.js';
|
|
17
17
|
import { renderActionAsText } from '../core/interaction-router.js';
|
|
18
18
|
import { buildEnvelope, sendInteractionPayload } from '../core/message/message-utils.js';
|
|
19
19
|
import { resolveCodexCapabilityThreadConfigForProject } from '../core/capability/capability-manager.js';
|
|
20
20
|
import { AGENT_DELEGATION_TOKEN_ENV, hashDelegatedCommandArgv } from '../core/auth/agent-delegation.js';
|
|
21
21
|
import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
|
|
22
|
-
import { classifyEvolcoreShellCommand, parseLiteralShellArgv } from '../core/permission/ec-command-parser.js';
|
|
22
|
+
import { classifyEvolcoreShellCommand, parseLiteralShellArgv, unwrapCodexShellCommandArgv } from '../core/permission/ec-command-parser.js';
|
|
23
23
|
import { compareVersions } from '../utils/npm-ops.js';
|
|
24
24
|
import { resolvePaths, resolveRoot } from '../paths.js';
|
|
25
25
|
import { buildSessionTurnList } from '../core/session/session-turns.js';
|
|
@@ -75,6 +75,39 @@ class AsyncEventQueue {
|
|
|
75
75
|
}
|
|
76
76
|
const managedTempDirsForExitCleanup = new Map();
|
|
77
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
|
+
}
|
|
78
111
|
function registerManagedTempDirForExitCleanup(directory, parent) {
|
|
79
112
|
managedTempDirsForExitCleanup.set(directory, parent);
|
|
80
113
|
if (managedTempExitCleanupRegistered)
|
|
@@ -83,10 +116,8 @@ function registerManagedTempDirForExitCleanup(directory, parent) {
|
|
|
83
116
|
process.once('exit', () => {
|
|
84
117
|
for (const [candidatePath, parentPath] of managedTempDirsForExitCleanup) {
|
|
85
118
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (candidate !== parentCandidate && isSameOrDescendant(candidate, parentCandidate)) {
|
|
89
|
-
fs.rmSync(candidate, { recursive: true, force: true });
|
|
119
|
+
if (isTrustedManagedTempDirectory(candidatePath, parentPath)) {
|
|
120
|
+
fs.rmSync(path.resolve(candidatePath), { recursive: true, force: true });
|
|
90
121
|
}
|
|
91
122
|
}
|
|
92
123
|
catch {
|
|
@@ -118,10 +149,9 @@ const CODEX_CATALOG_FALLBACK = [
|
|
|
118
149
|
];
|
|
119
150
|
const CODEX_DELEGATION_CARRIER_METADATA_KEY = '__codexDelegationCarrierV1';
|
|
120
151
|
let codexCatalogCache = null;
|
|
121
|
-
// The permission bridge is version-locked to the
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
export const MIN_CODEX_CLI_VERSION = '0.144.1';
|
|
152
|
+
// The permission bridge is version-locked to the audited approval schema and
|
|
153
|
+
// managed PreToolUse hook behavior used by proactive first-tool enforcement.
|
|
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;
|
|
@@ -223,6 +255,8 @@ export class CodexRunner {
|
|
|
223
255
|
trackedApprovalItems = new Map();
|
|
224
256
|
threadDelegationCarriers = new Map();
|
|
225
257
|
pendingInterrupts = new Map();
|
|
258
|
+
threadOperationLocks = new Map();
|
|
259
|
+
activeOperationReleases = new Map();
|
|
226
260
|
appServerClient = null;
|
|
227
261
|
onSessionIdUpdate;
|
|
228
262
|
onCompactStart;
|
|
@@ -280,25 +314,23 @@ export class CodexRunner {
|
|
|
280
314
|
createDelegationCarrier() {
|
|
281
315
|
return randomBytes(32).toString('base64url');
|
|
282
316
|
}
|
|
283
|
-
approvedLiteralEvolcoreCommandArgv(command) {
|
|
317
|
+
approvedLiteralEvolcoreCommandArgv(command, managedTempDir) {
|
|
284
318
|
const classification = classifyEvolcoreShellCommand(command);
|
|
285
319
|
if (classification.kind === 'bounded-output')
|
|
286
320
|
return classification.command.argv;
|
|
287
321
|
if (classification.kind !== 'literal')
|
|
288
322
|
return undefined;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (!['bash', '/bin/bash', '/usr/bin/bash', 'sh', '/bin/sh', '/usr/bin/sh'].includes(shell)) {
|
|
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')))
|
|
297
330
|
return undefined;
|
|
298
|
-
|
|
299
|
-
return argv[2];
|
|
331
|
+
return argv?.[0] === 'ec' ? argv : undefined;
|
|
300
332
|
}
|
|
301
|
-
approvedEvolcoreCommandArgv(toolInput) {
|
|
333
|
+
approvedEvolcoreCommandArgv(toolInput, managedTempDir) {
|
|
302
334
|
const explicitArgv = Array.isArray(toolInput.commandArgv)
|
|
303
335
|
&& toolInput.commandArgv.every(value => typeof value === 'string')
|
|
304
336
|
? toolInput.commandArgv
|
|
@@ -306,27 +338,30 @@ export class CodexRunner {
|
|
|
306
338
|
if (explicitArgv?.[0] === 'ec')
|
|
307
339
|
return explicitArgv;
|
|
308
340
|
const explicitWrappedCommand = explicitArgv
|
|
309
|
-
?
|
|
341
|
+
? unwrapCodexShellCommandArgv(explicitArgv)
|
|
310
342
|
: undefined;
|
|
311
343
|
if (explicitWrappedCommand !== undefined) {
|
|
312
|
-
return this.approvedLiteralEvolcoreCommandArgv(explicitWrappedCommand);
|
|
344
|
+
return this.approvedLiteralEvolcoreCommandArgv(explicitWrappedCommand, managedTempDir);
|
|
313
345
|
}
|
|
314
346
|
const command = typeof toolInput.command === 'string' ? toolInput.command : '';
|
|
315
|
-
const directArgv = this.approvedLiteralEvolcoreCommandArgv(command);
|
|
347
|
+
const directArgv = this.approvedLiteralEvolcoreCommandArgv(command, managedTempDir);
|
|
316
348
|
if (directArgv)
|
|
317
349
|
return directArgv;
|
|
318
|
-
const wrappedArgv = parseLiteralShellArgv(command
|
|
350
|
+
const wrappedArgv = parseLiteralShellArgv(command, {
|
|
351
|
+
allowManagedTmpDir: true,
|
|
352
|
+
...(managedTempDir ? { managedTempDir } : {}),
|
|
353
|
+
});
|
|
319
354
|
const wrappedCommand = wrappedArgv
|
|
320
|
-
?
|
|
355
|
+
? unwrapCodexShellCommandArgv(wrappedArgv)
|
|
321
356
|
: undefined;
|
|
322
357
|
return wrappedCommand === undefined
|
|
323
358
|
? undefined
|
|
324
|
-
: this.approvedLiteralEvolcoreCommandArgv(wrappedCommand);
|
|
359
|
+
: this.approvedLiteralEvolcoreCommandArgv(wrappedCommand, managedTempDir);
|
|
325
360
|
}
|
|
326
361
|
armApprovedDelegationCommand(sessionId, toolInput) {
|
|
327
362
|
const threadId = this.activeSessions.get(sessionId);
|
|
328
363
|
const carrierToken = threadId ? this.threadDelegationCarriers.get(threadId) : undefined;
|
|
329
|
-
const argv = this.approvedEvolcoreCommandArgv(toolInput);
|
|
364
|
+
const argv = this.approvedEvolcoreCommandArgv(toolInput, this.getManagedTempDir(sessionId));
|
|
330
365
|
if (!carrierToken || !argv)
|
|
331
366
|
return false;
|
|
332
367
|
const commandHash = hashDelegatedCommandArgv(argv);
|
|
@@ -796,6 +831,35 @@ export class CodexRunner {
|
|
|
796
831
|
}
|
|
797
832
|
setSendPrompt(fn) { this.sendPromptFn = fn; }
|
|
798
833
|
setPermissionContext(sessionId, context) { this.permissionContexts.set(sessionId, context); }
|
|
834
|
+
async evaluatePreToolUse(threadId, toolName, toolInput) {
|
|
835
|
+
const sessionKey = this.findSessionKeyByThread(threadId);
|
|
836
|
+
const activeThread = this.activeSessions.get(sessionKey);
|
|
837
|
+
const context = this.permissionContexts.get(sessionKey);
|
|
838
|
+
if (!activeThread || activeThread !== threadId) {
|
|
839
|
+
return { ok: true, applicable: false };
|
|
840
|
+
}
|
|
841
|
+
const proactiveSession = context?.chatmode === 'proactive';
|
|
842
|
+
if (!context || (proactiveSession && !context.preToolUsePolicyHook)) {
|
|
843
|
+
return { ok: false, applicable: true, decision: 'deny', reason: 'EvolCore proactive session policy context is unavailable' };
|
|
844
|
+
}
|
|
845
|
+
// A tracked interactive thread still needs an authenticated, explicit
|
|
846
|
+
// allow so the managed hook can fail closed when no runner matches.
|
|
847
|
+
if (!context.preToolUsePolicyHook)
|
|
848
|
+
return { ok: true, applicable: true, decision: 'allow' };
|
|
849
|
+
const normalizedInput = toolInput && typeof toolInput === 'object' && !Array.isArray(toolInput)
|
|
850
|
+
? toolInput
|
|
851
|
+
: {};
|
|
852
|
+
try {
|
|
853
|
+
const result = context.preToolUsePolicyHook(toolName, normalizedInput);
|
|
854
|
+
if (result?.block) {
|
|
855
|
+
return { ok: true, applicable: true, decision: 'deny', reason: result.reason };
|
|
856
|
+
}
|
|
857
|
+
return { ok: true, applicable: true, decision: 'allow' };
|
|
858
|
+
}
|
|
859
|
+
catch (error) {
|
|
860
|
+
return { ok: false, applicable: true, decision: 'deny', reason: `policy hook failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
861
|
+
}
|
|
862
|
+
}
|
|
799
863
|
setPermissionGateway(gw) { this.permissionGateway = gw; }
|
|
800
864
|
// ── Stream management (needed by MessageProcessor) ──
|
|
801
865
|
registerStream(key, stream) {
|
|
@@ -805,13 +869,135 @@ export class CodexRunner {
|
|
|
805
869
|
this.activeStreams.delete(key);
|
|
806
870
|
this.activeAbortControllers.delete(key);
|
|
807
871
|
this.pendingInterrupts.delete(key);
|
|
872
|
+
const release = this.activeOperationReleases.get(key);
|
|
873
|
+
if (release) {
|
|
874
|
+
this.activeOperationReleases.delete(key);
|
|
875
|
+
release();
|
|
876
|
+
}
|
|
808
877
|
}
|
|
809
878
|
hasActiveStream(key) {
|
|
810
879
|
return this.activeStreams.has(key) || this.activeAbortControllers.has(key) || this.activeTurns.has(key);
|
|
811
880
|
}
|
|
881
|
+
threadOperationKey(threadId, sessionId) {
|
|
882
|
+
// Once a thread id is known it is the shared identity, including when
|
|
883
|
+
// multiple EvolCore sessions happen to address the same Codex thread.
|
|
884
|
+
if (threadId)
|
|
885
|
+
return `thread:${threadId}`;
|
|
886
|
+
if (sessionId)
|
|
887
|
+
return `session:${sessionId}`;
|
|
888
|
+
return 'session:unknown';
|
|
889
|
+
}
|
|
890
|
+
getThreadOperationLock(key) {
|
|
891
|
+
const existing = this.threadOperationLocks.get(key);
|
|
892
|
+
if (existing)
|
|
893
|
+
return existing;
|
|
894
|
+
const lock = { aliases: new Set([key]) };
|
|
895
|
+
this.threadOperationLocks.set(key, lock);
|
|
896
|
+
return lock;
|
|
897
|
+
}
|
|
898
|
+
bindThreadOperationKeys(...keys) {
|
|
899
|
+
const uniqueKeys = [...new Set(keys.filter(Boolean))];
|
|
900
|
+
if (uniqueKeys.length < 2)
|
|
901
|
+
return;
|
|
902
|
+
const locks = uniqueKeys.map(key => this.getThreadOperationLock(key));
|
|
903
|
+
const primary = locks[0];
|
|
904
|
+
for (const other of locks.slice(1)) {
|
|
905
|
+
if (other === primary)
|
|
906
|
+
continue;
|
|
907
|
+
const tails = [primary.tail, other.tail].filter((tail) => !!tail);
|
|
908
|
+
if (tails.length === 1) {
|
|
909
|
+
primary.tail = tails[0];
|
|
910
|
+
}
|
|
911
|
+
else if (tails.length > 1) {
|
|
912
|
+
const merged = Promise.all(tails).then(() => undefined);
|
|
913
|
+
primary.tail = merged;
|
|
914
|
+
merged.then(() => {
|
|
915
|
+
if (primary.tail === merged)
|
|
916
|
+
primary.tail = undefined;
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
for (const alias of other.aliases) {
|
|
920
|
+
primary.aliases.add(alias);
|
|
921
|
+
this.threadOperationLocks.set(alias, primary);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
isActiveThreadOperationAlias(key) {
|
|
926
|
+
if (key.startsWith('session:'))
|
|
927
|
+
return this.activeSessions.has(key.slice('session:'.length));
|
|
928
|
+
if (key.startsWith('thread:')) {
|
|
929
|
+
const threadId = key.slice('thread:'.length);
|
|
930
|
+
for (const activeThreadId of this.activeSessions.values()) {
|
|
931
|
+
if (activeThreadId === threadId)
|
|
932
|
+
return true;
|
|
933
|
+
}
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
return true;
|
|
937
|
+
}
|
|
938
|
+
pruneThreadOperationLocks() {
|
|
939
|
+
for (const [key, lock] of this.threadOperationLocks) {
|
|
940
|
+
if (lock.tail || this.isActiveThreadOperationAlias(key))
|
|
941
|
+
continue;
|
|
942
|
+
this.threadOperationLocks.delete(key);
|
|
943
|
+
lock.aliases.delete(key);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
async acquireThreadOperation(key, operation) {
|
|
947
|
+
const lock = this.getThreadOperationLock(key);
|
|
948
|
+
const previous = lock.tail;
|
|
949
|
+
let releaseCurrent;
|
|
950
|
+
const current = new Promise(resolve => {
|
|
951
|
+
releaseCurrent = resolve;
|
|
952
|
+
});
|
|
953
|
+
lock.tail = current;
|
|
954
|
+
if (previous) {
|
|
955
|
+
logger.info(`[CodexRunner] queued ${operation} behind an active operation: ${key}`);
|
|
956
|
+
await previous;
|
|
957
|
+
}
|
|
958
|
+
let released = false;
|
|
959
|
+
return () => {
|
|
960
|
+
if (released)
|
|
961
|
+
return;
|
|
962
|
+
released = true;
|
|
963
|
+
if (lock.tail === current)
|
|
964
|
+
lock.tail = undefined;
|
|
965
|
+
releaseCurrent();
|
|
966
|
+
this.pruneThreadOperationLocks();
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
async *holdThreadOperation(stream, sessionId, release) {
|
|
970
|
+
try {
|
|
971
|
+
for await (const item of stream)
|
|
972
|
+
yield item;
|
|
973
|
+
}
|
|
974
|
+
finally {
|
|
975
|
+
if (this.activeOperationReleases.get(sessionId) === release) {
|
|
976
|
+
this.activeOperationReleases.delete(sessionId);
|
|
977
|
+
}
|
|
978
|
+
release();
|
|
979
|
+
}
|
|
980
|
+
}
|
|
812
981
|
// ── Core: runQuery ──
|
|
813
982
|
async runQuery(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
|
|
983
|
+
const knownThreadId = initialAgentSessionId || this.activeSessions.get(sessionId);
|
|
984
|
+
const operationKey = this.threadOperationKey(knownThreadId, sessionId);
|
|
985
|
+
const release = await this.acquireThreadOperation(operationKey, 'thread/resume+turn/start');
|
|
986
|
+
try {
|
|
987
|
+
const stream = await this.runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv);
|
|
988
|
+
this.activeOperationReleases.set(sessionId, release);
|
|
989
|
+
return this.holdThreadOperation(stream, sessionId, release);
|
|
990
|
+
}
|
|
991
|
+
catch (error) {
|
|
992
|
+
release();
|
|
993
|
+
throw error;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
async runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
|
|
814
997
|
let agentSessionId = initialAgentSessionId || this.activeSessions.get(sessionId);
|
|
998
|
+
if (this.permissionContexts.get(sessionId)?.chatmode === 'proactive' && process.platform !== 'linux') {
|
|
999
|
+
throw new Error('Codex proactive managed PreToolUse enforcement currently requires Linux');
|
|
1000
|
+
}
|
|
815
1001
|
const resumingThread = !!agentSessionId;
|
|
816
1002
|
const callModel = modelOverride?.model || this.model;
|
|
817
1003
|
const callEffort = modelOverride?.effortMode === 'model_default'
|
|
@@ -893,6 +1079,7 @@ export class CodexRunner {
|
|
|
893
1079
|
}
|
|
894
1080
|
agentSessionId = threadId;
|
|
895
1081
|
this.activeSessions.set(sessionId, threadId);
|
|
1082
|
+
this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
|
|
896
1083
|
this.threadProjectPaths.set(threadId, path.resolve(projectPath));
|
|
897
1084
|
this.bindExternalToolBoundary(threadId, externalToolConfig);
|
|
898
1085
|
this.onSessionIdUpdate?.(sessionId, threadId);
|
|
@@ -1035,10 +1222,12 @@ export class CodexRunner {
|
|
|
1035
1222
|
}
|
|
1036
1223
|
if (agentSessionId) {
|
|
1037
1224
|
this.activeSessions.set(sessionId, agentSessionId);
|
|
1225
|
+
this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${agentSessionId}`);
|
|
1038
1226
|
}
|
|
1039
1227
|
else {
|
|
1040
1228
|
this.activeSessions.delete(sessionId);
|
|
1041
1229
|
}
|
|
1230
|
+
this.pruneThreadOperationLocks();
|
|
1042
1231
|
this.onSessionIdUpdate?.(sessionId, agentSessionId);
|
|
1043
1232
|
}
|
|
1044
1233
|
async closeSession(sessionId) {
|
|
@@ -1057,6 +1246,7 @@ export class CodexRunner {
|
|
|
1057
1246
|
this.unbindExternalToolBoundary(threadId);
|
|
1058
1247
|
}
|
|
1059
1248
|
this.clearTrackedApprovalItemsForThread(threadId);
|
|
1249
|
+
this.pruneThreadOperationLocks();
|
|
1060
1250
|
}
|
|
1061
1251
|
resolveSessionFile(agentSessionId, _projectPath) {
|
|
1062
1252
|
// Codex session 文件: ~/.codex/sessions/YYYY/MM/DD/rollout-*-{threadId}.jsonl
|
|
@@ -1089,10 +1279,12 @@ export class CodexRunner {
|
|
|
1089
1279
|
this.threadDelegationCarriers.delete(threadId);
|
|
1090
1280
|
this.unbindExternalToolBoundary(threadId);
|
|
1091
1281
|
this.clearTrackedApprovalItemsForThread(threadId);
|
|
1282
|
+
this.pruneThreadOperationLocks();
|
|
1092
1283
|
this.onSessionIdUpdate?.(sessionId, '');
|
|
1093
1284
|
return true;
|
|
1094
1285
|
}
|
|
1095
1286
|
async compactSession(_sessionId, agentSessionId, _projectPath) {
|
|
1287
|
+
const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId, _sessionId), 'compact');
|
|
1096
1288
|
try {
|
|
1097
1289
|
const appServer = this.getAppServerClient();
|
|
1098
1290
|
this.onCompactStart?.(_sessionId);
|
|
@@ -1129,6 +1321,9 @@ export class CodexRunner {
|
|
|
1129
1321
|
logger.error('[CodexRunner] Compact failed:', error);
|
|
1130
1322
|
return false;
|
|
1131
1323
|
}
|
|
1324
|
+
finally {
|
|
1325
|
+
release();
|
|
1326
|
+
}
|
|
1132
1327
|
}
|
|
1133
1328
|
async compact(sessionId, agentSessionId, projectPath) {
|
|
1134
1329
|
return this.compactSession(sessionId, agentSessionId, projectPath);
|
|
@@ -1254,6 +1449,15 @@ export class CodexRunner {
|
|
|
1254
1449
|
return newest?.path;
|
|
1255
1450
|
}
|
|
1256
1451
|
async forkSession(agentSessionId, projectPath, title) {
|
|
1452
|
+
const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'fork');
|
|
1453
|
+
try {
|
|
1454
|
+
return await this.forkSessionLocked(agentSessionId, projectPath, title);
|
|
1455
|
+
}
|
|
1456
|
+
finally {
|
|
1457
|
+
release();
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
async forkSessionLocked(agentSessionId, projectPath, title) {
|
|
1257
1461
|
const sessionKey = this.findSessionKeyByThread(agentSessionId);
|
|
1258
1462
|
const mode = this.chatModes.get(sessionKey) ?? this.currentMode;
|
|
1259
1463
|
const executionSandbox = this.buildExecutionSandboxOptions(sessionKey, mode, projectPath);
|
|
@@ -1278,18 +1482,30 @@ export class CodexRunner {
|
|
|
1278
1482
|
return forkedThreadId;
|
|
1279
1483
|
}
|
|
1280
1484
|
async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title) {
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
const
|
|
1289
|
-
if (
|
|
1290
|
-
|
|
1485
|
+
const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'forkAt');
|
|
1486
|
+
try {
|
|
1487
|
+
const turns = buildSessionTurnList(await this.getSessionMessages(agentSessionId, projectPath));
|
|
1488
|
+
const targetIndex = turns.findIndex(turn => turn.assistantUuid === assistantMessageId);
|
|
1489
|
+
if (targetIndex < 0)
|
|
1490
|
+
throw new Error('fork point not found');
|
|
1491
|
+
const forkedThreadId = await this.forkSessionLocked(agentSessionId, projectPath, title);
|
|
1492
|
+
const turnsToDiscard = turns.length - targetIndex - 1;
|
|
1493
|
+
if (turnsToDiscard > 0) {
|
|
1494
|
+
const rollbackRelease = await this.acquireThreadOperation(this.threadOperationKey(forkedThreadId), 'rollback');
|
|
1495
|
+
try {
|
|
1496
|
+
const rolledBack = await this.rollbackSessionTurnsLocked(forkedThreadId, turnsToDiscard);
|
|
1497
|
+
if (!rolledBack)
|
|
1498
|
+
throw new Error('forked thread rollback failed');
|
|
1499
|
+
}
|
|
1500
|
+
finally {
|
|
1501
|
+
rollbackRelease();
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return forkedThreadId;
|
|
1505
|
+
}
|
|
1506
|
+
finally {
|
|
1507
|
+
release();
|
|
1291
1508
|
}
|
|
1292
|
-
return forkedThreadId;
|
|
1293
1509
|
}
|
|
1294
1510
|
async setSessionName(agentSessionId, name) {
|
|
1295
1511
|
return this.getAppServerClient().threadSetName(agentSessionId, name);
|
|
@@ -1421,6 +1637,25 @@ export class CodexRunner {
|
|
|
1421
1637
|
// proactive 模式行为策略(首次工具调用必须是 ec msg send)
|
|
1422
1638
|
const policyResult = this.permissionContexts.get(sessionKey)?.policyHook?.(toolName, toolInput);
|
|
1423
1639
|
if (policyResult?.block) {
|
|
1640
|
+
logger.info(`[CodexRunner] app-server approval declined by session policy: session=${sessionKey} ` +
|
|
1641
|
+
`requestId=${request.id ?? '<missing>'} code=${policyResult.policyCode ?? 'session_policy_hook'} ` +
|
|
1642
|
+
`reason=${policyResult.reason ?? 'policy denied'} executed=false`);
|
|
1643
|
+
try {
|
|
1644
|
+
await permissionContext?.recordExecutionAnomaly?.({
|
|
1645
|
+
code: 'operation_blocked',
|
|
1646
|
+
severity: 'warning',
|
|
1647
|
+
phase: 'execution',
|
|
1648
|
+
occurredAt: Date.now(),
|
|
1649
|
+
toolName,
|
|
1650
|
+
...(request.id !== undefined ? { requestId: String(request.id) } : {}),
|
|
1651
|
+
policyCode: policyResult.policyCode ?? 'session_policy_hook',
|
|
1652
|
+
summary: summarizeToolInputForAudit(toolName, toolInput).slice(0, 512),
|
|
1653
|
+
effect: 'operation_skipped',
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
catch (error) {
|
|
1657
|
+
logger.warn(`[CodexRunner] failed to record policy-hook denial: session=${sessionKey} tool=${toolName} error=${error instanceof Error ? error.message : String(error)}`);
|
|
1658
|
+
}
|
|
1424
1659
|
return this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
|
|
1425
1660
|
}
|
|
1426
1661
|
const summary = this.summarizeAppServerRequest(request.method, params);
|
|
@@ -1434,10 +1669,10 @@ export class CodexRunner {
|
|
|
1434
1669
|
const sessionMode = this.chatModes.get(sessionKey) ?? this.currentMode;
|
|
1435
1670
|
logger.info(`[CodexRunner] app-server approval request id=${request.id} method=${request.method} session=${sessionKey} mode=${sessionMode} tool=${toolName} summary=${summary}`);
|
|
1436
1671
|
try {
|
|
1437
|
-
const decision = await this.resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath, operationCwd);
|
|
1672
|
+
const decision = await this.resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath, operationCwd, request.id);
|
|
1438
1673
|
if (decision !== 'deny'
|
|
1439
1674
|
&& (request.method === 'item/commandExecution/requestApproval' || request.method === 'execCommandApproval')
|
|
1440
|
-
&& this.approvedEvolcoreCommandArgv(toolInput)) {
|
|
1675
|
+
&& this.approvedEvolcoreCommandArgv(toolInput, this.getManagedTempDir(sessionKey))) {
|
|
1441
1676
|
if (!this.armApprovedDelegationCommand(sessionKey, toolInput)) {
|
|
1442
1677
|
// Approval policy and delegation authority are separate boundaries.
|
|
1443
1678
|
// If no ticket can be armed, the CLI still fails closed at IPC token
|
|
@@ -1865,10 +2100,15 @@ export class CodexRunner {
|
|
|
1865
2100
|
channel: permCtx?.channel,
|
|
1866
2101
|
peerId: permCtx?.userId,
|
|
1867
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),
|
|
1868
2108
|
} : undefined;
|
|
1869
2109
|
if (toolName === 'Bash')
|
|
1870
2110
|
return checkReadonly(toolName, input, projectPath, readonlyContext);
|
|
1871
|
-
return { behavior: 'deny', message: '🔒 只读模式:权限升级和文件写入请求已拒绝' };
|
|
2111
|
+
return { behavior: 'deny', message: '🔒 只读模式:权限升级和文件写入请求已拒绝', policyCode: 'readonly_permission_or_write' };
|
|
1872
2112
|
}
|
|
1873
2113
|
permissionProfiles(input) {
|
|
1874
2114
|
return [input.permissions, input.additionalPermissions]
|
|
@@ -1965,7 +2205,7 @@ export class CodexRunner {
|
|
|
1965
2205
|
return true;
|
|
1966
2206
|
});
|
|
1967
2207
|
}
|
|
1968
|
-
async resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath = process.cwd(), operationCwd = workspacePath) {
|
|
2208
|
+
async resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath = process.cwd(), operationCwd = workspacePath, requestId) {
|
|
1969
2209
|
const permissionContext = this.permissionContexts.get(sessionKey);
|
|
1970
2210
|
const permissionPrompt = permissionContext?.sendPrompt ?? this.sendPromptFn;
|
|
1971
2211
|
const hasFilesystemPermissions = (toolName === 'Bash' || toolName === 'PermissionGrant')
|
|
@@ -1980,8 +2220,34 @@ export class CodexRunner {
|
|
|
1980
2220
|
// per-session 权限模式(runQuery 写入);缺省回落实例级 currentMode(兼容无 runQuery 上下文的调用)
|
|
1981
2221
|
const rawMode = this.chatModes.get(sessionKey) ?? this.currentMode;
|
|
1982
2222
|
const mode = normalizePermissionMode(rawMode).mode;
|
|
2223
|
+
const recordBlockedOperation = async (policyCode, operationInput = toolInput) => {
|
|
2224
|
+
try {
|
|
2225
|
+
await permissionContext?.recordExecutionAnomaly?.({
|
|
2226
|
+
code: 'operation_blocked',
|
|
2227
|
+
severity: 'warning',
|
|
2228
|
+
phase: 'execution',
|
|
2229
|
+
occurredAt: Date.now(),
|
|
2230
|
+
toolName,
|
|
2231
|
+
...(typeof requestId === 'string' || typeof requestId === 'number' ? { requestId: String(requestId) } : {}),
|
|
2232
|
+
policyCode,
|
|
2233
|
+
decisionSource: 'policy',
|
|
2234
|
+
agentAid: permissionContext?.selfAid,
|
|
2235
|
+
permissionMode: mode,
|
|
2236
|
+
summary: summarizeToolInputForAudit(toolName, operationInput).slice(0, 512),
|
|
2237
|
+
effect: 'operation_skipped',
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
catch (error) {
|
|
2241
|
+
logger.warn(`[CodexRunner] failed to record blocked operation: session=${sessionKey} tool=${toolName} code=${policyCode} error=${error instanceof Error ? error.message : String(error)}`);
|
|
2242
|
+
}
|
|
2243
|
+
};
|
|
1983
2244
|
const preflight = evaluateToolPreflight(toolName, toolInput, {
|
|
1984
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,
|
|
1985
2251
|
selfAid: permissionContext?.selfAid,
|
|
1986
2252
|
channel: permissionContext?.channel,
|
|
1987
2253
|
userId: permissionContext?.userId,
|
|
@@ -1990,8 +2256,14 @@ export class CodexRunner {
|
|
|
1990
2256
|
projectPath: operationCwd,
|
|
1991
2257
|
workspacePath,
|
|
1992
2258
|
});
|
|
1993
|
-
if (preflight.behavior === 'deny')
|
|
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'}`);
|
|
2262
|
+
if (preflight.policyCode) {
|
|
2263
|
+
await recordBlockedOperation(preflight.policyCode);
|
|
2264
|
+
}
|
|
1994
2265
|
return 'deny';
|
|
2266
|
+
}
|
|
1995
2267
|
if (preflight.behavior === 'allow')
|
|
1996
2268
|
return 'allow';
|
|
1997
2269
|
const checkedInput = preflight.input;
|
|
@@ -2015,8 +2287,10 @@ export class CodexRunner {
|
|
|
2015
2287
|
const grantScope = `codex:${this.permissionProfileName(sessionKey)}:${mode}`;
|
|
2016
2288
|
if (mode === 'readonly') {
|
|
2017
2289
|
const readonly = this.checkCodexReadonly(toolName, checkedInput, operationCwd, sessionKey);
|
|
2018
|
-
if (readonly.behavior === 'deny')
|
|
2290
|
+
if (readonly.behavior === 'deny') {
|
|
2291
|
+
await recordBlockedOperation(readonly.policyCode ?? 'readonly_mode', checkedInput);
|
|
2019
2292
|
return 'deny';
|
|
2293
|
+
}
|
|
2020
2294
|
return 'allow';
|
|
2021
2295
|
}
|
|
2022
2296
|
if (toolName.startsWith('MCP:')) {
|
|
@@ -2024,13 +2298,19 @@ export class CodexRunner {
|
|
|
2024
2298
|
}
|
|
2025
2299
|
// auto 模式下检查危险命令,危险命令自动拒绝
|
|
2026
2300
|
if (mode === 'auto') {
|
|
2027
|
-
if (hasFileChangeExpansion || hasFilesystemPermissions || hasUnknownPermissions)
|
|
2301
|
+
if (hasFileChangeExpansion || hasFilesystemPermissions || hasUnknownPermissions) {
|
|
2302
|
+
await recordBlockedOperation('auto_permission_expansion', checkedInput);
|
|
2028
2303
|
return 'deny';
|
|
2029
|
-
|
|
2304
|
+
}
|
|
2305
|
+
if (toolName === 'PermissionGrant' && !hasNetworkPermissions) {
|
|
2306
|
+
await recordBlockedOperation('auto_permission_expansion', checkedInput);
|
|
2030
2307
|
return 'deny';
|
|
2308
|
+
}
|
|
2031
2309
|
const dangerous = checkDangerousCommand(toolName, checkedInput);
|
|
2032
|
-
if (dangerous.isDangerous)
|
|
2310
|
+
if (dangerous.isDangerous) {
|
|
2311
|
+
await recordBlockedOperation('auto_dangerous_command', checkedInput);
|
|
2033
2312
|
return 'deny';
|
|
2313
|
+
}
|
|
2034
2314
|
return 'allow';
|
|
2035
2315
|
}
|
|
2036
2316
|
const requiresSandboxExpansionApproval = hasFileChangeExpansion
|
|
@@ -2083,6 +2363,15 @@ export class CodexRunner {
|
|
|
2083
2363
|
throw new Error('Unsupported Codex app-server request: ' + method);
|
|
2084
2364
|
}
|
|
2085
2365
|
async rollbackSessionTurns(agentSessionId, _projectPath, numTurns) {
|
|
2366
|
+
const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'rollback');
|
|
2367
|
+
try {
|
|
2368
|
+
return await this.rollbackSessionTurnsLocked(agentSessionId, numTurns);
|
|
2369
|
+
}
|
|
2370
|
+
finally {
|
|
2371
|
+
release();
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
async rollbackSessionTurnsLocked(agentSessionId, numTurns) {
|
|
2086
2375
|
if (numTurns < 1)
|
|
2087
2376
|
return true;
|
|
2088
2377
|
const response = await this.getAppServerClient().threadRollback(agentSessionId, numTurns);
|
|
@@ -2121,7 +2410,11 @@ export class CodexRunner {
|
|
|
2121
2410
|
}
|
|
2122
2411
|
readGitHeadFile(projectPath, filePath) {
|
|
2123
2412
|
try {
|
|
2124
|
-
return execFileSync('git', ['show', `HEAD:${filePath.replace(/\\/g, '/')}`], {
|
|
2413
|
+
return execFileSync('git', ['show', `HEAD:${filePath.replace(/\\/g, '/')}`], {
|
|
2414
|
+
cwd: projectPath,
|
|
2415
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
2416
|
+
windowsHide: process.platform === 'win32',
|
|
2417
|
+
});
|
|
2125
2418
|
}
|
|
2126
2419
|
catch {
|
|
2127
2420
|
return null;
|
|
@@ -2400,19 +2693,18 @@ export class CodexRunner {
|
|
|
2400
2693
|
*reconcileOpenAppServerToolCalls(state, turnId) {
|
|
2401
2694
|
if (state.openToolCalls.size === 0)
|
|
2402
2695
|
return;
|
|
2403
|
-
const
|
|
2696
|
+
const openCalls = [...state.openToolCalls.entries()];
|
|
2697
|
+
const callIds = openCalls.map(([callId]) => callId);
|
|
2404
2698
|
logger.warn(`[CodexRunner] Turn completed with ${callIds.length} open tool call(s): ` +
|
|
2405
2699
|
`thread=${state.threadId} turn=${turnId || state.turnId || 'unknown'} calls=${callIds.join(',')}`);
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
};
|
|
2415
|
-
}
|
|
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
|
+
};
|
|
2416
2708
|
state.openToolCalls.clear();
|
|
2417
2709
|
}
|
|
2418
2710
|
*mapAppServerItemStarted(item, state) {
|
|
@@ -2783,39 +3075,28 @@ export class CodexRunner {
|
|
|
2783
3075
|
}
|
|
2784
3076
|
}
|
|
2785
3077
|
isTrustedSessionTempDir(directory) {
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
const stat = fs.lstatSync(candidate);
|
|
2794
|
-
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
2795
|
-
return false;
|
|
2796
|
-
if (typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
2797
|
-
return false;
|
|
2798
|
-
if ((stat.mode & 0o077) !== 0)
|
|
2799
|
-
return false;
|
|
2800
|
-
}
|
|
2801
|
-
return canonical !== parent && isSameOrDescendant(canonical, parent);
|
|
2802
|
-
}
|
|
2803
|
-
catch {
|
|
2804
|
-
return false;
|
|
2805
|
-
}
|
|
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);
|
|
2806
3085
|
}
|
|
2807
3086
|
cleanupSessionTempDir(sessionId) {
|
|
2808
3087
|
const directory = this.sessionTempDirs.get(sessionId);
|
|
2809
3088
|
this.sessionTempDirs.delete(sessionId);
|
|
2810
3089
|
if (!directory)
|
|
2811
3090
|
return;
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
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)) {
|
|
2815
3095
|
managedTempDirsForExitCleanup.delete(directory);
|
|
2816
3096
|
logger.warn(`[CodexRunner] refused to clean unmanaged TMPDIR: session=${sessionId} path=${directory}`);
|
|
2817
3097
|
return;
|
|
2818
3098
|
}
|
|
3099
|
+
const candidate = path.resolve(directory);
|
|
2819
3100
|
try {
|
|
2820
3101
|
fs.rmSync(candidate, { recursive: true, force: true });
|
|
2821
3102
|
managedTempDirsForExitCleanup.delete(directory);
|
|
@@ -2827,7 +3108,7 @@ export class CodexRunner {
|
|
|
2827
3108
|
managedTempInstruction(sessionId) {
|
|
2828
3109
|
if (!this.ensureSessionTempDir(sessionId))
|
|
2829
3110
|
return undefined;
|
|
2830
|
-
return '临时文件必须使用环境变量 `$TMPDIR`。它指向当前 EvolCore 会话的受管临时工作区;不要直接使用 `/tmp
|
|
3111
|
+
return '临时文件必须使用环境变量 `$TMPDIR`。它指向当前 EvolCore 会话的受管临时工作区;不要直接使用 `/tmp`,也不要使用其他会话的临时目录。当前是可持久化 thread 的恢复轮次,早先上下文中的 `/tmp/evolcore-codex-*` 绝对路径可能已经失效;不要重用旧路径。若文件不存在,请先在当前 `$TMPDIR` 重新创建,再继续执行后续 `ec` 命令。';
|
|
2831
3112
|
}
|
|
2832
3113
|
buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrier) {
|
|
2833
3114
|
const managedTempDir = this.ensureSessionTempDir(sessionId);
|
|
@@ -2877,6 +3158,8 @@ export class CodexRunner {
|
|
|
2877
3158
|
this.pendingInterrupts.clear();
|
|
2878
3159
|
this.permissionContexts.clear();
|
|
2879
3160
|
this.chatModes.clear();
|
|
3161
|
+
this.pruneThreadOperationLocks();
|
|
3162
|
+
this.threadOperationLocks.clear();
|
|
2880
3163
|
for (const sessionId of [...this.sessionTempDirs.keys()])
|
|
2881
3164
|
this.cleanupSessionTempDir(sessionId);
|
|
2882
3165
|
await this.appServerClient?.close();
|