evolcore 0.0.20 → 0.0.21

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 (123) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +58 -9
  3. package/dist/agents/baseagent.js +10 -6
  4. package/dist/agents/claude-runner.js +379 -108
  5. package/dist/agents/codex-app-server-client.js +10 -2
  6. package/dist/agents/codex-runner.js +402 -135
  7. package/dist/agents/ecagent-runner.js +171 -61
  8. package/dist/agents/gemini-runner.js +130 -30
  9. package/dist/agents/request-identity.js +25 -0
  10. package/dist/agents/runner-types.js +19 -0
  11. package/dist/aun/aid/agentmd.js +59 -2
  12. package/dist/aun/aid/identity.js +4 -1
  13. package/dist/aun/aid/index.js +1 -1
  14. package/dist/aun/msg/group.js +72 -6
  15. package/dist/aun/msg/history.js +213 -36
  16. package/dist/aun/msg/managed-operation.js +58 -9
  17. package/dist/aun/msg/p2p.js +5 -0
  18. package/dist/aun/outbox.js +182 -80
  19. package/dist/aun/service-proxy.js +43 -25
  20. package/dist/channels/aun.js +409 -88
  21. package/dist/channels/daemon.js +6 -1
  22. package/dist/cli/agent-command.js +4 -3
  23. package/dist/cli/agent.js +66 -56
  24. package/dist/cli/aun-commands.js +177 -42
  25. package/dist/cli/command-log.js +10 -11
  26. package/dist/cli/contact.js +1 -0
  27. package/dist/cli/daemon-commands.js +69 -115
  28. package/dist/cli/init.js +27 -15
  29. package/dist/cli/task-context.js +46 -0
  30. package/dist/cli/trigger-command.js +1 -1
  31. package/dist/cli/watch-logs.js +10 -3
  32. package/dist/config/builtin-roles.js +1 -0
  33. package/dist/config/config-field-policy.js +16 -5
  34. package/dist/config/config-manager.js +135 -17
  35. package/dist/config/contact-operation-service.js +32 -1
  36. package/dist/config/contact-request-service.js +44 -0
  37. package/dist/config/daemon-services.js +186 -0
  38. package/dist/config/gateway-config.js +20 -9
  39. package/dist/config/role-service.js +54 -3
  40. package/dist/config/schema-migration.js +550 -0
  41. package/dist/config-store.js +151 -9
  42. package/dist/core/agent-application-service.js +279 -0
  43. package/dist/core/audit/log-integrity.js +102 -0
  44. package/dist/core/auth/agent-delegation.js +31 -1
  45. package/dist/core/auth/auth-gateway.js +33 -4
  46. package/dist/core/auth/authorization-audit.js +150 -2
  47. package/dist/core/auth/operation-authorizer.js +41 -1
  48. package/dist/core/auth/operation-catalog.js +9 -1
  49. package/dist/core/bootstrap-service.js +6 -2
  50. package/dist/core/causation/aun-association.js +7 -4
  51. package/dist/core/command/agent-control.js +56 -16
  52. package/dist/core/command/command-handler.js +290 -44
  53. package/dist/core/command/connect-menu.js +3 -4
  54. package/dist/core/command/group-menu.js +5 -7
  55. package/dist/core/command/menu-handler.js +279 -80
  56. package/dist/core/command/role-menu.js +21 -11
  57. package/dist/core/command/slash-gate.js +85 -18
  58. package/dist/core/command/slash-handler.js +350 -32
  59. package/dist/core/event-catalog.js +5 -0
  60. package/dist/core/evolagent.js +4 -0
  61. package/dist/core/handoff/dispatcher.js +4 -0
  62. package/dist/core/handoff/runtime.js +10 -0
  63. package/dist/core/handoff/store.js +32 -9
  64. package/dist/core/inference/text-inference.js +7 -15
  65. package/dist/core/message/im-renderer.js +83 -84
  66. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  67. package/dist/core/message/message-bridge.js +124 -10
  68. package/dist/core/message/message-log.js +14 -7
  69. package/dist/core/message/message-queue.js +206 -16
  70. package/dist/core/message/message-utils.js +12 -5
  71. package/dist/core/message/response-engine.js +486 -68
  72. package/dist/core/message/send-receipt.js +1 -0
  73. package/dist/core/message/stream-debouncer.js +9 -2
  74. package/dist/core/model/model-catalog.js +23 -15
  75. package/dist/core/model/model-diagnostics.js +28 -10
  76. package/dist/core/permission/approval-gateway.js +180 -6
  77. package/dist/core/permission/ec-command-parser.js +410 -54
  78. package/dist/core/permission/mode.js +18 -3
  79. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  80. package/dist/core/permission/readonly-shell-query.js +263 -9
  81. package/dist/core/permission/sandbox-runtime.js +159 -1
  82. package/dist/core/permission/tool-policy.js +575 -21
  83. package/dist/core/session/session-fs-store.js +154 -5
  84. package/dist/core/session/session-manager.js +299 -30
  85. package/dist/core/session/session-renew.js +19 -12
  86. package/dist/core/session/session-turn-coordinator.js +11 -4
  87. package/dist/eck/kit-renderer.js +1 -1
  88. package/dist/index.js +253 -46
  89. package/dist/ipc.js +374 -24
  90. package/dist/paths.js +64 -7
  91. package/dist/response-system/context-builder.js +1 -7
  92. package/dist/trigger/anomaly-store.js +1 -0
  93. package/dist/trigger/feedback.js +56 -5
  94. package/dist/trigger/history.js +79 -4
  95. package/dist/trigger/legacy-session-history.js +2 -2
  96. package/dist/trigger/parser.js +3 -2
  97. package/dist/trigger/validation.js +6 -1
  98. package/dist/utils/atomic-write.js +27 -0
  99. package/dist/utils/ecweb-utils.js +16 -2
  100. package/dist/utils/error-utils.js +4 -1
  101. package/dist/utils/logger.js +21 -2
  102. package/dist/utils/process-tree-stats.js +24 -4
  103. package/dist/utils/process-tree-worker.js +31 -0
  104. package/dist/utils/project-path.js +1 -2
  105. package/kits/docs/INDEX.md +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -1
  107. package/kits/docs/evolcore/contact.md +7 -1
  108. package/kits/docs/evolcore/msg.md +16 -0
  109. package/kits/schemas/_meta.json +4 -2
  110. package/kits/schemas/agent-config.schema.11.json +13 -0
  111. package/kits/schemas/daemon.schema.5.json +0 -1
  112. package/kits/schemas/daemon.schema.6.json +131 -0
  113. package/kits/schemas/defaults.schema.5.json +15 -3
  114. package/kits/schemas/migrations/README.md +3 -1
  115. package/kits/schemas/relation-config.schema.8.json +13 -0
  116. package/kits/schemas/role-config.schema.1.json +1 -2
  117. package/kits/schemas/single-session.schema.3.json +32 -0
  118. package/kits/templates/roles/admin.json +1 -0
  119. package/kits/templates/roles/member.json +1 -0
  120. package/kits/templates/roles/visitor.json +1 -0
  121. package/package.json +6 -3
  122. package/skills/eclink/SKILL.md +2 -0
  123. package/dist/config/aun-gateway-config.js +0 -2
