evolcore 0.0.19 → 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 (136) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +60 -9
  3. package/bin/install-codex-managed-hooks.mjs +61 -0
  4. package/dist/agents/baseagent.js +10 -6
  5. package/dist/agents/claude-runner.js +383 -111
  6. package/dist/agents/codex-app-server-client.js +10 -2
  7. package/dist/agents/codex-runner.js +405 -137
  8. package/dist/agents/ecagent-runner.js +174 -63
  9. package/dist/agents/gemini-runner.js +130 -30
  10. package/dist/agents/request-identity.js +25 -0
  11. package/dist/agents/runner-types.js +19 -0
  12. package/dist/aun/aid/agentmd.js +66 -2
  13. package/dist/aun/aid/identity.js +4 -1
  14. package/dist/aun/aid/index.js +1 -1
  15. package/dist/aun/msg/group.js +86 -9
  16. package/dist/aun/msg/history.js +213 -36
  17. package/dist/aun/msg/managed-operation.js +58 -9
  18. package/dist/aun/msg/p2p.js +26 -11
  19. package/dist/aun/outbox.js +295 -68
  20. package/dist/aun/service-proxy.js +43 -25
  21. package/dist/channels/aun.js +1004 -273
  22. package/dist/channels/daemon.js +6 -1
  23. package/dist/cli/agent-command.js +4 -3
  24. package/dist/cli/agent.js +66 -56
  25. package/dist/cli/aun-commands.js +177 -42
  26. package/dist/cli/command-log.js +10 -11
  27. package/dist/cli/contact.js +1 -0
  28. package/dist/cli/daemon-commands.js +81 -121
  29. package/dist/cli/index.js +1 -0
  30. package/dist/cli/init.js +76 -24
  31. package/dist/cli/restart-monitor.js +3 -3
  32. package/dist/cli/task-context.js +46 -0
  33. package/dist/cli/trigger-command.js +1 -1
  34. package/dist/cli/watch-logs.js +10 -3
  35. package/dist/config/builtin-roles.js +1 -0
  36. package/dist/config/config-field-policy.js +16 -5
  37. package/dist/config/config-manager.js +226 -24
  38. package/dist/config/config-operation-service.js +1 -2
  39. package/dist/config/contact-operation-service.js +32 -1
  40. package/dist/config/contact-request-service.js +44 -0
  41. package/dist/config/daemon-services.js +186 -0
  42. package/dist/config/gateway-config.js +29 -16
  43. package/dist/config/lifecycle.js +16 -5
  44. package/dist/config/role-service.js +54 -3
  45. package/dist/config/schema-migration.js +550 -0
  46. package/dist/config-store.js +161 -12
  47. package/dist/core/agent-application-service.js +279 -0
  48. package/dist/core/audit/log-integrity.js +102 -0
  49. package/dist/core/auth/agent-delegation.js +31 -1
  50. package/dist/core/auth/auth-gateway.js +33 -4
  51. package/dist/core/auth/authorization-audit.js +155 -4
  52. package/dist/core/auth/operation-authorizer.js +41 -1
  53. package/dist/core/auth/operation-catalog.js +9 -1
  54. package/dist/core/bootstrap-messages.js +2 -2
  55. package/dist/core/bootstrap-service.js +27 -38
  56. package/dist/core/causation/aun-association.js +7 -4
  57. package/dist/core/channel-loader.js +0 -2
  58. package/dist/core/command/agent-control.js +56 -16
  59. package/dist/core/command/command-handler.js +290 -44
  60. package/dist/core/command/connect-menu.js +3 -4
  61. package/dist/core/command/group-menu.js +5 -7
  62. package/dist/core/command/menu-handler.js +279 -80
  63. package/dist/core/command/role-menu.js +21 -11
  64. package/dist/core/command/slash-gate.js +85 -18
  65. package/dist/core/command/slash-handler.js +350 -32
  66. package/dist/core/event-catalog.js +5 -0
  67. package/dist/core/evolagent.js +9 -4
  68. package/dist/core/handoff/dispatcher.js +4 -0
  69. package/dist/core/handoff/runtime.js +10 -0
  70. package/dist/core/handoff/store.js +32 -9
  71. package/dist/core/inference/text-inference.js +7 -15
  72. package/dist/core/message/im-renderer.js +83 -84
  73. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  74. package/dist/core/message/message-bridge.js +124 -10
  75. package/dist/core/message/message-log.js +14 -7
  76. package/dist/core/message/message-queue.js +206 -16
  77. package/dist/core/message/message-utils.js +12 -5
  78. package/dist/core/message/response-engine.js +495 -72
  79. package/dist/core/message/send-receipt.js +1 -0
  80. package/dist/core/message/stream-debouncer.js +9 -2
  81. package/dist/core/model/model-catalog.js +23 -15
  82. package/dist/core/model/model-diagnostics.js +28 -10
  83. package/dist/core/permission/approval-gateway.js +180 -6
  84. package/dist/core/permission/ec-command-parser.js +465 -56
  85. package/dist/core/permission/mode.js +18 -3
  86. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  87. package/dist/core/permission/readonly-shell-query.js +263 -9
  88. package/dist/core/permission/sandbox-runtime.js +205 -13
  89. package/dist/core/permission/tool-policy.js +673 -62
  90. package/dist/core/relation/peer-identity.js +18 -0
  91. package/dist/core/session/session-fs-store.js +154 -5
  92. package/dist/core/session/session-manager.js +299 -30
  93. package/dist/core/session/session-renew.js +19 -12
  94. package/dist/core/session/session-turn-coordinator.js +11 -4
  95. package/dist/eck/kit-renderer.js +18 -9
  96. package/dist/index.js +283 -65
  97. package/dist/ipc.js +374 -24
  98. package/dist/paths.js +64 -7
  99. package/dist/response-system/context-builder.js +1 -7
  100. package/dist/trigger/anomaly-store.js +1 -0
  101. package/dist/trigger/feedback.js +56 -5
  102. package/dist/trigger/history.js +79 -4
  103. package/dist/trigger/legacy-session-history.js +2 -2
  104. package/dist/trigger/parser.js +3 -2
  105. package/dist/trigger/validation.js +6 -1
  106. package/dist/utils/atomic-write.js +27 -0
  107. package/dist/utils/ecweb-utils.js +16 -2
  108. package/dist/utils/error-utils.js +4 -1
  109. package/dist/utils/logger.js +21 -2
  110. package/dist/utils/process-tree-stats.js +24 -4
  111. package/dist/utils/process-tree-worker.js +31 -0
  112. package/dist/utils/project-path.js +1 -2
  113. package/dist/utils/stats.js +52 -18
  114. package/dist/utils/welcome.js +2 -2
  115. package/kits/docs/INDEX.md +1 -1
  116. package/kits/docs/evolcore/INDEX.md +1 -1
  117. package/kits/docs/evolcore/contact.md +7 -1
  118. package/kits/docs/evolcore/msg.md +16 -0
  119. package/kits/rules/01-overview.md +1 -1
  120. package/kits/rules/03-identity.md +1 -1
  121. package/kits/rules/05-venue.md +1 -1
  122. package/kits/schemas/_meta.json +10 -5
  123. package/kits/schemas/agent-config.schema.10.json +2 -1
  124. package/kits/schemas/agent-config.schema.11.json +421 -0
  125. package/kits/schemas/daemon.schema.5.json +135 -0
  126. package/kits/schemas/daemon.schema.6.json +131 -0
  127. package/kits/schemas/defaults.schema.5.json +119 -0
  128. package/kits/schemas/migrations/README.md +3 -1
  129. package/kits/schemas/relation-config.schema.8.json +13 -0
  130. package/kits/schemas/role-config.schema.1.json +1 -2
  131. package/kits/schemas/single-session.schema.3.json +32 -0
  132. package/kits/templates/roles/admin.json +1 -0
  133. package/kits/templates/roles/member.json +1 -0
  134. package/kits/templates/roles/visitor.json +1 -0
  135. package/package.json +7 -3
  136. package/skills/eclink/SKILL.md +2 -0
