evolcore 0.0.21 → 0.0.23

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 (112) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +53 -0
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/codex-managed-hook.mjs +3 -0
  6. package/bin/install-codex-managed-hooks.mjs +376 -96
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +1 -1
  9. package/dist/agents/claude-runner.js +44 -226
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +103 -17
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +154 -0
  14. package/dist/agents/codex-runner.js +1292 -219
  15. package/dist/agents/ecagent-runner.js +10 -31
  16. package/dist/agents/gemini-runner.js +2 -6
  17. package/dist/aun/aid/store.js +2 -0
  18. package/dist/aun/menu-client.js +272 -0
  19. package/dist/aun/msg/group.js +65 -4
  20. package/dist/aun/msg/index.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +51 -4
  22. package/dist/aun/outbox.js +7 -0
  23. package/dist/aun/rpc/client.js +8 -1
  24. package/dist/channels/aun.js +240 -36
  25. package/dist/channels/wechat.js +81 -10
  26. package/dist/cli/aun-commands.js +48 -4
  27. package/dist/cli/command-log.js +21 -2
  28. package/dist/cli/daemon-commands.js +169 -51
  29. package/dist/cli/index.js +20 -0
  30. package/dist/cli/init.js +2 -10
  31. package/dist/cli/menu-client.js +95 -0
  32. package/dist/cli/model.js +23 -8
  33. package/dist/cli/task-context.js +16 -2
  34. package/dist/cli/trigger-command.js +22 -5
  35. package/dist/config/config-field-policy.js +3 -0
  36. package/dist/config/config-manager.js +32 -5
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/schema-migration.js +14 -2
  39. package/dist/core/auth/agent-delegation.js +15 -0
  40. package/dist/core/auth/auth-gateway.js +8 -0
  41. package/dist/core/auth/authorization-audit.js +70 -6
  42. package/dist/core/auth/operation-catalog.js +4 -0
  43. package/dist/core/auth/remote-access-store.js +135 -0
  44. package/dist/core/bootstrap-messages.js +8 -0
  45. package/dist/core/bootstrap-service.js +93 -25
  46. package/dist/core/command/command-handler.js +62 -5
  47. package/dist/core/command/daemon-diagnostics.js +448 -0
  48. package/dist/core/command/menu-catalog.js +3 -0
  49. package/dist/core/command/menu-handler.js +150 -33
  50. package/dist/core/command/menu-protocol.js +120 -1
  51. package/dist/core/command/slash-gate.js +3 -3
  52. package/dist/core/command/slash-handler.js +83 -40
  53. package/dist/core/data-migration.js +11 -1
  54. package/dist/core/event-catalog.js +32 -0
  55. package/dist/core/handoff/runtime.js +23 -3
  56. package/dist/core/message/im-renderer.js +7 -3
  57. package/dist/core/message/message-bridge.js +60 -2
  58. package/dist/core/message/message-log.js +33 -0
  59. package/dist/core/message/message-queue.js +41 -15
  60. package/dist/core/message/response-engine.js +246 -114
  61. package/dist/core/model/catalog-service.js +308 -0
  62. package/dist/core/model/model-catalog.js +124 -334
  63. package/dist/core/model/model-diagnostics.js +4 -5
  64. package/dist/core/model/model-fallback.js +9 -16
  65. package/dist/core/model/model-permission.js +1 -1
  66. package/dist/core/permission/approval-gateway.js +16 -1
  67. package/dist/core/permission/ec-command-parser.js +272 -70
  68. package/dist/core/permission/protected-paths.js +74 -14
  69. package/dist/core/permission/sandbox-runtime.js +4 -2
  70. package/dist/core/permission/tool-error-code.js +12 -0
  71. package/dist/core/permission/tool-policy.js +290 -30
  72. package/dist/core/runtime-lock.js +13 -8
  73. package/dist/core/session/session-manager.js +30 -0
  74. package/dist/core/session/session-renew.js +18 -1
  75. package/dist/core/session/session-turn-coordinator.js +5 -1
  76. package/dist/index.js +104 -34
  77. package/dist/ipc.js +218 -96
  78. package/dist/paths.js +18 -0
  79. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  80. package/dist/stats/price-resolver.js +4 -0
  81. package/dist/trigger/feedback.js +14 -2
  82. package/dist/trigger/manager.js +3 -1
  83. package/dist/trigger/parser.js +86 -13
  84. package/dist/trigger/patch.js +104 -1
  85. package/dist/trigger/scheduler.js +20 -3
  86. package/dist/utils/ecweb-supervisor.js +11 -2
  87. package/dist/utils/ecweb-utils.js +8 -26
  88. package/dist/utils/error-utils.js +7 -0
  89. package/dist/utils/logger.js +9 -4
  90. package/dist/utils/npm-ops.js +47 -15
  91. package/dist/utils/tool-summary.js +59 -0
  92. package/dist/utils/windows-shell-trust.js +201 -0
  93. package/kits/docs/evolcore/INDEX.md +3 -2
  94. package/kits/docs/evolcore/agent-create.md +159 -0
  95. package/kits/docs/evolcore/agent.md +6 -0
  96. package/kits/docs/evolcore/group-collaboration.md +256 -0
  97. package/kits/docs/evolcore/group-rules.md +1 -19
  98. package/kits/docs/evolcore/group.md +3 -1
  99. package/kits/docs/evolcore/menu-client.md +34 -0
  100. package/kits/docs/evolcore/trigger.md +6 -3
  101. package/kits/docs/prompt-loading-architecture.md +6 -0
  102. package/kits/eck_message_manifest.json +6 -6
  103. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  104. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  105. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  106. package/kits/schemas/_meta.json +3 -2
  107. package/kits/schemas/agent-config.schema.12.json +427 -0
  108. package/kits/templates/message-fragments/item.md +1 -1
  109. package/kits/templates/roles/visitor.json +1 -0
  110. package/kits/templates/system-fragments/bootstrap.md +10 -2
  111. package/kits/templates/system-fragments/commands.md +2 -2
  112. package/package.json +11 -3
