evolcore 0.0.28 → 0.0.29

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 (183) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +19 -9
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +6 -2
  7. package/dist/agents/codex/diagnostics.js +4 -3
  8. package/dist/agents/codex-runner.js +396 -95
  9. package/dist/agents/gemini-runner.js +35 -17
  10. package/dist/aun/group/history.js +130 -90
  11. package/dist/aun/group/operations.js +1 -3
  12. package/dist/aun/menu/transport.js +5 -3
  13. package/dist/aun/msg/managed-operation.js +70 -0
  14. package/dist/aun/msg/p2p.js +1 -3
  15. package/dist/aun/msg/upload.js +2 -2
  16. package/dist/aun/outbox.js +1 -37
  17. package/dist/channels/aun.js +249 -89
  18. package/dist/channels/daemon.js +18 -1
  19. package/dist/channels/feishu.js +1 -2
  20. package/dist/cli/agent-command.js +23 -3
  21. package/dist/cli/agent-create.js +68 -2
  22. package/dist/cli/agent.js +12 -2
  23. package/dist/cli/aun-commands.js +87 -13
  24. package/dist/cli/cli-argv.js +26 -0
  25. package/dist/cli/daemon-commands.js +40 -10
  26. package/dist/cli/data-command.js +4 -4
  27. package/dist/cli/handoff-command.js +2 -0
  28. package/dist/cli/index.js +18 -1
  29. package/dist/cli/managed-command-guard.js +10 -0
  30. package/dist/cli/memory-command.js +441 -0
  31. package/dist/cli/restart-monitor.js +62 -6
  32. package/dist/cli/skill-command.js +41 -0
  33. package/dist/cli/task-context.js +2 -23
  34. package/dist/cli/trigger-command.js +9 -1
  35. package/dist/cli/watch-msg-diag.js +209 -0
  36. package/dist/config/config-field-policy.js +52 -3
  37. package/dist/config/config-manager.js +17 -4
  38. package/dist/config/contact-book-store.js +2 -2
  39. package/dist/config/mention-mode.js +7 -2
  40. package/dist/config/merge.js +28 -9
  41. package/dist/config/resolved-config-op.js +1 -1
  42. package/dist/config/runtime-env.js +137 -0
  43. package/dist/config/schema-registry.js +11 -1
  44. package/dist/config-store.js +10 -10
  45. package/dist/core/audit/event-key.js +3 -1
  46. package/dist/core/audit/log-integrity.js +4 -0
  47. package/dist/core/auth/agent-delegation.js +2 -1
  48. package/dist/core/auth/auth-gateway.js +13 -6
  49. package/dist/core/auth/authorization-audit.js +54 -19
  50. package/dist/core/auth/operation-authorizer.js +17 -0
  51. package/dist/core/auth/operation-catalog.js +9 -0
  52. package/dist/core/causation/context.js +5 -1
  53. package/dist/core/command/agent-control.js +1 -2
  54. package/dist/core/command/command-handler.js +51 -23
  55. package/dist/core/command/memory-slash.js +270 -0
  56. package/dist/core/command/menu-catalog.js +1 -0
  57. package/dist/core/command/menu-handler.js +315 -60
  58. package/dist/core/command/menu-protocol.js +42 -4
  59. package/dist/core/command/slash-gate.js +10 -10
  60. package/dist/core/command/slash-handler.js +206 -64
  61. package/dist/core/data-migration.js +282 -505
  62. package/dist/core/event-catalog.js +26 -0
  63. package/dist/core/evolagent-registry.js +6 -0
  64. package/dist/core/evolagent.js +0 -10
  65. package/dist/core/handoff/store.js +1 -20
  66. package/dist/core/inference/text-inference.js +57 -1
  67. package/dist/core/memory/consolidation-context.js +505 -0
  68. package/dist/core/memory/consolidation-ops-records.js +124 -0
  69. package/dist/core/memory/consolidation-trigger.js +394 -0
  70. package/dist/core/memory/index-manager.js +308 -0
  71. package/dist/core/memory/launch-consolidation.js +395 -0
  72. package/dist/core/memory/memory-admin.js +234 -0
  73. package/dist/core/memory/memory-document.js +100 -0
  74. package/dist/core/memory/memory-session-policy.js +41 -0
  75. package/dist/core/memory/memory-signature.js +257 -0
  76. package/dist/core/memory/memory-writer.js +432 -0
  77. package/dist/core/memory/staleness.js +117 -0
  78. package/dist/core/message/auto-mention-gate.js +454 -0
  79. package/dist/core/message/im-renderer.js +69 -16
  80. package/dist/core/message/message-bridge.js +287 -21
  81. package/dist/core/message/message-log.js +1 -0
  82. package/dist/core/message/message-queue.js +114 -27
  83. package/dist/core/message/pending-hints.js +329 -161
  84. package/dist/core/message/response-engine.js +567 -80
  85. package/dist/core/permission/ec-command-parser.js +1029 -80
  86. package/dist/core/permission/protected-paths.js +376 -126
  87. package/dist/core/permission/sandbox-runtime.js +40 -12
  88. package/dist/core/permission/tool-error-code.js +96 -13
  89. package/dist/core/permission/tool-policy.js +276 -23
  90. package/dist/core/role/runtime-policy.js +3 -7
  91. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  92. package/dist/core/session/session-fs-store.js +0 -76
  93. package/dist/core/session/session-idle-monitor.js +355 -0
  94. package/dist/core/session/session-manager.js +478 -17
  95. package/dist/core/session/session-mapper.js +33 -16
  96. package/dist/core/session/session-renew.js +217 -155
  97. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  98. package/dist/core/skill/global-watcher.js +282 -0
  99. package/dist/core/skill/index-generator.js +84 -0
  100. package/dist/core/skill/startup.js +73 -0
  101. package/dist/core/skill/switch-resolver.js +115 -0
  102. package/dist/core/task/task-activity-store.js +219 -0
  103. package/dist/eck/kit-renderer.js +30 -3
  104. package/dist/eck/manifest-engine.js +26 -4
  105. package/dist/index.js +708 -18
  106. package/dist/ipc.js +564 -2
  107. package/dist/paths.js +30 -9
  108. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  109. package/dist/response-system/modes/single-session/index.js +3 -0
  110. package/dist/trigger/feedback.js +1 -0
  111. package/dist/trigger/manager.js +0 -10
  112. package/dist/trigger/scheduler.js +11 -0
  113. package/dist/trigger/validation.js +22 -0
  114. package/dist/utils/avatar-upload.js +59 -18
  115. package/dist/utils/error-dict.json +6 -3
  116. package/dist/utils/error-utils.js +27 -0
  117. package/dist/utils/logger.js +32 -10
  118. package/dist/utils/media-cache.js +84 -0
  119. package/dist/utils/msg-diag.js +97 -0
  120. package/dist/utils/welcome.js +37 -8
  121. package/kits/docs/AUTHORING.md +314 -0
  122. package/kits/docs/GUIDE.md +2 -0
  123. package/kits/docs/INDEX.md +2 -0
  124. package/kits/docs/aun/CHEATSHEET.md +4 -0
  125. package/kits/docs/aun/SDK.md +60 -0
  126. package/kits/docs/context-assembly.md +10 -6
  127. package/kits/docs/evolcore/INDEX.md +17 -3
  128. package/kits/docs/evolcore/agent-create.md +24 -0
  129. package/kits/docs/evolcore/agent.md +5 -2
  130. package/kits/docs/evolcore/config.md +1 -1
  131. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  132. package/kits/docs/evolcore/group.md +34 -18
  133. package/kits/docs/evolcore/memory-architecture.md +78 -0
  134. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  135. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  136. package/kits/docs/evolcore/memory-tags.md +63 -0
  137. package/kits/docs/evolcore/memory.md +110 -0
  138. package/kits/docs/evolcore/menu-client.md +27 -11
  139. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  140. package/kits/docs/evolcore/menu-protocol.md +213 -0
  141. package/kits/docs/evolcore/msg.md +2 -0
  142. package/kits/docs/evolcore/trigger.md +7 -5
  143. package/kits/docs/path-registry.md +6 -8
  144. package/kits/docs/prompt-loading-architecture.md +5 -3
  145. package/kits/docs/venues/group.md +5 -4
  146. package/kits/eck_manifest.json +294 -88
  147. package/kits/eck_manifest.memory.json +257 -0
  148. package/kits/rules/03-identity.md +8 -8
  149. package/kits/rules/05-venue.md +4 -2
  150. package/kits/rules/06-channel.md +1 -1
  151. package/kits/schemas/_meta.json +276 -47
  152. package/kits/schemas/agent-config.schema.12.json +38 -2
  153. package/kits/schemas/agent-config.schema.13.json +591 -0
  154. package/kits/schemas/daemon.schema.6.json +9 -9
  155. package/kits/schemas/defaults.schema.5.json +29 -0
  156. package/kits/schemas/relation-config.schema.8.json +14 -1
  157. package/kits/schemas/relation-config.schema.9.json +129 -0
  158. package/kits/schemas/role-config.schema.1.json +3 -3
  159. package/kits/skills/.switches.json +3 -0
  160. package/kits/skills/ec-skill/SKILL.md +217 -0
  161. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  162. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  163. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  164. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  165. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  166. package/kits/templates/decision/auto-mention.md +25 -0
  167. package/kits/templates/decision/session-renew.md +22 -0
  168. package/kits/templates/memory-consolidation/forget.md +104 -0
  169. package/kits/templates/memory-consolidation/generalize.md +76 -0
  170. package/kits/templates/memory-consolidation/notes.md +133 -0
  171. package/kits/templates/memory-consolidation/profile.md +58 -0
  172. package/kits/templates/memory-consolidation/promote.md +72 -0
  173. package/kits/templates/memory-consolidation/workflow.md +75 -0
  174. package/kits/templates/system-fragments/bootstrap.md +14 -5
  175. package/kits/templates/system-fragments/commands.md +1 -0
  176. package/kits/templates/system-fragments/group-history.md +6 -0
  177. package/kits/templates/system-fragments/identity.md +0 -3
  178. package/kits/templates/system-fragments/memory.md +79 -0
  179. package/kits/templates/system-fragments/reply.md +1 -0
  180. package/kits/templates/system-fragments/venue.md +1 -1
  181. package/package.json +4 -6
  182. package/dist/trigger/legacy-session-history.js +0 -19
  183. package/kits/templates/system-fragments/group-unread-history.md +0 -7
