evolcore 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/bin/codex-managed-hook.mjs +4 -1
  3. package/bin/install-codex-managed-hooks.mjs +201 -0
  4. package/dist/agents/claude-runner.js +53 -5
  5. package/dist/agents/codex-app-server-client.js +123 -2
  6. package/dist/agents/codex-runner.js +149 -30
  7. package/dist/agents/ecagent-runner.js +17 -1
  8. package/dist/agents/gemini-runner.js +9 -4
  9. package/dist/aun/msg/managed-operation.js +63 -3
  10. package/dist/channels/aun.js +144 -15
  11. package/dist/channels/daemon.js +2 -0
  12. package/dist/cli/aun-commands.js +1 -1
  13. package/dist/cli/fs-command.js +46 -9
  14. package/dist/cli/task-context.js +172 -0
  15. package/dist/config/builtin-roles.js +2 -0
  16. package/dist/config/config-manager.js +6 -2
  17. package/dist/config/contact-book-store.js +7 -2
  18. package/dist/core/auth/auth-gateway.js +1 -0
  19. package/dist/core/auth/authorization-audit.js +32 -0
  20. package/dist/core/auth/operation-catalog.js +3 -3
  21. package/dist/core/bootstrap-service.js +7 -1
  22. package/dist/core/command/command-handler.js +3 -0
  23. package/dist/core/event-catalog.js +2 -0
  24. package/dist/core/message/im-renderer.js +15 -1
  25. package/dist/core/message/message-bridge.js +5 -2
  26. package/dist/core/message/response-engine.js +138 -10
  27. package/dist/core/permission/ec-command-parser.js +556 -4
  28. package/dist/core/permission/tool-policy.js +17 -29
  29. package/dist/core/runtime-lock.js +101 -0
  30. package/dist/index.js +30 -3
  31. package/dist/response-system/engines/v1/proactive-flow.js +92 -8
  32. package/dist/response-system/modes/single-session/index.js +3 -0
  33. package/dist/trigger/history.js +42 -7
  34. package/dist/utils/error-utils.js +7 -0
  35. package/dist/utils/logger.js +37 -4
  36. package/kits/templates/roles/admin.json +2 -0
  37. package/kits/templates/roles/member.json +1 -0
  38. package/package.json +1 -1
@@ -13,15 +13,17 @@ import { CodexAppServerClient } from './codex-app-server-client.js';
13
13
  import { resolveOpenaiConfig } from './baseagent.js';
14
14
  import { logger } from '../utils/logger.js';
15
15
  import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
16
+ import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
16
17
  import { isRetryableError } from '../utils/error-utils.js';
17
18
  import { renderActionAsText } from '../core/interaction-router.js';
18
19
  import { buildEnvelope, sendInteractionPayload } from '../core/message/message-utils.js';
19
20
  import { resolveCodexCapabilityThreadConfigForProject } from '../core/capability/capability-manager.js';
20
21
  import { AGENT_DELEGATION_TOKEN_ENV, hashDelegatedCommandArgv } from '../core/auth/agent-delegation.js';
21
22
  import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
22
- import { classifyEvolcoreShellCommand, parseLiteralShellArgv, unwrapCodexShellCommandArgv } from '../core/permission/ec-command-parser.js';
23
+ import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, parseLiteralShellArgv, unwrapCodexShellCommandArgv, } from '../core/permission/ec-command-parser.js';
23
24
  import { compareVersions } from '../utils/npm-ops.js';
24
25
  import { resolvePaths, resolveRoot } from '../paths.js';
26
+ import { ensureProcessManagedTempDir } from '../cli/task-context.js';
25
27
  import { buildSessionTurnList } from '../core/session/session-turns.js';
26
28
  import { execFileSync } from 'child_process';
27
29
  import { execCodexCliSync, resolveCodexCliPath } from '../utils/codex-cli.js';
@@ -151,7 +153,7 @@ const CODEX_DELEGATION_CARRIER_METADATA_KEY = '__codexDelegationCarrierV1';
151
153
  let codexCatalogCache = null;
152
154
  // The permission bridge is version-locked to the audited approval schema and
153
155
  // managed PreToolUse hook behavior used by proactive first-tool enforcement.
154
- export const MIN_CODEX_CLI_VERSION = '0.145.0';
156
+ export const MIN_CODEX_CLI_VERSION = '0.144.1';
155
157
  export function parseCodexCliVersion(output) {
156
158
  const match = output.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/);
157
159
  return match?.[1] ?? null;
@@ -326,7 +328,7 @@ export class CodexRunner {
326
328
  allowManagedTmpDir: true,
327
329
  ...(managedTempDir ? { managedTempDir } : {}),
328
330
  });
329
- if (argv?.some(value => value.includes('$TMPDIR')))
331
+ if (argv && containsLiteralManagedTmpDirOutsideSendContent(argv))
330
332
  return undefined;
