evolcore 0.0.20 → 0.0.22

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 (147) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +58 -9
  3. package/bin/codex-managed-hook.mjs +3 -0
  4. package/bin/install-codex-managed-hooks.mjs +3 -1
  5. package/dist/agents/baseagent.js +10 -6
  6. package/dist/agents/claude-runner.js +393 -108
  7. package/dist/agents/codex-app-server-client.js +41 -7
  8. package/dist/agents/codex-runner.js +1292 -220
  9. package/dist/agents/ecagent-runner.js +171 -61
  10. package/dist/agents/gemini-runner.js +130 -30
  11. package/dist/agents/request-identity.js +25 -0
  12. package/dist/agents/runner-types.js +19 -0
  13. package/dist/aun/aid/agentmd.js +59 -2
  14. package/dist/aun/aid/identity.js +4 -1
  15. package/dist/aun/aid/index.js +1 -1
  16. package/dist/aun/msg/group.js +72 -6
  17. package/dist/aun/msg/history.js +213 -36
  18. package/dist/aun/msg/managed-operation.js +58 -9
  19. package/dist/aun/msg/p2p.js +5 -0
  20. package/dist/aun/outbox.js +189 -80
  21. package/dist/aun/service-proxy.js +43 -25
  22. package/dist/channels/aun.js +618 -123
  23. package/dist/channels/daemon.js +6 -1
  24. package/dist/cli/agent-command.js +4 -3
  25. package/dist/cli/agent.js +66 -56
  26. package/dist/cli/aun-commands.js +177 -42
  27. package/dist/cli/command-log.js +10 -11
  28. package/dist/cli/contact.js +1 -0
  29. package/dist/cli/daemon-commands.js +98 -123
  30. package/dist/cli/init.js +27 -15
  31. package/dist/cli/task-context.js +50 -0
  32. package/dist/cli/trigger-command.js +14 -5
  33. package/dist/cli/watch-logs.js +10 -3
  34. package/dist/config/builtin-roles.js +1 -0
  35. package/dist/config/config-field-policy.js +19 -5
  36. package/dist/config/config-manager.js +167 -22
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/contact-operation-service.js +32 -1
  39. package/dist/config/contact-request-service.js +44 -0
  40. package/dist/config/daemon-services.js +186 -0
  41. package/dist/config/gateway-config.js +20 -9
  42. package/dist/config/role-service.js +54 -3
  43. package/dist/config/schema-migration.js +550 -0
  44. package/dist/config-store.js +151 -9
  45. package/dist/core/agent-application-service.js +279 -0
  46. package/dist/core/audit/log-integrity.js +102 -0
  47. package/dist/core/auth/agent-delegation.js +43 -1
  48. package/dist/core/auth/auth-gateway.js +41 -4
  49. package/dist/core/auth/authorization-audit.js +216 -8
  50. package/dist/core/auth/operation-authorizer.js +41 -1
  51. package/dist/core/auth/operation-catalog.js +9 -1
  52. package/dist/core/bootstrap-messages.js +8 -0
  53. package/dist/core/bootstrap-service.js +99 -27
  54. package/dist/core/causation/aun-association.js +7 -4
  55. package/dist/core/command/agent-control.js +56 -16
  56. package/dist/core/command/command-handler.js +311 -44
  57. package/dist/core/command/connect-menu.js +3 -4
  58. package/dist/core/command/group-menu.js +5 -7
  59. package/dist/core/command/menu-handler.js +288 -80
  60. package/dist/core/command/menu-protocol.js +1 -1
  61. package/dist/core/command/role-menu.js +21 -11
  62. package/dist/core/command/slash-gate.js +85 -18
  63. package/dist/core/command/slash-handler.js +377 -36
  64. package/dist/core/data-migration.js +11 -1
  65. package/dist/core/event-catalog.js +37 -0
  66. package/dist/core/evolagent.js +4 -0
  67. package/dist/core/handoff/dispatcher.js +4 -0
  68. package/dist/core/handoff/runtime.js +33 -3
  69. package/dist/core/handoff/store.js +32 -9
  70. package/dist/core/inference/text-inference.js +7 -15
  71. package/dist/core/message/im-renderer.js +90 -87
  72. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  73. package/dist/core/message/message-bridge.js +184 -12
  74. package/dist/core/message/message-log.js +47 -7
  75. package/dist/core/message/message-queue.js +227 -16
  76. package/dist/core/message/message-utils.js +12 -5
  77. package/dist/core/message/response-engine.js +658 -109
  78. package/dist/core/message/send-receipt.js +1 -0
  79. package/dist/core/message/stream-debouncer.js +9 -2
  80. package/dist/core/model/model-catalog.js +23 -15
  81. package/dist/core/model/model-diagnostics.js +28 -10
  82. package/dist/core/permission/approval-gateway.js +180 -6
  83. package/dist/core/permission/ec-command-parser.js +627 -69
  84. package/dist/core/permission/mode.js +18 -3
  85. package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
  86. package/dist/core/permission/readonly-shell-query.js +263 -9
  87. package/dist/core/permission/sandbox-runtime.js +159 -1
  88. package/dist/core/permission/tool-error-code.js +12 -0
  89. package/dist/core/permission/tool-policy.js +618 -23
  90. package/dist/core/session/session-fs-store.js +154 -5
  91. package/dist/core/session/session-manager.js +329 -30
  92. package/dist/core/session/session-renew.js +37 -13
  93. package/dist/core/session/session-turn-coordinator.js +16 -5
  94. package/dist/eck/kit-renderer.js +1 -1
  95. package/dist/index.js +316 -50
  96. package/dist/ipc.js +459 -29
  97. package/dist/paths.js +82 -7
  98. package/dist/response-system/context-builder.js +1 -7
  99. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  100. package/dist/stats/price-resolver.js +4 -0
  101. package/dist/trigger/anomaly-store.js +1 -0
  102. package/dist/trigger/feedback.js +70 -7
  103. package/dist/trigger/history.js +79 -4
  104. package/dist/trigger/legacy-session-history.js +2 -2
  105. package/dist/trigger/parser.js +13 -3
  106. package/dist/trigger/scheduler.js +20 -3
  107. package/dist/trigger/validation.js +6 -1
  108. package/dist/utils/atomic-write.js +27 -0
  109. package/dist/utils/ecweb-utils.js +16 -2
  110. package/dist/utils/error-utils.js +4 -1
  111. package/dist/utils/logger.js +30 -6
  112. package/dist/utils/process-tree-stats.js +24 -4
  113. package/dist/utils/process-tree-worker.js +31 -0
  114. package/dist/utils/project-path.js +1 -2
  115. package/dist/utils/tool-summary.js +59 -0
  116. package/dist/utils/windows-shell-trust.js +201 -0
  117. package/kits/docs/INDEX.md +1 -1
  118. package/kits/docs/evolcore/INDEX.md +3 -3
  119. package/kits/docs/evolcore/agent-create.md +146 -0
  120. package/kits/docs/evolcore/agent.md +6 -0
  121. package/kits/docs/evolcore/contact.md +7 -1
  122. package/kits/docs/evolcore/group-collaboration.md +251 -0
  123. package/kits/docs/evolcore/group-rules.md +1 -19
  124. package/kits/docs/evolcore/group.md +3 -1
  125. package/kits/docs/evolcore/msg.md +16 -0
  126. package/kits/docs/evolcore/trigger.md +6 -3
  127. package/kits/docs/prompt-loading-architecture.md +6 -0
  128. package/kits/eck_message_manifest.json +6 -6
  129. package/kits/schemas/_meta.json +7 -4
  130. package/kits/schemas/agent-config.schema.11.json +13 -0
  131. package/kits/schemas/agent-config.schema.12.json +427 -0
  132. package/kits/schemas/daemon.schema.5.json +0 -1
  133. package/kits/schemas/daemon.schema.6.json +131 -0
  134. package/kits/schemas/defaults.schema.5.json +15 -3
  135. package/kits/schemas/migrations/README.md +3 -1
  136. package/kits/schemas/relation-config.schema.8.json +13 -0
  137. package/kits/schemas/role-config.schema.1.json +1 -2
  138. package/kits/schemas/single-session.schema.3.json +32 -0
  139. package/kits/templates/message-fragments/item.md +1 -1
  140. package/kits/templates/roles/admin.json +1 -0
  141. package/kits/templates/roles/member.json +1 -0
  142. package/kits/templates/roles/visitor.json +1 -0
  143. package/kits/templates/system-fragments/bootstrap.md +2 -1
  144. package/kits/templates/system-fragments/commands.md +2 -2
  145. package/package.json +6 -3
  146. package/skills/eclink/SKILL.md +2 -0
  147. package/dist/config/aun-gateway-config.js +0 -2