@@ -5,7 +5,7 @@ import { formatAsyncQuestionAnswer, hasTrustedFullAccessContext, parseAsyncUserQ
5
5
  import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
6
6
  import { checkReadonly, checkDangerousCommand, evaluateToolPreflight } from '../core/permission/tool-policy.js';
7
7
  import { normalizeExecutionPermissionMode, normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
8
- import { assertWindowsCodexFilesystemRuleBudget, buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtectedCandidate, } from '../core/permission/protected-paths.js';
8
+ import { assertWindowsCodexFilesystemRuleBudget, buildCodexProtectedFilesystemRules, getExistingRelationAttachmentDirs, isSameOrDescendant, resolveProtectedCandidate, } from '../core/permission/protected-paths.js';
9
9
  import { CODEX_THREAD_ACTIVE_WRITER, CodexAppServerClient, } from './codex/app-server-client.js';
10
10
  import { resolveOpenaiConfig, resolveCodexCliPath, resolveCodexPowerShellShimPath, } from './baseagent.js';
11
11
  import { logger } from '../utils/logger.js';
@@ -17,26 +17,20 @@ import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from
17
17
  import { resolveCodexCapabilityThreadConfigForProject } from '../core/capability/capability-manager.js';
18
18
  import { AGENT_DELEGATION_TOKEN_ENV, CODEX_SESSION_TOKEN_ENV, hashDelegatedCommandArgv } from '../core/auth/agent-delegation.js';
19
19
  import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
20
- import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, hasCodexCmdCarrierEcIntent, hasPowerShellCredentialAssignment, normalizeEvolcoreCommandArgv, parseCodexToolCommand, parseCodexCarrierCommand, parseLiteralShellCommand, parseCodexShellCommandArgv, resolveCodexShellCarrierArgv, } from '../core/permission/ec-command-parser.js';
20
+ import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, containsUnresolvedManagedTmpDirReference, hasCodexCmdCarrierEcIntent, hasPowerShellCredentialAssignment, normalizeEvolcoreCommandArgv, canonicalizeEvolcoreCommandArgv, parseCodexToolCommand, parseCodexCarrierCommand, parseLiteralShellCommand, parseCodexShellCommandArgv, resolveCodexShellCarrierArgv, parseShellApplyPatchInvocation, } from '../core/permission/ec-command-parser.js';
21
21
  import { compareVersions } from '../utils/npm-ops.js';
22
- import { agentSessionsDir, resolvePaths, resolveRoot } from '../paths.js';
22
+ import { agentCodexTokenPath, resolvePaths, resolveRoot } from '../paths.js';
23
23
  import { ensureSessionRuntimeDir, getManagedTaskTempDir } from '../cli/task-context.js';
24
- import { buildSessionTurnList } from '../core/session/session-turns.js';
24
+ import { buildTranscriptTurns } from '../core/session/session-transcript.js';
25
25
  import { CodexSessionFileAdapter } from '../core/session/adapters/codex-session-file-adapter.js';
26
26
  import { execFileSync } from 'child_process';
27
27
  import { probeCli, probeCliSync } from '../utils/cross-platform.js';
28
28
  import { createHash, randomBytes } from 'crypto';
29
29
  import { buildModelRequestHeaders } from './request-identity.js';
30
+ import { normalizeToolErrorClassification, normalizeToolErrorCode, normalizeToolErrorDetailCode, } from '../core/permission/tool-error-code.js';
30
31
  import fs from 'fs';
31
32
  import path from 'path';
32
33
  import os from 'os';
33
- // ── MIME → 扩展名映射 ──
34
- const MIME_EXT = {
35
- 'image/png': '.png',
36
- 'image/jpeg': '.jpg',
37
- 'image/gif': '.gif',
38
- 'image/webp': '.webp',
39
- };
40
34
  const APPROVAL_LOG_SUMMARY_MAX_LENGTH = 256;
41
35
  const CODEX_INTERRUPT_TIMEOUT_MS = 5_000;
42
36
  function toAgentErrorType(message) {
@@ -92,7 +86,7 @@ class AsyncEventQueue {
92
86
  }
93
87
  }
94
88
  }
