evolcore 0.0.10 → 0.0.12

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 (131) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +3 -3
  3. package/bin/codex-managed-hook.mjs +80 -0
  4. package/dist/agents/baseagent.js +4 -0
  5. package/dist/agents/claude-runner.js +158 -44
  6. package/dist/agents/codex-app-server-client.js +153 -23
  7. package/dist/agents/codex-runner.js +331 -43
  8. package/dist/agents/ecagent-runner.js +53 -6
  9. package/dist/agents/gemini-runner.js +1 -0
  10. package/dist/agents/request-identity.js +55 -0
  11. package/dist/aun/aid/client.js +11 -21
  12. package/dist/aun/aid/managed-operation.js +107 -0
  13. package/dist/aun/msg/group.js +81 -12
  14. package/dist/aun/msg/managed-operation.js +804 -0
  15. package/dist/aun/msg/mention-schema.js +20 -0
  16. package/dist/aun/msg/p2p.js +7 -0
  17. package/dist/aun/outbox.js +28 -31
  18. package/dist/channels/aun.js +418 -235
  19. package/dist/channels/daemon.js +13 -0
  20. package/dist/cli/agent-command.js +72 -17
  21. package/dist/cli/agent.js +249 -22
  22. package/dist/cli/aun-commands.js +270 -69
  23. package/dist/cli/bench.js +12 -3
  24. package/dist/cli/daemon-commands.js +52 -33
  25. package/dist/cli/fs-command.js +60 -0
  26. package/dist/cli/handoff-command.js +3 -0
  27. package/dist/cli/index.js +106 -63
  28. package/dist/cli/init-cancel.js +208 -0
  29. package/dist/cli/init-channel.js +343 -195
  30. package/dist/cli/init.js +21 -9
  31. package/dist/cli/managed-command-guard.js +41 -0
  32. package/dist/cli/model.js +11 -4
  33. package/dist/cli/queue-command.js +11 -1
  34. package/dist/cli/response.js +71 -0
  35. package/dist/cli/restart-monitor.js +4 -18
  36. package/dist/cli/trigger-command.js +4 -0
  37. package/dist/config/builtin-roles.js +5 -0
  38. package/dist/config/contact-book-store.js +11 -3
  39. package/dist/config/contact-request-service.js +161 -15
  40. package/dist/config/gateway-config.js +26 -10
  41. package/dist/config/mention-mode.js +8 -24
  42. package/dist/core/agent-reload-coordinator.js +53 -0
  43. package/dist/core/auth/agent-delegation.js +0 -1
  44. package/dist/core/auth/operation-authorizer.js +62 -154
  45. package/dist/core/auth/operation-catalog.js +264 -22
  46. package/dist/core/bootstrap-messages.js +68 -0
  47. package/dist/core/bootstrap-service.js +144 -10
  48. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  49. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  50. package/dist/core/capability/skill-discovery.js +55 -0
  51. package/dist/core/channel-loader.js +24 -7
  52. package/dist/core/command/agent-control.js +14 -11
  53. package/dist/core/command/cli-intent-parser.js +9 -0
  54. package/dist/core/command/command-handler.js +467 -6
  55. package/dist/core/command/connect-menu.js +15 -0
  56. package/dist/core/command/group-menu.js +4 -4
  57. package/dist/core/command/menu-catalog.js +366 -0
  58. package/dist/core/command/menu-handler.js +213 -110
  59. package/dist/core/command/menu-protocol.js +71 -0
  60. package/dist/core/command/slash-gate.js +5 -2
  61. package/dist/core/command/slash-handler.js +45 -25
  62. package/dist/core/daemon-file-cache.js +2 -1
  63. package/dist/core/data-migration.js +9 -2
  64. package/dist/core/evolagent-registry.js +125 -35
  65. package/dist/core/evolagent.js +12 -5
  66. package/dist/core/handoff/dispatcher.js +5 -2
  67. package/dist/core/handoff/runtime.js +145 -21
  68. package/dist/core/handoff/store.js +24 -0
  69. package/dist/core/handoff/types.js +1 -0
  70. package/dist/core/inference/text-inference.js +38 -4
  71. package/dist/core/message/logical-queue-bridge.js +1 -0
  72. package/dist/core/message/mention-schema.js +126 -0
  73. package/dist/core/message/message-bridge.js +237 -79
  74. package/dist/core/message/message-log.js +23 -0
  75. package/dist/core/message/message-queue.js +20 -4
  76. package/dist/core/message/response-engine.js +54 -36
  77. package/dist/core/model/model-catalog.js +143 -24
  78. package/dist/core/model/model-diagnostics.js +28 -10
  79. package/dist/core/permission/ec-command-parser.js +156 -10
  80. package/dist/core/permission/index.js +1 -0
  81. package/dist/core/permission/readonly-shell-query.js +532 -0
  82. package/dist/core/permission/sandbox-runtime.js +22 -1
  83. package/dist/core/permission/shell-environment.js +46 -0
  84. package/dist/core/permission/tool-policy.js +292 -111
  85. package/dist/core/protected-paths.js +17 -9
  86. package/dist/core/runner-reload-transaction.js +57 -0
  87. package/dist/core/session/session-manager.js +21 -2
  88. package/dist/eck/manifest-engine.js +39 -13
  89. package/dist/index.js +644 -180
  90. package/dist/ipc.js +246 -19
  91. package/dist/response-system/coordinator.js +6 -4
  92. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  93. package/dist/response-system/modes/single-session/index.js +14 -1
  94. package/dist/response-system/selector.js +6 -5
  95. package/dist/trigger/anomaly-store.js +4 -0
  96. package/dist/trigger/script-executor.js +1 -0
  97. package/dist/utils/aid-bind.js +3 -8
  98. package/dist/utils/codex-app-server-registry.js +90 -0
  99. package/dist/utils/codex-cli.js +5 -1
  100. package/dist/utils/cross-platform.js +15 -0
  101. package/dist/utils/log-writer.js +6 -10
  102. package/dist/utils/logger.js +5 -5
  103. package/dist/utils/npm-ops.js +5 -12
  104. package/dist/utils/tool-summary.js +11 -0
  105. package/kits/docs/channels/aun.md +1 -1
  106. package/kits/docs/context-assembly.md +2 -2
  107. package/kits/docs/evolcore/agent.md +11 -6
  108. package/kits/docs/evolcore/aid.md +14 -0
  109. package/kits/docs/evolcore/msg.md +13 -0
  110. package/kits/docs/prompt-loading-architecture.md +1 -2
  111. package/kits/docs/venues/group.md +1 -1
  112. package/kits/eck_manifest.json +0 -12
  113. package/kits/rules/01-overview.md +9 -0
  114. package/kits/rules/04-relation.md +1 -1
  115. package/kits/rules/05-venue.md +2 -2
  116. package/kits/schemas/agent-config.schema.3.json +1 -1
  117. package/kits/schemas/agent-config.schema.4.json +1 -1
  118. package/kits/schemas/relation-config.schema.2.json +1 -1
  119. package/kits/schemas/role-config.schema.1.json +1 -1
  120. package/kits/schemas/single-session.schema.1.json +3 -3
  121. package/kits/schemas/single-session.schema.2.json +3 -3
  122. package/kits/templates/roles/admin.json +27 -0
  123. package/kits/templates/roles/member.json +44 -0
  124. package/kits/templates/roles/visitor.json +40 -2
  125. package/kits/templates/system-fragments/bootstrap.md +12 -6
  126. package/kits/templates/system-fragments/channel.md +6 -0
  127. package/kits/templates/system-fragments/session.md +2 -0
  128. package/package.json +3 -2
  129. package/skills/eclink/SKILL.md +15 -3
  130. package/skills/eclink/agents/openai.yaml +3 -3
  131. package/dist/core/command/evol-menu-version-gate.js +0 -39
