evolcore 0.0.11 → 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 (102) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/codex-managed-hook.mjs +80 -0
  3. package/dist/agents/claude-runner.js +35 -2
  4. package/dist/agents/codex-app-server-client.js +120 -14
  5. package/dist/agents/codex-runner.js +273 -35
  6. package/dist/agents/ecagent-runner.js +36 -4
  7. package/dist/agents/gemini-runner.js +1 -0
  8. package/dist/aun/aid/client.js +11 -21
  9. package/dist/aun/aid/managed-operation.js +107 -0
  10. package/dist/aun/msg/group.js +81 -12
  11. package/dist/aun/msg/managed-operation.js +804 -0
  12. package/dist/aun/msg/mention-schema.js +20 -0
  13. package/dist/aun/msg/p2p.js +7 -0
  14. package/dist/channels/aun.js +288 -108
  15. package/dist/channels/daemon.js +13 -0
  16. package/dist/cli/agent-command.js +56 -8
  17. package/dist/cli/agent.js +170 -6
  18. package/dist/cli/aun-commands.js +270 -69
  19. package/dist/cli/bench.js +12 -3
  20. package/dist/cli/daemon-commands.js +31 -31
  21. package/dist/cli/fs-command.js +60 -0
  22. package/dist/cli/handoff-command.js +3 -0
  23. package/dist/cli/index.js +30 -2
  24. package/dist/cli/managed-command-guard.js +41 -0
  25. package/dist/cli/model.js +11 -4
  26. package/dist/cli/queue-command.js +11 -1
  27. package/dist/cli/response.js +71 -0
  28. package/dist/cli/restart-monitor.js +4 -18
  29. package/dist/cli/trigger-command.js +4 -0
  30. package/dist/config/builtin-roles.js +4 -0
  31. package/dist/config/contact-book-store.js +11 -3
  32. package/dist/config/contact-request-service.js +161 -15
  33. package/dist/config/mention-mode.js +8 -24
  34. package/dist/core/auth/agent-delegation.js +0 -1
  35. package/dist/core/auth/operation-authorizer.js +30 -7
  36. package/dist/core/auth/operation-catalog.js +187 -25
  37. package/dist/core/bootstrap-messages.js +18 -0
  38. package/dist/core/bootstrap-service.js +63 -4
  39. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  40. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  41. package/dist/core/capability/skill-discovery.js +55 -0
  42. package/dist/core/channel-loader.js +1 -1
  43. package/dist/core/command/cli-intent-parser.js +9 -0
  44. package/dist/core/command/command-handler.js +467 -6
  45. package/dist/core/command/connect-menu.js +15 -0
  46. package/dist/core/command/group-menu.js +4 -4
  47. package/dist/core/command/menu-catalog.js +366 -0
  48. package/dist/core/command/menu-handler.js +147 -35
  49. package/dist/core/command/menu-protocol.js +71 -0
  50. package/dist/core/command/slash-gate.js +5 -2
  51. package/dist/core/command/slash-handler.js +41 -21
  52. package/dist/core/daemon-file-cache.js +2 -1
  53. package/dist/core/data-migration.js +9 -2
  54. package/dist/core/evolagent.js +4 -2
  55. package/dist/core/handoff/dispatcher.js +5 -2
  56. package/dist/core/handoff/runtime.js +145 -21
  57. package/dist/core/handoff/store.js +24 -0
  58. package/dist/core/handoff/types.js +1 -0
  59. package/dist/core/message/logical-queue-bridge.js +1 -0
  60. package/dist/core/message/mention-schema.js +126 -0
  61. package/dist/core/message/message-bridge.js +236 -78
  62. package/dist/core/message/message-log.js +1 -0
  63. package/dist/core/message/message-queue.js +1 -0
  64. package/dist/core/message/response-engine.js +54 -36
  65. package/dist/core/permission/ec-command-parser.js +156 -10
  66. package/dist/core/permission/sandbox-runtime.js +22 -1
  67. package/dist/core/permission/tool-policy.js +73 -30
  68. package/dist/core/protected-paths.js +7 -2
  69. package/dist/core/session/session-manager.js +21 -2
  70. package/dist/eck/manifest-engine.js +39 -13
  71. package/dist/index.js +384 -98
  72. package/dist/ipc.js +218 -9
  73. package/dist/response-system/coordinator.js +6 -4
  74. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  75. package/dist/response-system/modes/single-session/index.js +14 -1
  76. package/dist/response-system/selector.js +6 -5
  77. package/dist/trigger/anomaly-store.js +4 -0
  78. package/dist/trigger/script-executor.js +1 -0
  79. package/dist/utils/codex-app-server-registry.js +90 -0
  80. package/dist/utils/codex-cli.js +5 -1
  81. package/dist/utils/cross-platform.js +15 -0
  82. package/dist/utils/npm-ops.js +5 -12
  83. package/dist/utils/tool-summary.js +11 -0
  84. package/kits/docs/channels/aun.md +1 -1
  85. package/kits/docs/context-assembly.md +2 -2
  86. package/kits/docs/evolcore/agent.md +11 -6
  87. package/kits/docs/evolcore/aid.md +14 -0
  88. package/kits/docs/prompt-loading-architecture.md +1 -2
  89. package/kits/docs/venues/group.md +1 -1
  90. package/kits/eck_manifest.json +0 -12
  91. package/kits/rules/04-relation.md +1 -1
  92. package/kits/rules/05-venue.md +2 -2
  93. package/kits/schemas/single-session.schema.1.json +3 -3
  94. package/kits/schemas/single-session.schema.2.json +3 -3
  95. package/kits/templates/roles/admin.json +22 -0
  96. package/kits/templates/roles/member.json +27 -0
  97. package/kits/templates/roles/visitor.json +33 -3
  98. package/kits/templates/system-fragments/bootstrap.md +1 -1
  99. package/kits/templates/system-fragments/channel.md +1 -1
  100. package/kits/templates/system-fragments/session.md +1 -1
  101. package/package.json +2 -2
  102. package/dist/core/command/evol-menu-version-gate.js +0 -39