@@ -10,6 +10,13 @@ const CODEX_POWERSHELL_EXECUTABLES = new Set([
10
10
  'pwsh', 'pwsh.exe', 'powershell', 'powershell.exe',
11
11
  ]);
12
12
  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;
13
+ const CODEX_CMD_EXECUTABLES = new Set(['cmd', 'cmd.exe']);
14
+ const CODEX_CMD_INSTALL_PATH_RE = /^[A-Za-z]:[\\/](?:Windows[\\/](?:System32|SysWOW64|Sysnative)[\\/]cmd\.exe)$/i;
15
+ // These switches affect only cmd startup/output formatting. `/c` is handled
16
+ // separately as the required execute switch. Flags such as `/k`, `/v:on`,
17
+ // `/e:*`, and `/f:*` can change process lifetime or command semantics and are
18
+ // intentionally not treated as a trusted Codex carrier.
19
+ const CODEX_CMD_INERT_SWITCHES = new Set(['/d', '/s', '/q', '/a', '/u']);
13
20
  const SHELL_UNQUOTED_META = new Set([
14
21
  ';', '&', '|', '`', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#',
15
22
  ]);
@@ -140,12 +147,31 @@ function isLiteralArgBoundary(value) {
140
147
  function isShellParameterExpansionStart(value) {
141
148
  return value === '(' || value === '{' || !!value && /[A-Za-z0-9_@*#?$!\-]/.test(value);
142
149
  }
150
+ function hasCarrierEcCommandPrefix(command, dialect) {
151
+ if (dialect !== 'cmd')
152
+ return hasEcCommandPrefix(command);
153
+ const input = trimShellSpacing(command);
154
+ const inner = input.length >= 2 && input.startsWith('"') && input.endsWith('"')
155
+ ? input.slice(1, -1)
156
+ : input;
157
+ return /^\s*ec(?:\s|$)/i.test(inner);
158
+ }
159
+ function parseFailure(issue, input, offset, tokenIndex, dialect) {
160
+ return {
161
+ ok: false,
162
+ issue,
163
+ offset: Math.max(0, Math.min(offset, input.length)),
164
+ tokenIndex: Math.max(0, tokenIndex),
165
+ dialect,
166
+ inputLength: input.length,
167
+ };
168
+ }
143
169
  function parseLiteralPowerShellCommand(command) {
144
170
  const input = trimShellSpacing(command);
145
171
  if (!input)
146
- return { ok: false, issue: 'empty-command' };
172
+ return parseFailure('empty-command', input, 0, 0, 'powershell');
147
173
  if (input.includes('\0'))
148
- return { ok: false, issue: 'invalid-control-char' };
174
+ return parseFailure('invalid-control-char', input, input.indexOf('\0'), 0, 'powershell');
149
175
  const argv = [];
150
176
  let token = '';
151
177
  let tokenStarted = false;
@@ -166,7 +192,7 @@ function parseLiteralPowerShellCommand(command) {
166
192
  }
167
193
  else if (char === "'") {
168
194
  if (!isLiteralArgBoundary(input[index + 1]))
169
- return { ok: false, issue: 'invalid-quote' };
195
+ return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
170
196
  quote = null;
171
197
  }
172
198
  else {
@@ -177,21 +203,21 @@ function parseLiteralPowerShellCommand(command) {
177
203
  if (quote === 'double') {
178
204
  if (char === '"') {
179
205
  if (!isLiteralArgBoundary(input[index + 1]))
180
- return { ok: false, issue: 'invalid-quote' };
206
+ return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
181
207
  quote = null;
182
208
  continue;
183
209
  }
184
210
  if (char === '`') {
185
211
  const next = input[++index];
186
212
  if (next === undefined)
187
- return { ok: false, issue: 'invalid-quote' };
213
+ return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
188
214
  if (next === '\r' || next === '\n')
189
- return { ok: false, issue: 'structural-newline' };
215
+ return parseFailure('structural-newline', input, index, argv.length, 'powershell');
190
216
  token += next;
191
217
  continue;
192
218
  }
193
219
  if (char === '$' && isShellParameterExpansionStart(input[index + 1])) {
194
- return { ok: false, issue: 'unsafe-expansion' };
220
+ return parseFailure('unsafe-expansion', input, index, argv.length, 'powershell');
195
221
  }
196
222
  token += char;
197
223
  continue;
@@ -201,9 +227,9 @@ function parseLiteralPowerShellCommand(command) {
201
227
  continue;
202
228
  }
203
229
  if (char === '\r' || char === '\n')
204
- return { ok: false, issue: 'structural-newline' };
230
+ return parseFailure('structural-newline', input, index, argv.length, 'powershell');
205
231
  if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
206
- return { ok: false, issue: 'invalid-control-char' };
232
+ return parseFailure('invalid-control-char', input, index, argv.length, 'powershell');
207
233
  }
208
234
  if (char === "'") {
209
235
  tokenStarted = true;
@@ -218,9 +244,9 @@ function parseLiteralPowerShellCommand(command) {
218
244
  if (char === '`') {
219
245
  const next = input[++index];
220
246
  if (next === undefined)
221
- return { ok: false, issue: 'invalid-quote' };
247
+ return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
222
248
  if (next === '\r' || next === '\n')
223
- return { ok: false, issue: 'structural-newline' };
249
+ return parseFailure('structural-newline', input, index, argv.length, 'powershell');
224
250
  tokenStarted = true;
225
251
  token += next;
226
252
  continue;
@@ -228,17 +254,245 @@ function parseLiteralPowerShellCommand(command) {
228
254
  const isWordInitialMeta = (char === '#' || char === '@') && !tokenStarted;
229
255
  const isAlwaysMeta = POWERSHELL_UNQUOTED_META.has(char) && char !== '#' && char !== '@';
230
256
  if (isAlwaysMeta || isWordInitialMeta || (char === '~' && !tokenStarted)) {
231
- return { ok: false, issue: 'shell-composition' };
257
+ return parseFailure('shell-composition', input, index, argv.length, 'powershell');
232
258
  }
233
259
  tokenStarted = true;
234
260
  token += char;
235
261
  }
236
262
  if (quote)
237
- return { ok: false, issue: 'invalid-quote' };
263
+ return parseFailure('invalid-quote', input, input.length, argv.length, 'powershell');
238
264
  finishToken();
239
265
  return argv.length > 0
240
266
  ? { ok: true, argv }
241
- : { ok: false, issue: 'empty-command' };
267
+ : parseFailure('empty-command', input, input.length, argv.length, 'powershell');
268
+ }
269
+ function parseLiteralCmdCommand(command, options = {}) {
270
+ let input = trimShellSpacing(command);
271
+ if (!input)
272
+ return parseFailure('empty-command', input, 0, 0, 'cmd');
273
+ if (input.includes('\0'))
274
+ return parseFailure('invalid-control-char', input, input.indexOf('\0'), 0, 'cmd');
275
+ // `cmd.exe /c` commonly wraps the complete command in one quote pair.
276
+ // Those quotes belong to the carrier, not to the first EC argument; remove
277
+ // exactly that outer pair before tokenizing the inner command.
278
+ if (input.length >= 2 && input.startsWith('"') && input.endsWith('"')) {
279
+ input = input.slice(1, -1);
280
+ }
281
+ const argv = [];
282
+ let token = '';
283
+ let tokenStarted = false;
284
+ let quote = false;
285
+ const finishToken = () => {
286
+ if (!tokenStarted)
287
+ return;
288
+ argv.push(token);
289
+ token = '';
290
+ tokenStarted = false;
291
+ };
292
+ for (let index = 0; index < input.length; index++) {
293
+ const char = input[index];
294
+ if (quote) {
295
+ if (char === '"') {
296
+ // cmd.exe represents a literal quote in a quoted argument as `""`.
297
+ if (input[index + 1] === '"') {
298
+ token += '"';
299
+ index++;
300
+ }
301
+ else {
302
+ quote = false;
303
+ }
304
+ continue;
305
+ }
306
+ // Environment expansion and caret escaping remain active in cmd quoted
307
+ // strings, so accepting either would make the canonical argv differ
308
+ // from what cmd.exe actually executes.
309
+ if (char === '%' || char === '!' || char === '^') {
310
+ return parseFailure(char === '%' ? 'unsafe-expansion' : 'shell-composition', input, index, argv.length, 'cmd');
311
+ }
312
+ if (char === '\r' || char === '\n')
313
+ return parseFailure('structural-newline', input, index, argv.length, 'cmd');
314
+ token += char;
315
+ continue;
316
+ }
317
+ if (char === ' ' || char === '\t') {
318
+ finishToken();
319
+ continue;
320
+ }
321
+ if (char === '\r' || char === '\n')
322
+ return parseFailure('structural-newline', input, index, argv.length, 'cmd');
323
+ if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
324
+ return parseFailure('invalid-control-char', input, index, argv.length, 'cmd');
325
+ }
326
+ if (char === '"') {
327
+ tokenStarted = true;
328
+ quote = true;
329
+ continue;
330
+ }
331
+ if (char === '%' || char === '!') {
332
+ return parseFailure(char === '%' ? 'unsafe-expansion' : 'shell-composition', input, index, argv.length, 'cmd');
333
+ }
334
+ if (char === '^' || char === '&' || char === '|' || char === '<' || char === '>'
335
+ || char === ';' || char === '(' || char === ')' || char === '*' || char === '?'
336
+ || char === '[' || char === ']' || char === '{' || char === '}') {
337
+ return parseFailure('shell-composition', input, index, argv.length, 'cmd');
338
+ }
339
+ tokenStarted = true;
340
+ token += char;
341
+ }
342
+ if (quote)
343
+ return parseFailure('invalid-quote', input, input.length, argv.length, 'cmd');
344
+ finishToken();
345
+ return argv.length > 0
346
+ ? {
347
+ ok: true,
348
+ argv: options.allowManagedTmpDir
349
+ ? argv.map(value => restoreManagedTmpDirToken(value, options.managedTempDir))
350
+ : argv,
351
+ }
352
+ : parseFailure('empty-command', input, input.length, argv.length, 'cmd');
353
+ }
354
+ function isCodexCmdExecutable(value) {
355
+ if (!value)
356
+ return false;
357
+ const normalized = value.toLowerCase();
358
+ return CODEX_CMD_EXECUTABLES.has(normalized) || CODEX_CMD_INSTALL_PATH_RE.test(value);
359
+ }
360
+ function resolveCodexCmdCarrierArgv(argv) {
361
+ if (argv.length < 3 || !isCodexCmdExecutable(argv[0]))
362
+ return undefined;
363
+ const executeIndex = argv.slice(1, -1).findIndex(value => value.toLowerCase() === '/c');
364
+ if (executeIndex < 0)
365
+ return undefined;
366
+ const absoluteExecuteIndex = executeIndex + 1;
367
+ // The command must remain one argv item. Joining multiple items would lose
368
+ // the quoting boundary that the Windows launcher actually executed.
369
+ if (absoluteExecuteIndex !== argv.length - 2)
370
+ return undefined;
371
+ const seen = new Set();
372
+ for (const rawSwitch of argv.slice(1, absoluteExecuteIndex)) {
373
+ const switchValue = rawSwitch.toLowerCase();
374
+ if (!CODEX_CMD_INERT_SWITCHES.has(switchValue) || seen.has(switchValue))
375
+ return undefined;
376
+ seen.add(switchValue);
377
+ }
378
+ const command = argv.at(-1);
379
+ return typeof command === 'string' && command.trim()
380
+ ? { command, dialect: 'cmd' }
381
+ : undefined;
382
+ }
383
+ /** Resolve a Windows cmd carrier when the app-server provides only a string. */
384
+ function resolveCodexCmdCarrierString(command) {
385
+ const input = trimShellSpacing(command);
386
+ // The executable may be quoted when the app-server serializes a Windows
387
+ // path. Parse the prefix into argv-like switches, then use the same
388
+ // validation as the commandArgv path so both representations agree.
389
+ const match = input.match(/^(?:"([^"]+)"|(\S+))((?:\s+\/[^\s]+)*)\s+(.+)$/i);
390
+ if (!match)
391
+ return undefined;
392
+ const executable = match[1] ?? match[2];
393
+ const rawSwitches = match[3].trim();
394
+ const switches = rawSwitches ? rawSwitches.split(/[ \t]+/) : [];
395
+ const argv = [executable, ...switches, match[4]];
396
+ return resolveCodexCmdCarrierArgv(argv);
397
+ }
398
+ /** Resolve a PowerShell carrier when the app-server provides only a string. */
399
+ function resolveCodexPowerShellCarrierString(command) {
400
+ const input = trimShellSpacing(command);
401
+ const match = input.match(/^(?:"([^"]+)"|(\S+))\s+-command\s+(.+)$/i);
402
+ if (!match)
403
+ return undefined;
404
+ 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)
409
+ return undefined;
410
+ // A string-form carrier has one additional command-line quoting layer. The
411
+ // command argument is commonly serialized as `"ec ..."`; remove exactly
412
+ // that outer pair and leave quotes belonging to EC arguments intact.
413
+ let nested = match[3].trim();
414
+ if (nested.length >= 2
415
+ && ((nested.startsWith('"') && nested.endsWith('"'))
416
+ || (nested.startsWith("'") && nested.endsWith("'")))) {
417
+ nested = nested.slice(1, -1);
418
+ }
419
+ return nested ? { command: nested, dialect: 'powershell' } : undefined;
420
+ }
421
+ export function resolveCodexShellCarrierString(command) {
422
+ return resolveCodexCmdCarrierString(command)
423
+ ?? resolveCodexPowerShellCarrierString(command);
424
+ }
425
+ /**
426
+ * Extract a trusted cmd executable's command body without trusting its startup
427
+ * switches. This is used only to classify malformed EC carriers as a
428
+ * controlled denial; a command with a non-EC body remains an ordinary shell
429
+ * command and follows the normal approval path.
430
+ */
431
+ function extractCodexCmdCarrierBody(command) {
432
+ const input = trimShellSpacing(command);
433
+ const match = input.match(/^(?:"([^"]+)"|(\S+))((?:\s+\/[^\s]+)*)\s+(.+)$/i);
434
+ if (!match)
435
+ return undefined;
436
+ const executable = match[1] ?? match[2];
437
+ if (!isCodexCmdExecutable(executable))
438
+ return undefined;
439
+ const body = match[4].trim();
440
+ return body || undefined;
441
+ }
442
+ function extractCodexCmdCarrierBodyArgv(argv) {
443
+ if (!argv.length || !isCodexCmdExecutable(argv[0]))
444
+ return undefined;
445
+ let bodyIndex = 1;
446
+ for (; bodyIndex < argv.length; bodyIndex++) {
447
+ const value = argv[bodyIndex];
448
+ if (value.toLowerCase() === '/c') {
449
+ bodyIndex++;
450
+ break;
451
+ }
452
+ // This is deliberately loose. It is only used to identify a malformed
453
+ // carrier that should be denied, never to authorize a command.
454
+ if (!value.startsWith('/'))
455
+ break;
456
+ }
457
+ const body = argv.slice(bodyIndex).join(' ').trim();
458
+ return body || undefined;
459
+ }
460
+ function looksLikeEcCommandBody(command) {
461
+ if (!command)
462
+ return false;
463
+ const input = command.trim();
464
+ const body = (input.startsWith('"') ? input.slice(1) : input).trimStart();
465
+ return /^ec(?:\s|$)/i.test(body);
466
+ }
467
+ /** True only when a trusted cmd executable is carrying an EC command body. */
468
+ export function hasCodexCmdCarrierEcIntent(input) {
469
+ if (Array.isArray(input.commandArgv)
470
+ && input.commandArgv.every(value => typeof value === 'string')) {
471
+ return looksLikeEcCommandBody(extractCodexCmdCarrierBodyArgv(input.commandArgv));
472
+ }
473
+ return typeof input.command === 'string'
474
+ && looksLikeEcCommandBody(extractCodexCmdCarrierBody(input.command));
475
+ }
476
+ function parseCodexCmdCarrierArgvFailure(argv) {
477
+ const body = extractCodexCmdCarrierBodyArgv(argv);
478
+ if (!body || !looksLikeEcCommandBody(body))
479
+ return undefined;
480
+ const nested = parseLiteralShellCommand(body, { dialect: 'cmd', allowManagedTmpDir: true });
481
+ if (!nested.ok)
482
+ return nested;
483
+ return parseFailure('shell-composition', body, 0, 0, 'cmd');
484
+ }
485
+ function parseCodexCmdCarrierStringFailure(command, options = {}) {
486
+ const body = extractCodexCmdCarrierBody(command);
487
+ if (!body || !looksLikeEcCommandBody(body))
488
+ return undefined;
489
+ const nested = parseLiteralShellCommand(body, {
490
+ ...options,
491
+ dialect: 'cmd',
492
+ });
493
+ return nested.ok
494
+ ? parseFailure('shell-composition', body, 0, 0, 'cmd')
495
+ : nested;
242
496
  }
243
497
  function doubleQuotedIssueAt(input, index) {
244
498
  const char = input[index];
@@ -262,13 +516,16 @@ export function parseLiteralShellCommand(command, options = {}) {
262
516
  if (options.dialect === 'powershell') {
263
517
  return parseLiteralPowerShellCommand(command);
264
518
  }
519
+ if (options.dialect === 'cmd') {
520
+ return parseLiteralCmdCommand(command, options);
521
+ }
265
522
  const input = trimShellSpacing(options.allowManagedTmpDir
266
523
  ? substituteManagedTmpDirToken(command)
267
524
  : command);
268
525
  if (!input)
269
- return { ok: false, issue: 'empty-command' };
526
+ return parseFailure('empty-command', input, 0, 0, 'posix');
270
527
  if (input.includes('\0'))
271
- return { ok: false, issue: 'invalid-control-char' };
528
+ return parseFailure('invalid-control-char', input, input.indexOf('\0'), 0, 'posix');
272
529
  const argv = [];
273
530
  let token = '';
274
531
  let tokenStarted = false;
@@ -285,7 +542,7 @@ export function parseLiteralShellCommand(command, options = {}) {
285
542
  if (quote === 'single') {
286
543
  if (char === "'") {
287
544
  if (!isLiteralArgBoundary(input[index + 1]))
288
- return { ok: false, issue: 'invalid-quote' };
545
+ return parseFailure('invalid-quote', input, index, argv.length, 'posix');
289
546
  quote = null;
290
547
  }
291
548
  else
@@ -295,13 +552,13 @@ export function parseLiteralShellCommand(command, options = {}) {
295
552
  if (quote === 'double') {
296
553
  if (char === '"') {
297
554
  if (!isLiteralArgBoundary(input[index + 1]))
298
- return { ok: false, issue: 'invalid-quote' };
555
+ return parseFailure('invalid-quote', input, index, argv.length, 'posix');
299
556
  quote = null;
300
557
  continue;
301
558
  }
302
559
  const issue = doubleQuotedIssueAt(input, index);
303
560
  if (issue)
304
- return { ok: false, issue };
561
+ return parseFailure(issue, input, index, argv.length, 'posix');
305
562
  if (char === '\\') {
306
563
  const next = input[++index];
307
564
  token += ['"', '\\', '$', '`'].includes(next) ? next : `\\${next}`;
@@ -321,9 +578,9 @@ export function parseLiteralShellCommand(command, options = {}) {
321
578
  // keeps inside the executable name; accepting those would misclassify a
322
579
  // different executable as `ec`.
323
580
  if (char === '\r' || char === '\n')
324
- return { ok: false, issue: 'structural-newline' };
581
+ return parseFailure('structural-newline', input, index, argv.length, 'posix');
325
582
  if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
326
- return { ok: false, issue: 'invalid-control-char' };
583
+ return parseFailure('invalid-control-char', input, index, argv.length, 'posix');
327
584
  }
328
585
  if (char === "'") {
329
586
  tokenStarted = true;
@@ -338,9 +595,9 @@ export function parseLiteralShellCommand(command, options = {}) {
338
595
  if (char === '\\') {
339
596
  const next = input[++index];
340
597
  if (next === undefined)
341
- return { ok: false, issue: 'invalid-quote' };
598
+ return parseFailure('invalid-quote', input, index, argv.length, 'posix');
342
599
  if (next === '\r' || next === '\n')
343
- return { ok: false, issue: 'structural-newline' };
600
+ return parseFailure('structural-newline', input, index, argv.length, 'posix');
344
601
  tokenStarted = true;
345
602
  token += next;
346
603
  continue;
@@ -349,12 +606,12 @@ export function parseLiteralShellCommand(command, options = {}) {
349
606
  // peer keys such as `aun#alice` are ordinary literal arguments.
350
607
  if ((SHELL_UNQUOTED_META.has(char) && (char !== '#' || !tokenStarted))
351
608
  || (char === '~' && !tokenStarted))
352
- return { ok: false, issue: 'shell-composition' };
609
+ return parseFailure('shell-composition', input, index, argv.length, 'posix');
353
610
  tokenStarted = true;
354
611
  token += char;
355
612
  }
356
613
  if (quote)
357
- return { ok: false, issue: 'invalid-quote' };
614
+ return parseFailure('invalid-quote', input, input.length, argv.length, 'posix');
358
615
  finishToken();
359
616
  return argv.length > 0
360
617
  ? {
@@ -363,24 +620,28 @@ export function parseLiteralShellCommand(command, options = {}) {
363
620
  ? argv.map(value => restoreManagedTmpDirToken(value, options.managedTempDir))
364
621
  : argv,
365
622
  }
366
- : { ok: false, issue: 'empty-command' };
623
+ : parseFailure('empty-command', input, input.length, argv.length, 'posix');
367
624
  }
368
625
  export function parseLiteralShellArgv(command, options = {}) {
369
626
  const result = parseLiteralShellCommand(command, options);
370
627
  return result.ok ? result.argv : null;
371
628
  }
372
629
  export function resolveCodexShellCarrierArgv(argv) {
373
- if (argv.length !== 3)
374
- return undefined;
375
- if (argv[1] === '-lc' && CODEX_POSIX_SHELL_EXECUTABLES.has(argv[0])) {
630
+ if (argv.length === 3 && argv[1] === '-lc' && CODEX_POSIX_SHELL_EXECUTABLES.has(argv[0])) {
376
631
  return { command: argv[2], dialect: 'posix' };
377
632
  }
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;
633
+ 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' };
639
+ }
640
+ }
641
+ // Node/cross-spawn and Codex use cmd.exe with a small set of startup flags.
642
+ // The execute switch must be the final switch before one command argument;
643
+ // validation itself is shared with string-only Windows payloads below.
644
+ return resolveCodexCmdCarrierArgv(argv);
384
645
  }
385
646
  export function unwrapCodexShellCommandArgv(argv) {
386
647
  return resolveCodexShellCarrierArgv(argv)?.command;
@@ -392,31 +653,81 @@ export function unwrapCodexShellCommandArgv(argv) {
392
653
  */
393
654
  export function parseCodexShellCommandArgv(command, options = {}) {
394
655
  const outer = parseLiteralShellArgv(command, options);
395
- if (!outer)
656
+ if (outer) {
657
+ const carrier = resolveCodexShellCarrierArgv(outer);
658
+ if (carrier !== undefined) {
659
+ return parseLiteralShellArgv(carrier.command, { ...options, dialect: carrier.dialect });
660
+ }
661
+ const stringCarrier = resolveCodexShellCarrierString(command);
662
+ if (stringCarrier) {
663
+ return parseLiteralShellArgv(stringCarrier.command, { ...options, dialect: stringCarrier.dialect });
664
+ }
665
+ if (parseCodexCmdCarrierStringFailure(command, options))
666
+ return null;
667
+ return outer;
668
+ }
669
+ const stringCarrier = resolveCodexShellCarrierString(command);
670
+ if (!stringCarrier)
396
671
  return null;
397
- const carrier = resolveCodexShellCarrierArgv(outer);
398
- return carrier === undefined
399
- ? outer
400
- : parseLiteralShellArgv(carrier.command, { ...options, dialect: carrier.dialect });
672
+ const parsed = parseLiteralShellCommand(stringCarrier.command, {
673
+ ...options,
674
+ dialect: stringCarrier.dialect,
675
+ });
676
+ return parsed.ok ? parsed.argv : null;
401
677
  }
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')) {
678
+ /** Parse a Codex tool input while preserving safe syntax failure metadata. */
679
+ export function parseCodexToolCommand(input) {
680
+ if (Array.isArray(input.commandArgv)) {
681
+ if (!input.commandArgv.every(value => typeof value === 'string')) {
682
+ return parseFailure('invalid-control-char', '', 0, 0, 'posix');
683
+ }
409
684
  const outer = input.commandArgv;
685
+ const nulIndex = outer.findIndex(value => value.includes('\0'));
686
+ if (nulIndex >= 0)
687
+ return parseFailure('invalid-control-char', '', 0, nulIndex, 'posix');
410
688
  const carrier = resolveCodexShellCarrierArgv(outer);
411
- return carrier === undefined
412
- ? outer
413
- : parseLiteralShellArgv(carrier.command, {
689
+ if (carrier === undefined) {
690
+ return parseCodexCmdCarrierArgvFailure(outer) ?? { ok: true, argv: outer };
691
+ }
692
+ return parseLiteralShellCommand(carrier.command, {
693
+ allowManagedTmpDir: true,
694
+ dialect: carrier.dialect,
695
+ });
696
+ }
697
+ const command = typeof input.command === 'string' ? input.command : '';
698
+ const outer = parseLiteralShellCommand(command, { allowManagedTmpDir: true });
699
+ if (outer.ok) {
700
+ const carrier = resolveCodexShellCarrierArgv(outer.argv);
701
+ if (carrier !== undefined) {
702
+ return parseLiteralShellCommand(carrier.command, {
414
703
  allowManagedTmpDir: true,
415
704
  dialect: carrier.dialect,
416
705
  });
706
+ }
707
+ const stringCarrier = resolveCodexShellCarrierString(command);
708
+ return stringCarrier
709
+ ? parseLiteralShellCommand(stringCarrier.command, {
710
+ allowManagedTmpDir: true,
711
+ dialect: stringCarrier.dialect,
712
+ })
713
+ : parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true }) ?? outer;
417
714
  }
418
- const command = typeof input.command === 'string' ? input.command : '';
419
- return parseCodexShellCommandArgv(command, { allowManagedTmpDir: true });
715
+ const stringCarrier = resolveCodexShellCarrierString(command);
716
+ return stringCarrier
717
+ ? parseLiteralShellCommand(stringCarrier.command, {
718
+ allowManagedTmpDir: true,
719
+ dialect: stringCarrier.dialect,
720
+ })
721
+ : parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true }) ?? outer;
722
+ }
723
+ /**
724
+ * Resolve a Codex command input while preserving the raw `commandArgv` form
725
+ * 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.
727
+ */
728
+ export function parseCodexToolCommandArgv(input) {
729
+ const result = parseCodexToolCommand(input);
730
+ return result.ok ? result.argv : null;
420
731
  }
421
732
  function isSimpleExecStringLiteral(value) {
422
733
  const quote = value[0];
@@ -1109,13 +1420,58 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
1109
1420
  });
1110
1421
  if (nested.ok && nested.argv[0] === 'ec')
1111
1422
  return { kind: 'literal' };
1112
- if (!nested.ok && hasEcCommandPrefix(carrier.command)) {
1423
+ if (!nested.ok && hasCarrierEcCommandPrefix(carrier.command, carrier.dialect)) {
1424
+ return { kind: 'composite', issue: nested.issue };
1425
+ }
1426
+ }
1427
+ const stringCarrier = resolveCodexShellCarrierString(command);
1428
+ if (stringCarrier) {
1429
+ const nested = parseLiteralShellCommand(stringCarrier.command, {
1430
+ ...parseOptions,
1431
+ dialect: stringCarrier.dialect,
1432
+ });
1433
+ if (nested.ok && nested.argv[0] === 'ec')
1434
+ return { kind: 'literal' };
1435
+ if (!nested.ok && hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)) {
1113
1436
  return { kind: 'composite', issue: nested.issue };
1114
1437
  }
1115
1438
  }
1439
+ const malformedCmdBody = extractCodexCmdCarrierBody(command);
1440
+ if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
1441
+ const nested = parseLiteralShellCommand(malformedCmdBody, {
1442
+ ...parseOptions,
1443
+ dialect: 'cmd',
1444
+ });
1445
+ return nested.ok
1446
+ ? { kind: 'composite', issue: 'shell-composition' }
1447
+ : { kind: 'composite', issue: nested.issue };
1448
+ }
1449
+ return { kind: 'none' };
1450
+ }
1451
+ const stringCarrier = resolveCodexShellCarrierString(command);
1452
+ if (stringCarrier) {
1453
+ const nested = parseLiteralShellCommand(stringCarrier.command, {
1454
+ ...parseOptions,
1455
+ dialect: stringCarrier.dialect,
1456
+ });
1457
+ if (nested.ok && nested.argv[0] === 'ec')
1458
+ return { kind: 'literal' };
1459
+ if (!nested.ok && hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)) {
1460
+ return { kind: 'composite', issue: nested.issue };
1461
+ }
1116
1462
  return { kind: 'none' };
1117
1463
  }
1118
- const boundedOutput = options.dialect === 'powershell'
1464
+ const malformedCmdBody = extractCodexCmdCarrierBody(command);
1465
+ if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
1466
+ const nested = parseLiteralShellCommand(malformedCmdBody, {
1467
+ ...parseOptions,
1468
+ dialect: 'cmd',
1469
+ });
1470
+ return nested.ok
1471
+ ? { kind: 'composite', issue: 'shell-composition' }
1472
+ : { kind: 'composite', issue: nested.issue };
1473
+ }
1474
+ const boundedOutput = options.dialect === 'powershell' || options.dialect === 'cmd'
1119
1475
  ? null
1120
1476
  : parseBoundedOutputShellCommand(command);
1121
1477
  if (boundedOutput && boundedOutput.argv[0] === 'ec') {