95
- function errorCodeForToolDenial(decisionSource, policyCode) {
89
+ function fallbackErrorCodeForToolDenial(decisionSource, policyCode) {
96
90
  if (decisionSource === 'policy')
97
91
  return 'POLICY_DENIED';
98
92
  if (decisionSource === 'approval')
@@ -103,6 +97,35 @@ function errorCodeForToolDenial(decisionSource, policyCode) {
103
97
  return 'INVALID_ARGUMENT';
104
98
  return 'EXECUTION_FAILED';
105
99
  }
100
+ function errorCodeForToolDenial(decisionSource, policyCode) {
101
+ return normalizeToolErrorClassification({
102
+ errorCode: fallbackErrorCodeForToolDenial(decisionSource, policyCode),
103
+ decisionSource,
104
+ policyCode,
105
+ executed: false,
106
+ executionState: 'blocked',
107
+ }).errorCode;
108
+ }
109
+ function errorDetailCodeForToolDenial(decisionSource, policyCode, errorDetailCode) {
110
+ return normalizeToolErrorClassification({
111
+ errorCode: fallbackErrorCodeForToolDenial(decisionSource, policyCode),
112
+ errorDetailCode,
113
+ decisionSource,
114
+ policyCode,
115
+ executed: false,
116
+ executionState: 'blocked',
117
+ }).errorDetailCode;
118
+ }
119
+ function codexItemErrorFields(item) {
120
+ const rawCode = item?.error?.code ?? item?.error?.errorCode;
121
+ if (typeof rawCode !== 'string' || !rawCode.trim())
122
+ return {};
123
+ const errorCode = normalizeToolErrorCode(rawCode);
124
+ if (errorCode)
125
+ return { errorCode };
126
+ const errorDetailCode = normalizeToolErrorDetailCode(rawCode);
127
+ return errorDetailCode ? { errorDetailCode } : {};
128
+ }
106
129
  function isCodexReasoningNotification(method) {
107
130
  return method === 'item/reasoning/textDelta'
108
131
  || method === 'item/reasoning/summaryTextDelta'
@@ -429,6 +452,7 @@ export class CodexRunner {
429
452
  enableRequestUserInput: this.resolvedConfig.enableRequestUserInput,
430
453
  approvalsReviewer: 'user',
431
454
  protectEvolcoreFilesystem: !fullAccess,
455
+ selfAid: this.resolvedConfig.agentAid,
432
456
  // A completed turn is explicitly unsubscribed below. This is only an
433
457
  // optimization on newer Codex versions; old versions retain the thread
434
458
  // until the cross-client archive fallback runs.
@@ -613,6 +637,13 @@ export class CodexRunner {
613
637
  this.threadSubscriptions.set(threadId, { client: targetClient, state: 'subscribed' });
614
638
  }
615
639
  approvedLiteralEvolcoreCommand(command, managedTempDir, dialect = 'posix', expectedCredentialToken, executable) {
640
+ if (/\\\$(?:TMPDIR|\{TMPDIR\})/i.test(command)
641
+ && /\bec\s+fs\s+(?:cp|ls|stat|find|cat|mkdir|rm|mv|upload)\b/i.test(command)) {
642
+ return { parseFailure: {
643
+ issue: 'unsafe-expansion', offset: 0, tokenIndex: 0,
644
+ dialect, inputLength: command.length,
645
+ } };
646
+ }
616
647
  const parseOptions = {
617
648
  allowManagedTmpDir: true,
618
649
  dialect,
@@ -644,11 +675,21 @@ export class CodexRunner {
644
675
  } };
645
676
  }
646
677
  return {
647
- argv: normalizeEvolcoreCommandArgv(parsed.argv),
678
+ argv: canonicalizeEvolcoreCommandArgv(parsed.argv),
648
679
  ...(parsed.credentialToken ? { credentialToken: parsed.credentialToken } : {}),
649
680
  };
650
681
  }
651
682
  resolveCanonicalEcApproval(toolInput, managedTempDir, expectedCredentialToken) {
683
+ const rawCommand = typeof toolInput.command === 'string' ? toolInput.command : '';
684
+ if (/\\\$(?:TMPDIR|\{TMPDIR\})/i.test(rawCommand)
685
+ && /\bec\s+fs\s+(?:cp|ls|stat|find|cat|mkdir|rm|mv|upload)\b/i.test(rawCommand)
686
+ && !/\bec\s+(?:msg|group)\s+send\b/i.test(rawCommand)
687
+ && !/\bec\s+trigger\s+(?:create|set|update)\b/i.test(rawCommand)) {
688
+ return { parseFailure: {
689
+ issue: 'unsafe-expansion', offset: 0, tokenIndex: 0,
690
+ dialect: 'posix', inputLength: rawCommand.length,
691
+ } };
692
+ }
652
693
  const explicitArgv = Array.isArray(toolInput.commandArgv)
653
694
  && toolInput.commandArgv.every(value => typeof value === 'string')
654
695
  ? toolInput.commandArgv
@@ -661,9 +702,9 @@ export class CodexRunner {
661
702
  dialect: 'posix', inputLength: 0,
662
703
  } };
663
704
  }
664
- return containsLiteralManagedTmpDirOutsideSendContent(explicitArgv)
705
+ return containsLiteralManagedTmpDirOutsideSendContent(explicitArgv) || containsUnresolvedManagedTmpDirReference(explicitArgv)
665
706
  ? { parseFailure: { issue: 'unsafe-expansion', offset: 0, tokenIndex: 0, dialect: 'posix', inputLength: 0 } }
666
- : { argv: normalizeEvolcoreCommandArgv(explicitArgv) };
707
+ : { argv: canonicalizeEvolcoreCommandArgv(explicitArgv) };
667
708
  }
668
709
  const explicitCarrier = explicitArgv
669
710
  ? resolveCodexShellCarrierArgv(explicitArgv)
@@ -671,6 +712,21 @@ export class CodexRunner {
671
712
  if (explicitCarrier !== undefined) {
672
713
  return this.approvedLiteralEvolcoreCommand(explicitCarrier.command, managedTempDir, explicitCarrier.dialect, expectedCredentialToken, explicitCarrier.executable);
673
714
  }
715
+ if (explicitArgv?.length === 1 && typeof explicitArgv[0] === 'string') {
716
+ const parsed = parseCodexToolCommand({ commandArgv: explicitArgv }, {
717
+ allowManagedTmpDir: true,
718
+ verifyShellExecutable: process.platform === 'win32',
719
+ ...(managedTempDir ? { managedTempDir } : {}),
720
+ ...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
721
+ });
722
+ if (parsed.ok && parsed.argv[0] === 'ec') {
723
+ return containsLiteralManagedTmpDirOutsideSendContent(parsed.argv) || containsUnresolvedManagedTmpDirReference(parsed.argv)
724
+ ? { parseFailure: { issue: 'unsafe-expansion', offset: 0, tokenIndex: 0, dialect: 'powershell', inputLength: explicitArgv[0].length } }
725
+ : { argv: canonicalizeEvolcoreCommandArgv(parsed.argv) };
726
+ }
727
+ if (!parsed.ok)
728
+ return { parseFailure: parsed };
729
+ }
674
730
  if (hasCodexCmdCarrierEcIntent(toolInput)) {
675
731
  const parsed = parseCodexToolCommand(toolInput, {
676
732
  verifyShellExecutable: process.platform === 'win32',
@@ -679,13 +735,13 @@ export class CodexRunner {
679
735
  if (!parsed.ok)
680
736
  return { parseFailure: parsed };
681
737
  if (parsed.argv[0] === 'ec') {
682
- return containsLiteralManagedTmpDirOutsideSendContent(parsed.argv)
738
+ return containsLiteralManagedTmpDirOutsideSendContent(parsed.argv) || containsUnresolvedManagedTmpDirReference(parsed.argv)
683
739
  ? { parseFailure: {
684
740
  issue: 'unsafe-expansion', offset: 0, tokenIndex: 0,
685
741
  dialect: 'cmd', inputLength: 0,
686
742
  } }
687
743
  : {
688
- argv: normalizeEvolcoreCommandArgv(parsed.argv),
744
+ argv: canonicalizeEvolcoreCommandArgv(parsed.argv),
689
745
  ...(parsed.credentialToken ? { credentialToken: parsed.credentialToken } : {}),
690
746
  };
691
747
  }
@@ -709,7 +765,7 @@ export class CodexRunner {
709
765
  ...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
710
766
  });
711
767
  if (canonicalCommand?.[0] === 'ec') {
712
- return containsLiteralManagedTmpDirOutsideSendContent(canonicalCommand)
768
+ return containsLiteralManagedTmpDirOutsideSendContent(canonicalCommand) || containsUnresolvedManagedTmpDirReference(canonicalCommand)
713
769
  ? { parseFailure: {
714
770
  issue: 'unsafe-expansion',
715
771
  offset: 0,
@@ -717,7 +773,7 @@ export class CodexRunner {
717
773
  dialect: 'posix',
718
774
  inputLength: command.length,
719
775
  } }
720
- : { argv: canonicalCommand };
776
+ : { argv: canonicalizeEvolcoreCommandArgv(canonicalCommand) };
721
777
  }
722
778
  const outer = parseLiteralShellCommand(command, {
723
779
  allowManagedTmpDir: true,
@@ -747,6 +803,10 @@ export class CodexRunner {
747
803
  || shellCarrier !== undefined
748
804
  && classifyEvolcoreShellCommand(shellCarrier.command, {
749
805
  dialect: shellCarrier.dialect,
806
+ }).kind !== 'none'
807
+ || rawArgv.length === 1
808
+ && classifyEvolcoreShellCommand(rawArgv[0], {
809
+ dialect: 'powershell',
750
810
  }).kind !== 'none';
751
811
  }
752
812
  const command = typeof toolInput.command === 'string' ? toolInput.command : '';
@@ -1218,8 +1278,15 @@ export class CodexRunner {
1218
1278
  enabled: true,
1219
1279
  unix_sockets: { [resolvePaths().instanceSocket]: 'allow' },
1220
1280
  };
1281
+ const relationAttachmentDenies = {};
1282
+ for (const directory of getExistingRelationAttachmentDirs(resolveRoot(), {
1283
+ selfAid: this.resolvedConfig.agentAid,
1284
+ onlyOtherAgents: true,
1285
+ }))
1286
+ relationAttachmentDenies[directory] = 'deny';
1221
1287
  const filesystem = {
1222
1288
  ...buildCodexProtectedFilesystemRules(resolveRoot(), {
1289
+ selfAid: this.resolvedConfig.agentAid,
1223
1290
  denyLClassRead: mode === 'readonly',
1224
1291
  // The app-server itself is launched behind EvolCore's Linux
1225
1292
  // H-class guard, which already mounts every L-class directory
@@ -1235,6 +1302,7 @@ export class CodexRunner {
1235
1302
  ...(personalMdPath ? {
1236
1303
  [path.resolve(personalMdPath)]: mode === 'readonly' ? 'read' : 'write',
1237
1304
  } : {}),
1305
+ ...relationAttachmentDenies,
1238
1306
  ...(mode !== 'readonly' && managedTempDir ? {
1239
1307
  [managedTempDir]: 'write',
1240
1308
  [managedTempDir + '/**']: 'write',
@@ -1395,6 +1463,7 @@ export class CodexRunner {
1395
1463
  const normalizedInput = toolInput && typeof toolInput === 'object' && !Array.isArray(toolInput)
1396
1464
  ? toolInput
1397
1465
  : {};
1466
+ const isShellTool = toolName === 'Bash' || toolName === 'Shell' || toolName === 'exec_command';
1398
1467
  const recordPreToolUseDenial = async (policyCode, reason) => {
1399
1468
  const summary = summarizeToolInputForAudit(toolName, normalizedInput).slice(0, 512);
1400
1469
  auditToolPreflightDenial({
@@ -1469,6 +1538,28 @@ export class CodexRunner {
1469
1538
  await recordPreToolUseDenial('fullaccess_context_untrusted', reason);
1470
1539
  return { ok: false, applicable: true, decision: 'deny', reason };
1471
1540
  }
1541
+ const preflightProjectPath = this.threadProjectPaths.get(threadId)
1542
+ ?? (activeThread ? this.threadProjectPaths.get(activeThread) : undefined)
1543
+ ?? process.cwd();
1544
+ const preflight = evaluateToolPreflight(isShellTool ? 'Bash' : toolName, normalizedInput, {
1545
+ sessionId: sessionKey,
1546
+ managedTempDir: context.managedTempDir ?? this.getManagedTempDir(sessionKey) ?? '',
1547
+ sessionCredential: sessionToken,
1548
+ allowProtectedMetadata: false,
1549
+ selfAid: context.selfAid,
1550
+ channel: context.channel,
1551
+ userId: context.userId,
1552
+ role: context.role,
1553
+ permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
1554
+ allowReadonlySourceDiagnostics: context.allowReadonlySourceDiagnostics === true,
1555
+ projectPath: preflightProjectPath,
1556
+ workspacePath: preflightProjectPath,
1557
+ });
1558
+ if (preflight.behavior === 'deny') {
1559
+ const reason = preflight.message ?? 'tool preflight denied';
1560
+ await recordPreToolUseDenial(preflight.policyCode ?? 'tool_preflight_denied', reason);
1561
+ return { ok: true, applicable: true, decision: 'deny', reason };
1562
+ }
1472
1563
  // A tracked interactive thread still needs an authenticated, explicit
1473
1564
  // allow so the managed hook can fail closed when no runner matches.
1474
1565
  if (!context.preToolUsePolicyHook)
@@ -1710,7 +1801,7 @@ export class CodexRunner {
1710
1801
  }
1711
1802
  this.chatModes.set(sessionId, callMode);
1712
1803
  const callApprovalPolicy = this.toApprovalPolicy(callMode);
1713
- const executionSandbox = this.buildExecutionSandboxOptions(sessionId, callMode, projectPath, modelOverride?.bootstrapAgentMdPath, modelOverride?.personalMdPath);
1804
+ const executionSandbox = this.buildExecutionSandboxOptions(sessionId, callMode, projectPath, modelOverride?.bootstrapAgentMdPath, modelOverride?.personalMdPath, undefined);
1714
1805
  const appServer = this.getAppServerClient(callMode === 'fullaccess');
1715
1806
  this.dropExpiredPendingInterrupt(sessionId);
1716
1807
  const effectiveApprovalPolicy = callApprovalPolicy;
@@ -1839,12 +1930,11 @@ export class CodexRunner {
1839
1930
  this.bindExternalToolBoundary(threadId, externalToolConfig);
1840
1931
  const controller = new AbortController();
1841
1932
  this.activeAbortControllers.set(sessionId, controller);
1842
- const tempFiles = [];
1843
1933
  const answer = modelOverride?.asyncQuestionAnswer;
1844
1934
  if (answer && answer.providerThreadId !== threadId) {
1845
1935
  throw new Error('Async question reply cannot resume a different Codex thread');
1846
1936
  }
1847
- const input = this.buildAppServerInput(answer ? formatAsyncQuestionAnswer(answer) : prompt, images, tempFiles, sessionId, runtimeEnv);
1937
+ const input = this.buildAppServerInput(answer ? formatAsyncQuestionAnswer(answer) : prompt, images);
1848
1938
  const queue = new AsyncEventQueue();
1849
1939
  controller.signal.addEventListener('abort', () => queue.end(), { once: true });
1850
1940
  const state = {
@@ -1931,7 +2021,7 @@ export class CodexRunner {
1931
2021
  if (!resumingThread)
1932
2022
  this.cleanupProvisionalThread(sessionId, threadId);
1933
2023
  logger.info(`[CodexRunner] Applied pending interrupt before turn start: ${sessionId}`);
1934
- return this.transformAppServerStream(queue, sessionId, state, controller, unsubscribe, appServer, tempFiles);
2024
+ return this.transformAppServerStream(queue, sessionId, state, controller, unsubscribe, appServer);
1935
2025
  }
1936
2026
  try {
1937
2027
  const turnResponse = await appServer.turnStart(threadId, input, {
@@ -2013,12 +2103,11 @@ export class CodexRunner {
2013
2103
  this.activeAbortControllers.delete(sessionId);
2014
2104
  this.activeTurns.delete(sessionId);
2015
2105
  this.pendingInterrupts.delete(sessionId);
2016
- this.cleanupTempFiles(tempFiles);
2017
2106
  if (!resumingThread)
2018
2107
  this.cleanupProvisionalThread(sessionId, threadId);
2019
2108
  throw error;
2020
2109
  }
2021
- return this.transformAppServerStream(queue, sessionId, state, controller, unsubscribe, appServer, tempFiles);
2110
+ return this.transformAppServerStream(queue, sessionId, state, controller, unsubscribe, appServer);
2022
2111
  }
2023
2112
  // ── Interrupt ──
2024
2113
  async interrupt(sessionKey) {
@@ -2467,15 +2556,9 @@ export class CodexRunner {
2467
2556
  // They follow the thread (not the daemon process) so a daemon restart does
2468
2557
  // not strand a resumable thread: the token file is re-read on resume, and
2469
2558
  // it is removed lazily once the rollout file itself disappears.
2470
- threadCredentialsDir() {
2471
- const aid = this.resolvedConfig.agentAid;
2472
- if (!aid)
2473
- return undefined;
2474
- return path.join(agentSessionsDir(aid), 'codex-tokens');
2475
- }
2476
2559
  threadCredentialPath(threadId) {
2477
- const dir = this.threadCredentialsDir();
2478
- return dir ? path.join(dir, `${encodeURIComponent(threadId)}.json`) : undefined;
2560
+ const aid = this.resolvedConfig.agentAid;
2561
+ return aid ? agentCodexTokenPath(aid, threadId) : undefined;
2479
2562
  }
2480
2563
  createThreadCredential() {
2481
2564
  return randomBytes(32).toString('hex');
@@ -2615,7 +2698,7 @@ export class CodexRunner {
2615
2698
  const appServer = this.getAppServerClient();
2616
2699
  const history = await this.readForkHistory(appServer, agentSessionId);
2617
2700
  const messages = this.mapThreadToSessionMessages(history, agentSessionId);
2618
- const turns = buildSessionTurnList(messages);
2701
+ const turns = buildTranscriptTurns(messages);
2619
2702
  const targetIndex = turns.findIndex(turn => turn.assistantUuid === assistantMessageId);
2620
2703
  if (targetIndex < 0)
2621
2704
  throw new Error('fork point not found');
@@ -2633,7 +2716,7 @@ export class CodexRunner {
2633
2716
  if (typeof turn.id !== 'string' || !turn.id)
2634
2717
  continue;
2635
2718
  for (const item of this.getTurnItems(turn)) {
2636
- if (item.type === 'agentMessage' && item.id === assistantMessageId)
2719
+ if (item.id === assistantMessageId)
2637
2720
  return turn.id;
2638
2721
  }
2639
2722
  // Older app-server histories may omit item ids; the mapper then uses the
@@ -3039,7 +3122,7 @@ export class CodexRunner {
3039
3122
  ? this.resolveCanonicalEcApproval(toolInput, this.getManagedTempDir(sessionKey), sessionCredential)
3040
3123
  : undefined;
3041
3124
  const approvedCommand = canonicalEcApproval?.argv;
3042
- const denyInfrastructure = async (policyCode, message) => {
3125
+ const denyInfrastructure = async (policyCode, message, errorDetailCode) => {
3043
3126
  logger.warn(`[CodexRunner] ${message}: session=${sessionKey} requestId=${request.id ?? '<missing>'}`);
3044
3127
  try {
3045
3128
  await permissionContext?.recordExecutionAnomaly?.({
@@ -3067,6 +3150,7 @@ export class CodexRunner {
3067
3150
  decision: 'deny',
3068
3151
  decisionSource: 'infrastructure',
3069
3152
  policyCode,
3153
+ ...(errorDetailCode ? { errorDetailCode } : {}),
3070
3154
  reason: message,
3071
3155
  });
3072
3156
  this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'infrastructure', policyCode, message, canonicalEcApproval?.parseFailure);
@@ -3077,18 +3161,28 @@ export class CodexRunner {
3077
3161
  if (canonicalEcApproval?.parseFailure?.issue === 'untrusted-shell') {
3078
3162
  return await denyInfrastructure('ec_shell_untrusted_executable', 'EC command approval denied because the shell executable failed Windows Authenticode trust validation');
3079
3163
  }
3164
+ if (canonicalEcApproval?.parseFailure?.issue === 'unsupported-escape') {
3165
+ return await denyInfrastructure('ec_shell_unsupported_escape', 'EC inline text does not support PowerShell `n. Use literal \\n in the argument instead.');
3166
+ }
3167
+ if (canonicalEcApproval?.parseFailure?.issue === 'lossy-cmd-shim') {
3168
+ return await denyInfrastructure('ec_shell_lossy_cmd_shim', 'PowerShell cannot pass physical multiline EC arguments through ec.cmd intact. Use ec ... via the PowerShell-native shim instead.');
3169
+ }
3080
3170
  return await denyInfrastructure('ec_command_not_canonical', canonicalEcApproval?.parseFailure
3081
3171
  ? `EC command approval denied because canonical argv parsing failed (${canonicalEcApproval.parseFailure.issue})`
3082
3172
  + (canonicalEcApproval.parseFailure.dialect === 'powershell' && canonicalEcApproval.parseFailure.issue === 'invalid-quote'
3083
3173
  ? '. PowerShell treats smart quotes as delimiters too; escape quotes for PowerShell or use --text-from-file for message text.'
3084
3174
  : '')
3085
- : 'EC command approval denied because no canonical argv could be derived');
3175
+ : 'EC command approval denied because no canonical argv could be derived', canonicalEcApproval?.parseFailure?.issue === 'invalid-quote' ? 'INVALID_QUOTE' : undefined);
3086
3176
  }
3087
3177
  try {
3088
3178
  const resolution = await this.resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath, operationCwd, request.id, typeof params.itemId === 'string' ? params.itemId : typeof params.item_id === 'string' ? params.item_id : undefined);
3089
3179
  const decision = typeof resolution === 'string' ? resolution : resolution.decision;
3090
3180
  const resolutionMeta = typeof resolution === 'string' ? undefined : resolution;
3091
3181
  if (decision !== 'deny' && managedEcIntent && approvedCommand) {
3182
+ if (containsLiteralManagedTmpDirOutsideSendContent(approvedCommand)
3183
+ || containsUnresolvedManagedTmpDirReference(approvedCommand)) {
3184
+ return await denyInfrastructure('ec_command_unsafe_tmpdir_reference', 'EC command contains an unresolved or unsafe managed TMPDIR reference');
3185
+ }
3092
3186
  const credentialThreadIds = [
3093
3187
  this.activeSessions.get(sessionKey),
3094
3188
  this.provisionalSessions.get(sessionKey),
@@ -3123,6 +3217,7 @@ export class CodexRunner {
3123
3217
  decision: 'deny',
3124
3218
  decisionSource,
3125
3219
  ...(policyCode ? { policyCode } : {}),
3220
+ ...(resolutionMeta?.errorDetailCode ? { errorDetailCode: resolutionMeta.errorDetailCode } : {}),
3126
3221
  reason: denialReason,
3127
3222
  });
3128
3223
  setImmediate(() => this.steerPolicyReason(sessionKey, policyCode, denialReason));
@@ -3166,6 +3261,7 @@ export class CodexRunner {
3166
3261
  decision,
3167
3262
  decisionSource,
3168
3263
  policyCode,
3264
+ ...(parseFailure?.issue === 'invalid-quote' ? { errorDetailCode: 'INVALID_QUOTE' } : {}),
3169
3265
  permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
3170
3266
  role: context?.role,
3171
3267
  taskId: context?.taskId,
@@ -3687,6 +3783,7 @@ export class CodexRunner {
3687
3783
  channel: permCtx?.channel,
3688
3784
  peerId: permCtx?.userId,
3689
3785
  role: permCtx?.role,
3786
+ selfAid: permCtx?.selfAid ?? this.resolvedConfig.agentAid,
3690
3787
  allowProtectedMetadata: false,
3691
3788
  // Revalidate ownership, mode, and parent containment at the final
3692
3789
  // readonly decision. The map entry alone is stale if the directory was
@@ -3795,9 +3892,17 @@ export class CodexRunner {
3795
3892
  && this.hasNetworkPermissionRequest(toolInput);
3796
3893
  const hasUnknownPermissions = (toolName === 'Bash' || toolName === 'PermissionGrant')
3797
3894
  && this.hasUnknownPermissionRequest(toolInput);
3895
+ const applyPatch = (toolName === 'Bash' || toolName === 'Shell' || toolName === 'exec_command')
3896
+ ? parseShellApplyPatchInvocation(toolInput)
3897
+ : { kind: 'not-apply-patch' };
3898
+ const applyPatchFileChanges = applyPatch.kind === 'file-change' ? applyPatch.fileChanges : undefined;
3899
+ const applyPatchInput = applyPatchFileChanges
3900
+ ? { ...toolInput, fileChanges: applyPatchFileChanges }
3901
+ : toolInput;
3798
3902
  const hasPermissionExpansion = hasFilesystemPermissions || hasNetworkPermissions || hasUnknownPermissions;
3799
3903
  const hasFileChangeExpansion = toolName === 'FileChange'
3800
- && this.fileChangeRequiresExpansion(toolInput, workspacePath, sessionKey);
3904
+ ? this.fileChangeRequiresExpansion(toolInput, workspacePath, sessionKey)
3905
+ : !!applyPatchFileChanges && this.fileChangeRequiresExpansion(applyPatchInput, workspacePath, sessionKey);
3801
3906
  // per-session 权限模式(runQuery 写入);缺省回落实例级 currentMode(兼容无 runQuery 上下文的调用)
3802
3907
  const rawMode = this.chatModes.get(sessionKey) ?? this.currentMode;
3803
3908
  const mode = normalizeExecutionPermissionMode(rawMode);
@@ -3876,12 +3981,17 @@ export class CodexRunner {
3876
3981
  decision: 'deny',
3877
3982
  decisionSource: 'policy',
3878
3983
  policyCode: preflight.policyCode ?? 'tool_preflight_denied',
3984
+ ...(preflight.errorDetailCode ? { errorDetailCode: preflight.errorDetailCode } : {}),
3879
3985
  reason: preflight.message ?? 'tool preflight denied',
3880
3986
  };
3881
3987
  }
3882
3988
  if (preflight.behavior === 'allow')
3883
3989
  return 'allow';
3884
3990
  const checkedInput = preflight.input;
3991
+ const policyInput = applyPatchFileChanges
3992
+ ? { ...checkedInput, fileChanges: applyPatchFileChanges }
3993
+ : checkedInput;
3994
+ const policyToolName = applyPatchFileChanges ? 'FileChange' : toolName;
3885
3995
  if (toolName === 'FileChange') {
3886
3996
  const hasGrantRoot = typeof checkedInput.grantRoot === 'string'
3887
3997
  && checkedInput.grantRoot.length > 0;
@@ -3901,9 +4011,9 @@ export class CodexRunner {
3901
4011
  return 'deny';
3902
4012
  const grantScope = `codex:${this.permissionProfileName(sessionKey)}:${mode}`;
3903
4013
  if (mode === 'readonly') {
3904
- const readonly = this.checkCodexReadonly(toolName, checkedInput, operationCwd, sessionKey);
4014
+ const readonly = this.checkCodexReadonly(policyToolName, policyInput, operationCwd, sessionKey);
3905
4015
  if (readonly.behavior === 'deny') {
3906
- await recordBlockedOperation(readonly.policyCode ?? 'readonly_mode', checkedInput, readonly.message);
4016
+ await recordBlockedOperation(readonly.policyCode ?? 'readonly_mode', policyInput, readonly.message);
3907
4017
  return 'deny';
3908
4018
  }
3909
4019
  return 'allow';
@@ -3914,7 +4024,7 @@ export class CodexRunner {
3914
4024
  // auto 模式下检查危险命令,危险命令自动拒绝
3915
4025
  if (mode === 'auto') {
3916
4026
  if (hasFileChangeExpansion || hasFilesystemPermissions || hasUnknownPermissions) {
3917
- await recordBlockedOperation('auto_permission_expansion', checkedInput);
4027
+ await recordBlockedOperation('auto_permission_expansion', policyInput);
3918
4028
  return 'deny';
3919
4029
  }
3920
4030
  if (toolName === 'PermissionGrant' && !hasNetworkPermissions) {
@@ -3934,8 +4044,8 @@ export class CodexRunner {
3934
4044
  if (requiresSandboxExpansionApproval) {
3935
4045
  if (!this.permissionGateway || !permissionPrompt)
3936
4046
  return 'deny';
3937
- if (toolName === 'FileChange') {
3938
- return this.permissionGateway.requestPermission(sessionKey, toolName, checkedInput, permissionPrompt, this.permissionContexts.get(sessionKey), summary, reason || 'Codex 请求扩大当前命令或 turn 的文件系统权限', grantScope, undefined, { fileChangeCwd: workspacePath });
4047
+ if (policyToolName === 'FileChange') {
4048
+ return this.permissionGateway.requestPermission(sessionKey, policyToolName, policyInput, permissionPrompt, this.permissionContexts.get(sessionKey), summary, reason || 'Codex 请求扩大当前命令或 turn 的文件系统权限', grantScope, undefined, { fileChangeCwd: workspacePath });
3939
4049
  }
3940
4050
  return this.permissionGateway.requestPermission(sessionKey, toolName, checkedInput, permissionPrompt, this.permissionContexts.get(sessionKey), summary, reason || 'Codex 请求扩大当前命令或 turn 的文件系统权限', grantScope);
3941
4051
  }
@@ -3980,20 +4090,80 @@ export class CodexRunner {
3980
4090
  }
3981
4091
  throw new Error('Unsupported Codex app-server request: ' + method);
3982
4092
  }
3983
- async rollbackSessionTurns(agentSessionId, _projectPath, numTurns) {
3984
- const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'rollback');
4093
+ async rollbackSessionTurns(agentSessionId, projectPath, numTurns, targetAssistantMessageId) {
4094
+ const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'resume+rollback');
3985
4095
  try {
3986
- return await this.rollbackSessionTurnsLocked(agentSessionId, numTurns);
4096
+ return await this.rollbackSessionTurnsLocked(agentSessionId, projectPath, numTurns, targetAssistantMessageId);
3987
4097
  }
3988
4098
  finally {
3989
4099
  release();
3990
4100
  }
3991
4101
  }
3992
- async rollbackSessionTurnsLocked(agentSessionId, numTurns) {
4102
+ async rollbackSessionTurnsLocked(agentSessionId, projectPath, numTurns, targetAssistantMessageId) {
3993
4103
  if (numTurns < 1)
3994
4104
  return true;
3995
- const response = await this.getAppServerClientForThread(agentSessionId).threadRollback(agentSessionId, numTurns);
3996
- return !!response.thread;
4105
+ const appServer = this.getAppServerClientForThread(agentSessionId);
4106
+ const previousAppServer = await this.prepareThreadSubscription(appServer, agentSessionId);
4107
+ let resumed = false;
4108
+ try {
4109
+ let resumeResponse;
4110
+ try {
4111
+ resumeResponse = await appServer.threadResume(agentSessionId, projectPath);
4112
+ }
4113
+ catch (error) {
4114
+ if (isCodexActiveWriterError(error) && previousAppServer && previousAppServer !== appServer) {
4115
+ await this.archiveThreadForHandoff(previousAppServer, appServer, agentSessionId);
4116
+ resumeResponse = await appServer.threadResume(agentSessionId, projectPath);
4117
+ }
4118
+ else {
4119
+ throw error;
4120
+ }
4121
+ }
4122
+ this.markThreadSubscribed(appServer, agentSessionId);
4123
+ this.threadProjectPaths.set(agentSessionId, path.resolve(projectPath));
4124
+ resumed = true;
4125
+ if (resumeResponse.thread?.historyMode === 'paginated') {
4126
+ const beforeTurnId = await this.findPaginatedRewindTurnId(appServer, agentSessionId, numTurns, targetAssistantMessageId);
4127
+ const response = await appServer.threadRevert(agentSessionId, beforeTurnId);
4128
+ return !!response.thread;
4129
+ }
4130
+ const response = await appServer.threadRollback(agentSessionId, numTurns);
4131
+ return !!response.thread;
4132
+ }
4133
+ finally {
4134
+ if (resumed) {
4135
+ await this.releaseThreadSubscription(appServer, agentSessionId, 'rollback-finished');
4136
+ }
4137
+ }
4138
+ }
4139
+ async findPaginatedRewindTurnId(appServer, threadId, numTurns, targetAssistantMessageId) {
4140
+ if (targetAssistantMessageId) {
4141
+ const history = await this.readForkHistory(appServer, threadId);
4142
+ const targetTurnId = this.findTurnIdForAssistantMessage(history, targetAssistantMessageId);
4143
+ if (!targetTurnId)
4144
+ throw new Error('Codex paginated rewind target turn not found');
4145
+ return targetTurnId;
4146
+ }
4147
+ let cursor;
4148
+ let seenCursor;
4149
+ let seenTurns = 0;
4150
+ do {
4151
+ const page = await appServer.threadTurnsList(threadId, 100, cursor);
4152
+ for (const turn of page.data ?? []) {
4153
+ if (seenTurns === numTurns - 1) {
4154
+ if (typeof turn.id !== 'string' || !turn.id)
4155
+ throw new Error('Codex paginated thread rewind target has no turn id');
4156
+ return turn.id;
4157
+ }
4158
+ seenTurns++;
4159
+ }
4160
+ const nextCursor = typeof page.nextCursor === 'string' && page.nextCursor ? page.nextCursor : undefined;
4161
+ if (!nextCursor || nextCursor === seenCursor)
4162
+ break;
4163
+ seenCursor = nextCursor;
4164
+ cursor = nextCursor;
4165
+ } while (true);
4166
+ throw new Error('Codex paginated thread has fewer than ' + numTurns + ' turns');
3997
4167
  }
3998
4168
  async rewindFiles(agentSessionId, projectPath, userMessageId) {
3999
4169
  const messages = await this.getSessionMessages(agentSessionId, projectPath);
@@ -4062,6 +4232,26 @@ export class CodexRunner {
4062
4232
  parent_tool_use_id: null,
4063
4233
  });
4064
4234
  }
4235
+ else if (item.id && [
4236
+ 'commandExecution', 'mcpToolCall', 'dynamicToolCall', 'fileChange',
4237
+ 'webSearch', 'collabAgentToolCall', 'imageView', 'sleep', 'imageGeneration',
4238
+ ].includes(item.type || '')) {
4239
+ messages.push({
4240
+ type: 'assistant',
4241
+ uuid: item.id,
4242
+ session_id: threadId,
4243
+ message: { role: 'assistant', content: this.mapCodexToolItemToContent(item) },
4244
+ parent_tool_use_id: null,
4245
+ });
4246
+ if (item.type === 'fileChange')
4247
+ messages.push({
4248
+ type: 'system',
4249
+ uuid: `${item.id}-file-change`,
4250
+ session_id: threadId,
4251
+ message: { role: 'system', content: this.mapFileChangeToContent(item) },
4252
+ parent_tool_use_id: null,
4253
+ });
4254
+ }
4065
4255
  else if (item.type === 'file_change') {
4066
4256
  messages.push({
4067
4257
  type: 'system',
@@ -4075,6 +4265,86 @@ export class CodexRunner {
4075
4265
  }
4076
4266
  return messages;
4077
4267
  }
4268
+ /** Preserve the same bounded display data that live tool cards receive. */
4269
+ mapCodexToolItemToContent(item) {
4270
+ const id = typeof item.id === 'string' ? item.id : undefined;
4271
+ const type = item.type || 'tool';
4272
+ let name = type;
4273
+ let input;
4274
+ let result;
4275
+ let isError = false;
4276
+ switch (type) {
4277
+ case 'commandExecution':
4278
+ name = 'Shell';
4279
+ input = { command: item.command, cwd: item.cwd };
4280
+ result = item.aggregatedOutput ?? item.output ?? '';
4281
+ isError = item.exitCode != null
4282
+ ? Number(item.exitCode) !== 0
4283
+ : item.status === 'failed' || item.status === 'declined';
4284
+ break;
4285
+ case 'mcpToolCall':
4286
+ name = `MCP:${item.server}/${item.tool}`;
4287
+ input = item.arguments;
4288
+ result = item.result ?? item.contentItems;
4289
+ isError = item.status === 'failed';
4290
+ break;
4291
+ case 'dynamicToolCall':
4292
+ name = item.namespace ? `${item.namespace}:${item.tool}` : String(item.tool || 'tool');
4293
+ input = item.arguments;
4294
+ result = item.result ?? item.contentItems;
4295
+ isError = item.success === false || item.status === 'failed';
4296
+ break;
4297
+ case 'fileChange':
4298
+ name = 'FileChange';
4299
+ input = { changes: item.changes };
4300
+ result = item.changes;
4301
+ isError = item.status === 'failed';
4302
+ break;
4303
+ case 'webSearch':
4304
+ name = 'WebSearch';
4305
+ input = { query: item.query, ...(item.action != null ? { action: item.action } : {}) };
4306
+ result = input;
4307
+ break;
4308
+ case 'collabAgentToolCall':
4309
+ name = `Collab:${item.tool}`;
4310
+ input = {
4311
+ receiverThreadIds: item.receiverThreadIds,
4312
+ ...(item.prompt != null ? { prompt: item.prompt } : {}),
4313
+ ...(item.model != null ? { model: item.model } : {}),
4314
+ ...(item.reasoningEffort != null ? { reasoningEffort: item.reasoningEffort } : {}),
4315
+ };
4316
+ result = item.contentItems ?? { receiverThreadIds: item.receiverThreadIds, agentsStates: item.agentsStates };
4317
+ isError = item.status === 'failed';
4318
+ break;
4319
+ case 'imageView':
4320
+ name = 'ViewImage';
4321
+ input = { path: item.path };
4322
+ result = input;
4323
+ break;
4324
+ case 'sleep':
4325
+ name = 'Sleep';
4326
+ input = { durationMs: item.durationMs };
4327
+ result = input;
4328
+ break;
4329
+ case 'imageGeneration':
4330
+ name = 'ImageGeneration';
4331
+ input = item.revisedPrompt != null ? { prompt: item.revisedPrompt } : {};
4332
+ result = item.result ?? item.output;
4333
+ break;
4334
+ default:
4335
+ input = item.input;
4336
+ result = item.result ?? item.output;
4337
+ }
4338
+ return [
4339
+ { type: 'tool_use', id, name, input },
4340
+ {
4341
+ type: 'tool_result',
4342
+ ...(id ? { tool_use_id: id } : {}),
4343
+ content: result,
4344
+ is_error: isError,
4345
+ },
4346
+ ];
4347
+ }
4078
4348
  getTurnItems(turn) {
4079
4349
  const items = Array.isArray(turn?.items) ? turn.items : [];
4080
4350
  const input = Array.isArray(turn?.input) ? turn.input : [];
@@ -4143,36 +4413,18 @@ export class CodexRunner {
4143
4413
  this.onCompactStart = callback;
4144
4414
  }
4145
4415
  // ── Event stream transformation ──
4146
- buildAppServerInput(prompt, images, tempFiles, sessionId, runtimeEnv) {
4416
+ buildAppServerInput(prompt, images) {
4147
4417
  const input = [{ type: 'text', text: prompt, text_elements: [] }];
4148
4418
  if (!images?.length)
4149
4419
  return input;
4150
- const tmpDir = getManagedTaskTempDir(runtimeEnv)
4151
- ?? runtimeEnv?.EVOLCORE_SESSION_RUNTIME_DIR
4152
- ?? (sessionId ? this.getManagedTempDir(sessionId) : undefined)
4153
- ?? process.env.TMPDIR;
4154
- if (!tmpDir || !path.isAbsolute(tmpDir))
4155
- throw new Error('managed TMPDIR is unset or not absolute');
4156
- for (let i = 0; i < images.length; i++) {
4157
- const img = images[i];
4158
- const ext = MIME_EXT[img.mimeType || ''] || '.jpg';
4159
- const tmpPath = path.join(tmpDir, `evolcore-img-${Date.now()}-${i}${ext}`);
4160
- fs.writeFileSync(tmpPath, Buffer.from(img.data, 'base64'));
4161
- tempFiles.push(tmpPath);
4162
- input.push({ type: 'localImage', path: tmpPath });
4163
- }
4164
- logger.info(`[CodexRunner] Attached ${images.length} image(s) as localImage`);
4165
- return input;
4166
- }
4167
- cleanupTempFiles(tempFiles) {
4168
- if (!tempFiles?.length)
4169
- return;
4170
- for (const tempFile of tempFiles) {
4171
- try {
4172
- fs.unlinkSync(tempFile);
4173
- }
4174
- catch { /* ignore */ }
4420
+ for (const image of images) {
4421
+ input.push({
4422
+ type: 'image',
4423
+ url: `data:${image.mimeType || 'image/png'};base64,${image.data}`,
4424
+ });
4175
4425
  }
4426
+ logger.info(`[CodexRunner] Attached ${images.length} image(s) as base64 data URL`);
4427
+ return input;
4176
4428
  }
4177
4429
  extractTurnId(notification) {
4178
4430
  const params = notification.params || {};
@@ -4293,7 +4545,7 @@ export class CodexRunner {
4293
4545
  }
4294
4546
  queue.end();
4295
4547
  }
4296
- async *transformAppServerStream(notifications, sessionId, state, controller, unsubscribe, appServer, tempFiles) {
4548
+ async *transformAppServerStream(notifications, sessionId, state, controller, unsubscribe, appServer) {
4297
4549
  try {
4298
4550
  let sessionIdEmitted = state.committed;
4299
4551
  if (sessionIdEmitted)
@@ -4347,7 +4599,6 @@ export class CodexRunner {
4347
4599
  for (const childThreadId of state.activeSubagentThreads)
4348
4600
  this.unregisterChildThread(childThreadId);
4349
4601
  state.activeSubagentThreads.clear();
4350
- this.cleanupTempFiles(tempFiles);
4351
4602
  }
4352
4603
  }
4353
4604
  *mapAppServerNotification(notification, sessionId, state) {
@@ -4365,7 +4616,8 @@ export class CodexRunner {
4365
4616
  case 'item/started': {
4366
4617
  for (const event of this.mapAppServerItemStarted(params.item, state)) {
4367
4618
  this.trackAppServerToolLifecycle(event, state);
4368
- yield event;
4619
+ yield event.type === 'tool_use' && params.item?.id
4620
+ ? { ...event, assistantUuid: params.item.id } : event;
4369
4621
  }
4370
4622
  break;
4371
4623
  }
@@ -4397,7 +4649,8 @@ export class CodexRunner {
4397
4649
  state.completedItemIds.add(item.id);
4398
4650
  for (const event of this.mapAppServerItemCompleted(item, state)) {
4399
4651
  this.trackAppServerToolLifecycle(event, state);
4400
- yield event;
4652
+ yield event.type === 'tool_result' && item?.id
4653
+ ? { ...event, assistantUuid: item.id } : event;
4401
4654
  }
4402
4655
  break;
4403
4656
  }
@@ -4629,6 +4882,9 @@ export class CodexRunner {
4629
4882
  isError: true,
4630
4883
  error: denial.reason,
4631
4884
  errorCode: errorCodeForToolDenial(denial.decisionSource, denial.policyCode),
4885
+ ...(errorDetailCodeForToolDenial(denial.decisionSource, denial.policyCode, denial.errorDetailCode)
4886
+ ? { errorDetailCode: errorDetailCodeForToolDenial(denial.decisionSource, denial.policyCode, denial.errorDetailCode) }
4887
+ : {}),
4632
4888
  decision: 'deny',
4633
4889
  decisionSource: denial.decisionSource,
4634
4890
  policyCode: denial.policyCode,
@@ -4655,6 +4911,9 @@ export class CodexRunner {
4655
4911
  isError: true,
4656
4912
  error: managedHookDenial.reason,
4657
4913
  errorCode: 'POLICY_DENIED',
4914
+ ...(errorDetailCodeForToolDenial('policy', managedHookDenial.policyCode, managedHookDenial.errorDetailCode)
4915
+ ? { errorDetailCode: errorDetailCodeForToolDenial('policy', managedHookDenial.policyCode, managedHookDenial.errorDetailCode) }
4916
+ : {}),
4658
4917
  decision: 'deny',
4659
4918
  decisionSource: 'policy',
4660
4919
  policyCode: managedHookDenial.policyCode,
@@ -4716,17 +4975,29 @@ export class CodexRunner {
4716
4975
  }
4717
4976
  break;
4718
4977
  case 'commandExecution':
4719
- yield {
4720
- type: 'tool_result',
4721
- name: 'Shell',
4722
- result: item.aggregatedOutput ?? '',
4723
- ...(blockedFields ?? {
4724
- isError: item.exitCode !== null && item.exitCode !== undefined ? item.exitCode !== 0 : item.status === 'failed' || item.status === 'declined',
4725
- ...(item.error?.code || item.error?.errorCode ? { errorCode: String(item.error.code ?? item.error.errorCode) } : {}),
4726
- }),
4727
- callId: item.id,
4728
- durationMs: typeof item.durationMs === 'number' ? item.durationMs : undefined,
4729
- };
4978
+ {
4979
+ const exitCode = typeof item.exitCode === 'number' && Number.isInteger(item.exitCode)
4980
+ ? item.exitCode
4981
+ : undefined;
4982
+ const isError = exitCode !== undefined
4983
+ ? exitCode !== 0
4984
+ : item.status === 'failed' || item.status === 'declined';
4985
+ yield {
4986
+ type: 'tool_result',
4987
+ name: 'Shell',
4988
+ result: item.aggregatedOutput ?? '',
4989
+ ...(blockedFields ?? {
4990
+ isError,
4991
+ ...codexItemErrorFields(item),
4992
+ executed: true,
4993
+ executionState: isError ? 'failed' : 'completed',
4994
+ ...(exitCode !== undefined ? { exitCode } : {}),
4995
+ }),
4996
+ ...(blockedFields ? {} : exitCode !== undefined ? { exitCode } : {}),
4997
+ callId: item.id,
4998
+ durationMs: typeof item.durationMs === 'number' ? item.durationMs : undefined,
4999
+ };
5000
+ }
4730
5001
  break;
4731
5002
  case 'mcpToolCall':
4732
5003
  yield {
@@ -4735,7 +5006,11 @@ export class CodexRunner {
4735
5006
  result: item.result,
4736
5007
  ...(blockedFields ?? { isError: item.status === 'failed' }),
4737
5008
  ...(blockedFields ? {} : { error: item.error?.message }),
4738
- ...(item.error?.code || item.error?.errorCode ? { errorCode: String(item.error.code ?? item.error.errorCode) } : {}),
5009
+ ...(blockedFields ? {} : codexItemErrorFields(item)),
5010
+ ...(blockedFields ?? {
5011
+ executed: true,
5012
+ executionState: item.status === 'failed' ? 'failed' : 'completed',
5013
+ }),
4739
5014
  callId: item.id,
4740
5015
  durationMs: typeof item.durationMs === 'number' ? item.durationMs : undefined,
4741
5016
  };
@@ -4746,7 +5021,11 @@ export class CodexRunner {
4746
5021
  name: item.namespace ? `${item.namespace}:${item.tool}` : item.tool,
4747
5022
  result: item.contentItems,
4748
5023
  ...(blockedFields ?? { isError: item.success === false || item.status === 'failed' }),
4749
- ...(item.error?.code || item.error?.errorCode ? { errorCode: String(item.error.code ?? item.error.errorCode) } : {}),
5024
+ ...(blockedFields ? {} : codexItemErrorFields(item)),
5025
+ ...(blockedFields ?? {
5026
+ executed: true,
5027
+ executionState: item.success === false || item.status === 'failed' ? 'failed' : 'completed',
5028
+ }),
4750
5029
  callId: item.id,
4751
5030
  durationMs: typeof item.durationMs === 'number' ? item.durationMs : undefined,
4752
5031
  };
@@ -4760,12 +5039,34 @@ export class CodexRunner {
4760
5039
  yield editEvent;
4761
5040
  }
4762
5041
  }
4763
- yield { type: 'tool_result', name: 'Edit', result: item.changes, ...(blockedFields ?? { isError: item.status === 'failed' }), callId: item.id };
5042
+ yield {
5043
+ type: 'tool_result',
5044
+ name: 'Edit',
5045
+ result: item.changes,
5046
+ ...(blockedFields ?? {
5047
+ isError: item.status === 'failed',
5048
+ executed: true,
5049
+ executionState: item.status === 'failed' ? 'failed' : 'completed',
5050
+ ...codexItemErrorFields(item),
5051
+ }),
5052
+ callId: item.id,
5053
+ };
4764
5054
  }
4765
5055
  else {
4766
5056
  const desc = this.normalizeFileChanges(item.changes).map((change) => this.describeFileChange(change)).join(', ');
4767
5057
  yield { type: 'tool_use', name: 'FileChange', input: { description: desc }, callId: item.id };
4768
- yield { type: 'tool_result', name: 'FileChange', result: item.changes, ...(blockedFields ?? { isError: item.status === 'failed' }), callId: item.id };
5058
+ yield {
5059
+ type: 'tool_result',
5060
+ name: 'FileChange',
5061
+ result: item.changes,
5062
+ ...(blockedFields ?? {
5063
+ isError: item.status === 'failed',
5064
+ executed: true,
5065
+ executionState: item.status === 'failed' ? 'failed' : 'completed',
5066
+ ...codexItemErrorFields(item),
5067
+ }),
5068
+ callId: item.id,
5069
+ };
4769
5070
  }
4770
5071
  break;
4771
5072
  case 'webSearch':
@@ -4970,7 +5271,7 @@ export class CodexRunner {
4970
5271
  const managedTempDir = ensureSessionRuntimeDir(sessionId);
4971
5272
  if (!managedTempDir)
4972
5273
  return undefined;
4973
- return `临时文件必须使用环境变量 \`$TMPDIR\`。当前值是 \`${managedTempDir}\`,它是当前 EvolCore 会话唯一允许的临时工作区;不要直接使用 \`/tmp\`,也不要使用其他会话的临时目录。该目录随 EvolCore sessionId 保持稳定;若文件不存在,请先在当前 \`$TMPDIR\` 重新创建,再继续执行后续 \`ec\` 命令。`;
5274
+ return `临时文件必须使用环境变量 \`$TMPDIR\`。当前值是 \`${managedTempDir}\`,它是当前 EvolCore 会话唯一允许的临时工作区;不要直接使用 \`/tmp\`,也不要使用其他会话的临时目录。该目录随 EvolCore sessionId 保持稳定;若文件不存在,请先在当前 \`$TMPDIR\` 重新创建,再继续执行后续 \`ec\` 命令。文件修改优先使用 Codex 原生 FileChange/Edit;若当前 Codex 版本未暴露该工具,可使用独立且可解析的 \`apply_patch\` patch。不得把 patch 与其它命令拼接、放入管道或使用任意重定向;目标文件仍受 workspace、H/L 路径和当前权限模式限制。`;
4974
5275
  }
4975
5276
  buildEvolcoreShellEnvironmentConfig(sessionId, sessionToken, taskDelegationToken) {
4976
5277
  const managedTempDir = ensureSessionRuntimeDir(sessionId);