331
333
  return argv?.[0] === 'ec' ? argv : undefined;
332
334
  }
@@ -335,8 +337,12 @@ export class CodexRunner {
335
337
  && toolInput.commandArgv.every(value => typeof value === 'string')
336
338
  ? toolInput.commandArgv
337
339
  : undefined;
338
- if (explicitArgv?.[0] === 'ec')
339
- return explicitArgv;
340
+ if (explicitArgv?.[0] === 'ec') {
341
+ return explicitArgv.some(value => value.includes('\0'))
342
+ || containsLiteralManagedTmpDirOutsideSendContent(explicitArgv)
343
+ ? undefined
344
+ : explicitArgv;
345
+ }
340
346
  const explicitWrappedCommand = explicitArgv
341
347
  ? unwrapCodexShellCommandArgv(explicitArgv)
342
348
  : undefined;
@@ -358,17 +364,34 @@ export class CodexRunner {
358
364
  ? undefined
359
365
  : this.approvedLiteralEvolcoreCommandArgv(wrappedCommand, managedTempDir);
360
366
  }
361
- armApprovedDelegationCommand(sessionId, toolInput) {
367
+ isManagedEvolcoreCommandIntent(toolInput) {
368
+ if (Array.isArray(toolInput.commandArgv)) {
369
+ const rawArgv = toolInput.commandArgv;
370
+ if (rawArgv[0] === 'ec')
371
+ return true;
372
+ if (!rawArgv.every(value => typeof value === 'string'))
373
+ return false;
374
+ const wrappedCommand = unwrapCodexShellCommandArgv(rawArgv);
375
+ return wrappedCommand !== undefined
376
+ && classifyEvolcoreShellCommand(wrappedCommand).kind !== 'none';
377
+ }
378
+ const command = typeof toolInput.command === 'string' ? toolInput.command : '';
379
+ return classifyEvolcoreShellCommand(command).kind !== 'none';
380
+ }
381
+ armApprovedDelegationCommand(sessionId, argv) {
362
382
  const threadId = this.activeSessions.get(sessionId);
363
383
  const carrierToken = threadId ? this.threadDelegationCarriers.get(threadId) : undefined;
364
- const argv = this.approvedEvolcoreCommandArgv(toolInput, this.getManagedTempDir(sessionId));
365
- if (!carrierToken || !argv)
366
- return false;
367
384
  const commandHash = hashDelegatedCommandArgv(argv);
368
385
  const arm = this.permissionContexts.get(sessionId)?.armApprovedDelegationCommand;
369
- if (!commandHash || !arm)
386
+ if (!carrierToken || !commandHash || !arm)
387
+ return false;
388
+ try {
389
+ return arm(carrierToken, commandHash);
390
+ }
391
+ catch (error) {
392
+ logger.warn(`[CodexRunner] delegation arm callback failed: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
370
393
  return false;
371
- return arm(carrierToken, commandHash);
394
+ }
372
395
  }
373
396
  async readThreadDelegationCarrier(appServer, threadId) {
374
397
  try {
@@ -792,6 +815,8 @@ export class CodexRunner {
792
815
  ...(mode !== 'readonly' && managedTempDir ? {
793
816
  [managedTempDir]: 'write',
794
817
  [path.join(managedTempDir, '**')]: 'write',
818
+ [path.join(path.dirname(managedTempDir), 'evolcore-locks')]: 'write',
819
+ [path.join(path.dirname(managedTempDir), 'evolcore-locks', '**')]: 'write',
795
820
  } : {}),
796
821
  },
797
822
  network,
@@ -831,6 +856,17 @@ export class CodexRunner {
831
856
  }
832
857
  setSendPrompt(fn) { this.sendPromptFn = fn; }
833
858
  setPermissionContext(sessionId, context) { this.permissionContexts.set(sessionId, context); }
859
+ async assertProactiveHookAvailability(sessionId, appServer) {
860
+ if (this.permissionContexts.get(sessionId)?.chatmode !== 'proactive')
861
+ return;
862
+ if (process.platform === 'win32') {
863
+ await appServer.assertManagedHooksAvailable();
864
+ return;
865
+ }
866
+ if (process.platform !== 'linux') {
867
+ throw new Error('Codex proactive managed PreToolUse enforcement requires Linux or an installed Windows managed hook');
868
+ }
869
+ }
834
870
  async evaluatePreToolUse(threadId, toolName, toolInput) {
835
871
  const sessionKey = this.findSessionKeyByThread(threadId);
836
872
  const activeThread = this.activeSessions.get(sessionKey);
@@ -995,9 +1031,6 @@ export class CodexRunner {
995
1031
  }
996
1032
  async runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
997
1033
  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
- }
1001
1034
  const resumingThread = !!agentSessionId;
1002
1035
  const callModel = modelOverride?.model || this.model;
1003
1036
  const callEffort = modelOverride?.effortMode === 'model_default'
@@ -1017,6 +1050,7 @@ export class CodexRunner {
1017
1050
  const effectiveApprovalPolicy = callApprovalPolicy;
1018
1051
  const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
1019
1052
  const externalToolConfig = await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, callMode);
1053
+ await this.assertProactiveHookAvailability(sessionId, appServer);
1020
1054
  const managedTempInstruction = this.managedTempInstruction(sessionId);
1021
1055
  const developerInstructions = [systemPromptAppend, managedTempInstruction].filter(Boolean).join('\n\n') || undefined;
1022
1056
  let knownDelegationCarrier = agentSessionId
@@ -1086,7 +1120,7 @@ export class CodexRunner {
1086
1120
  const controller = new AbortController();
1087
1121
  this.activeAbortControllers.set(sessionId, controller);
1088
1122
  const tempFiles = [];
1089
- const input = this.buildAppServerInput(prompt, images, tempFiles);
1123
+ const input = this.buildAppServerInput(prompt, images, tempFiles, sessionId, runtimeEnv);
1090
1124
  const queue = new AsyncEventQueue();
1091
1125
  controller.signal.addEventListener('abort', () => queue.end(), { once: true });
1092
1126
  const state = {
@@ -1301,6 +1335,7 @@ export class CodexRunner {
1301
1335
  const executionSandbox = this.buildExecutionSandboxOptions(_sessionId, compactMode, _projectPath);
1302
1336
  const capabilityConfig = await this.resolveCapabilityThreadConfig(_projectPath);
1303
1337
  const externalToolConfig = await this.resolveExternalToolApprovalConfig(appServer, _projectPath, capabilityConfig, compactMode);
1338
+ await this.assertProactiveHookAvailability(_sessionId, appServer);
1304
1339
  const compactDelegationCarrier = this.threadDelegationCarriers.get(agentSessionId)
1305
1340
  ?? this.createDelegationCarrier();
1306
1341
  await appServer.threadResume(agentSessionId, _projectPath, {
@@ -1464,6 +1499,8 @@ export class CodexRunner {
1464
1499
  const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
1465
1500
  const appServer = this.getAppServerClient();
1466
1501
  const externalToolConfig = await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, mode);
1502
+ if (sessionKey)
1503
+ await this.assertProactiveHookAvailability(sessionKey, appServer);
1467
1504
  const delegationCarrier = this.createDelegationCarrier();
1468
1505
  const response = await appServer.threadFork(agentSessionId, projectPath, title, {
1469
1506
  model: this.model,
@@ -1656,31 +1693,77 @@ export class CodexRunner {
1656
1693
  catch (error) {
1657
1694
  logger.warn(`[CodexRunner] failed to record policy-hook denial: session=${sessionKey} tool=${toolName} error=${error instanceof Error ? error.message : String(error)}`);
1658
1695
  }
1659
- return this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
1696
+ const response = this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
1697
+ this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'policy', policyResult.policyCode ?? 'session_policy_hook');
1698
+ return response;
1660
1699
  }
1661
1700
  const summary = this.summarizeAppServerRequest(request.method, params);
1662
1701
  const reason = params.reason || params.decisionReason || undefined;
1663
1702
  const workspacePath = this.resolvePermissionWorkspacePath(params);
1664
1703
  if (!workspacePath) {
1665
1704
  logger.warn(`[CodexRunner] approval denied because thread workspace is unknown: method=${request.method} thread=${params.threadId ?? params.conversationId ?? '<missing>'}`);
1666
- return this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
1705
+ const response = this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
1706
+ this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'infrastructure', 'approval_workspace_unknown');
1707
+ return response;
1667
1708
  }
1668
1709
  const operationCwd = this.resolvePermissionOperationCwd(params, workspacePath, toolName);
1669
1710
  const sessionMode = this.chatModes.get(sessionKey) ?? this.currentMode;
1670
1711
  logger.info(`[CodexRunner] app-server approval request id=${request.id} method=${request.method} session=${sessionKey} mode=${sessionMode} tool=${toolName} summary=${summary}`);
1712
+ const isCommandApproval = request.method === 'item/commandExecution/requestApproval'
1713
+ || request.method === 'execCommandApproval';
1714
+ const managedEcIntent = isCommandApproval && this.isManagedEvolcoreCommandIntent(toolInput);
1715
+ const approvedCommand = isCommandApproval
1716
+ ? this.approvedEvolcoreCommandArgv(toolInput, this.getManagedTempDir(sessionKey))
1717
+ : undefined;
1718
+ const denyInfrastructure = async (policyCode, message) => {
1719
+ logger.warn(`[CodexRunner] ${message}: session=${sessionKey} requestId=${request.id ?? '<missing>'}`);
1720
+ try {
1721
+ await permissionContext?.recordExecutionAnomaly?.({
1722
+ code: 'operation_blocked',
1723
+ severity: 'warning',
1724
+ phase: 'execution',
1725
+ occurredAt: Date.now(),
1726
+ toolName,
1727
+ ...(request.id !== undefined ? { requestId: String(request.id) } : {}),
1728
+ policyCode,
1729
+ decisionSource: 'infrastructure',
1730
+ agentAid: permissionContext?.selfAid,
1731
+ sessionId: sessionKey,
1732
+ permissionMode: normalizePermissionMode(sessionMode).mode,
1733
+ summary: summarizeToolInputForAudit(toolName, toolInput).slice(0, 512),
1734
+ effect: 'operation_skipped',
1735
+ });
1736
+ }
1737
+ catch (error) {
1738
+ logger.warn(`[CodexRunner] failed to record infrastructure approval denial: session=${sessionKey} code=${policyCode} error=${error instanceof Error ? error.message : String(error)}`);
1739
+ }
1740
+ const response = this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
1741
+ this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'infrastructure', policyCode);
1742
+ return response;
1743
+ };
1744
+ if (managedEcIntent && !approvedCommand) {
1745
+ return await denyInfrastructure('ec_command_not_canonical', 'EC command approval denied because no canonical argv could be derived');
1746
+ }
1671
1747
  try {
1672
1748
  const decision = await this.resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath, operationCwd, request.id);
1673
- if (decision !== 'deny'
1674
- && (request.method === 'item/commandExecution/requestApproval' || request.method === 'execCommandApproval')
1675
- && this.approvedEvolcoreCommandArgv(toolInput, this.getManagedTempDir(sessionKey))) {
1676
- if (!this.armApprovedDelegationCommand(sessionKey, toolInput)) {
1677
- // Approval policy and delegation authority are separate boundaries.
1678
- // If no ticket can be armed, the CLI still fails closed at IPC token
1679
- // validation instead of changing an otherwise valid tool decision.
1680
- logger.warn(`[CodexRunner] EC command approved without a task-bound delegation ticket: session=${sessionKey}`);
1749
+ if (decision !== 'deny' && managedEcIntent && approvedCommand) {
1750
+ const carrierThreadIds = [
1751
+ this.activeSessions.get(sessionKey),
1752
+ typeof params.threadId === 'string' ? params.threadId : undefined,
1753
+ typeof params.conversationId === 'string' ? params.conversationId : undefined,
1754
+ ];
1755
+ const hasDelegationCarrier = carrierThreadIds.some(threadId => !!threadId && this.threadDelegationCarriers.has(threadId));
1756
+ const canArmDelegation = typeof permissionContext?.armApprovedDelegationCommand === 'function';
1757
+ // A carrier-bearing thread is managed even if its permission context
1758
+ // was not wired with an arm callback; it must fail closed here rather
1759
+ // than reaching daemon IPC with an unarmed carrier.
1760
+ if ((hasDelegationCarrier || canArmDelegation)
1761
+ && !this.armApprovedDelegationCommand(sessionKey, approvedCommand)) {
1762
+ return await denyInfrastructure('delegation_not_armed', 'EC command approval failed to arm delegation');
1681
1763
  }
1682
1764
  }
1683
1765
  const response = this.toAppServerApprovalResponse(request.method, decision, toolInput, unattended);
1766
+ this.logAppServerApprovalAudit(request, sessionKey, toolName, decision === 'deny' ? 'deny' : 'allow', decision === 'deny' ? (sessionMode === 'auto' || sessionMode === 'readonly' ? 'policy' : 'approval') : 'approval');
1684
1767
  logger.info(`[CodexRunner] app-server approval response id=${request.id} method=${request.method} decision=${decision} response=${JSON.stringify(response)}`);
1685
1768
  return response;
1686
1769
  }
@@ -1690,6 +1773,18 @@ export class CodexRunner {
1690
1773
  throw error;
1691
1774
  }
1692
1775
  }
1776
+ logAppServerApprovalAudit(request, sessionKey, toolName, decision, decisionSource, policyCode) {
1777
+ logger.info(`[CodexRunner] app-server approval audit ${JSON.stringify({
1778
+ requestId: request.id !== undefined ? String(request.id) : undefined,
1779
+ method: request.method,
1780
+ sessionId: sessionKey,
1781
+ toolName,
1782
+ decision,
1783
+ decisionSource,
1784
+ ...(policyCode ? { policyCode } : {}),
1785
+ executed: false,
1786
+ })}`);
1787
+ }
1693
1788
  classifyMcpApprovalLabel(label) {
1694
1789
  const normalized = label.trim().toLowerCase().replace(/[\s_-]+/g, ' ');
1695
1790
  const negative = /\b(decline|deny|reject|cancel|no|block|stop)\b|do not|don't|not\s+(?:accept|approve|allow)|拒绝|不同意|不允许|取消|否|停止/i.test(normalized);
@@ -2221,6 +2316,22 @@ export class CodexRunner {
2221
2316
  const rawMode = this.chatModes.get(sessionKey) ?? this.currentMode;
2222
2317
  const mode = normalizePermissionMode(rawMode).mode;
2223
2318
  const recordBlockedOperation = async (policyCode, operationInput = toolInput) => {
2319
+ const summary = summarizeToolInputForAudit(toolName, operationInput).slice(0, 512);
2320
+ auditToolPreflightDenial({
2321
+ toolName,
2322
+ policyCode,
2323
+ reason: 'policy denied',
2324
+ summary,
2325
+ sessionId: sessionKey,
2326
+ agentAid: permissionContext?.selfAid,
2327
+ permissionMode: mode,
2328
+ channel: permissionContext?.channel,
2329
+ actorId: permissionContext?.userId,
2330
+ role: permissionContext?.role,
2331
+ selfAid: permissionContext?.selfAid,
2332
+ requestId: typeof requestId === 'string' || typeof requestId === 'number' ? String(requestId) : undefined,
2333
+ taskId: permissionContext?.taskId,
2334
+ });
2224
2335
  try {
2225
2336
  await permissionContext?.recordExecutionAnomaly?.({
2226
2337
  code: 'operation_blocked',
@@ -2233,7 +2344,7 @@ export class CodexRunner {
2233
2344
  decisionSource: 'policy',
2234
2345
  agentAid: permissionContext?.selfAid,
2235
2346
  permissionMode: mode,
2236
- summary: summarizeToolInputForAudit(toolName, operationInput).slice(0, 512),
2347
+ summary,
2237
2348
  effect: 'operation_skipped',
2238
2349
  });
2239
2350
  }
@@ -2525,11 +2636,15 @@ export class CodexRunner {
2525
2636
  this.onCompactStart = callback;
2526
2637
  }
2527
2638
  // ── Event stream transformation ──
2528
- buildAppServerInput(prompt, images, tempFiles) {
2639
+ buildAppServerInput(prompt, images, tempFiles, sessionId, runtimeEnv) {
2529
2640
  const input = [{ type: 'text', text: prompt, text_elements: [] }];
2530
2641
  if (!images?.length)
2531
2642
  return input;
2532
- const tmpDir = os.tmpdir();
2643
+ const tmpDir = runtimeEnv?.EVOLCORE_SESSION_RUNTIME_DIR
2644
+ ?? (sessionId ? this.getManagedTempDir(sessionId) : undefined)
2645
+ ?? process.env.TMPDIR;
2646
+ if (!tmpDir || !path.isAbsolute(tmpDir))
2647
+ throw new Error('managed TMPDIR is unset or not absolute');
2533
2648
  for (let i = 0; i < images.length; i++) {
2534
2649
  const img = images[i];
2535
2650
  const ext = MIME_EXT[img.mimeType || ''] || '.jpg';
@@ -3018,7 +3133,8 @@ export class CodexRunner {
3018
3133
  }
3019
3134
  managedTempParent() {
3020
3135
  const uid = typeof process.getuid === 'function' ? process.getuid() : 'user';
3021
- return path.join(resolveProtectedCandidate(os.tmpdir()), `evolcore-codex-${uid}`);
3136
+ const base = ensureProcessManagedTempDir();
3137
+ return path.join(resolveProtectedCandidate(base), `evolcore-codex-${uid}`);
3022
3138
  }
3023
3139
  ensurePrivateDirectory(directory) {
3024
3140
  try {
@@ -3055,6 +3171,7 @@ export class CodexRunner {
3055
3171
  try {
3056
3172
  const parent = this.managedTempParent();
3057
3173
  this.ensurePrivateDirectory(parent);
3174
+ this.ensurePrivateDirectory(path.join(parent, 'evolcore-locks'));
3058
3175
  // Include a per-runner nonce so a daemon restart never reuses contents
3059
3176
  // left behind by an unclean shutdown for the same EvolCore session id.
3060
3177
  const digest = createHash('sha256')
@@ -3078,7 +3195,7 @@ export class CodexRunner {
3078
3195
  return isTrustedManagedTempDirectory(directory, this.managedTempParent());
3079
3196
  }
3080
3197
  getManagedTempDir(sessionId) {
3081
- const directory = this.sessionTempDirs.get(sessionId);
3198
+ const directory = this.ensureSessionTempDir(sessionId);
3082
3199
  if (!directory || !this.isTrustedSessionTempDir(directory))
3083
3200
  return undefined;
3084
3201
  return resolveProtectedCandidate(directory);
@@ -3117,6 +3234,8 @@ export class CodexRunner {
3117
3234
  set: {
3118
3235
  EVOLCORE_SESSION_ID: sessionId,
3119
3236
  EVOLCORE_HOME: resolveRoot(),
3237
+ ...(managedTempDir ? { EVOLCORE_SESSION_RUNTIME_DIR: managedTempDir } : {}),
3238
+ ...(managedTempDir ? { EVOLCORE_RUNTIME_LOCK_DIR: path.join(path.dirname(managedTempDir), 'evolcore-locks') } : {}),
3120
3239
  ...(delegationCarrier ? { [AGENT_DELEGATION_TOKEN_ENV]: delegationCarrier } : {}),
3121
3240
  ...(managedTempDir ? { TMPDIR: managedTempDir } : {}),
3122
3241
  },
@@ -10,6 +10,7 @@ import { normalizePermissionMode } from '../core/permission/mode.js';
10
10
  import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
11
11
  import { logger } from '../utils/logger.js';
12
12
  import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
13
+ import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
13
14
  import { resolveEcagentConfig } from './baseagent.js';
14
15
  import { buildModelRequestHeaders } from './request-identity.js';
15
16
  const PROVIDER_ID = 'evolcore-gateway';
@@ -959,6 +960,21 @@ export class EcagentRunner {
959
960
  async authorizeTool(sessionId, projectPath, mode, toolName, input) {
960
961
  const permissionContext = this.permissionContexts.get(sessionId);
961
962
  const recordBlockedOperation = async (policyCode, operationInput = input) => {
963
+ const summary = summarizeToolInputForAudit(toolName, operationInput).slice(0, 512);
964
+ auditToolPreflightDenial({
965
+ toolName,
966
+ policyCode,
967
+ reason: 'policy denied',
968
+ summary,
969
+ sessionId,
970
+ agentAid: permissionContext?.selfAid,
971
+ permissionMode: mode,
972
+ channel: permissionContext?.channel,
973
+ actorId: permissionContext?.userId,
974
+ role: permissionContext?.role,
975
+ selfAid: permissionContext?.selfAid,
976
+ taskId: permissionContext?.taskId,
977
+ });
962
978
  try {
963
979
  await permissionContext?.recordExecutionAnomaly?.({
964
980
  code: 'operation_blocked',
@@ -970,7 +986,7 @@ export class EcagentRunner {
970
986
  decisionSource: 'policy',
971
987
  agentAid: permissionContext?.selfAid,
972
988
  permissionMode: mode,
973
- summary: summarizeToolInputForAudit(toolName, operationInput).slice(0, 512),
989
+ summary,
974
990
  effect: 'operation_skipped',
975
991
  });
976
992
  }
@@ -279,11 +279,16 @@ export class GeminiRunner {
279
279
  const requestedPermissionMode = modelOverride?.permissionMode || this.currentMode;
280
280
  const permissionProfile = resolveGeminiPermissionProfile(requestedPermissionMode);
281
281
  const callModel = modelOverride?.model || this.model;
282
+ const runtimeDir = runtimeEnv?.EVOLCORE_SESSION_RUNTIME_DIR;
283
+ const managedTmpDir = runtimeDir ?? process.env.TMPDIR;
284
+ if (!managedTmpDir || !path.isAbsolute(managedTmpDir))
285
+ throw new Error('managed TMPDIR is unset or not absolute');
286
+ const runtimeLockDir = runtimeEnv?.EVOLCORE_RUNTIME_LOCK_DIR;
282
287
  const geminiHome = path.join(os.homedir(), '.gemini');
283
288
  fs.mkdirSync(geminiHome, { recursive: true, mode: 0o700 });
284
289
  const sandboxProbe = buildBubblewrapCommand(this.resolved.cliPath, [], {
285
290
  projectPath,
286
- writablePaths: [geminiHome],
291
+ writablePaths: [geminiHome, ...(runtimeDir ? [runtimeDir] : []), ...(runtimeLockDir ? [runtimeLockDir] : [])],
287
292
  });
288
293
  if (!sandboxProbe && workspaceContainsHClassPaths(projectPath)) {
289
294
  throw new Error('Gemini 缺少可用的路径级隔离运行时,且项目覆盖 EvolCore 受保护根;已拒绝启动本轮任务');
@@ -303,13 +308,13 @@ export class GeminiRunner {
303
308
  if (hasStandardGeminiAdminPolicy()) {
304
309
  throw new Error('Gemini 系统级 admin policy 会忽略 EvolCore 的临时安全策略,已拒绝启动本轮任务');
305
310
  }
306
- const policyPath = path.join(os.tmpdir(), `evolcore-gemini-permission-${crypto.randomUUID()}.toml`);
311
+ const policyPath = path.join(managedTmpDir, `evolcore-gemini-permission-${crypto.randomUUID()}.toml`);
307
312
  fs.writeFileSync(policyPath, buildGeminiAdminPolicy(permissionProfile), { mode: 0o600, flag: 'wx' });
308
313
  tempFiles.push(policyPath);
309
314
  args.push(...buildGeminiPermissionArgs(permissionProfile, policyPath, !!sandboxProbe));
310
315
  }
311
316
  if (images?.length) {
312
- const tmpDir = os.tmpdir();
317
+ const tmpDir = managedTmpDir;
313
318
  const fileParts = [];
314
319
  for (let i = 0; i < images.length; i++) {
315
320
  const img = images[i];
@@ -337,7 +342,7 @@ export class GeminiRunner {
337
342
  const env = this.buildAgentEnv(sessionId, runtimeEnv);
338
343
  const sandboxedCommand = buildBubblewrapCommand(this.resolved.cliPath, args, {
339
344
  projectPath,
340
- writablePaths: [geminiHome],
345
+ writablePaths: [geminiHome, ...(runtimeDir ? [runtimeDir] : []), ...(runtimeLockDir ? [runtimeLockDir] : [])],
341
346
  readonlyPaths: tempFiles,
342
347
  });
343
348
  const child = spawn(sandboxedCommand?.command ?? this.resolved.cliPath, sandboxedCommand?.args ?? args, {
@@ -561,7 +561,7 @@ export class ManagedFsOperationError extends Error {
561
561
  }
562
562
  const FS_VALUE_FLAGS = new Set([
563
563
  '--as', '--format', '--max-bytes', '--head-bytes', '--name', '--pattern', '--type', '--node-type',
564
- '--size', '--mtime', '--page', '--page-size',
564
+ '--size', '--mtime', '--page', '--page-size', '-m', '-x',
565
565
  ]);
566
566
  const FS_BOOLEAN_FLAGS = new Set(['-p', '--parents', '-r', '--recursive', '-f', '--force', '--overwrite']);
567
567
  function failFs(code, reason) {
@@ -599,7 +599,7 @@ function parseRemote(value, allowEmpty) {
599
599
  export function resolveManagedFsOperation(argv) {
600
600
  const raw = argv[0] === 'fs' ? argv.slice(1) : argv;
601
601
  const rawKind = raw[0];
602
- if (!rawKind || !['ls', 'stat', 'lstat', 'cat', 'find', 'df', 'mkdir', 'cp', 'mv', 'rm'].includes(rawKind)) {
602
+ if (!rawKind || !['ls', 'stat', 'lstat', 'cat', 'find', 'df', 'getfacl', 'setfacl', 'mkdir', 'cp', 'mv', 'rm'].includes(rawKind)) {
603
603
  return failFs('NOT_ALLOWED', `unsupported managed fs command: ${rawKind || '<missing>'}`);
604
604
  }
605
605
  const values = new Map();
@@ -652,9 +652,10 @@ export function resolveManagedFsOperation(argv) {
652
652
  find: new Set(['--as', '--format', '--name', '--pattern', '--type', '--node-type', '--size', '--mtime', '--page', '--page-size']),
653
653
  df: new Set(['--as', '--format']), mkdir: new Set(['--as', '--format']), cp: new Set(['--as', '--format']),
654
654
  mv: new Set(['--as', '--format']), rm: new Set(['--as', '--format']),
655
+ getfacl: new Set(['--as', '--format']), setfacl: new Set(['--as', '--format', '-m', '-x']),
655
656
  };
656
657
  const allowedBoolean = {
657
- ls: new Set(), stat: new Set(), lstat: new Set(), cat: new Set(), find: new Set(), df: new Set(),
658
+ ls: new Set(), stat: new Set(), lstat: new Set(), cat: new Set(), find: new Set(), df: new Set(), getfacl: new Set(), setfacl: new Set(),
658
659
  mkdir: new Set(['-p', '--parents']), cp: new Set(['-r', '--recursive', '-f', '--force', '--overwrite']),
659
660
  mv: new Set(['-f', '--force', '--overwrite']), rm: new Set(['-r', '--recursive', '-f', '--force', '--overwrite']),
660
661
  };
@@ -667,6 +668,35 @@ export function resolveManagedFsOperation(argv) {
667
668
  const overwrite = booleanFlags.has('-f') || booleanFlags.has('--force') || booleanFlags.has('--overwrite');
668
669
  const recursive = booleanFlags.has('-r') || booleanFlags.has('--recursive');
669
670
  const remoteOrError = (value, allowEmpty = false) => parseRemote(value, allowEmpty);
671
+ if (rawKind === 'getfacl') {
672
+ if (values.has('-m') || values.has('-x'))
673
+ return failFs('NOT_ALLOWED', 'getfacl does not accept ACL mutation flags');
674
+ const remote = remoteOrError(positionals[0]);
675
+ if (!('groupId' in remote))
676
+ return remote;
677
+ return { ok: true, command: {
678
+ kind: 'getfacl', operationId: 'ec.fs.getfacl', canonicalArgv: ['fs', 'getfacl', positionals[0], ...(actorAid ? ['--as', actorAid] : [])],
679
+ actorAid, groupId: remote.groupId, remotePath: remote.remotePath,
680
+ } };
681
+ }
682
+ if (rawKind === 'setfacl') {
683
+ const modify = values.get('-m');
684
+ const remove = values.get('-x');
685
+ if (!!modify === !!remove)
686
+ return failFs('INVALID_ARGUMENT', 'setfacl requires exactly one of -m or -x');
687
+ const spec = modify ?? remove ?? '';
688
+ const parsedAcl = parseManagedGroupAclSpec(spec, modify !== undefined);
689
+ if (!('grantee' in parsedAcl))
690
+ return parsedAcl;
691
+ const remote = remoteOrError(positionals[0]);
692
+ if (!('groupId' in remote))
693
+ return remote;
694
+ return { ok: true, command: {
695
+ kind: 'setfacl', operationId: 'ec.fs.setfacl', canonicalArgv: ['fs', 'setfacl', positionals[0], modify !== undefined ? '-m' : '-x', spec, ...(actorAid ? ['--as', actorAid] : [])],
696
+ actorAid, groupId: remote.groupId, remotePath: remote.remotePath,
697
+ aclGrantee: parsedAcl.grantee, ...(parsedAcl.perms ? { aclPerms: parsedAcl.perms } : {}), aclRemove: remove !== undefined,
698
+ } };
699
+ }
670
700
  if (rawKind === 'cp') {
671
701
  const destination = remoteOrError(positionals[1]);
672
702
  if (!('groupId' in destination))
@@ -735,6 +765,26 @@ export function resolveManagedFsOperation(argv) {
735
765
  name: values.get('--name') ?? values.get('--pattern'), nodeType, size: values.get('--size'), mtime: values.get('--mtime'), page, pageSize,
736
766
  } };
737
767
  }
768
+ function parseManagedGroupAclSpec(value, requirePerms) {
769
+ const parts = value.split(':');
770
+ if (parts[0] !== 'role' || !parts[1] || parts.length !== (requirePerms ? 3 : 2)) {
771
+ return failFs('INVALID_ARGUMENT', 'group ACL entry must use role:member:<perms>, role:admin:<perms>, or role:<role>');
772
+ }
773
+ const grantee = `role:${parts[1]}`;
774
+ if (grantee !== 'role:member' && grantee !== 'role:admin') {
775
+ return failFs('INVALID_ARGUMENT', 'group ACL only supports role:member or role:admin');
776
+ }
777
+ if (!requirePerms)
778
+ return { grantee };
779
+ const perms = parts[2];
780
+ if (!perms || !/^(?:r|rw|rwx)$/.test(perms)) {
781
+ return failFs('INVALID_ARGUMENT', 'group ACL permissions must be r, rw, or rwx');
782
+ }
783
+ if (grantee === 'role:member' && perms !== 'rw') {
784
+ return failFs('INVALID_ARGUMENT', 'role:member may only receive rw group ACL permissions');
785
+ }
786
+ return { grantee, perms };
787
+ }
738
788
  function numberFrom(value, keys) {
739
789
  for (const key of keys) {
740
790
  const number = Number(value?.[key]);
@@ -823,6 +873,16 @@ export async function executeManagedFsOperation(command) {
823
873
  }
824
874
  case 'df':
825
875
  return { ok: true, command: 'df', path: `${command.groupId}:`, usage: await client.group.fs.df(`${command.groupId}:`) };
876
+ case 'getfacl':
877
+ return { ok: true, command: 'getfacl', path: ref, result: await client.group.fs.getAcl(ref) };
878
+ case 'setfacl': {
879
+ if (!command.aclGrantee)
880
+ throw new ManagedFsOperationError('INVALID_ACL', 'group ACL grantee is required');
881
+ const result = command.aclRemove
882
+ ? await client.group.fs.removeAcl(ref, { granteeAid: command.aclGrantee })
883
+ : await client.group.fs.setAcl(ref, { granteeAid: command.aclGrantee, perms: command.aclPerms });
884
+ return { ok: true, command: 'setfacl', path: ref, grantee: command.aclGrantee, removed: !!command.aclRemove, result };
885
+ }
826
886
  case 'cat': {
827
887
  const node = await client.group.fs.stat(ref);
828
888
  const size = numberFrom(node, ['size', 'size_bytes', 'bytes']);