@@ -13,18 +13,21 @@ import { buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtecte
13
13
  import { CodexAppServerClient } from './codex-app-server-client.js';
14
14
  import { resolveOpenaiConfig } from './baseagent.js';
15
15
  import { logger } from '../utils/logger.js';
16
+ import { summarizeToolInputForAudit } from '../utils/tool-summary.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
- import { classifyEvolcoreShellCommand, parseLiteralShellArgv } from '../core/permission/ec-command-parser.js';
22
+ import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
23
+ import { classifyEvolcoreShellCommand, parseLiteralShellArgv, unwrapCodexShellCommandArgv } from '../core/permission/ec-command-parser.js';
22
24
  import { compareVersions } from '../utils/npm-ops.js';
23
25
  import { resolvePaths, resolveRoot } from '../paths.js';
24
26
  import { buildSessionTurnList } from '../core/session/session-turns.js';
25
27
  import { execFileSync } from 'child_process';
26
28
  import { execCodexCliSync, resolveCodexCliPath } from '../utils/codex-cli.js';
27
29
  import { createHash, randomBytes } from 'crypto';
30
+ import { buildModelRequestHeaders } from './request-identity.js';
28
31
  import fs from 'fs';
29
32
  import path from 'path';
30
33
  import os from 'os';
@@ -116,10 +119,9 @@ const CODEX_CATALOG_FALLBACK = [
116
119
  ];
117
120
  const CODEX_DELEGATION_CARRIER_METADATA_KEY = '__codexDelegationCarrierV1';
118
121
  let codexCatalogCache = null;
119
- // The permission bridge is version-locked to the v2 schema audited below:
120
- // named profiles, approvalsReviewer, additional/network approval context, and
121
- // item/permissions/requestApproval must all be present together.
122
- export const MIN_CODEX_CLI_VERSION = '0.144.1';
122
+ // The permission bridge is version-locked to the audited approval schema and
123
+ // managed PreToolUse hook behavior used by proactive first-tool enforcement.
124
+ export const MIN_CODEX_CLI_VERSION = '0.147.0';
123
125
  export function parseCodexCliVersion(output) {
124
126
  const match = output.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/);
125
127
  return match?.[1] ?? null;
@@ -221,6 +223,8 @@ export class CodexRunner {
221
223
  trackedApprovalItems = new Map();
222
224
  threadDelegationCarriers = new Map();
223
225
  pendingInterrupts = new Map();
226
+ threadOperationLocks = new Map();
227
+ activeOperationReleases = new Map();
224
228
  appServerClient = null;
225
229
  onSessionIdUpdate;
226
230
  onCompactStart;
@@ -231,6 +235,12 @@ export class CodexRunner {
231
235
  pendingInterruptTtlMs = 30_000;
232
236
  constructor(config, callbacks) {
233
237
  this.resolvedConfig = resolveOpenaiConfig(config);
238
+ this.resolvedConfig.headers = buildModelRequestHeaders({
239
+ baseagent: 'codex',
240
+ baseUrl: this.resolvedConfig.baseUrl,
241
+ agentAid: config.agents?.codex?.evolcoreAgentAid,
242
+ configuredHeaders: this.resolvedConfig.headers,
243
+ });
234
244
  this.resolvedConfig.evolcoreAgentAid = config.agents?.codex?.evolcoreAgentAid;
235
245
  this.resolvedConfig.evolcoreAgentConfig = config.agents?.codex?.evolcoreAgentConfig;
236
246
  this.capabilities = {
@@ -281,15 +291,6 @@ export class CodexRunner {
281
291
  const argv = parseLiteralShellArgv(command);
282
292
  return argv?.[0] === 'ec' ? argv : undefined;
283
293
  }
284
- unwrapCodexShellCommandArgv(argv) {
285
- if (argv.length !== 3 || argv[1] !== '-lc')
286
- return undefined;
287
- const shell = argv[0];
288
- if (!['bash', '/bin/bash', '/usr/bin/bash', 'sh', '/bin/sh', '/usr/bin/sh'].includes(shell)) {
289
- return undefined;
290
- }
291
- return argv[2];
292
- }
293
294
  approvedEvolcoreCommandArgv(toolInput) {
294
295
  const explicitArgv = Array.isArray(toolInput.commandArgv)
295
296
  && toolInput.commandArgv.every(value => typeof value === 'string')
@@ -298,7 +299,7 @@ export class CodexRunner {
298
299
  if (explicitArgv?.[0] === 'ec')
299
300
  return explicitArgv;
300
301
  const explicitWrappedCommand = explicitArgv
301
- ? this.unwrapCodexShellCommandArgv(explicitArgv)
302
+ ? unwrapCodexShellCommandArgv(explicitArgv)
302
303
  : undefined;
303
304
  if (explicitWrappedCommand !== undefined) {
304
305
  return this.approvedLiteralEvolcoreCommandArgv(explicitWrappedCommand);
@@ -309,7 +310,7 @@ export class CodexRunner {
309
310
  return directArgv;
310
311
  const wrappedArgv = parseLiteralShellArgv(command);
311
312
  const wrappedCommand = wrappedArgv
312
- ? this.unwrapCodexShellCommandArgv(wrappedArgv)
313
+ ? unwrapCodexShellCommandArgv(wrappedArgv)
313
314
  : undefined;
314
315
  return wrappedCommand === undefined
315
316
  ? undefined
@@ -416,14 +417,35 @@ export class CodexRunner {
416
417
  }
417
418
  async resolveCapabilityThreadConfig(projectPath) {
418
419
  const agentConfig = this.resolvedConfig.evolcoreAgentConfig;
419
- if (!agentConfig)
420
- return {};
420
+ if (!agentConfig) {
421
+ return {
422
+ skills: { config: [{ name: 'eclink', enabled: false }] },
423
+ };
424
+ }
421
425
  try {
422
- return await resolveCodexCapabilityThreadConfigForProject(agentConfig, projectPath, 'codex');
426
+ const resolved = await resolveCodexCapabilityThreadConfigForProject(agentConfig, projectPath, 'codex');
427
+ const skills = this.configRecord(resolved.skills);
428
+ const configured = Array.isArray(skills?.config)
429
+ ? skills.config.filter(entry => {
430
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
431
+ return true;
432
+ return entry.name !== 'eclink';
433
+ })
434
+ : [];
435
+ return this.mergeThreadConfig(resolved, {
436
+ skills: {
437
+ ...(skills ?? {}),
438
+ config: [...configured, { name: 'eclink', enabled: false }],
439
+ },
440
+ }) ?? {
441
+ skills: { config: [{ name: 'eclink', enabled: false }] },
442
+ };
423
443
  }
424
444
  catch (error) {
425
445
  logger.warn(`[CodexRunner] Failed to resolve Codex capability thread config: ${error}`);
426
- return {};
446
+ return {
447
+ skills: { config: [{ name: 'eclink', enabled: false }] },
448
+ };
427
449
  }
428
450
  }
429
451
  configRecord(value) {
@@ -721,7 +743,9 @@ export class CodexRunner {
721
743
  [profileName]: {
722
744
  extends: mode === 'readonly' ? ':read-only' : ':workspace',
723
745
  filesystem: {
724
- ...buildCodexProtectedFilesystemRules(resolveRoot()),
746
+ ...buildCodexProtectedFilesystemRules(resolveRoot(), {
747
+ denyLClassRead: mode === 'readonly',
748
+ }),
725
749
  ...this.resolveGitMetadataWriteRules(projectPath, mode),
726
750
  ...(mode !== 'readonly' && managedTempDir ? {
727
751
  [managedTempDir]: 'write',
@@ -765,6 +789,35 @@ export class CodexRunner {
765
789
  }
766
790
  setSendPrompt(fn) { this.sendPromptFn = fn; }
767
791
  setPermissionContext(sessionId, context) { this.permissionContexts.set(sessionId, context); }
792
+ async evaluatePreToolUse(threadId, toolName, toolInput) {
793
+ const sessionKey = this.findSessionKeyByThread(threadId);
794
+ const activeThread = this.activeSessions.get(sessionKey);
795
+ const context = this.permissionContexts.get(sessionKey);
796
+ if (!activeThread || activeThread !== threadId) {
797
+ return { ok: true, applicable: false };
798
+ }
799
+ const proactiveSession = context?.chatmode === 'proactive';
800
+ if (!context || (proactiveSession && !context.preToolUsePolicyHook)) {
801
+ return { ok: false, applicable: true, decision: 'deny', reason: 'EvolCore proactive session policy context is unavailable' };
802
+ }
803
+ // A tracked interactive thread still needs an authenticated, explicit
804
+ // allow so the managed hook can fail closed when no runner matches.
805
+ if (!context.preToolUsePolicyHook)
806
+ return { ok: true, applicable: true, decision: 'allow' };
807
+ const normalizedInput = toolInput && typeof toolInput === 'object' && !Array.isArray(toolInput)
808
+ ? toolInput
809
+ : {};
810
+ try {
811
+ const result = context.preToolUsePolicyHook(toolName, normalizedInput);
812
+ if (result?.block) {
813
+ return { ok: true, applicable: true, decision: 'deny', reason: result.reason };
814
+ }
815
+ return { ok: true, applicable: true, decision: 'allow' };
816
+ }
817
+ catch (error) {
818
+ return { ok: false, applicable: true, decision: 'deny', reason: `policy hook failed: ${error instanceof Error ? error.message : String(error)}` };
819
+ }
820
+ }
768
821
  setPermissionGateway(gw) { this.permissionGateway = gw; }
769
822
  // ── Stream management (needed by MessageProcessor) ──
770
823
  registerStream(key, stream) {
@@ -774,13 +827,135 @@ export class CodexRunner {
774
827
  this.activeStreams.delete(key);
775
828
  this.activeAbortControllers.delete(key);
776
829
  this.pendingInterrupts.delete(key);
830
+ const release = this.activeOperationReleases.get(key);
831
+ if (release) {
832
+ this.activeOperationReleases.delete(key);
833
+ release();
834
+ }
777
835
  }
778
836
  hasActiveStream(key) {
779
837
  return this.activeStreams.has(key) || this.activeAbortControllers.has(key) || this.activeTurns.has(key);
780
838
  }
839
+ threadOperationKey(threadId, sessionId) {
840
+ // Once a thread id is known it is the shared identity, including when
841
+ // multiple EvolCore sessions happen to address the same Codex thread.
842
+ if (threadId)
843
+ return `thread:${threadId}`;
844
+ if (sessionId)
845
+ return `session:${sessionId}`;
846
+ return 'session:unknown';
847
+ }
848
+ getThreadOperationLock(key) {
849
+ const existing = this.threadOperationLocks.get(key);
850
+ if (existing)
851
+ return existing;
852
+ const lock = { aliases: new Set([key]) };
853
+ this.threadOperationLocks.set(key, lock);
854
+ return lock;
855
+ }
856
+ bindThreadOperationKeys(...keys) {
857
+ const uniqueKeys = [...new Set(keys.filter(Boolean))];
858
+ if (uniqueKeys.length < 2)
859
+ return;
860
+ const locks = uniqueKeys.map(key => this.getThreadOperationLock(key));
861
+ const primary = locks[0];
862
+ for (const other of locks.slice(1)) {
863
+ if (other === primary)
864
+ continue;
865
+ const tails = [primary.tail, other.tail].filter((tail) => !!tail);
866
+ if (tails.length === 1) {
867
+ primary.tail = tails[0];
868
+ }
869
+ else if (tails.length > 1) {
870
+ const merged = Promise.all(tails).then(() => undefined);
871
+ primary.tail = merged;
872
+ merged.then(() => {
873
+ if (primary.tail === merged)
874
+ primary.tail = undefined;
875
+ });
876
+ }
877
+ for (const alias of other.aliases) {
878
+ primary.aliases.add(alias);
879
+ this.threadOperationLocks.set(alias, primary);
880
+ }
881
+ }
882
+ }
883
+ isActiveThreadOperationAlias(key) {
884
+ if (key.startsWith('session:'))
885
+ return this.activeSessions.has(key.slice('session:'.length));
886
+ if (key.startsWith('thread:')) {
887
+ const threadId = key.slice('thread:'.length);
888
+ for (const activeThreadId of this.activeSessions.values()) {
889
+ if (activeThreadId === threadId)
890
+ return true;
891
+ }
892
+ return false;
893
+ }
894
+ return true;
895
+ }
896
+ pruneThreadOperationLocks() {
897
+ for (const [key, lock] of this.threadOperationLocks) {
898
+ if (lock.tail || this.isActiveThreadOperationAlias(key))
899
+ continue;
900
+ this.threadOperationLocks.delete(key);
901
+ lock.aliases.delete(key);
902
+ }
903
+ }
904
+ async acquireThreadOperation(key, operation) {
905
+ const lock = this.getThreadOperationLock(key);
906
+ const previous = lock.tail;
907
+ let releaseCurrent;
908
+ const current = new Promise(resolve => {
909
+ releaseCurrent = resolve;
910
+ });
911
+ lock.tail = current;
912
+ if (previous) {
913
+ logger.info(`[CodexRunner] queued ${operation} behind an active operation: ${key}`);
914
+ await previous;
915
+ }
916
+ let released = false;
917
+ return () => {
918
+ if (released)
919
+ return;
920
+ released = true;
921
+ if (lock.tail === current)
922
+ lock.tail = undefined;
923
+ releaseCurrent();
924
+ this.pruneThreadOperationLocks();
925
+ };
926
+ }
927
+ async *holdThreadOperation(stream, sessionId, release) {
928
+ try {
929
+ for await (const item of stream)
930
+ yield item;
931
+ }
932
+ finally {
933
+ if (this.activeOperationReleases.get(sessionId) === release) {
934
+ this.activeOperationReleases.delete(sessionId);
935
+ }
936
+ release();
937
+ }
938
+ }
781
939
  // ── Core: runQuery ──
782
940
  async runQuery(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
941
+ const knownThreadId = initialAgentSessionId || this.activeSessions.get(sessionId);
942
+ const operationKey = this.threadOperationKey(knownThreadId, sessionId);
943
+ const release = await this.acquireThreadOperation(operationKey, 'thread/resume+turn/start');
944
+ try {
945
+ const stream = await this.runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv);
946
+ this.activeOperationReleases.set(sessionId, release);
947
+ return this.holdThreadOperation(stream, sessionId, release);
948
+ }
949
+ catch (error) {
950
+ release();
951
+ throw error;
952
+ }
953
+ }
954
+ async runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
783
955
  let agentSessionId = initialAgentSessionId || this.activeSessions.get(sessionId);
956
+ if (this.permissionContexts.get(sessionId)?.chatmode === 'proactive' && process.platform !== 'linux') {
957
+ throw new Error('Codex proactive managed PreToolUse enforcement currently requires Linux');
958
+ }
784
959
  const resumingThread = !!agentSessionId;
785
960
  const callModel = modelOverride?.model || this.model;
786
961
  const callEffort = modelOverride?.effortMode === 'model_default'
@@ -819,7 +994,7 @@ export class CodexRunner {
819
994
  approvalPolicy: effectiveApprovalPolicy,
820
995
  approvalsReviewer: 'user',
821
996
  ...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
822
- config: this.mergeThreadConfig(executionSandbox.permissionProfileConfig, runtimeEnv ? { shell_environment_policy: { set: runtimeEnv } } : undefined, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrierCandidate), this.buildLifecycleLockdownConfig()),
997
+ config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, runtimeEnv ? { shell_environment_policy: { set: runtimeEnv } } : undefined, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrierCandidate), this.buildLifecycleLockdownConfig()), callMode),
823
998
  ...(developerInstructions ? { developerInstructions } : {}),
824
999
  };
825
1000
  logger.info(`[CodexRunner] runQuery permMode=${requestedPermissionMode}->${callMode} ` +
@@ -862,6 +1037,7 @@ export class CodexRunner {
862
1037
  }
863
1038
  agentSessionId = threadId;
864
1039
  this.activeSessions.set(sessionId, threadId);
1040
+ this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
865
1041
  this.threadProjectPaths.set(threadId, path.resolve(projectPath));
866
1042
  this.bindExternalToolBoundary(threadId, externalToolConfig);
867
1043
  this.onSessionIdUpdate?.(sessionId, threadId);
@@ -1004,10 +1180,12 @@ export class CodexRunner {
1004
1180
  }
1005
1181
  if (agentSessionId) {
1006
1182
  this.activeSessions.set(sessionId, agentSessionId);
1183
+ this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${agentSessionId}`);
1007
1184
  }
1008
1185
  else {
1009
1186
  this.activeSessions.delete(sessionId);
1010
1187
  }
1188
+ this.pruneThreadOperationLocks();
1011
1189
  this.onSessionIdUpdate?.(sessionId, agentSessionId);
1012
1190
  }
1013
1191
  async closeSession(sessionId) {
@@ -1026,6 +1204,7 @@ export class CodexRunner {
1026
1204
  this.unbindExternalToolBoundary(threadId);
1027
1205
  }
1028
1206
  this.clearTrackedApprovalItemsForThread(threadId);
1207
+ this.pruneThreadOperationLocks();
1029
1208
  }
1030
1209
  resolveSessionFile(agentSessionId, _projectPath) {
1031
1210
  // Codex session 文件: ~/.codex/sessions/YYYY/MM/DD/rollout-*-{threadId}.jsonl
@@ -1058,10 +1237,12 @@ export class CodexRunner {
1058
1237
  this.threadDelegationCarriers.delete(threadId);
1059
1238
  this.unbindExternalToolBoundary(threadId);
1060
1239
  this.clearTrackedApprovalItemsForThread(threadId);
1240
+ this.pruneThreadOperationLocks();
1061
1241
  this.onSessionIdUpdate?.(sessionId, '');
1062
1242
  return true;
1063
1243
  }
1064
1244
  async compactSession(_sessionId, agentSessionId, _projectPath) {
1245
+ const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId, _sessionId), 'compact');
1065
1246
  try {
1066
1247
  const appServer = this.getAppServerClient();
1067
1248
  this.onCompactStart?.(_sessionId);
@@ -1086,7 +1267,7 @@ export class CodexRunner {
1086
1267
  approvalPolicy: compactPolicy,
1087
1268
  approvalsReviewer: 'user',
1088
1269
  ...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
1089
- config: this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactDelegationCarrier), this.buildLifecycleLockdownConfig()),
1270
+ config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactDelegationCarrier), this.buildLifecycleLockdownConfig()), compactMode),
1090
1271
  });
1091
1272
  this.threadProjectPaths.set(agentSessionId, path.resolve(_projectPath));
1092
1273
  this.threadDelegationCarriers.set(agentSessionId, compactDelegationCarrier);
@@ -1098,6 +1279,9 @@ export class CodexRunner {
1098
1279
  logger.error('[CodexRunner] Compact failed:', error);
1099
1280
  return false;
1100
1281
  }
1282
+ finally {
1283
+ release();
1284
+ }
1101
1285
  }
1102
1286
  async compact(sessionId, agentSessionId, projectPath) {
1103
1287
  return this.compactSession(sessionId, agentSessionId, projectPath);
@@ -1223,6 +1407,15 @@ export class CodexRunner {
1223
1407
  return newest?.path;
1224
1408
  }
1225
1409
  async forkSession(agentSessionId, projectPath, title) {
1410
+ const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'fork');
1411
+ try {
1412
+ return await this.forkSessionLocked(agentSessionId, projectPath, title);
1413
+ }
1414
+ finally {
1415
+ release();
1416
+ }
1417
+ }
1418
+ async forkSessionLocked(agentSessionId, projectPath, title) {
1226
1419
  const sessionKey = this.findSessionKeyByThread(agentSessionId);
1227
1420
  const mode = this.chatModes.get(sessionKey) ?? this.currentMode;
1228
1421
  const executionSandbox = this.buildExecutionSandboxOptions(sessionKey, mode, projectPath);
@@ -1236,7 +1429,7 @@ export class CodexRunner {
1236
1429
  approvalPolicy: this.toApprovalPolicy(mode),
1237
1430
  approvalsReviewer: 'user',
1238
1431
  ...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
1239
- config: this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionKey, delegationCarrier), this.buildLifecycleLockdownConfig()),
1432
+ config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionKey, delegationCarrier), this.buildLifecycleLockdownConfig()), mode),
1240
1433
  });
1241
1434
  const forkedThreadId = response.thread?.id;
1242
1435
  if (!forkedThreadId)
@@ -1247,18 +1440,30 @@ export class CodexRunner {
1247
1440
  return forkedThreadId;
1248
1441
  }
1249
1442
  async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title) {
1250
- const turns = buildSessionTurnList(await this.getSessionMessages(agentSessionId, projectPath));
1251
- const targetIndex = turns.findIndex(turn => turn.assistantUuid === assistantMessageId);
1252
- if (targetIndex < 0)
1253
- throw new Error('fork point not found');
1254
- const forkedThreadId = await this.forkSession(agentSessionId, projectPath, title);
1255
- const turnsToDiscard = turns.length - targetIndex - 1;
1256
- if (turnsToDiscard > 0) {
1257
- const rolledBack = await this.rollbackSessionTurns(forkedThreadId, projectPath, turnsToDiscard);
1258
- if (!rolledBack)
1259
- throw new Error('forked thread rollback failed');
1443
+ const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'forkAt');
1444
+ try {
1445
+ const turns = buildSessionTurnList(await this.getSessionMessages(agentSessionId, projectPath));
1446
+ const targetIndex = turns.findIndex(turn => turn.assistantUuid === assistantMessageId);
1447
+ if (targetIndex < 0)
1448
+ throw new Error('fork point not found');
1449
+ const forkedThreadId = await this.forkSessionLocked(agentSessionId, projectPath, title);
1450
+ const turnsToDiscard = turns.length - targetIndex - 1;
1451
+ if (turnsToDiscard > 0) {
1452
+ const rollbackRelease = await this.acquireThreadOperation(this.threadOperationKey(forkedThreadId), 'rollback');
1453
+ try {
1454
+ const rolledBack = await this.rollbackSessionTurnsLocked(forkedThreadId, turnsToDiscard);
1455
+ if (!rolledBack)
1456
+ throw new Error('forked thread rollback failed');
1457
+ }
1458
+ finally {
1459
+ rollbackRelease();
1460
+ }
1461
+ }
1462
+ return forkedThreadId;
1463
+ }
1464
+ finally {
1465
+ release();
1260
1466
  }
1261
- return forkedThreadId;
1262
1467
  }
1263
1468
  async setSessionName(agentSessionId, name) {
1264
1469
  return this.getAppServerClient().threadSetName(agentSessionId, name);
@@ -1390,6 +1595,25 @@ export class CodexRunner {
1390
1595
  // proactive 模式行为策略(首次工具调用必须是 ec msg send)
1391
1596
  const policyResult = this.permissionContexts.get(sessionKey)?.policyHook?.(toolName, toolInput);
1392
1597
  if (policyResult?.block) {
1598
+ logger.info(`[CodexRunner] app-server approval declined by session policy: session=${sessionKey} ` +
1599
+ `requestId=${request.id ?? '<missing>'} code=${policyResult.policyCode ?? 'session_policy_hook'} ` +
1600
+ `reason=${policyResult.reason ?? 'policy denied'} executed=false`);
1601
+ try {
1602
+ await permissionContext?.recordExecutionAnomaly?.({
1603
+ code: 'operation_blocked',
1604
+ severity: 'warning',
1605
+ phase: 'execution',
1606
+ occurredAt: Date.now(),
1607
+ toolName,
1608
+ ...(request.id !== undefined ? { requestId: String(request.id) } : {}),
1609
+ policyCode: policyResult.policyCode ?? 'session_policy_hook',
1610
+ summary: summarizeToolInputForAudit(toolName, toolInput).slice(0, 512),
1611
+ effect: 'operation_skipped',
1612
+ });
1613
+ }
1614
+ catch (error) {
1615
+ logger.warn(`[CodexRunner] failed to record policy-hook denial: session=${sessionKey} tool=${toolName} error=${error instanceof Error ? error.message : String(error)}`);
1616
+ }
1393
1617
  return this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
1394
1618
  }
1395
1619
  const summary = this.summarizeAppServerRequest(request.method, params);
@@ -1403,7 +1627,7 @@ export class CodexRunner {
1403
1627
  const sessionMode = this.chatModes.get(sessionKey) ?? this.currentMode;
1404
1628
  logger.info(`[CodexRunner] app-server approval request id=${request.id} method=${request.method} session=${sessionKey} mode=${sessionMode} tool=${toolName} summary=${summary}`);
1405
1629
  try {
1406
- const decision = await this.resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath, operationCwd);
1630
+ const decision = await this.resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath, operationCwd, request.id);
1407
1631
  if (decision !== 'deny'
1408
1632
  && (request.method === 'item/commandExecution/requestApproval' || request.method === 'execCommandApproval')
1409
1633
  && this.approvedEvolcoreCommandArgv(toolInput)) {
@@ -1934,7 +2158,7 @@ export class CodexRunner {
1934
2158
  return true;
1935
2159
  });
1936
2160
  }
1937
- async resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath = process.cwd(), operationCwd = workspacePath) {
2161
+ async resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath = process.cwd(), operationCwd = workspacePath, requestId) {
1938
2162
  const permissionContext = this.permissionContexts.get(sessionKey);
1939
2163
  const permissionPrompt = permissionContext?.sendPrompt ?? this.sendPromptFn;
1940
2164
  const hasFilesystemPermissions = (toolName === 'Bash' || toolName === 'PermissionGrant')
@@ -1949,8 +2173,27 @@ export class CodexRunner {
1949
2173
  // per-session 权限模式(runQuery 写入);缺省回落实例级 currentMode(兼容无 runQuery 上下文的调用)
1950
2174
  const rawMode = this.chatModes.get(sessionKey) ?? this.currentMode;
1951
2175
  const mode = normalizePermissionMode(rawMode).mode;
2176
+ const recordBlockedOperation = async (policyCode, operationInput = toolInput) => {
2177
+ try {
2178
+ await permissionContext?.recordExecutionAnomaly?.({
2179
+ code: 'operation_blocked',
2180
+ severity: 'warning',
2181
+ phase: 'execution',
2182
+ occurredAt: Date.now(),
2183
+ toolName,
2184
+ ...(typeof requestId === 'string' || typeof requestId === 'number' ? { requestId: String(requestId) } : {}),
2185
+ policyCode,
2186
+ summary: summarizeToolInputForAudit(toolName, operationInput).slice(0, 512),
2187
+ effect: 'operation_skipped',
2188
+ });
2189
+ }
2190
+ catch (error) {
2191
+ logger.warn(`[CodexRunner] failed to record blocked operation: session=${sessionKey} tool=${toolName} code=${policyCode} error=${error instanceof Error ? error.message : String(error)}`);
2192
+ }
2193
+ };
1952
2194
  const preflight = evaluateToolPreflight(toolName, toolInput, {
1953
2195
  sessionId: sessionKey,
2196
+ selfAid: permissionContext?.selfAid,
1954
2197
  channel: permissionContext?.channel,
1955
2198
  userId: permissionContext?.userId,
1956
2199
  role: permissionContext?.role,
@@ -1958,8 +2201,12 @@ export class CodexRunner {
1958
2201
  projectPath: operationCwd,
1959
2202
  workspacePath,
1960
2203
  });
1961
- if (preflight.behavior === 'deny')
2204
+ if (preflight.behavior === 'deny') {
2205
+ if (preflight.policyCode) {
2206
+ await recordBlockedOperation(preflight.policyCode);
2207
+ }
1962
2208
  return 'deny';
2209
+ }
1963
2210
  if (preflight.behavior === 'allow')
1964
2211
  return 'allow';
1965
2212
  const checkedInput = preflight.input;
@@ -1983,8 +2230,10 @@ export class CodexRunner {
1983
2230
  const grantScope = `codex:${this.permissionProfileName(sessionKey)}:${mode}`;
1984
2231
  if (mode === 'readonly') {
1985
2232
  const readonly = this.checkCodexReadonly(toolName, checkedInput, operationCwd, sessionKey);
1986
- if (readonly.behavior === 'deny')
2233
+ if (readonly.behavior === 'deny') {
2234
+ await recordBlockedOperation('readonly_mode', checkedInput);
1987
2235
  return 'deny';
2236
+ }
1988
2237
  return 'allow';
1989
2238
  }
1990
2239
  if (toolName.startsWith('MCP:')) {
@@ -1992,13 +2241,19 @@ export class CodexRunner {
1992
2241
  }
1993
2242
  // auto 模式下检查危险命令,危险命令自动拒绝
1994
2243
  if (mode === 'auto') {
1995
- if (hasFileChangeExpansion || hasFilesystemPermissions || hasUnknownPermissions)
2244
+ if (hasFileChangeExpansion || hasFilesystemPermissions || hasUnknownPermissions) {
2245
+ await recordBlockedOperation('auto_permission_expansion', checkedInput);
1996
2246
  return 'deny';
1997
- if (toolName === 'PermissionGrant' && !hasNetworkPermissions)
2247
+ }
2248
+ if (toolName === 'PermissionGrant' && !hasNetworkPermissions) {
2249
+ await recordBlockedOperation('auto_permission_expansion', checkedInput);
1998
2250
  return 'deny';
2251
+ }
1999
2252
  const dangerous = checkDangerousCommand(toolName, checkedInput);
2000
- if (dangerous.isDangerous)
2253
+ if (dangerous.isDangerous) {
2254
+ await recordBlockedOperation('auto_dangerous_command', checkedInput);
2001
2255
  return 'deny';
2256
+ }
2002
2257
  return 'allow';
2003
2258
  }
2004
2259
  const requiresSandboxExpansionApproval = hasFileChangeExpansion
@@ -2051,6 +2306,15 @@ export class CodexRunner {
2051
2306
  throw new Error('Unsupported Codex app-server request: ' + method);
2052
2307
  }
2053
2308
  async rollbackSessionTurns(agentSessionId, _projectPath, numTurns) {
2309
+ const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'rollback');
2310
+ try {
2311
+ return await this.rollbackSessionTurnsLocked(agentSessionId, numTurns);
2312
+ }
2313
+ finally {
2314
+ release();
2315
+ }
2316
+ }
2317
+ async rollbackSessionTurnsLocked(agentSessionId, numTurns) {
2054
2318
  if (numTurns < 1)
2055
2319
  return true;
2056
2320
  const response = await this.getAppServerClient().threadRollback(agentSessionId, numTurns);
@@ -2089,7 +2353,11 @@ export class CodexRunner {
2089
2353
  }
2090
2354
  readGitHeadFile(projectPath, filePath) {
2091
2355
  try {
2092
- return execFileSync('git', ['show', `HEAD:${filePath.replace(/\\/g, '/')}`], { cwd: projectPath, stdio: ['pipe', 'pipe', 'ignore'] });
2356
+ return execFileSync('git', ['show', `HEAD:${filePath.replace(/\\/g, '/')}`], {
2357
+ cwd: projectPath,
2358
+ stdio: ['pipe', 'pipe', 'ignore'],
2359
+ windowsHide: process.platform === 'win32',
2360
+ });
2093
2361
  }
2094
2362
  catch {
2095
2363
  return null;
@@ -2810,6 +3078,24 @@ export class CodexRunner {
2810
3078
  },
2811
3079
  };
2812
3080
  }
3081
+ sanitizeThreadEnvironmentConfig(config, permissionMode) {
3082
+ if (!config)
3083
+ return null;
3084
+ const policy = this.configRecord(config.shell_environment_policy);
3085
+ const set = this.configRecord(policy?.set);
3086
+ if (!policy || !set)
3087
+ return config;
3088
+ const safeSet = sanitizeShellExecutionEnvironment(set, {
3089
+ lockExecutableResolution: permissionMode === 'readonly',
3090
+ });
3091
+ return {
3092
+ ...config,
3093
+ shell_environment_policy: {
3094
+ ...policy,
3095
+ set: safeSet,
3096
+ },
3097
+ };
3098
+ }
2813
3099
  async dispose() {
2814
3100
  // Abort all active streams
2815
3101
  for (const [key, controller] of this.activeAbortControllers) {
@@ -2827,6 +3113,8 @@ export class CodexRunner {
2827
3113
  this.pendingInterrupts.clear();
2828
3114
  this.permissionContexts.clear();
2829
3115
  this.chatModes.clear();
3116
+ this.pruneThreadOperationLocks();
3117
+ this.threadOperationLocks.clear();
2830
3118
  for (const sessionId of [...this.sessionTempDirs.keys()])
2831
3119
  this.cleanupSessionTempDir(sessionId);
2832
3120
  await this.appServerClient?.close();