@@ -1,15 +1,32 @@
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']);
17
+ const CODEX_CMD_EXECUTABLES = new Set(['cmd', 'cmd.exe']);
18
+ const CODEX_CMD_INSTALL_PATH_RE = /^[A-Za-z]:[\\/](?:Windows[\\/](?:System32|SysWOW64|Sysnative)[\\/]cmd\.exe)$/i;
19
+ // These switches affect only cmd startup/output formatting. `/c` is handled
20
+ // separately as the required execute switch. Flags such as `/k`, `/v:on`,
21
+ // `/e:*`, and `/f:*` can change process lifetime or command semantics and are
22
+ // intentionally not treated as a trusted Codex carrier.
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']);
13
30
  const SHELL_UNQUOTED_META = new Set([
14
31
  ';', '&', '|', '`', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#',
15
32
  ]);
@@ -61,8 +78,7 @@ function hasEcCommandPrefix(command) {
61
78
  }
62
79
  if (boundary && /\s/.test(char))
63
80
  continue;
64
- if (boundary && command.slice(index, index + 2).toLowerCase() === 'ec'
65
- && (index + 2 === command.length || /\s|[;&|`()<>]/.test(command[index + 2])))
81
+ if (boundary && /^ec(?:\.cmd|\.exe)?(?=$|\s|[;&|`()<>])/i.test(command.slice(index)))
66
82
  return true;
67
83
  boundary = false;
68
84
  }