@@ -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
  ]);
@@ -17,7 +24,57 @@ const POWERSHELL_UNQUOTED_META = new Set([
17
24
  ';', '&', '|', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#', '@',
18
25
  ]);
19
26
  const AMBIGUOUS_FILE_PATH_RE = /[\0\r\n$`*?\[\]{}<>|;&]/;
20
- const EVOLCORE_COMMAND_POSITION_RE = /(?:^|[;&|()`])\s*ec(?=\s|[;&|()<>`]|$)/i;
27
+ // This is intentionally limited to the command boundary. It is not a search
28
+ // for `ec` anywhere in the input: quoted arguments, regexps and log text must
29
+ // never establish an EC invocation.
30
+ function hasEcCommandPrefix(command) {
31
+ let quote = null;
32
+ let escaped = false;
33
+ let boundary = true;
34
+ for (let index = 0; index < command.length; index++) {
35
+ const char = command[index];
36
+ if (escaped) {
37
+ escaped = false;
38
+ boundary = false;
39
+ continue;
40
+ }
41
+ if (quote === 'single') {
42
+ if (char === "'")
43
+ quote = null;
44
+ continue;
45
+ }
46
+ if (quote === 'double') {
47
+ if (char === '\\') {
48
+ escaped = true;
49
+ continue;
50
+ }
51
+ if (char === '"')
52
+ quote = null;
53
+ continue;
54
+ }
55
+ if (char === "'") {
56
+ quote = 'single';
57
+ boundary = false;
58
+ continue;
59
+ }
60
+ if (char === '"') {
61
+ quote = 'double';
62
+ boundary = false;
63
+ continue;
64
+ }
65
+ if (/[;&|`()<>]/.test(char)) {
66
+ boundary = true;
67
+ continue;
68
+ }
69
+ if (boundary && /\s/.test(char))
70
+ continue;
71
+ if (boundary && command.slice(index, index + 2).toLowerCase() === 'ec'
72
+ && (index + 2 === command.length || /\s|[;&|`()<>]/.test(command[index + 2])))
73
+ return true;
74
+ boundary = false;
75
+ }
76
+ return false;
77
+ }
21
78
  function trimShellSpacing(value) {
22
79
  return value.replace(/^[ \t]+|[ \t]+$/g, '');
23
80
  }
@@ -90,12 +147,31 @@ function isLiteralArgBoundary(value) {
90
147
  function isShellParameterExpansionStart(value) {
91
148
  return value === '(' || value === '{' || !!value && /[A-Za-z0-9_@*#?$!\-]/.test(value);
92
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
+ }
93
169
  function parseLiteralPowerShellCommand(command) {
94
170
  const input = trimShellSpacing(command);
95
171
  if (!input)
96
- return { ok: false, issue: 'empty-command' };
172
+ return parseFailure('empty-command', input, 0, 0, 'powershell');
97
173
  if (input.includes('\0'))
98
- return { ok: false, issue: 'invalid-control-char' };
174
+ return parseFailure('invalid-control-char', input, input.indexOf('\0'), 0, 'powershell');
99
175
  const argv = [];
100
176
  let token = '';
101
177
  let tokenStarted = false;
@@ -116,7 +192,7 @@ function parseLiteralPowerShellCommand(command) {
116
192
  }
117
193
  else if (char === "'") {
118
194
  if (!isLiteralArgBoundary(input[index + 1]))
119
- return { ok: false, issue: 'invalid-quote' };
195
+ return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
120
196
  quote = null;
121
197
  }
122
198
  else {
@@ -127,21 +203,21 @@ function parseLiteralPowerShellCommand(command) {
127
203
  if (quote === 'double') {
128
204
  if (char === '"') {
129
205
  if (!isLiteralArgBoundary(input[index + 1]))
130
- return { ok: false, issue: 'invalid-quote' };
206
+ return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
131
207
  quote = null;
132
208
  continue;
133
209
  }
134
210
  if (char === '`') {
135
211
  const next = input[++index];
136
212
  if (next === undefined)
137
- return { ok: false, issue: 'invalid-quote' };
213
+ return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
138
214
  if (next === '\r' || next === '\n')
139
- return { ok: false, issue: 'structural-newline' };
215
+ return parseFailure('structural-newline', input, index, argv.length, 'powershell');
140
216
  token += next;
141
217
  continue;
142
218
  }
143
219
  if (char === '$' && isShellParameterExpansionStart(input[index + 1])) {
144
- return { ok: false, issue: 'unsafe-expansion' };
220
+ return parseFailure('unsafe-expansion', input, index, argv.length, 'powershell');
145
221
  }
146
222
  token += char;
147
223
  continue;
@@ -151,9 +227,9 @@ function parseLiteralPowerShellCommand(command) {
151
227
  continue;
152
228
  }
153
229
  if (char === '\r' || char === '\n')
154
- return { ok: false, issue: 'structural-newline' };
230
+ return parseFailure('structural-newline', input, index, argv.length, 'powershell');
155
231
  if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
156
- return { ok: false, issue: 'invalid-control-char' };
232
+ return parseFailure('invalid-control-char', input, index, argv.length, 'powershell');
157
233
  }
158
234
  if (char === "'") {
159
235
  tokenStarted = true;
@@ -168,9 +244,9 @@ function parseLiteralPowerShellCommand(command) {
168
244
  if (char === '`') {
169
245
  const next = input[++index];
170
246
  if (next === undefined)
171
- return { ok: false, issue: 'invalid-quote' };
247
+ return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
172
248
  if (next === '\r' || next === '\n')
173
- return { ok: false, issue: 'structural-newline' };
249
+ return parseFailure('structural-newline', input, index, argv.length, 'powershell');
174
250
  tokenStarted = true;
175
251
  token += next;
176
252
  continue;
@@ -178,17 +254,245 @@ function parseLiteralPowerShellCommand(command) {
178
254
  const isWordInitialMeta = (char === '#' || char === '@') && !tokenStarted;
179
255
  const isAlwaysMeta = POWERSHELL_UNQUOTED_META.has(char) && char !== '#' && char !== '@';
180
256
  if (isAlwaysMeta || isWordInitialMeta || (char === '~' && !tokenStarted)) {
181
- return { ok: false, issue: 'shell-composition' };
257
+ return parseFailure('shell-composition', input, index, argv.length, 'powershell');
182
258
  }
183
259
  tokenStarted = true;
184
260
  token += char;
185
261
  }
186
262
  if (quote)
187
- return { ok: false, issue: 'invalid-quote' };
263
+ return parseFailure('invalid-quote', input, input.length, argv.length, 'powershell');
188
264
  finishToken();
189
265
  return argv.length > 0
190
266
  ? { ok: true, argv }
191
- : { 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;
192
496
  }
193
497
  function doubleQuotedIssueAt(input, index) {
194
498
  const char = input[index];
@@ -212,13 +516,16 @@ export function parseLiteralShellCommand(command, options = {}) {
212
516
  if (options.dialect === 'powershell') {
213
517
  return parseLiteralPowerShellCommand(command);
214
518
  }
519
+ if (options.dialect === 'cmd') {
520
+ return parseLiteralCmdCommand(command, options);
521
+ }
215
522
  const input = trimShellSpacing(options.allowManagedTmpDir
216
523
  ? substituteManagedTmpDirToken(command)
217
524
  : command);
218
525
  if (!input)
219
- return { ok: false, issue: 'empty-command' };
526
+ return parseFailure('empty-command', input, 0, 0, 'posix');
220
527
  if (input.includes('\0'))
221
- return { ok: false, issue: 'invalid-control-char' };
528
+ return parseFailure('invalid-control-char', input, input.indexOf('\0'), 0, 'posix');
222
529
  const argv = [];
223
530
  let token = '';
224
531
  let tokenStarted = false;
@@ -235,7 +542,7 @@ export function parseLiteralShellCommand(command, options = {}) {
235
542
  if (quote === 'single') {
236
543
  if (char === "'") {
237
544
  if (!isLiteralArgBoundary(input[index + 1]))
238
- return { ok: false, issue: 'invalid-quote' };
545
+ return parseFailure('invalid-quote', input, index, argv.length, 'posix');
239
546
  quote = null;
240
547
  }
241
548
  else
@@ -245,13 +552,13 @@ export function parseLiteralShellCommand(command, options = {}) {
245
552
  if (quote === 'double') {
246
553
  if (char === '"') {
247
554
  if (!isLiteralArgBoundary(input[index + 1]))
248
- return { ok: false, issue: 'invalid-quote' };
555
+ return parseFailure('invalid-quote', input, index, argv.length, 'posix');
249
556
  quote = null;
250
557
  continue;
251
558
  }
252
559
  const issue = doubleQuotedIssueAt(input, index);
253
560
  if (issue)
254
- return { ok: false, issue };
561
+ return parseFailure(issue, input, index, argv.length, 'posix');
255
562
  if (char === '\\') {
256
563
  const next = input[++index];
257
564
  token += ['"', '\\', '$', '`'].includes(next) ? next : `\\${next}`;
@@ -271,9 +578,9 @@ export function parseLiteralShellCommand(command, options = {}) {
271
578
  // keeps inside the executable name; accepting those would misclassify a
272
579
  // different executable as `ec`.
273
580
  if (char === '\r' || char === '\n')
274
- return { ok: false, issue: 'structural-newline' };
581
+ return parseFailure('structural-newline', input, index, argv.length, 'posix');
275
582
  if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
276
- return { ok: false, issue: 'invalid-control-char' };
583
+ return parseFailure('invalid-control-char', input, index, argv.length, 'posix');
277
584
  }
278
585
  if (char === "'") {
279
586
  tokenStarted = true;
@@ -288,9 +595,9 @@ export function parseLiteralShellCommand(command, options = {}) {
288
595
  if (char === '\\') {
289
596
  const next = input[++index];
290
597
  if (next === undefined)
291
- return { ok: false, issue: 'invalid-quote' };
598
+ return parseFailure('invalid-quote', input, index, argv.length, 'posix');
292
599
  if (next === '\r' || next === '\n')
293
- return { ok: false, issue: 'structural-newline' };
600
+ return parseFailure('structural-newline', input, index, argv.length, 'posix');
294
601
  tokenStarted = true;
295
602
  token += next;
296
603
  continue;
@@ -299,12 +606,12 @@ export function parseLiteralShellCommand(command, options = {}) {
299
606
  // peer keys such as `aun#alice` are ordinary literal arguments.
300
607
  if ((SHELL_UNQUOTED_META.has(char) && (char !== '#' || !tokenStarted))
301
608
  || (char === '~' && !tokenStarted))
302
- return { ok: false, issue: 'shell-composition' };
609
+ return parseFailure('shell-composition', input, index, argv.length, 'posix');
303
610
  tokenStarted = true;
304
611
  token += char;
305
612
  }
306
613
  if (quote)
307
- return { ok: false, issue: 'invalid-quote' };
614
+ return parseFailure('invalid-quote', input, input.length, argv.length, 'posix');
308
615
  finishToken();
309
616
  return argv.length > 0
310
617
  ? {
@@ -313,24 +620,28 @@ export function parseLiteralShellCommand(command, options = {}) {
313
620
  ? argv.map(value => restoreManagedTmpDirToken(value, options.managedTempDir))
314
621
  : argv,
315
622
  }
316
- : { ok: false, issue: 'empty-command' };
623
+ : parseFailure('empty-command', input, input.length, argv.length, 'posix');
317
624
  }
318
625
  export function parseLiteralShellArgv(command, options = {}) {
319
626
  const result = parseLiteralShellCommand(command, options);
320
627
  return result.ok ? result.argv : null;
321
628
  }
322
629
  export function resolveCodexShellCarrierArgv(argv) {
323
- if (argv.length !== 3)
324
- return undefined;
325
- 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])) {
326
631
  return { command: argv[2], dialect: 'posix' };
327
632
  }
328
- const powershellExecutable = argv[0].toLowerCase();
329
- const isPowerShell = CODEX_POWERSHELL_EXECUTABLES.has(powershellExecutable)
330
- || CODEX_POWERSHELL_INSTALL_PATH_RE.test(argv[0]);
331
- return isPowerShell && argv[1].toLowerCase() === '-command'
332
- ? { command: argv[2], dialect: 'powershell' }
333
- : 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);
334
645
  }
335
646
  export function unwrapCodexShellCommandArgv(argv) {
336
647
  return resolveCodexShellCarrierArgv(argv)?.command;
@@ -342,31 +653,81 @@ export function unwrapCodexShellCommandArgv(argv) {
342
653
  */
343
654
  export function parseCodexShellCommandArgv(command, options = {}) {
344
655
  const outer = parseLiteralShellArgv(command, options);
345
- 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)
346
671
  return null;
347
- const carrier = resolveCodexShellCarrierArgv(outer);
348
- return carrier === undefined
349
- ? outer
350
- : 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;
351
677
  }
352
- /**
353
- * Resolve a Codex command input while preserving the raw `commandArgv` form
354
- * supplied by app-server. Joining that argv with spaces loses the quoting of
355
- * the inner `-lc` command and was the source of a false proactive denial.
356
- */
357
- export function parseCodexToolCommandArgv(input) {
358
- 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
+ }
359
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');
360
688
  const carrier = resolveCodexShellCarrierArgv(outer);
361
- return carrier === undefined
362
- ? outer
363
- : 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, {
364
703
  allowManagedTmpDir: true,
365
704
  dialect: carrier.dialect,
366
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;
367
714
  }
368
- const command = typeof input.command === 'string' ? input.command : '';
369
- 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;
370
731
  }
371
732
  function isSimpleExecStringLiteral(value) {
372
733
  const quote = value[0];
@@ -1059,19 +1420,67 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
1059
1420
  });
1060
1421
  if (nested.ok && nested.argv[0] === 'ec')
1061
1422
  return { kind: 'literal' };
1062
- if (!nested.ok && EVOLCORE_COMMAND_POSITION_RE.test(carrier.command)) {
1423
+ if (!nested.ok && hasCarrierEcCommandPrefix(carrier.command, carrier.dialect)) {
1063
1424
  return { kind: 'composite', issue: nested.issue };
1064
1425
  }
1065
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)) {
1436
+ return { kind: 'composite', issue: nested.issue };
1437
+ }
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
+ }
1066
1449
  return { kind: 'none' };
1067
1450
  }
1068
- const boundedOutput = options.dialect === 'powershell'
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
+ }
1462
+ return { kind: 'none' };
1463
+ }
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'
1069
1475
  ? null
1070
1476
  : parseBoundedOutputShellCommand(command);
1071
1477
  if (boundedOutput && boundedOutput.argv[0] === 'ec') {
1072
1478
  return { kind: 'bounded-output', command: boundedOutput };
1073
1479
  }
1074
- return EVOLCORE_COMMAND_POSITION_RE.test(command)
1480
+ // Only a command-boundary prefix is enough to preserve a useful syntax
1481
+ // error for a malformed direct EC invocation. Never scan for EC tokens in
1482
+ // arbitrary raw text after parsing fails.
1483
+ return hasEcCommandPrefix(command)
1075
1484
  ? { kind: 'composite', issue: literal.issue }
1076
1485
  : { kind: 'none' };
1077
1486
  }