@@ -13,13 +13,14 @@ 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
22
  import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
22
- import { classifyEvolcoreShellCommand, parseLiteralShellArgv } from '../core/permission/ec-command-parser.js';
23
+ import { classifyEvolcoreShellCommand, 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';
25
26
  import { buildSessionTurnList } from '../core/session/session-turns.js';
@@ -118,10 +119,9 @@ const CODEX_CATALOG_FALLBACK = [
118
119
  ];
119
120
  const CODEX_DELEGATION_CARRIER_METADATA_KEY = '__codexDelegationCarrierV1';
120
121
  let codexCatalogCache = null;
121
- // The permission bridge is version-locked to the v2 schema audited below:
122
- // named profiles, approvalsReviewer, additional/network approval context, and
123
- // item/permissions/requestApproval must all be present together.
124
- 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';
125
125
  export function parseCodexCliVersion(output) {
126
126
  const match = output.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/);
127
127
  return match?.[1] ?? null;
@@ -223,6 +223,8 @@ export class CodexRunner {
223
223
  trackedApprovalItems = new Map();
224
224
  threadDelegationCarriers = new Map();
225
225
  pendingInterrupts = new Map();
226
+ threadOperationLocks = new Map();
227
+ activeOperationReleases = new Map();
226
228
  appServerClient = null;
227
229
  onSessionIdUpdate;
228
230
  onCompactStart;
@@ -289,15 +291,6 @@ export class CodexRunner {
289
291
  const argv = parseLiteralShellArgv(command);
290
292
  return argv?.[0] === 'ec' ? argv : undefined;
291
293
  }
292
- unwrapCodexShellCommandArgv(argv) {
293
- if (argv.length !== 3 || argv[1] !== '-lc')
294
- return undefined;
295
- const shell = argv[0];
296
- if (!['bash', '/bin/bash', '/usr/bin/bash', 'sh', '/bin/sh', '/usr/bin/sh'].includes(shell)) {
297
- return undefined;
298
- }
299
- return argv[2];
300
- }
301
294
  approvedEvolcoreCommandArgv(toolInput) {
302
295
  const explicitArgv = Array.isArray(toolInput.commandArgv)
303
296
  && toolInput.commandArgv.every(value => typeof value === 'string')
@@ -306,7 +299,7 @@ export class CodexRunner {
306
299
  if (explicitArgv?.[0] === 'ec')
307
300
  return explicitArgv;
308
301
  const explicitWrappedCommand = explicitArgv
309
- ? this.unwrapCodexShellCommandArgv(explicitArgv)
302
+ ? unwrapCodexShellCommandArgv(explicitArgv)
310
303
  : undefined;
311
304
  if (explicitWrappedCommand !== undefined) {
312
305
  return this.approvedLiteralEvolcoreCommandArgv(explicitWrappedCommand);
@@ -317,7 +310,7 @@ export class CodexRunner {
317
310
  return directArgv;
318
311
  const wrappedArgv = parseLiteralShellArgv(command);
319
312
  const wrappedCommand = wrappedArgv
320
- ? this.unwrapCodexShellCommandArgv(wrappedArgv)
313
+ ? unwrapCodexShellCommandArgv(wrappedArgv)
321
314
  : undefined;
322
315
  return wrappedCommand === undefined
323
316
  ? undefined
@@ -796,6 +789,35 @@ export class CodexRunner {
796
789
  }
797
790
  setSendPrompt(fn) { this.sendPromptFn = fn; }
798
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
+ }
799
821
  setPermissionGateway(gw) { this.permissionGateway = gw; }
800
822
  // ── Stream management (needed by MessageProcessor) ──
801
823
  registerStream(key, stream) {
@@ -805,13 +827,135 @@ export class CodexRunner {
805
827
  this.activeStreams.delete(key);
806
828
  this.activeAbortControllers.delete(key);
807
829
  this.pendingInterrupts.delete(key);
830
+ const release = this.activeOperationReleases.get(key);
831
+ if (release) {
832
+ this.activeOperationReleases.delete(key);
833
+ release();
834
+ }
808
835
  }
809
836
  hasActiveStream(key) {
810
837
  return this.activeStreams.has(key) || this.activeAbortControllers.has(key) || this.activeTurns.has(key);
811
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
+ }
812
939
  // ── Core: runQuery ──
813
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) {
814
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
+ }
815
959
  const resumingThread = !!agentSessionId;
816
960
  const callModel = modelOverride?.model || this.model;
817
961
  const callEffort = modelOverride?.effortMode === 'model_default'
@@ -893,6 +1037,7 @@ export class CodexRunner {
893
1037
  }
894
1038
  agentSessionId = threadId;
895
1039
  this.activeSessions.set(sessionId, threadId);
1040
+ this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
896
1041
  this.threadProjectPaths.set(threadId, path.resolve(projectPath));
897
1042
  this.bindExternalToolBoundary(threadId, externalToolConfig);
898
1043
  this.onSessionIdUpdate?.(sessionId, threadId);
@@ -1035,10 +1180,12 @@ export class CodexRunner {
1035
1180
  }
1036
1181
  if (agentSessionId) {
1037
1182
  this.activeSessions.set(sessionId, agentSessionId);
1183
+ this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${agentSessionId}`);
1038
1184
  }
1039
1185
  else {
1040
1186
  this.activeSessions.delete(sessionId);
1041
1187
  }
1188
+ this.pruneThreadOperationLocks();
1042
1189
  this.onSessionIdUpdate?.(sessionId, agentSessionId);
1043
1190
  }
1044
1191
  async closeSession(sessionId) {
@@ -1057,6 +1204,7 @@ export class CodexRunner {
1057
1204
  this.unbindExternalToolBoundary(threadId);
1058
1205
  }
1059
1206
  this.clearTrackedApprovalItemsForThread(threadId);
1207
+ this.pruneThreadOperationLocks();
1060
1208
  }
1061
1209
  resolveSessionFile(agentSessionId, _projectPath) {
1062
1210
  // Codex session 文件: ~/.codex/sessions/YYYY/MM/DD/rollout-*-{threadId}.jsonl
@@ -1089,10 +1237,12 @@ export class CodexRunner {
1089
1237
  this.threadDelegationCarriers.delete(threadId);
1090
1238
  this.unbindExternalToolBoundary(threadId);
1091
1239
  this.clearTrackedApprovalItemsForThread(threadId);
1240
+ this.pruneThreadOperationLocks();
1092
1241
  this.onSessionIdUpdate?.(sessionId, '');
1093
1242
  return true;
1094
1243
  }
1095
1244
  async compactSession(_sessionId, agentSessionId, _projectPath) {
1245
+ const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId, _sessionId), 'compact');
1096
1246
  try {
1097
1247
  const appServer = this.getAppServerClient();
1098
1248
  this.onCompactStart?.(_sessionId);
@@ -1129,6 +1279,9 @@ export class CodexRunner {
1129
1279
  logger.error('[CodexRunner] Compact failed:', error);
1130
1280
  return false;
1131
1281
  }
1282
+ finally {
1283
+ release();
1284
+ }
1132
1285
  }
1133
1286
  async compact(sessionId, agentSessionId, projectPath) {
1134
1287
  return this.compactSession(sessionId, agentSessionId, projectPath);
@@ -1254,6 +1407,15 @@ export class CodexRunner {
1254
1407
  return newest?.path;
1255
1408
  }
1256
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) {
1257
1419
  const sessionKey = this.findSessionKeyByThread(agentSessionId);
1258
1420
  const mode = this.chatModes.get(sessionKey) ?? this.currentMode;
1259
1421
  const executionSandbox = this.buildExecutionSandboxOptions(sessionKey, mode, projectPath);
@@ -1278,18 +1440,30 @@ export class CodexRunner {
1278
1440
  return forkedThreadId;
1279
1441
  }
1280
1442
  async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title) {
1281
- const turns = buildSessionTurnList(await this.getSessionMessages(agentSessionId, projectPath));
1282
- const targetIndex = turns.findIndex(turn => turn.assistantUuid === assistantMessageId);
1283
- if (targetIndex < 0)
1284
- throw new Error('fork point not found');
1285
- const forkedThreadId = await this.forkSession(agentSessionId, projectPath, title);
1286
- const turnsToDiscard = turns.length - targetIndex - 1;
1287
- if (turnsToDiscard > 0) {
1288
- const rolledBack = await this.rollbackSessionTurns(forkedThreadId, projectPath, turnsToDiscard);
1289
- if (!rolledBack)
1290
- 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();
1291
1466
  }
1292
- return forkedThreadId;
1293
1467
  }
1294
1468
  async setSessionName(agentSessionId, name) {
1295
1469
  return this.getAppServerClient().threadSetName(agentSessionId, name);
@@ -1421,6 +1595,25 @@ export class CodexRunner {
1421
1595
  // proactive 模式行为策略(首次工具调用必须是 ec msg send)
1422
1596
  const policyResult = this.permissionContexts.get(sessionKey)?.policyHook?.(toolName, toolInput);
1423
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
+ }
1424
1617
  return this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
1425
1618
  }
1426
1619
  const summary = this.summarizeAppServerRequest(request.method, params);
@@ -1434,7 +1627,7 @@ export class CodexRunner {
1434
1627
  const sessionMode = this.chatModes.get(sessionKey) ?? this.currentMode;
1435
1628
  logger.info(`[CodexRunner] app-server approval request id=${request.id} method=${request.method} session=${sessionKey} mode=${sessionMode} tool=${toolName} summary=${summary}`);
1436
1629
  try {
1437
- 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);
1438
1631
  if (decision !== 'deny'
1439
1632
  && (request.method === 'item/commandExecution/requestApproval' || request.method === 'execCommandApproval')
1440
1633
  && this.approvedEvolcoreCommandArgv(toolInput)) {
@@ -1965,7 +2158,7 @@ export class CodexRunner {
1965
2158
  return true;
1966
2159
  });
1967
2160
  }
1968
- 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) {
1969
2162
  const permissionContext = this.permissionContexts.get(sessionKey);
1970
2163
  const permissionPrompt = permissionContext?.sendPrompt ?? this.sendPromptFn;
1971
2164
  const hasFilesystemPermissions = (toolName === 'Bash' || toolName === 'PermissionGrant')
@@ -1980,6 +2173,24 @@ export class CodexRunner {
1980
2173
  // per-session 权限模式(runQuery 写入);缺省回落实例级 currentMode(兼容无 runQuery 上下文的调用)
1981
2174
  const rawMode = this.chatModes.get(sessionKey) ?? this.currentMode;
1982
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
+ };
1983
2194
  const preflight = evaluateToolPreflight(toolName, toolInput, {
1984
2195
  sessionId: sessionKey,
1985
2196
  selfAid: permissionContext?.selfAid,
@@ -1990,8 +2201,12 @@ export class CodexRunner {
1990
2201
  projectPath: operationCwd,
1991
2202
  workspacePath,
1992
2203
  });
1993
- if (preflight.behavior === 'deny')
2204
+ if (preflight.behavior === 'deny') {
2205
+ if (preflight.policyCode) {
2206
+ await recordBlockedOperation(preflight.policyCode);
2207
+ }
1994
2208
  return 'deny';
2209
+ }
1995
2210
  if (preflight.behavior === 'allow')
1996
2211
  return 'allow';
1997
2212
  const checkedInput = preflight.input;
@@ -2015,8 +2230,10 @@ export class CodexRunner {
2015
2230
  const grantScope = `codex:${this.permissionProfileName(sessionKey)}:${mode}`;
2016
2231
  if (mode === 'readonly') {
2017
2232
  const readonly = this.checkCodexReadonly(toolName, checkedInput, operationCwd, sessionKey);
2018
- if (readonly.behavior === 'deny')
2233
+ if (readonly.behavior === 'deny') {
2234
+ await recordBlockedOperation('readonly_mode', checkedInput);
2019
2235
  return 'deny';
2236
+ }
2020
2237
  return 'allow';
2021
2238
  }
2022
2239
  if (toolName.startsWith('MCP:')) {
@@ -2024,13 +2241,19 @@ export class CodexRunner {
2024
2241
  }
2025
2242
  // auto 模式下检查危险命令,危险命令自动拒绝
2026
2243
  if (mode === 'auto') {
2027
- if (hasFileChangeExpansion || hasFilesystemPermissions || hasUnknownPermissions)
2244
+ if (hasFileChangeExpansion || hasFilesystemPermissions || hasUnknownPermissions) {
2245
+ await recordBlockedOperation('auto_permission_expansion', checkedInput);
2028
2246
  return 'deny';
2029
- if (toolName === 'PermissionGrant' && !hasNetworkPermissions)
2247
+ }
2248
+ if (toolName === 'PermissionGrant' && !hasNetworkPermissions) {
2249
+ await recordBlockedOperation('auto_permission_expansion', checkedInput);
2030
2250
  return 'deny';
2251
+ }
2031
2252
  const dangerous = checkDangerousCommand(toolName, checkedInput);
2032
- if (dangerous.isDangerous)
2253
+ if (dangerous.isDangerous) {
2254
+ await recordBlockedOperation('auto_dangerous_command', checkedInput);
2033
2255
  return 'deny';
2256
+ }
2034
2257
  return 'allow';
2035
2258
  }
2036
2259
  const requiresSandboxExpansionApproval = hasFileChangeExpansion
@@ -2083,6 +2306,15 @@ export class CodexRunner {
2083
2306
  throw new Error('Unsupported Codex app-server request: ' + method);
2084
2307
  }
2085
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) {
2086
2318
  if (numTurns < 1)
2087
2319
  return true;
2088
2320
  const response = await this.getAppServerClient().threadRollback(agentSessionId, numTurns);
@@ -2121,7 +2353,11 @@ export class CodexRunner {
2121
2353
  }
2122
2354
  readGitHeadFile(projectPath, filePath) {
2123
2355
  try {
2124
- 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
+ });
2125
2361
  }
2126
2362
  catch {
2127
2363
  return null;
@@ -2877,6 +3113,8 @@ export class CodexRunner {
2877
3113
  this.pendingInterrupts.clear();
2878
3114
  this.permissionContexts.clear();
2879
3115
  this.chatModes.clear();
3116
+ this.pruneThreadOperationLocks();
3117
+ this.threadOperationLocks.clear();
2880
3118
  for (const sessionId of [...this.sessionTempDirs.keys()])
2881
3119
  this.cleanupSessionTempDir(sessionId);
2882
3120
  await this.appServerClient?.close();
@@ -9,6 +9,7 @@ import { requestDangerousCommandPermission } from '../core/permission/approval-g
9
9
  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
+ import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
12
13
  import { resolveEcagentConfig } from './baseagent.js';
13
14
  import { buildModelRequestHeaders } from './request-identity.js';
14
15
  const PROVIDER_ID = 'evolcore-gateway';
@@ -957,9 +958,28 @@ export class EcagentRunner {
957
958
  }
958
959
  async authorizeTool(sessionId, projectPath, mode, toolName, input) {
959
960
  const permissionContext = this.permissionContexts.get(sessionId);
961
+ const recordBlockedOperation = async (policyCode, operationInput = input) => {
962
+ try {
963
+ await permissionContext?.recordExecutionAnomaly?.({
964
+ code: 'operation_blocked',
965
+ severity: 'warning',
966
+ phase: 'execution',
967
+ occurredAt: Date.now(),
968
+ toolName,
969
+ policyCode,
970
+ summary: summarizeToolInputForAudit(toolName, operationInput).slice(0, 512),
971
+ effect: 'operation_skipped',
972
+ });
973
+ }
974
+ catch (error) {
975
+ logger.warn(`[EcagentRunner] failed to record blocked operation: session=${sessionId} tool=${toolName} code=${policyCode} error=${error instanceof Error ? error.message : String(error)}`);
976
+ }
977
+ };
960
978
  const policy = permissionContext?.policyHook?.(toolName, input);
961
- if (policy?.block)
979
+ if (policy?.block) {
980
+ await recordBlockedOperation(policy.policyCode ?? 'session_policy_hook');
962
981
  return { block: true, reason: policy.reason || '当前会话策略拒绝此工具调用' };
982
+ }
963
983
  const preflight = evaluateToolPreflight(toolName, input, {
964
984
  sessionId,
965
985
  selfAid: permissionContext?.selfAid,
@@ -969,8 +989,12 @@ export class EcagentRunner {
969
989
  permissionMode: mode,
970
990
  projectPath,
971
991
  });
972
- if (preflight.behavior === 'deny')
992
+ if (preflight.behavior === 'deny') {
993
+ if (preflight.policyCode) {
994
+ await recordBlockedOperation(preflight.policyCode, input);
995
+ }
973
996
  return { block: true, reason: preflight.message };
997
+ }
974
998
  if (preflight.behavior === 'allow')
975
999
  return {};
976
1000
  const checkedInput = preflight.input;
@@ -978,11 +1002,19 @@ export class EcagentRunner {
978
1002
  const decision = checkReadonly(toolName, checkedInput, projectPath, {
979
1003
  sessionId, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
980
1004
  });
981
- return decision.behavior === 'deny' ? { block: true, reason: decision.message } : {};
1005
+ if (decision.behavior === 'deny') {
1006
+ await recordBlockedOperation('readonly_mode', checkedInput);
1007
+ return { block: true, reason: decision.message };
1008
+ }
1009
+ return {};
982
1010
  }
983
1011
  const dangerous = checkDangerousCommand(toolName, checkedInput);
984
1012
  if (mode === 'auto') {
985
- return dangerous.isDangerous ? { block: true, reason: `危险操作已由 auto 模式拒绝:${dangerous.reason}` } : {};
1013
+ if (dangerous.isDangerous) {
1014
+ await recordBlockedOperation('auto_dangerous_command', checkedInput);
1015
+ return { block: true, reason: `危险操作已由 auto 模式拒绝:${dangerous.reason}` };
1016
+ }
1017
+ return {};
986
1018
  }
987
1019
  const prompt = permissionContext?.sendPrompt ?? this.sendPromptFn;
988
1020
  const decision = await requestDangerousCommandPermission(this.permissionGateway, sessionId, toolName, checkedInput, prompt, permissionContext, `ecagent:${mode}`, mode);
@@ -344,6 +344,7 @@ export class GeminiRunner {
344
344
  cwd: projectPath,
345
345
  stdio: ['pipe', 'pipe', 'pipe'],
346
346
  env,
347
+ windowsHide: process.platform === 'win32',
347
348
  });
348
349
  this.activeProcesses.set(sessionId, child);
349
350
  // Log stderr
@@ -1,8 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
- import { execFileSync } from 'child_process';
5
- import { isWindows } from '../../utils/cross-platform.js';
6
4
  /**
7
5
  * Suppress SDK console logs (DEBUG/INFO/WARN) in CLI context.
8
6
  * Call once at CLI entry point — NOT at module load time, to avoid
@@ -40,10 +38,9 @@ export function suppressSdkLogs() {
40
38
  return _origStderrWrite(chunk, encoding, callback);
41
39
  };
42
40
  }
43
- // ==================== Constants ====================
44
- export const MIN_AUN_CORE_SDK = [0, 4, 3];
41
+ // ==================== SDK package ====================
42
+ export const MIN_AUN_CORE_SDK = [0, 5, 6];
45
43
  export const AUN_CORE_SDK_PKG = '@agentunion/fastaun';
46
- // ==================== SDK & Environment ====================
47
44
  function compareVersion(a, min) {
48
45
  const parts = a.split('.').map(n => parseInt(n, 10));
49
46
  if (parts.length < 3 || parts.some(isNaN))
@@ -57,6 +54,7 @@ function compareVersion(a, min) {
57
54
  export function isAunSdkVersionOk(version) {
58
55
  return compareVersion(version, MIN_AUN_CORE_SDK);
59
56
  }
57
+ /** Resolve only the SDK bundled in EvolCore's own dependency tree. */
60
58
  export function resolveAunCoreSdkPkg() {
61
59
  try {
62
60
  let dir = path.dirname(fileURLToPath(import.meta.url));
@@ -73,28 +71,20 @@ export function resolveAunCoreSdkPkg() {
73
71
  dir = parent;
74
72
  }
75
73
  }
76
- catch { /* fall through */ }
77
- try {
78
- const npmCmd = isWindows ? 'npm.cmd' : 'npm';
79
- const globalRoot = execFileSync(npmCmd, ['root', '-g'], {
80
- encoding: 'utf-8', timeout: 10000, shell: isWindows,
81
- }).trim();
82
- const pkgPath = path.join(globalRoot, AUN_CORE_SDK_PKG, 'package.json');
83
- if (fs.existsSync(pkgPath)) {
84
- const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
85
- return { version: data.version, path: pkgPath };
86
- }
87
- }
88
- catch { /* not found */ }
74
+ catch { /* invalid or missing package metadata */ }
89
75
  return null;
90
76
  }
77
+ /**
78
+ * Compatibility guard retained for callers that explicitly check SDK readiness.
79
+ * The SDK is bundled with EvolCore and must never be installed independently.
80
+ */
91
81
  export async function ensureAunSdk() {
92
82
  const installed = resolveAunCoreSdkPkg();
93
83
  if (installed && isAunSdkVersionOk(installed.version))
94
84
  return;
95
- const { npmInstallGlobal } = await import('../../utils/npm-ops.js');
96
- console.log(`正在安装 ${AUN_CORE_SDK_PKG}@latest...`);
97
- await npmInstallGlobal(`${AUN_CORE_SDK_PKG}@latest`);
85
+ const expected = MIN_AUN_CORE_SDK.join('.');
86
+ const detected = installed?.version ?? 'missing';
87
+ throw new Error(`${AUN_CORE_SDK_PKG} ${detected} is not the bundled SDK expected by EvolCore (>= ${expected}); reinstall or upgrade EvolCore`);
98
88
  }
99
89
  export function isAunSdkReady() {
100
90
  const installed = resolveAunCoreSdkPkg();