@@ -71,6 +87,45 @@ function hasEcCommandPrefix(command) {
71
87
  function trimShellSpacing(value) {
72
88
  return value.replace(/^[ \t]+|[ \t]+$/g, '');
73
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
+ }
74
129
  const MANAGED_TMPDIR_TOKEN = '__EC_MANAGED_TMPDIR__';
75
130
  /**
76
131
  * Preserve the one environment reference allowed in managed EC paths.
@@ -140,12 +195,179 @@ function isLiteralArgBoundary(value) {
140
195
  function isShellParameterExpansionStart(value) {
141
196
  return value === '(' || value === '{' || !!value && /[A-Za-z0-9_@*#?$!\-]/.test(value);
142
197
  }
198
+ function hasCarrierEcCommandPrefix(command, dialect) {
199
+ if (dialect !== 'cmd')
200
+ return hasEcCommandPrefix(command);
201
+ const input = trimShellSpacing(command);
202
+ const inner = input.length >= 2 && input.startsWith('"') && input.endsWith('"')
203
+ ? input.slice(1, -1)
204
+ : input;
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;
354
+ }
355
+ function parseFailure(issue, input, offset, tokenIndex, dialect) {
356
+ return {
357
+ ok: false,
358
+ issue,
359
+ offset: Math.max(0, Math.min(offset, input.length)),
360
+ tokenIndex: Math.max(0, tokenIndex),
361
+ dialect,
362
+ inputLength: input.length,
363
+ };
364
+ }
143
365
  function parseLiteralPowerShellCommand(command) {
144
366
  const input = trimShellSpacing(command);
145
367
  if (!input)
146
- return { ok: false, issue: 'empty-command' };
368
+ return parseFailure('empty-command', input, 0, 0, 'powershell');
147
369
  if (input.includes('\0'))
148
- return { ok: false, issue: 'invalid-control-char' };
370
+ return parseFailure('invalid-control-char', input, input.indexOf('\0'), 0, 'powershell');
149
371
  const argv = [];
150
372
  let token = '';
151
373
  let tokenStarted = false;
@@ -166,7 +388,7 @@ function parseLiteralPowerShellCommand(command) {
166
388
  }
167
389
  else if (char === "'") {
168
390
  if (!isLiteralArgBoundary(input[index + 1]))
169
- return { ok: false, issue: 'invalid-quote' };
391
+ return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
170
392
  quote = null;
171
393
  }
172
394
  else {
@@ -177,21 +399,21 @@ function parseLiteralPowerShellCommand(command) {
177
399
  if (quote === 'double') {
178
400
  if (char === '"') {
179
401
  if (!isLiteralArgBoundary(input[index + 1]))
180
- return { ok: false, issue: 'invalid-quote' };
402
+ return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
181
403
  quote = null;
182
404
  continue;
183
405
  }
184
406
  if (char === '`') {
185
407
  const next = input[++index];
186
408
  if (next === undefined)
187
- return { ok: false, issue: 'invalid-quote' };
409
+ return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
188
410
  if (next === '\r' || next === '\n')
189
- return { ok: false, issue: 'structural-newline' };
411
+ return parseFailure('structural-newline', input, index, argv.length, 'powershell');
190
412
  token += next;
191
413
  continue;
192
414
  }
193
415
  if (char === '$' && isShellParameterExpansionStart(input[index + 1])) {
194
- return { ok: false, issue: 'unsafe-expansion' };
416
+ return parseFailure('unsafe-expansion', input, index, argv.length, 'powershell');
195
417
  }
196
418
  token += char;
197
419
  continue;
@@ -201,9 +423,9 @@ function parseLiteralPowerShellCommand(command) {
201
423
  continue;
202
424
  }
203
425
  if (char === '\r' || char === '\n')
204
- return { ok: false, issue: 'structural-newline' };
426
+ return parseFailure('structural-newline', input, index, argv.length, 'powershell');
205
427
  if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
206
- return { ok: false, issue: 'invalid-control-char' };
428
+ return parseFailure('invalid-control-char', input, index, argv.length, 'powershell');
207
429
  }
208
430
  if (char === "'") {
209
431
  tokenStarted = true;
@@ -218,9 +440,9 @@ function parseLiteralPowerShellCommand(command) {
218
440
  if (char === '`') {
219
441
  const next = input[++index];
220
442
  if (next === undefined)
221
- return { ok: false, issue: 'invalid-quote' };
443
+ return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
222
444
  if (next === '\r' || next === '\n')
223
- return { ok: false, issue: 'structural-newline' };
445
+ return parseFailure('structural-newline', input, index, argv.length, 'powershell');
224
446
  tokenStarted = true;
225
447
  token += next;
226
448
  continue;
@@ -228,17 +450,264 @@ function parseLiteralPowerShellCommand(command) {
228
450
  const isWordInitialMeta = (char === '#' || char === '@') && !tokenStarted;
229
451
  const isAlwaysMeta = POWERSHELL_UNQUOTED_META.has(char) && char !== '#' && char !== '@';
230
452
  if (isAlwaysMeta || isWordInitialMeta || (char === '~' && !tokenStarted)) {
231
- return { ok: false, issue: 'shell-composition' };
453
+ return parseFailure('shell-composition', input, index, argv.length, 'powershell');
232
454
  }
233
455
  tokenStarted = true;
234
456
  token += char;
235
457
  }
236
458
  if (quote)
237
- return { ok: false, issue: 'invalid-quote' };
459
+ return parseFailure('invalid-quote', input, input.length, argv.length, 'powershell');
238
460
  finishToken();
239
461
  return argv.length > 0
240
462
  ? { ok: true, argv }
241
- : { ok: false, issue: 'empty-command' };
463
+ : parseFailure('empty-command', input, input.length, argv.length, 'powershell');
464
+ }
465
+ function parseLiteralCmdCommand(command, options = {}) {
466
+ let input = trimShellSpacing(command);
467
+ if (!input)
468
+ return parseFailure('empty-command', input, 0, 0, 'cmd');
469
+ if (input.includes('\0'))
470
+ return parseFailure('invalid-control-char', input, input.indexOf('\0'), 0, 'cmd');
471
+ // `cmd.exe /c` commonly wraps the complete command in one quote pair.
472
+ // Those quotes belong to the carrier, not to the first EC argument; remove
473
+ // exactly that outer pair before tokenizing the inner command.
474
+ if (input.length >= 2 && input.startsWith('"') && input.endsWith('"')) {
475
+ input = input.slice(1, -1);
476
+ }
477
+ const argv = [];
478
+ let token = '';
479
+ let tokenStarted = false;
480
+ let quote = false;
481
+ const finishToken = () => {
482
+ if (!tokenStarted)
483
+ return;
484
+ argv.push(token);
485
+ token = '';
486
+ tokenStarted = false;
487
+ };
488
+ for (let index = 0; index < input.length; index++) {
489
+ const char = input[index];
490
+ if (quote) {
491
+ if (char === '"') {
492
+ // cmd.exe represents a literal quote in a quoted argument as `""`.
493
+ if (input[index + 1] === '"') {
494
+ token += '"';
495
+ index++;
496
+ }
497
+ else {
498
+ quote = false;
499
+ }
500
+ continue;
501
+ }
502
+ // Environment expansion and caret escaping remain active in cmd quoted
503
+ // strings, so accepting either would make the canonical argv differ
504
+ // from what cmd.exe actually executes.
505
+ if (char === '%' || char === '!' || char === '^') {
506
+ return parseFailure(char === '%' ? 'unsafe-expansion' : 'shell-composition', input, index, argv.length, 'cmd');
507
+ }
508
+ if (char === '\r' || char === '\n')
509
+ return parseFailure('structural-newline', input, index, argv.length, 'cmd');
510
+ token += char;
511
+ continue;
512
+ }
513
+ if (char === ' ' || char === '\t') {
514
+ finishToken();
515
+ continue;
516
+ }
517
+ if (char === '\r' || char === '\n')
518
+ return parseFailure('structural-newline', input, index, argv.length, 'cmd');
519
+ if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
520
+ return parseFailure('invalid-control-char', input, index, argv.length, 'cmd');
521
+ }
522
+ if (char === '"') {
523
+ tokenStarted = true;
524
+ quote = true;
525
+ continue;
526
+ }
527
+ if (char === '%' || char === '!') {
528
+ return parseFailure(char === '%' ? 'unsafe-expansion' : 'shell-composition', input, index, argv.length, 'cmd');
529
+ }
530
+ if (char === '^' || char === '&' || char === '|' || char === '<' || char === '>'
531
+ || char === ';' || char === '(' || char === ')' || char === '*' || char === '?'
532
+ || char === '[' || char === ']' || char === '{' || char === '}') {
533
+ return parseFailure('shell-composition', input, index, argv.length, 'cmd');
534
+ }
535
+ tokenStarted = true;
536
+ token += char;
537
+ }
538
+ if (quote)
539
+ return parseFailure('invalid-quote', input, input.length, argv.length, 'cmd');
540
+ finishToken();
541
+ return argv.length > 0
542
+ ? {
543
+ ok: true,
544
+ argv: options.allowManagedTmpDir
545
+ ? argv.map(value => restoreManagedTmpDirToken(value, options.managedTempDir))
546
+ : argv,
547
+ }
548
+ : parseFailure('empty-command', input, input.length, argv.length, 'cmd');
549
+ }
550
+ function isCodexCmdExecutable(value) {
551
+ if (!value)
552
+ return false;
553
+ const normalized = value.toLowerCase();
554
+ return CODEX_CMD_EXECUTABLES.has(normalized)
555
+ || CODEX_CMD_INSTALL_PATH_RE.test(value)
556
+ || isAbsoluteCustomShellPath(value, CODEX_CUSTOM_CMD_NAMES);
557
+ }
558
+ function resolveCodexCmdCarrierArgv(argv) {
559
+ if (argv.length < 3 || !isCodexCmdExecutable(argv[0]))
560
+ return undefined;
561
+ const executeIndex = argv.slice(1, -1).findIndex(value => value.toLowerCase() === '/c');
562
+ if (executeIndex < 0)
563
+ return undefined;
564
+ const absoluteExecuteIndex = executeIndex + 1;
565
+ // The command must remain one argv item. Joining multiple items would lose
566
+ // the quoting boundary that the Windows launcher actually executed.
567
+ if (absoluteExecuteIndex !== argv.length - 2)
568
+ return undefined;
569
+ const seen = new Set();
570
+ for (const rawSwitch of argv.slice(1, absoluteExecuteIndex)) {
571
+ const switchValue = rawSwitch.toLowerCase();
572
+ if (!CODEX_CMD_INERT_SWITCHES.has(switchValue) || seen.has(switchValue))
573
+ return undefined;
574
+ seen.add(switchValue);
575
+ }
576
+ const command = argv.at(-1);
577
+ return typeof command === 'string' && command.trim()
578
+ ? { command, dialect: 'cmd', executable: argv[0] }
579
+ : undefined;
580
+ }
581
+ /** Resolve a Windows cmd carrier when the app-server provides only a string. */
582
+ function resolveCodexCmdCarrierString(command) {
583
+ const input = trimShellSpacing(command);
584
+ // The executable may be quoted when the app-server serializes a Windows
585
+ // path. Parse the prefix into argv-like switches, then use the same
586
+ // validation as the commandArgv path so both representations agree.
587
+ const match = input.match(/^(?:"([^"]+)"|(\S+))((?:\s+\/[^\s]+)*)\s+(.+)$/i);
588
+ if (!match)
589
+ return undefined;
590
+ const executable = match[1] ?? match[2];
591
+ const rawSwitches = match[3].trim();
592
+ const switches = rawSwitches ? rawSwitches.split(/[ \t]+/) : [];
593
+ const argv = [executable, ...switches, match[4]];
594
+ return resolveCodexCmdCarrierArgv(argv);
595
+ }
596
+ /** Resolve a PowerShell carrier when the app-server provides only a string. */
597
+ function resolveCodexPowerShellCarrierString(command) {
598
+ const input = trimShellSpacing(command);
599
+ const match = input.match(/^(?:"([^"]+)"|(\S+))\s+-command\s+(.+)$/i);
600
+ if (!match)
601
+ return undefined;
602
+ const executable = match[1] ?? match[2];
603
+ if (!isCodexPowerShellExecutable(executable))
604
+ return undefined;
605
+ // A string-form carrier has one additional command-line quoting layer. The
606
+ // command argument is commonly serialized as `"ec ..."`; remove exactly
607
+ // that outer pair and leave quotes belonging to EC arguments intact.
608
+ let nested = match[3].trim();
609
+ if (nested.length >= 2
610
+ && ((nested.startsWith('"') && nested.endsWith('"'))
611
+ || (nested.startsWith("'") && nested.endsWith("'")))) {
612
+ nested = nested.slice(1, -1);
613
+ }
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;
634
+ }
635
+ export function resolveCodexShellCarrierString(command) {
636
+ return resolveCodexCmdCarrierString(command)
637
+ ?? resolveCodexPowerShellCarrierString(command)
638
+ ?? resolveCodexPosixShellCarrierString(command);
639
+ }
640
+ /**
641
+ * Extract a trusted cmd executable's command body without trusting its startup
642
+ * switches. This is used only to classify malformed EC carriers as a
643
+ * controlled denial; a command with a non-EC body remains an ordinary shell
644
+ * command and follows the normal approval path.
645
+ */
646
+ function extractCodexCmdCarrierBody(command) {
647
+ const input = trimShellSpacing(command);
648
+ const match = input.match(/^(?:"([^"]+)"|(\S+))((?:\s+\/[^\s]+)*)\s+(.+)$/i);
649
+ if (!match)
650
+ return undefined;
651
+ const executable = match[1] ?? match[2];
652
+ if (!isCodexCmdExecutable(executable))
653
+ return undefined;
654
+ const body = match[4].trim();
655
+ return body || undefined;
656
+ }
657
+ function extractCodexCmdCarrierBodyArgv(argv) {
658
+ if (!argv.length || !isCodexCmdExecutable(argv[0]))
659
+ return undefined;
660
+ let bodyIndex = 1;
661
+ for (; bodyIndex < argv.length; bodyIndex++) {
662
+ const value = argv[bodyIndex];
663
+ if (value.toLowerCase() === '/c') {
664
+ bodyIndex++;
665
+ break;
666
+ }
667
+ // This is deliberately loose. It is only used to identify a malformed
668
+ // carrier that should be denied, never to authorize a command.
669
+ if (!value.startsWith('/'))
670
+ break;
671
+ }
672
+ const body = argv.slice(bodyIndex).join(' ').trim();
673
+ return body || undefined;
674
+ }
675
+ function looksLikeEcCommandBody(command) {
676
+ if (!command)
677
+ return false;
678
+ const input = command.trim();
679
+ const body = (input.startsWith('"') ? input.slice(1) : input).trimStart();
680
+ return /^ec(?:\.cmd|\.exe)?(?:\s|$)/i.test(body);
681
+ }
682
+ /** True only when a trusted cmd executable is carrying an EC command body. */
683
+ export function hasCodexCmdCarrierEcIntent(input) {
684
+ if (Array.isArray(input.commandArgv)
685
+ && input.commandArgv.every(value => typeof value === 'string')) {
686
+ return looksLikeEcCommandBody(extractCodexCmdCarrierBodyArgv(input.commandArgv));
687
+ }
688
+ return typeof input.command === 'string'
689
+ && looksLikeEcCommandBody(extractCodexCmdCarrierBody(input.command));
690
+ }
691
+ function parseCodexCmdCarrierArgvFailure(argv) {
692
+ const body = extractCodexCmdCarrierBodyArgv(argv);
693
+ if (!body || !looksLikeEcCommandBody(body))
694
+ return undefined;
695
+ const nested = parseLiteralShellCommand(body, { dialect: 'cmd', allowManagedTmpDir: true });
696
+ if (!nested.ok)
697
+ return nested;
698
+ return parseFailure('shell-composition', body, 0, 0, 'cmd');
699
+ }
700
+ function parseCodexCmdCarrierStringFailure(command, options = {}) {
701
+ const body = extractCodexCmdCarrierBody(command);
702
+ if (!body || !looksLikeEcCommandBody(body))
703
+ return undefined;
704
+ const nested = parseLiteralShellCommand(body, {
705
+ ...options,
706
+ dialect: 'cmd',
707
+ });
708
+ return nested.ok
709
+ ? parseFailure('shell-composition', body, 0, 0, 'cmd')
710
+ : nested;
242
711
  }
243
712
  function doubleQuotedIssueAt(input, index) {
244
713
  const char = input[index];
@@ -262,13 +731,16 @@ export function parseLiteralShellCommand(command, options = {}) {
262
731
  if (options.dialect === 'powershell') {
263
732
  return parseLiteralPowerShellCommand(command);
264
733
  }
734
+ if (options.dialect === 'cmd') {
735
+ return parseLiteralCmdCommand(command, options);
736
+ }
265
737
  const input = trimShellSpacing(options.allowManagedTmpDir
266
738
  ? substituteManagedTmpDirToken(command)
267
739
  : command);
268
740
  if (!input)
269
- return { ok: false, issue: 'empty-command' };
741
+ return parseFailure('empty-command', input, 0, 0, 'posix');
270
742
  if (input.includes('\0'))
271
- return { ok: false, issue: 'invalid-control-char' };
743
+ return parseFailure('invalid-control-char', input, input.indexOf('\0'), 0, 'posix');
272
744
  const argv = [];
273
745
  let token = '';
274
746
  let tokenStarted = false;
@@ -285,7 +757,7 @@ export function parseLiteralShellCommand(command, options = {}) {
285
757
  if (quote === 'single') {
286
758
  if (char === "'") {
287
759
  if (!isLiteralArgBoundary(input[index + 1]))
288
- return { ok: false, issue: 'invalid-quote' };
760
+ return parseFailure('invalid-quote', input, index, argv.length, 'posix');
289
761
  quote = null;
290
762
  }
291
763
  else
@@ -295,13 +767,13 @@ export function parseLiteralShellCommand(command, options = {}) {
295
767
  if (quote === 'double') {
296
768
  if (char === '"') {
297
769
  if (!isLiteralArgBoundary(input[index + 1]))
298
- return { ok: false, issue: 'invalid-quote' };
770
+ return parseFailure('invalid-quote', input, index, argv.length, 'posix');
299
771
  quote = null;
300
772
  continue;
301
773
  }
302
774
  const issue = doubleQuotedIssueAt(input, index);
303
775
  if (issue)
304
- return { ok: false, issue };
776
+ return parseFailure(issue, input, index, argv.length, 'posix');
305
777
  if (char === '\\') {
306
778
  const next = input[++index];
307
779
  token += ['"', '\\', '$', '`'].includes(next) ? next : `\\${next}`;
@@ -321,9 +793,9 @@ export function parseLiteralShellCommand(command, options = {}) {
321
793
  // keeps inside the executable name; accepting those would misclassify a
322
794
  // different executable as `ec`.
323
795
  if (char === '\r' || char === '\n')
324
- return { ok: false, issue: 'structural-newline' };
796
+ return parseFailure('structural-newline', input, index, argv.length, 'posix');
325
797
  if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
326
- return { ok: false, issue: 'invalid-control-char' };
798
+ return parseFailure('invalid-control-char', input, index, argv.length, 'posix');
327
799
  }
328
800
  if (char === "'") {
329
801
  tokenStarted = true;
@@ -338,9 +810,9 @@ export function parseLiteralShellCommand(command, options = {}) {
338
810
  if (char === '\\') {
339
811
  const next = input[++index];
340
812
  if (next === undefined)
341
- return { ok: false, issue: 'invalid-quote' };
813
+ return parseFailure('invalid-quote', input, index, argv.length, 'posix');
342
814
  if (next === '\r' || next === '\n')
343
- return { ok: false, issue: 'structural-newline' };
815
+ return parseFailure('structural-newline', input, index, argv.length, 'posix');
344
816
  tokenStarted = true;
345
817
  token += next;
346
818
  continue;
@@ -349,12 +821,12 @@ export function parseLiteralShellCommand(command, options = {}) {
349
821
  // peer keys such as `aun#alice` are ordinary literal arguments.
350
822
  if ((SHELL_UNQUOTED_META.has(char) && (char !== '#' || !tokenStarted))
351
823
  || (char === '~' && !tokenStarted))
352
- return { ok: false, issue: 'shell-composition' };
824
+ return parseFailure('shell-composition', input, index, argv.length, 'posix');
353
825
  tokenStarted = true;
354
826
  token += char;
355
827
  }
356
828
  if (quote)
357
- return { ok: false, issue: 'invalid-quote' };
829
+ return parseFailure('invalid-quote', input, input.length, argv.length, 'posix');
358
830
  finishToken();
359
831
  return argv.length > 0
360
832
  ? {
@@ -363,24 +835,27 @@ export function parseLiteralShellCommand(command, options = {}) {
363
835
  ? argv.map(value => restoreManagedTmpDirToken(value, options.managedTempDir))
364
836
  : argv,
365
837
  }
366
- : { ok: false, issue: 'empty-command' };
838
+ : parseFailure('empty-command', input, input.length, argv.length, 'posix');
367
839
  }
368
840
  export function parseLiteralShellArgv(command, options = {}) {
369
841
  const result = parseLiteralShellCommand(command, options);
370
842
  return result.ok ? result.argv : null;
371
843
  }
372
844
  export function resolveCodexShellCarrierArgv(argv) {
373
- if (argv.length !== 3)
374
- return undefined;
375
- if (argv[1] === '-lc' && CODEX_POSIX_SHELL_EXECUTABLES.has(argv[0])) {
376
- 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] };
377
849
  }
378
- const powershellExecutable = argv[0].toLowerCase();
379
- const isPowerShell = CODEX_POWERSHELL_EXECUTABLES.has(powershellExecutable)
380
- || CODEX_POWERSHELL_INSTALL_PATH_RE.test(argv[0]);
381
- return isPowerShell && argv[1].toLowerCase() === '-command'
382
- ? { command: argv[2], dialect: 'powershell' }
383
- : undefined;
850
+ if (argv.length === 3) {
851
+ if (isCodexPowerShellExecutable(argv[0]) && argv[1].toLowerCase() === '-command') {
852
+ return { command: argv[2], dialect: 'powershell', executable: argv[0] };
853
+ }
854
+ }
855
+ // Node/cross-spawn and Codex use cmd.exe with a small set of startup flags.
856
+ // The execute switch must be the final switch before one command argument;
857
+ // validation itself is shared with string-only Windows payloads below.
858
+ return resolveCodexCmdCarrierArgv(argv);
384
859
  }
385
860
  export function unwrapCodexShellCommandArgv(argv) {
386
861
  return resolveCodexShellCarrierArgv(argv)?.command;
@@ -392,31 +867,70 @@ export function unwrapCodexShellCommandArgv(argv) {
392
867
  */
393
868
  export function parseCodexShellCommandArgv(command, options = {}) {
394
869
  const outer = parseLiteralShellArgv(command, options);
395
- if (!outer)
870
+ if (outer) {
871
+ const carrier = resolveCodexShellCarrierArgv(outer);
872
+ if (carrier !== undefined) {
873
+ const parsed = parseCodexCarrierCommand(carrier, options);
874
+ return parsed.ok ? parsed.argv : null;
875
+ }
876
+ const stringCarrier = resolveCodexShellCarrierString(command);
877
+ if (stringCarrier) {
878
+ const parsed = parseCodexCarrierCommand(stringCarrier, options);
879
+ return parsed.ok ? parsed.argv : null;
880
+ }
881
+ if (parseCodexCmdCarrierStringFailure(command, options))
882
+ return null;
883
+ return normalizeEvolcoreCommandArgv(outer);
884
+ }
885
+ const stringCarrier = resolveCodexShellCarrierString(command);
886
+ if (!stringCarrier)
396
887
  return null;
397
- const carrier = resolveCodexShellCarrierArgv(outer);
398
- return carrier === undefined
399
- ? outer
400
- : parseLiteralShellArgv(carrier.command, { ...options, dialect: carrier.dialect });
888
+ const parsed = parseCodexCarrierCommand(stringCarrier, options);
889
+ return parsed.ok ? normalizeEvolcoreCommandArgv(parsed.argv) : null;
401
890
  }
402
- /**
403
- * Resolve a Codex command input while preserving the raw `commandArgv` form
404
- * supplied by app-server. Joining that argv with spaces loses the quoting of
405
- * the inner `-lc` command and was the source of a false proactive denial.
406
- */
407
- export function parseCodexToolCommandArgv(input) {
408
- if (Array.isArray(input.commandArgv) && input.commandArgv.every(value => typeof value === 'string')) {
891
+ /** Parse a Codex tool input while preserving safe syntax failure metadata. */
892
+ export function parseCodexToolCommand(input, options = {}) {
893
+ if (Array.isArray(input.commandArgv)) {
894
+ if (!input.commandArgv.every(value => typeof value === 'string')) {
895
+ return parseFailure('invalid-control-char', '', 0, 0, 'posix');
896
+ }
409
897
  const outer = input.commandArgv;
898
+ const nulIndex = outer.findIndex(value => value.includes('\0'));
899
+ if (nulIndex >= 0)
900
+ return parseFailure('invalid-control-char', '', 0, nulIndex, 'posix');
410
901
  const carrier = resolveCodexShellCarrierArgv(outer);
411
- return carrier === undefined
412
- ? outer
413
- : parseLiteralShellArgv(carrier.command, {
414
- allowManagedTmpDir: true,
415
- dialect: carrier.dialect,
416
- });
902
+ if (carrier === undefined) {
903
+ return parseCodexCmdCarrierArgvFailure(outer)
904
+ ?? { ok: true, argv: normalizeEvolcoreCommandArgv(outer) };
905
+ }
906
+ return parseCodexCarrierCommand(carrier, { allowManagedTmpDir: true, ...options });
417
907
  }
418
908
  const command = typeof input.command === 'string' ? input.command : '';
419
- return parseCodexShellCommandArgv(command, { allowManagedTmpDir: true });
909
+ const outer = parseLiteralShellCommand(command, { allowManagedTmpDir: true });
910
+ if (outer.ok) {
911
+ const carrier = resolveCodexShellCarrierArgv(outer.argv);
912
+ if (carrier !== undefined) {
913
+ return parseCodexCarrierCommand(carrier, { allowManagedTmpDir: true, ...options });
914
+ }
915
+ const stringCarrier = resolveCodexShellCarrierString(command);
916
+ return stringCarrier
917
+ ? parseCodexCarrierCommand(stringCarrier, { allowManagedTmpDir: true, ...options })
918
+ : parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true })
919
+ ?? { ...outer, argv: normalizeEvolcoreCommandArgv(outer.argv) };
920
+ }
921
+ const stringCarrier = resolveCodexShellCarrierString(command);
922
+ return stringCarrier
923
+ ? parseCodexCarrierCommand(stringCarrier, { allowManagedTmpDir: true, ...options })
924
+ : parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true }) ?? outer;
925
+ }
926
+ /**
927
+ * Resolve a Codex command input while preserving the raw `commandArgv` form
928
+ * supplied by app-server. Joining that argv with spaces loses the quoting of
929
+ * the inner `-c`/`-lc` command and was the source of a false proactive denial.
930
+ */
931
+ export function parseCodexToolCommandArgv(input, options = {}) {
932
+ const result = parseCodexToolCommand(input, options);
933
+ return result.ok ? result.argv : null;
420
934
  }
421
935
  function isSimpleExecStringLiteral(value) {
422
936
  const quote = value[0];
@@ -903,12 +1417,15 @@ export function parseCodexExecToolCommand(input) {
903
1417
  return decoded;
904
1418
  }
905
1419
  function parseProactiveToolCommandArgv(toolName, input) {
1420
+ const options = {
1421
+ verifyShellExecutable: process.platform === 'win32',
1422
+ };
906
1423
  if (toolName === 'Bash' || toolName === 'Shell')
907
- return parseCodexToolCommandArgv(input);
1424
+ return parseCodexToolCommandArgv(input, options);
908
1425
  if (toolName !== 'exec')
909
1426
  return null;
910
1427
  const command = parseCodexExecToolCommand(input);
911
- return command ? parseCodexShellCommandArgv(command, { allowManagedTmpDir: true }) : null;
1428
+ return command ? parseCodexShellCommandArgv(command, { allowManagedTmpDir: true, ...options }) : null;
912
1429
  }
913
1430
  function scanBoundedOutputOperators(command) {
914
1431
  const operators = [];
@@ -1099,23 +1616,62 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
1099
1616
  const parseOptions = { allowManagedTmpDir: true, ...options };
1100
1617
  const literal = parseLiteralShellCommand(command, parseOptions);
1101
1618
  if (literal.ok) {
1102
- if (literal.argv[0] === 'ec')
1619
+ if (normalizeEvolcoreCommandArgv(literal.argv)[0] === 'ec')
1103
1620
  return { kind: 'literal' };
1104
1621
  const carrier = resolveCodexShellCarrierArgv(literal.argv);
1105
1622
  if (carrier !== undefined) {
1106
- const nested = parseLiteralShellCommand(carrier.command, {
1107
- ...parseOptions,
1108
- dialect: carrier.dialect,
1109
- });
1623
+ const nested = parseCodexCarrierCommand(carrier, parseOptions);
1110
1624
  if (nested.ok && nested.argv[0] === 'ec')
1111
1625
  return { kind: 'literal' };
1112
- if (!nested.ok && hasEcCommandPrefix(carrier.command)) {
1626
+ if (!nested.ok && (hasCarrierEcCommandPrefix(carrier.command, carrier.dialect)
1627
+ || (carrier.dialect === 'powershell' && hasPowerShellDelegationAssignment(carrier.command)))) {
1113
1628
  return { kind: 'composite', issue: nested.issue };
1114
1629
  }
1115
1630
  }
1631
+ const stringCarrier = resolveCodexShellCarrierString(command);
1632
+ if (stringCarrier) {
1633
+ const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
1634
+ if (nested.ok && nested.argv[0] === 'ec')
1635
+ return { kind: 'literal' };
1636
+ if (!nested.ok && (hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)
1637
+ || (stringCarrier.dialect === 'powershell' && hasPowerShellDelegationAssignment(stringCarrier.command)))) {
1638
+ return { kind: 'composite', issue: nested.issue };
1639
+ }
1640
+ }
1641
+ const malformedCmdBody = extractCodexCmdCarrierBody(command);
1642
+ if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
1643
+ const nested = parseLiteralShellCommand(malformedCmdBody, {
1644
+ ...parseOptions,
1645
+ dialect: 'cmd',
1646
+ });
1647
+ return nested.ok
1648
+ ? { kind: 'composite', issue: 'shell-composition' }
1649
+ : { kind: 'composite', issue: nested.issue };
1650
+ }
1116
1651
  return { kind: 'none' };
1117
1652
  }
1118
- const boundedOutput = options.dialect === 'powershell'
1653
+ const stringCarrier = resolveCodexShellCarrierString(command);
1654
+ if (stringCarrier) {
1655
+ const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
1656
+ if (nested.ok && nested.argv[0] === 'ec')
1657
+ return { kind: 'literal' };
1658
+ if (!nested.ok && (hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)
1659
+ || (stringCarrier.dialect === 'powershell' && hasPowerShellDelegationAssignment(stringCarrier.command)))) {
1660
+ return { kind: 'composite', issue: nested.issue };
1661
+ }
1662
+ return { kind: 'none' };
1663
+ }
1664
+ const malformedCmdBody = extractCodexCmdCarrierBody(command);
1665
+ if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
1666
+ const nested = parseLiteralShellCommand(malformedCmdBody, {
1667
+ ...parseOptions,
1668
+ dialect: 'cmd',
1669
+ });
1670
+ return nested.ok
1671
+ ? { kind: 'composite', issue: 'shell-composition' }
1672
+ : { kind: 'composite', issue: nested.issue };
1673
+ }
1674
+ const boundedOutput = options.dialect === 'powershell' || options.dialect === 'cmd'
1119
1675
  ? null
1120
1676
  : parseBoundedOutputShellCommand(command);
1121
1677
  if (boundedOutput && boundedOutput.argv[0] === 'ec') {
@@ -1331,7 +1887,9 @@ export function parseEvolcoreSendCommand(command) {
1331
1887
  export function isEvolcoreSendCommandForSession(toolName, input, channelId) {
1332
1888
  if (toolName !== 'Bash')
1333
1889
  return false;
1334
- const argv = parseCodexToolCommandArgv(input);
1890
+ const argv = parseCodexToolCommandArgv(input, {
1891
+ verifyShellExecutable: process.platform === 'win32',
1892
+ });
1335
1893
  const parsed = argv ? parseEvolcoreSendArgv(argv) : null;
1336
1894
  if (!parsed)
1337
1895
  return false;