@@ -3,7 +3,7 @@ import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
3
3
  import { renderActionAsText } from '../interaction-router.js';
4
4
  import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from '../message/message-utils.js';
5
5
  import { logger } from '../../utils/logger.js';
6
- import { summarizeToolInput } from '../../utils/tool-summary.js';
6
+ import { summarizeToolInput, summarizeToolInputForAudit } from '../../utils/tool-summary.js';
7
7
  import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
8
8
  import { recordCausationSpan } from '../causation/audit.js';
9
9
  import { checkDangerousCommand } from './tool-policy.js';
@@ -31,6 +31,7 @@ export async function requestDangerousCommandPermission(gateway, sessionId, tool
31
31
  agentName: context?.agentName,
32
32
  role: context?.role,
33
33
  taskId: context?.taskId,
34
+ summary: summarizeToolInputForAudit(toolName, input).slice(0, 512),
34
35
  });
35
36
  return { matched: true, decision: 'deny' };
36
37
  }
@@ -132,6 +133,7 @@ function truncateApprovalDetail(value) {
132
133
  ? `${trimmed.slice(0, APPROVAL_DETAIL_LIMIT - 3)}...`
133
134
  : trimmed;
134
135
  }
136
+ /** Keep unattended execution anomalies useful without copying command values. */
135
137
  function structuredApprovalAuditSummary(input) {
136
138
  const fields = [...new Set(Object.keys(input)
137
139
  .map(key => key.replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 48))
@@ -459,6 +461,7 @@ export class PermissionGateway {
459
461
  agentName: pending.agentName,
460
462
  role: pending.role,
461
463
  taskId: pending.taskId,
464
+ summary: pending.auditSummary,
462
465
  });
463
466
  }
464
467
  void this.notifyPermissionState(pending, approved
@@ -511,6 +514,7 @@ export class PermissionGateway {
511
514
  agentName: pending.agentName,
512
515
  role: pending.role,
513
516
  taskId: pending.taskId,
517
+ summary: pending.auditSummary,
514
518
  });
515
519
  if (notifyState) {
516
520
  const failure = permissionFailure(reason);
@@ -558,6 +562,7 @@ export class PermissionGateway {
558
562
  async requestCrossSessionPermission(sessionId, challenge, route, sendPrompt, context, grantScope, requestCausation, grantMatch) {
559
563
  const approval = context.approvalRouting;
560
564
  const interactionRouter = context.interactionRouter;
565
+ const auditSummary = summarizeToolInputForAudit(challenge.toolName, challenge.toolInput).slice(0, 512);
561
566
  if (!approval || !interactionRouter) {
562
567
  await sendPrompt('当前会话权限不足,且没有可用的 owner 审批通道。');
563
568
  auditPermissionDecision({
@@ -571,6 +576,7 @@ export class PermissionGateway {
571
576
  agentName: context.agentName,
572
577
  role: context.role,
573
578
  taskId: context.taskId,
579
+ summary: structuredApprovalAuditSummary(challenge.toolInput),
574
580
  });
575
581
  return 'deny';
576
582
  }
@@ -673,6 +679,7 @@ export class PermissionGateway {
673
679
  interactionRouter,
674
680
  causation: requestCausation,
675
681
  permissionStateChanged: context.permissionStateChanged,
682
+ auditSummary,
676
683
  };
677
684
  this.crossSessionPending.set(requestId, pending);
678
685
  this.syncManagementRevalidationTimer();
@@ -788,6 +795,7 @@ export class PermissionGateway {
788
795
  agentName: pending.agentName,
789
796
  role: pending.role,
790
797
  taskId: pending.taskId,
798
+ summary: pending.auditSummary,
791
799
  });
792
800
  const decisionCausation = deriveCausation(pending.causation);
793
801
  recordCausationSpan(decisionCausation, 'permission.decision', {
@@ -815,6 +823,7 @@ export class PermissionGateway {
815
823
  const effectiveApproverPolicy = approverPolicy
816
824
  ?? context?.approvalRouting?.approverPolicy
817
825
  ?? 'requester';
826
+ const auditSummary = summarizeToolInputForAudit(toolName, toolInput).slice(0, 512);
818
827
  if (context?.approvalInteractionPolicy === 'deny') {
819
828
  const denialRequestId = `perm-policy-${randomUUID()}`;
820
829
  const anomaly = {
@@ -852,6 +861,7 @@ export class PermissionGateway {
852
861
  agentName: context.agentName,
853
862
  role: context.role,
854
863
  taskId: context.taskId,
864
+ summary: auditSummary,
855
865
  });
856
866
  return 'deny';
857
867
  }
@@ -868,6 +878,7 @@ export class PermissionGateway {
868
878
  agentName: context?.agentName,
869
879
  role: context?.role,
870
880
  taskId: context?.taskId,
881
+ summary: auditSummary,
871
882
  });
872
883
  return 'deny';
873
884
  }
@@ -942,6 +953,7 @@ export class PermissionGateway {
942
953
  agentName: context.agentName,
943
954
  role: context.role,
944
955
  taskId: context.taskId,
956
+ summary: summarizeToolInputForAudit(toolName, toolInput).slice(0, 512),
945
957
  });
946
958
  return 'deny';
947
959
  }
@@ -1006,6 +1018,7 @@ export class PermissionGateway {
1006
1018
  permissionMode: context?.permissionMode,
1007
1019
  role: context?.role,
1008
1020
  taskId: context?.taskId,
1021
+ auditSummary,
1009
1022
  };
1010
1023
  pending.timeout = setTimeout(() => {
1011
1024
  this.failPendingPermission(sessionId, requestId, 'expired');
@@ -1136,6 +1149,7 @@ export class PermissionGateway {
1136
1149
  agentName: pending.agentName,
1137
1150
  role: pending.role,
1138
1151
  taskId: pending.taskId,
1152
+ summary: pending.auditSummary,
1139
1153
  });
1140
1154
  }
1141
1155
  this.eventBus?.publish({ type: 'permission:resolved', sessionId, requestId, toolName: pending.toolName, approved: normalizedDecision !== 'deny', ...(policyCode ? { decisionSource, policyCode } : { decisionSource }), causation: decisionCausation });
@@ -1213,6 +1227,7 @@ export class PermissionGateway {
1213
1227
  agentName: pending.agentName,
1214
1228
  role: pending.role,
1215
1229
  taskId: pending.taskId,
1230
+ summary: pending.auditSummary,
1216
1231
  });
1217
1232
  this.pending.delete(requestId);
1218
1233
  const decisionCausation = deriveCausation(pending.causation);
@@ -1,15 +1,19 @@
1
+ import { isTrustedWindowsShellExecutable } from '../../utils/windows-shell-trust.js';
1
2
  const HEAD_EXECUTABLES = new Set(['head', '/bin/head', '/usr/bin/head']);
2
3
  // Codex app-server sends command executions through a fixed shell carrier.
3
4
  // Treat the carrier as syntax only after parsing it as literal argv; never
4
5
  // accept an arbitrary shell command string or unwrap composition here.
5
6
  const CODEX_POSIX_SHELL_EXECUTABLES = new Set([
6
- 'bash', '/bin/bash', '/usr/bin/bash',
7
- 'sh', '/bin/sh', '/usr/bin/sh',
7
+ 'bash', 'bash.exe', '/bin/bash', '/usr/bin/bash',
8
+ 'sh', 'sh.exe', '/bin/sh', '/usr/bin/sh',
8
9
  ]);
9
10
  const CODEX_POWERSHELL_EXECUTABLES = new Set([
10
11
  'pwsh', 'pwsh.exe', 'powershell', 'powershell.exe',
11
12
  ]);
12
13
  const CODEX_POWERSHELL_INSTALL_PATH_RE = /^[A-Za-z]:[\\/](?:Program Files[\\/]PowerShell[\\/][0-9]+(?:\.[0-9]+)*(?:-[A-Za-z0-9.-]+)?[\\/]pwsh\.exe|Windows[\\/]System32[\\/]WindowsPowerShell[\\/]v1\.0[\\/]powershell\.exe)$/i;
14
+ const CODEX_CUSTOM_POSIX_SHELL_NAMES = new Set(['bash', 'bash.exe', 'sh', 'sh.exe']);
15
+ const CODEX_CUSTOM_POWERSHELL_NAMES = new Set(['pwsh.exe', 'powershell.exe']);
16
+ const CODEX_CUSTOM_CMD_NAMES = new Set(['cmd.exe']);
13
17
  const CODEX_CMD_EXECUTABLES = new Set(['cmd', 'cmd.exe']);
14
18
  const CODEX_CMD_INSTALL_PATH_RE = /^[A-Za-z]:[\\/](?:Windows[\\/](?:System32|SysWOW64|Sysnative)[\\/]cmd\.exe)$/i;
15
19
  // These switches affect only cmd startup/output formatting. `/c` is handled
@@ -17,6 +21,12 @@ const CODEX_CMD_INSTALL_PATH_RE = /^[A-Za-z]:[\\/](?:Windows[\\/](?:System32|Sys
17
21
  // `/e:*`, and `/f:*` can change process lifetime or command semantics and are
18
22
  // intentionally not treated as a trusted Codex carrier.
19
23
  const CODEX_CMD_INERT_SWITCHES = new Set(['/d', '/s', '/q', '/a', '/u']);
24
+ const EVOLCORE_EXECUTABLE_ALIASES = new Set(['ec', 'ec.cmd', 'ec.exe']);
25
+ const EVOLCORE_NODE_EXECUTABLES = new Set(['node', 'node.exe']);
26
+ // Development/install entrypoints that are part of this repository's CLI.
27
+ // Only these exact relative paths are canonicalized; arbitrary Node scripts
28
+ // must never become an EC command merely because their filename is similar.
29
+ const EVOLCORE_NODE_WRAPPER_PATHS = new Set(['bin/ec.js', 'dist/cli/index.js']);
20
30
  const SHELL_UNQUOTED_META = new Set([
21
31
  ';', '&', '|', '`', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#',
22
32
  ]);
@@ -68,8 +78,7 @@ function hasEcCommandPrefix(command) {
68
78
  }
69
79
  if (boundary && /\s/.test(char))
70
80
  continue;
71
- if (boundary && command.slice(index, index + 2).toLowerCase() === 'ec'
72
- && (index + 2 === command.length || /\s|[;&|`()<>]/.test(command[index + 2])))
81
+ if (boundary && /^ec(?:\.cmd|\.exe)?(?=$|\s|[;&|`()<>])/i.test(command.slice(index)))
73
82
  return true;
74
83
  boundary = false;
75
84
  }
@@ -78,6 +87,45 @@ function hasEcCommandPrefix(command) {
78
87
  function trimShellSpacing(value) {
79
88
  return value.replace(/^[ \t]+|[ \t]+$/g, '');
80
89
  }
90
+ /**
91
+ * Accept an explicitly materialized absolute shell path, including Windows
92
+ * Store/App Execution Alias paths such as `...\\WindowsApps\\pwsh.exe`.
93
+ * Relative names remain restricted to the fixed executable allowlists above.
94
+ */
95
+ function isAbsoluteCustomShellPath(value, executableNames) {
96
+ if (!value || /[\0\r\n"'<>|?*]/.test(value))
97
+ return false;
98
+ const normalized = value.replaceAll('\\', '/');
99
+ if (!/^[A-Za-z]:\//.test(normalized) && !normalized.startsWith('//'))
100
+ return false;
101
+ const basename = normalized.slice(normalized.lastIndexOf('/') + 1).toLowerCase();
102
+ return executableNames.has(basename);
103
+ }
104
+ function isNodeExecutable(value) {
105
+ if (!value)
106
+ return false;
107
+ const normalized = value.toLowerCase();
108
+ return EVOLCORE_NODE_EXECUTABLES.has(normalized)
109
+ || isAbsoluteCustomShellPath(value, EVOLCORE_NODE_EXECUTABLES);
110
+ }
111
+ function normalizeNodeWrapperPath(value) {
112
+ const normalized = value.replaceAll('\\', '/').replace(/^\.\//, '').toLowerCase();
113
+ return EVOLCORE_NODE_WRAPPER_PATHS.has(normalized) ? normalized : undefined;
114
+ }
115
+ function isCodexPosixShellExecutable(value) {
116
+ if (!value)
117
+ return false;
118
+ return CODEX_POSIX_SHELL_EXECUTABLES.has(value.toLowerCase())
119
+ || isAbsoluteCustomShellPath(value, CODEX_CUSTOM_POSIX_SHELL_NAMES);
120
+ }
121
+ function isCodexPowerShellExecutable(value) {
122
+ if (!value)
123
+ return false;
124
+ const normalized = value.toLowerCase();
125
+ return CODEX_POWERSHELL_EXECUTABLES.has(normalized)
126
+ || CODEX_POWERSHELL_INSTALL_PATH_RE.test(value)
127
+ || isAbsoluteCustomShellPath(value, CODEX_CUSTOM_POWERSHELL_NAMES);
128
+ }
81
129
  const MANAGED_TMPDIR_TOKEN = '__EC_MANAGED_TMPDIR__';
82
130
  /**
83
131
  * Preserve the one environment reference allowed in managed EC paths.
@@ -154,7 +202,155 @@ function hasCarrierEcCommandPrefix(command, dialect) {
154
202
  const inner = input.length >= 2 && input.startsWith('"') && input.endsWith('"')
155
203
  ? input.slice(1, -1)
156
204
  : input;
157
- return /^\s*ec(?:\s|$)/i.test(inner);
205
+ return /^\s*ec(?:\.cmd|\.exe)?(?:\s|$)/i.test(inner);
206
+ }
207
+ /** Normalize the Windows npm shims to the canonical EvolCore CLI name. */
208
+ export function normalizeEvolcoreCommandArgv(argv) {
209
+ if (argv.length === 0)
210
+ return [];
211
+ const executable = argv[0];
212
+ if (typeof executable === 'string' && EVOLCORE_EXECUTABLE_ALIASES.has(executable.toLowerCase())) {
213
+ return ['ec', ...argv.slice(1)];
214
+ }
215
+ if (typeof executable === 'string' && isNodeExecutable(executable) && argv.length >= 2
216
+ && typeof argv[1] === 'string' && normalizeNodeWrapperPath(argv[1])) {
217
+ return ['ec', ...argv.slice(2)];
218
+ }
219
+ return [...argv];
220
+ }
221
+ /** Detect the historical PowerShell pattern that overwrote the thread carrier. */
222
+ export function hasPowerShellDelegationAssignment(command) {
223
+ return /^\s*\$env\s*:\s*EVOLCORE_DELEGATION_TOKEN\b/i.test(command)
224
+ && /\bec(?:\.cmd|\.exe)?(?:\s|$)/i.test(command);
225
+ }
226
+ /**
227
+ * Windows Codex may materialize one environment variable assignment directly
228
+ * in the PowerShell command body instead of inheriting it from the process
229
+ * environment. Only the EvolCore delegation variable is accepted, and only
230
+ * when followed by one command on the same carrier. The value is validated for
231
+ * shape but never trusted for authorization; the runner still arms the stable
232
+ * in-memory thread carrier.
233
+ */
234
+ function unwrapPowerShellDelegationPrefix(command) {
235
+ const input = command.trim();
236
+ if (!/^\$env\s*:\s*EVOLCORE_DELEGATION_TOKEN\b/i.test(input)) {
237
+ return { kind: 'none', command };
238
+ }
239
+ let index = 0;
240
+ const skipSpace = () => {
241
+ while (index < input.length && (input[index] === ' ' || input[index] === '\t'))
242
+ index++;
243
+ };
244
+ const consume = (value) => {
245
+ if (input.slice(index, index + value.length).toLowerCase() !== value.toLowerCase())
246
+ return false;
247
+ index += value.length;
248
+ return true;
249
+ };
250
+ skipSpace();
251
+ if (!consume('$env') || input[index] !== ':')
252
+ return { kind: 'invalid', command };
253
+ index++;
254
+ skipSpace();
255
+ if (!consume('EVOLCORE_DELEGATION_TOKEN'))
256
+ return { kind: 'invalid', command };
257
+ skipSpace();
258
+ if (input[index] !== '=')
259
+ return { kind: 'invalid', command };
260
+ index++;
261
+ skipSpace();
262
+ const quote = input[index];
263
+ if (quote !== "'" && quote !== '"')
264
+ return { kind: 'invalid', command };
265
+ const valueStart = index++;
266
+ let value = '';
267
+ let closed = false;
268
+ // When the complete -Command body is itself single-quoted, command-line
269
+ // serialization can leave the assignment value in a doubled `''token''`
270
+ // form. Its token alphabet cannot contain quotes, so this exact shape is
271
+ // unambiguous and can be decoded without executing PowerShell grammar.
272
+ if (quote === "'" && input[valueStart + 1] === "'") {
273
+ const contentStart = valueStart + 2;
274
+ const end = input.indexOf("''", contentStart);
275
+ if (end < contentStart)
276
+ return { kind: 'invalid', command };
277
+ value = input.slice(contentStart, end);
278
+ index = end + 2;
279
+ closed = true;
280
+ }
281
+ while (index < input.length) {
282
+ if (closed)
283
+ break;
284
+ const char = input[index];
285
+ if (char === quote) {
286
+ // PowerShell single-quoted strings escape a quote as `''`. For the
287
+ // delegation token alphabet this is not needed, but accepting it lets us
288
+ // decode the doubled-quote form emitted by powershell.exe argv quoting.
289
+ if (quote === "'" && input[index + 1] === "'") {
290
+ value += "'";
291
+ index += 2;
292
+ continue;
293
+ }
294
+ index++;
295
+ closed = true;
296
+ break;
297
+ }
298
+ if (quote === '"' && char === '`')
299
+ return { kind: 'invalid', command };
300
+ if (char === '\r' || char === '\n')
301
+ return { kind: 'invalid', command };
302
+ value += char;
303
+ index++;
304
+ }
305
+ if (!closed || index <= valueStart + 1 || !/^[A-Za-z0-9_-]{32,128}$/.test(value)) {
306
+ return { kind: 'invalid', command };
307
+ }
308
+ if (input[index] !== ';')
309
+ return { kind: 'invalid', command };
310
+ index++;
311
+ const remainder = input.slice(index).trim();
312
+ if (!remainder)
313
+ return { kind: 'invalid', command };
314
+ return { kind: 'valid', command: remainder, token: value };
315
+ }
316
+ export function parseCodexCarrierCommand(carrier, options = {}) {
317
+ if (options.verifyShellExecutable) {
318
+ if (!carrier.executable) {
319
+ return parseFailure('untrusted-shell', carrier.command, 0, 0, carrier.dialect);
320
+ }
321
+ const kind = carrier.dialect === 'posix' ? 'bash' : carrier.dialect;
322
+ if (!isTrustedWindowsShellExecutable(carrier.executable, kind)) {
323
+ return parseFailure('untrusted-shell', carrier.command, 0, 0, carrier.dialect);
324
+ }
325
+ }
326
+ const unwrapped = carrier.dialect === 'powershell'
327
+ ? unwrapPowerShellDelegationPrefix(carrier.command)
328
+ : { kind: 'none', command: carrier.command };
329
+ // Windows PowerShell may materialize the thread's shell environment as a
330
+ // single `$env:EVOLCORE_DELEGATION_TOKEN = '...'` prefix. Keep the token as
331
+ // metadata for the approval layer, which must compare it with the runner's
332
+ // in-memory carrier before arming delegation. Malformed or unrelated
333
+ // assignments remain syntax failures and can never be authorized.
334
+ if (unwrapped.kind === 'invalid'
335
+ || (carrier.dialect === 'powershell'
336
+ && hasPowerShellDelegationAssignment(carrier.command)
337
+ && unwrapped.kind === 'none')
338
+ || (unwrapped.kind === 'valid'
339
+ && options.expectedDelegationToken !== undefined
340
+ && options.expectedDelegationToken !== unwrapped.token)) {
341
+ return parseFailure('shell-composition', carrier.command, 0, 0, carrier.dialect);
342
+ }
343
+ const parsed = parseLiteralShellCommand(unwrapped.command, {
344
+ ...options,
345
+ dialect: carrier.dialect,
346
+ });
347
+ return parsed.ok
348
+ ? {
349
+ ...parsed,
350
+ argv: normalizeEvolcoreCommandArgv(parsed.argv),
351
+ ...(unwrapped.kind === 'valid' ? { delegationToken: unwrapped.token } : {}),
352
+ }
353
+ : parsed;
158
354
  }
159
355
  function parseFailure(issue, input, offset, tokenIndex, dialect) {
160
356
  return {
@@ -355,7 +551,9 @@ function isCodexCmdExecutable(value) {
355
551
  if (!value)
356
552
  return false;
357
553
  const normalized = value.toLowerCase();
358
- return CODEX_CMD_EXECUTABLES.has(normalized) || CODEX_CMD_INSTALL_PATH_RE.test(value);
554
+ return CODEX_CMD_EXECUTABLES.has(normalized)
555
+ || CODEX_CMD_INSTALL_PATH_RE.test(value)
556
+ || isAbsoluteCustomShellPath(value, CODEX_CUSTOM_CMD_NAMES);
359
557
  }
360
558
  function resolveCodexCmdCarrierArgv(argv) {
361
559
  if (argv.length < 3 || !isCodexCmdExecutable(argv[0]))
@@ -377,7 +575,7 @@ function resolveCodexCmdCarrierArgv(argv) {
377
575
  }
378
576
  const command = argv.at(-1);
379
577
  return typeof command === 'string' && command.trim()
380
- ? { command, dialect: 'cmd' }
578
+ ? { command, dialect: 'cmd', executable: argv[0] }
381
579
  : undefined;
382
580
  }
383
581
  /** Resolve a Windows cmd carrier when the app-server provides only a string. */
@@ -402,10 +600,7 @@ function resolveCodexPowerShellCarrierString(command) {
402
600
  if (!match)
403
601
  return undefined;
404
602
  const executable = match[1] ?? match[2];
405
- const normalized = executable.toLowerCase();
406
- const isPowerShell = CODEX_POWERSHELL_EXECUTABLES.has(normalized)
407
- || CODEX_POWERSHELL_INSTALL_PATH_RE.test(executable);
408
- if (!isPowerShell)
603
+ if (!isCodexPowerShellExecutable(executable))
409
604
  return undefined;
410
605
  // A string-form carrier has one additional command-line quoting layer. The
411
606
  // command argument is commonly serialized as `"ec ..."`; remove exactly
@@ -416,11 +611,31 @@ function resolveCodexPowerShellCarrierString(command) {
416
611
  || (nested.startsWith("'") && nested.endsWith("'")))) {
417
612
  nested = nested.slice(1, -1);
418
613
  }
419
- return nested ? { command: nested, dialect: 'powershell' } : undefined;
614
+ return nested ? { command: nested, dialect: 'powershell', executable } : undefined;
615
+ }
616
+ /** Resolve a POSIX shell carrier when app-server provides only a string. */
617
+ function resolveCodexPosixShellCarrierString(command) {
618
+ const input = trimShellSpacing(command);
619
+ const match = input.match(/^(?:"([^"]+)"|(\S+))\s+(-lc|-c)\s+(.+)$/);
620
+ if (!match)
621
+ return undefined;
622
+ const executable = match[1] ?? match[2];
623
+ if (!isCodexPosixShellExecutable(executable))
624
+ return undefined;
625
+ // A string-form carrier has one additional command-line quoting layer. Strip
626
+ // only the outer pair; quotes belonging to the nested command remain intact.
627
+ let nested = match[4].trim();
628
+ if (nested.length >= 2
629
+ && ((nested.startsWith('"') && nested.endsWith('"'))
630
+ || (nested.startsWith("'") && nested.endsWith("'")))) {
631
+ nested = nested.slice(1, -1);
632
+ }
633
+ return nested ? { command: nested, dialect: 'posix', executable } : undefined;
420
634
  }
421
635
  export function resolveCodexShellCarrierString(command) {
422
636
  return resolveCodexCmdCarrierString(command)
423
- ?? resolveCodexPowerShellCarrierString(command);
637
+ ?? resolveCodexPowerShellCarrierString(command)
638
+ ?? resolveCodexPosixShellCarrierString(command);
424
639
  }
425
640
  /**
426
641
  * Extract a trusted cmd executable's command body without trusting its startup
@@ -462,7 +677,7 @@ function looksLikeEcCommandBody(command) {
462
677
  return false;
463
678
  const input = command.trim();
464
679
  const body = (input.startsWith('"') ? input.slice(1) : input).trimStart();
465
- return /^ec(?:\s|$)/i.test(body);
680
+ return /^ec(?:\.cmd|\.exe)?(?:\s|$)/i.test(body);
466
681
  }
467
682
  /** True only when a trusted cmd executable is carrying an EC command body. */
468
683
  export function hasCodexCmdCarrierEcIntent(input) {
@@ -627,15 +842,14 @@ export function parseLiteralShellArgv(command, options = {}) {
627
842
  return result.ok ? result.argv : null;
628
843
  }
629
844
  export function resolveCodexShellCarrierArgv(argv) {
630
- if (argv.length === 3 && argv[1] === '-lc' && CODEX_POSIX_SHELL_EXECUTABLES.has(argv[0])) {
631
- return { command: argv[2], dialect: 'posix' };
845
+ if (argv.length === 3
846
+ && (argv[1] === '-lc' || argv[1] === '-c')
847
+ && isCodexPosixShellExecutable(argv[0])) {
848
+ return { command: argv[2], dialect: 'posix', executable: argv[0] };
632
849
  }
633
850
  if (argv.length === 3) {
634
- const powershellExecutable = argv[0].toLowerCase();
635
- const isPowerShell = CODEX_POWERSHELL_EXECUTABLES.has(powershellExecutable)
636
- || CODEX_POWERSHELL_INSTALL_PATH_RE.test(argv[0]);
637
- if (isPowerShell && argv[1].toLowerCase() === '-command') {
638
- return { command: argv[2], dialect: 'powershell' };
851
+ if (isCodexPowerShellExecutable(argv[0]) && argv[1].toLowerCase() === '-command') {
852
+ return { command: argv[2], dialect: 'powershell', executable: argv[0] };
639
853
  }
640
854
  }
641
855
  // Node/cross-spawn and Codex use cmd.exe with a small set of startup flags.
@@ -656,27 +870,26 @@ export function parseCodexShellCommandArgv(command, options = {}) {
656
870
  if (outer) {
657
871
  const carrier = resolveCodexShellCarrierArgv(outer);
658
872
  if (carrier !== undefined) {
659
- return parseLiteralShellArgv(carrier.command, { ...options, dialect: carrier.dialect });
873
+ const parsed = parseCodexCarrierCommand(carrier, options);
874
+ return parsed.ok ? parsed.argv : null;
660
875
  }
661
876
  const stringCarrier = resolveCodexShellCarrierString(command);
662
877
  if (stringCarrier) {
663
- return parseLiteralShellArgv(stringCarrier.command, { ...options, dialect: stringCarrier.dialect });
878
+ const parsed = parseCodexCarrierCommand(stringCarrier, options);
879
+ return parsed.ok ? parsed.argv : null;
664
880
  }
665
881
  if (parseCodexCmdCarrierStringFailure(command, options))
666
882
  return null;
667
- return outer;
883
+ return normalizeEvolcoreCommandArgv(outer);
668
884
  }
669
885
  const stringCarrier = resolveCodexShellCarrierString(command);
670
886
  if (!stringCarrier)
671
887
  return null;
672
- const parsed = parseLiteralShellCommand(stringCarrier.command, {
673
- ...options,
674
- dialect: stringCarrier.dialect,
675
- });
676
- return parsed.ok ? parsed.argv : null;
888
+ const parsed = parseCodexCarrierCommand(stringCarrier, options);
889
+ return parsed.ok ? normalizeEvolcoreCommandArgv(parsed.argv) : null;
677
890
  }
678
891
  /** Parse a Codex tool input while preserving safe syntax failure metadata. */
679
- export function parseCodexToolCommand(input) {
892
+ export function parseCodexToolCommand(input, options = {}) {
680
893
  if (Array.isArray(input.commandArgv)) {
681
894
  if (!input.commandArgv.every(value => typeof value === 'string')) {
682
895
  return parseFailure('invalid-control-char', '', 0, 0, 'posix');
@@ -687,46 +900,36 @@ export function parseCodexToolCommand(input) {
687
900
  return parseFailure('invalid-control-char', '', 0, nulIndex, 'posix');
688
901
  const carrier = resolveCodexShellCarrierArgv(outer);
689
902
  if (carrier === undefined) {
690
- return parseCodexCmdCarrierArgvFailure(outer) ?? { ok: true, argv: outer };
903
+ return parseCodexCmdCarrierArgvFailure(outer)
904
+ ?? { ok: true, argv: normalizeEvolcoreCommandArgv(outer) };
691
905
  }
692
- return parseLiteralShellCommand(carrier.command, {
693
- allowManagedTmpDir: true,
694
- dialect: carrier.dialect,
695
- });
906
+ return parseCodexCarrierCommand(carrier, { allowManagedTmpDir: true, ...options });
696
907
  }
697
908
  const command = typeof input.command === 'string' ? input.command : '';
698
909
  const outer = parseLiteralShellCommand(command, { allowManagedTmpDir: true });
699
910
  if (outer.ok) {
700
911
  const carrier = resolveCodexShellCarrierArgv(outer.argv);
701
912
  if (carrier !== undefined) {
702
- return parseLiteralShellCommand(carrier.command, {
703
- allowManagedTmpDir: true,
704
- dialect: carrier.dialect,
705
- });
913
+ return parseCodexCarrierCommand(carrier, { allowManagedTmpDir: true, ...options });
706
914
  }
707
915
  const stringCarrier = resolveCodexShellCarrierString(command);
708
916
  return stringCarrier
709
- ? parseLiteralShellCommand(stringCarrier.command, {
710
- allowManagedTmpDir: true,
711
- dialect: stringCarrier.dialect,
712
- })
713
- : parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true }) ?? outer;
917
+ ? parseCodexCarrierCommand(stringCarrier, { allowManagedTmpDir: true, ...options })
918
+ : parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true })
919
+ ?? { ...outer, argv: normalizeEvolcoreCommandArgv(outer.argv) };
714
920
  }
715
921
  const stringCarrier = resolveCodexShellCarrierString(command);
716
922
  return stringCarrier
717
- ? parseLiteralShellCommand(stringCarrier.command, {
718
- allowManagedTmpDir: true,
719
- dialect: stringCarrier.dialect,
720
- })
923
+ ? parseCodexCarrierCommand(stringCarrier, { allowManagedTmpDir: true, ...options })
721
924
  : parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true }) ?? outer;
722
925
  }
723
926
  /**
724
927
  * Resolve a Codex command input while preserving the raw `commandArgv` form
725
928
  * supplied by app-server. Joining that argv with spaces loses the quoting of
726
- * the inner `-lc` command and was the source of a false proactive denial.
929
+ * the inner `-c`/`-lc` command and was the source of a false proactive denial.
727
930
  */
728
- export function parseCodexToolCommandArgv(input) {
729
- const result = parseCodexToolCommand(input);
931
+ export function parseCodexToolCommandArgv(input, options = {}) {
932
+ const result = parseCodexToolCommand(input, options);
730
933
  return result.ok ? result.argv : null;
731
934
  }
732
935
  function isSimpleExecStringLiteral(value) {
@@ -1214,12 +1417,15 @@ export function parseCodexExecToolCommand(input) {
1214
1417
  return decoded;
1215
1418
  }
1216
1419
  function parseProactiveToolCommandArgv(toolName, input) {
1420
+ const options = {
1421
+ verifyShellExecutable: process.platform === 'win32',
1422
+ };
1217
1423
  if (toolName === 'Bash' || toolName === 'Shell')
1218
- return parseCodexToolCommandArgv(input);
1424
+ return parseCodexToolCommandArgv(input, options);
1219
1425
  if (toolName !== 'exec')
1220
1426
  return null;
1221
1427
  const command = parseCodexExecToolCommand(input);
1222
- return command ? parseCodexShellCommandArgv(command, { allowManagedTmpDir: true }) : null;
1428
+ return command ? parseCodexShellCommandArgv(command, { allowManagedTmpDir: true, ...options }) : null;
1223
1429
  }
1224
1430
  function scanBoundedOutputOperators(command) {
1225
1431
  const operators = [];
@@ -1410,29 +1616,25 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
1410
1616
  const parseOptions = { allowManagedTmpDir: true, ...options };
1411
1617
  const literal = parseLiteralShellCommand(command, parseOptions);
1412
1618
  if (literal.ok) {
1413
- if (literal.argv[0] === 'ec')
1619
+ if (normalizeEvolcoreCommandArgv(literal.argv)[0] === 'ec')
1414
1620
  return { kind: 'literal' };
1415
1621
  const carrier = resolveCodexShellCarrierArgv(literal.argv);
1416
1622
  if (carrier !== undefined) {
1417
- const nested = parseLiteralShellCommand(carrier.command, {
1418
- ...parseOptions,
1419
- dialect: carrier.dialect,
1420
- });
1623
+ const nested = parseCodexCarrierCommand(carrier, parseOptions);
1421
1624
  if (nested.ok && nested.argv[0] === 'ec')
1422
1625
  return { kind: 'literal' };
1423
- if (!nested.ok && hasCarrierEcCommandPrefix(carrier.command, carrier.dialect)) {
1626
+ if (!nested.ok && (hasCarrierEcCommandPrefix(carrier.command, carrier.dialect)
1627
+ || (carrier.dialect === 'powershell' && hasPowerShellDelegationAssignment(carrier.command)))) {
1424
1628
  return { kind: 'composite', issue: nested.issue };
1425
1629
  }
1426
1630
  }
1427
1631
  const stringCarrier = resolveCodexShellCarrierString(command);
1428
1632
  if (stringCarrier) {
1429
- const nested = parseLiteralShellCommand(stringCarrier.command, {
1430
- ...parseOptions,
1431
- dialect: stringCarrier.dialect,
1432
- });
1633
+ const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
1433
1634
  if (nested.ok && nested.argv[0] === 'ec')
1434
1635
  return { kind: 'literal' };
1435
- if (!nested.ok && hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)) {
1636
+ if (!nested.ok && (hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)
1637
+ || (stringCarrier.dialect === 'powershell' && hasPowerShellDelegationAssignment(stringCarrier.command)))) {
1436
1638
  return { kind: 'composite', issue: nested.issue };
1437
1639
  }
1438
1640
  }
@@ -1450,13 +1652,11 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
1450
1652
  }
1451
1653
  const stringCarrier = resolveCodexShellCarrierString(command);
1452
1654
  if (stringCarrier) {
1453
- const nested = parseLiteralShellCommand(stringCarrier.command, {
1454
- ...parseOptions,
1455
- dialect: stringCarrier.dialect,
1456
- });
1655
+ const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
1457
1656
  if (nested.ok && nested.argv[0] === 'ec')
1458
1657
  return { kind: 'literal' };
1459
- if (!nested.ok && hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)) {
1658
+ if (!nested.ok && (hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)
1659
+ || (stringCarrier.dialect === 'powershell' && hasPowerShellDelegationAssignment(stringCarrier.command)))) {
1460
1660
  return { kind: 'composite', issue: nested.issue };
1461
1661
  }
1462
1662
  return { kind: 'none' };
@@ -1687,7 +1887,9 @@ export function parseEvolcoreSendCommand(command) {
1687
1887
  export function isEvolcoreSendCommandForSession(toolName, input, channelId) {
1688
1888
  if (toolName !== 'Bash')
1689
1889
  return false;
1690
- const argv = parseCodexToolCommandArgv(input);
1890
+ const argv = parseCodexToolCommandArgv(input, {
1891
+ verifyShellExecutable: process.platform === 'win32',
1892
+ });
1691
1893
  const parsed = argv ? parseEvolcoreSendArgv(argv) : null;
1692
1894
  if (!parsed)
1693
1895
  return false;