evolcore 0.0.12 → 0.0.14

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 (116) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/codex-managed-hook.mjs +4 -1
  3. package/bin/ec-safe-output.js +26 -7
  4. package/bin/install-codex-managed-hooks.mjs +201 -0
  5. package/dist/agents/claude-runner.js +105 -11
  6. package/dist/agents/codex-app-server-client.js +176 -7
  7. package/dist/agents/codex-runner.js +248 -84
  8. package/dist/agents/ecagent-runner.js +21 -2
  9. package/dist/agents/gemini-runner.js +9 -4
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/store.js +13 -7
  12. package/dist/aun/group-fs-download.js +56 -0
  13. package/dist/aun/group-identity.js +1 -2
  14. package/dist/aun/msg/group.js +11 -33
  15. package/dist/aun/msg/index.js +1 -2
  16. package/dist/aun/msg/managed-operation.js +176 -55
  17. package/dist/aun/msg/upload.js +16 -1
  18. package/dist/aun/outbox.js +31 -2
  19. package/dist/aun/rpc/client.js +66 -0
  20. package/dist/aun/rpc/index.js +1 -2
  21. package/dist/aun/storage/{upload.js → client.js} +11 -2
  22. package/dist/aun/storage/index.js +1 -2
  23. package/dist/channels/aun.js +514 -150
  24. package/dist/channels/daemon.js +2 -0
  25. package/dist/cli/agent.js +9 -96
  26. package/dist/cli/aun-commands.js +1 -1
  27. package/dist/cli/cli-argv.js +10 -0
  28. package/dist/cli/config.js +9 -17
  29. package/dist/cli/ctl-command.js +1 -1
  30. package/dist/cli/daemon-commands.js +39 -24
  31. package/dist/cli/fs-command.js +65 -19
  32. package/dist/cli/init.js +77 -8
  33. package/dist/cli/queue-command.js +42 -0
  34. package/dist/cli/restart-monitor.js +3 -14
  35. package/dist/cli/task-context.js +172 -0
  36. package/dist/cli/trigger-command.js +7 -0
  37. package/dist/config/access-policy.js +1 -2
  38. package/dist/config/builtin-roles.js +7 -0
  39. package/dist/config/config-manager.js +27 -3
  40. package/dist/config/contact-book-store.js +7 -2
  41. package/dist/config/peer-role-resolver.js +6 -2
  42. package/dist/config/role-migration-startup.js +53 -0
  43. package/dist/config/role-store.js +1 -1
  44. package/dist/core/auth/agent-delegation.js +1 -1
  45. package/dist/core/auth/auth-gateway.js +30 -1
  46. package/dist/core/auth/authorization-audit.js +47 -0
  47. package/dist/core/auth/operation-authorizer.js +1 -0
  48. package/dist/core/auth/operation-catalog.js +10 -1
  49. package/dist/core/bootstrap-messages.js +9 -2
  50. package/dist/core/bootstrap-service.js +8 -1
  51. package/dist/core/command/command-handler.js +201 -51
  52. package/dist/core/command/menu-catalog.js +4 -0
  53. package/dist/core/command/menu-handler.js +85 -18
  54. package/dist/core/command/menu-protocol.js +23 -2
  55. package/dist/core/command/slash-handler.js +58 -10
  56. package/dist/core/event-catalog.js +8 -0
  57. package/dist/core/handoff/runtime.js +12 -1
  58. package/dist/core/handoff/store.js +5 -1
  59. package/dist/core/interaction-router.js +10 -0
  60. package/dist/core/message/im-renderer.js +34 -1
  61. package/dist/core/message/message-bridge.js +68 -13
  62. package/dist/core/message/message-log.js +2 -0
  63. package/dist/core/message/message-queue.js +267 -39
  64. package/dist/core/message/message-utils.js +81 -4
  65. package/dist/core/message/response-engine.js +242 -48
  66. package/dist/core/message/stream-debouncer.js +10 -2
  67. package/dist/core/permission/approval-gateway.js +1 -0
  68. package/dist/core/permission/ec-command-parser.js +649 -18
  69. package/dist/core/permission/index.js +1 -1
  70. package/dist/core/permission/mode.js +15 -0
  71. package/dist/core/permission/readonly-shell-query.js +148 -8
  72. package/dist/core/permission/sandbox-runtime.js +21 -0
  73. package/dist/core/permission/tool-policy.js +266 -56
  74. package/dist/core/protected-paths.js +107 -2
  75. package/dist/core/runtime-lock.js +101 -0
  76. package/dist/core/session/session-manager.js +21 -2
  77. package/dist/core/session/session-mapper.js +20 -1
  78. package/dist/eck/detect.js +18 -1
  79. package/dist/eck/group-rules-sync.js +15 -44
  80. package/dist/index.js +191 -89
  81. package/dist/ipc.js +16 -3
  82. package/dist/paths.js +3 -0
  83. package/dist/response-system/engines/v1/proactive-flow.js +92 -8
  84. package/dist/response-system/modes/single-session/index.js +3 -0
  85. package/dist/trigger/feedback.js +64 -11
  86. package/dist/trigger/history.js +42 -7
  87. package/dist/trigger/manager.js +21 -1
  88. package/dist/trigger/parser.js +14 -2
  89. package/dist/trigger/scheduler.js +1 -0
  90. package/dist/trigger/validation.js +67 -6
  91. package/dist/utils/ecweb-supervisor.js +332 -0
  92. package/dist/utils/error-utils.js +17 -0
  93. package/dist/utils/logger.js +37 -4
  94. package/kits/docs/evolcore/trigger.md +17 -1
  95. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  96. package/kits/schemas/role-config.schema.1.json +1 -0
  97. package/kits/templates/roles/admin.json +7 -0
  98. package/kits/templates/roles/member.json +8 -1
  99. package/kits/templates/roles/visitor.json +2 -0
  100. package/package.json +1 -1
  101. package/dist/aun/msg/payload-type.js +0 -27
  102. package/dist/aun/rpc/caller.js +0 -42
  103. package/dist/aun/rpc/connection.js +0 -25
  104. package/dist/aun/storage/manage.js +0 -10
  105. package/dist/config/access-policy-domain.js +0 -18
  106. package/dist/config/config-batch-get.js +0 -11
  107. package/dist/config/owner-policy.js +0 -4
  108. package/dist/config/role-config-v4-startup.js +0 -32
  109. package/dist/config/role-config-v5-startup.js +0 -27
  110. package/dist/core/auth/trigger-authorization.js +0 -15
  111. package/dist/core/interaction-registration.js +0 -10
  112. package/dist/core/permission/execution-sandbox.js +0 -16
  113. package/dist/core/relation/peer-key.js +0 -1
  114. package/dist/core/session/session-key.js +0 -24
  115. package/dist/eck/baseagent-caps.js +0 -18
  116. package/dist/eck/rules-loader.js +0 -28
@@ -14,6 +14,66 @@ const EVOLCORE_COMMAND_POSITION_RE = /(?:^|[;&|()`])\s*ec(?=\s|[;&|()<>`]|$)/i;
14
14
  function trimShellSpacing(value) {
15
15
  return value.replace(/^[ \t]+|[ \t]+$/g, '');
16
16
  }
17
+ const MANAGED_TMPDIR_TOKEN = '__EC_MANAGED_TMPDIR__';
18
+ /**
19
+ * Preserve the one environment reference allowed in managed EC paths.
20
+ * The regular literal parser still rejects all expansion by default. The
21
+ * privileged EC classifier opts into this token only after the surrounding
22
+ * policy has validated the path against the session-owned TMPDIR.
23
+ */
24
+ function substituteManagedTmpDirToken(value) {
25
+ let result = '';
26
+ let quote = null;
27
+ for (let index = 0; index < value.length; index++) {
28
+ const char = value[index];
29
+ if (quote === 'single') {
30
+ result += char;
31
+ if (char === "'")
32
+ quote = null;
33
+ continue;
34
+ }
35
+ if (quote === 'double') {
36
+ if (char === '\\') {
37
+ result += char;
38
+ if (index + 1 < value.length)
39
+ result += value[++index];
40
+ continue;
41
+ }
42
+ if (char === '"') {
43
+ quote = null;
44
+ result += char;
45
+ continue;
46
+ }
47
+ }
48
+ else if (char === "'") {
49
+ quote = 'single';
50
+ result += char;
51
+ continue;
52
+ }
53
+ else if (char === '\\') {
54
+ result += char;
55
+ if (index + 1 < value.length)
56
+ result += value[++index];
57
+ continue;
58
+ }
59
+ if (value.startsWith('${TMPDIR}', index)) {
60
+ result += MANAGED_TMPDIR_TOKEN;
61
+ index += '${TMPDIR}'.length - 1;
62
+ }
63
+ else if (value.startsWith('$TMPDIR', index)
64
+ && !/[A-Za-z0-9_]/.test(value[index + '$TMPDIR'.length] ?? '')) {
65
+ result += MANAGED_TMPDIR_TOKEN;
66
+ index += '$TMPDIR'.length - 1;
67
+ }
68
+ else {
69
+ result += char;
70
+ }
71
+ }
72
+ return result;
73
+ }
74
+ function restoreManagedTmpDirToken(value, managedTempDir) {
75
+ return value.replaceAll(MANAGED_TMPDIR_TOKEN, managedTempDir ?? '$TMPDIR');
76
+ }
17
77
  function isShellSpacing(value) {
18
78
  return value === ' ' || value === '\t';
19
79
  }
@@ -41,8 +101,10 @@ function doubleQuotedIssueAt(input, index) {
41
101
  * expansion, glob, pipeline, command substitution, redirection, or comment.
42
102
  * Anything outside this subset is never granted the privileged EC path.
43
103
  */
44
- export function parseLiteralShellCommand(command) {
45
- const input = trimShellSpacing(command);
104
+ export function parseLiteralShellCommand(command, options = {}) {
105
+ const input = trimShellSpacing(options.allowManagedTmpDir
106
+ ? substituteManagedTmpDirToken(command)
107
+ : command);
46
108
  if (!input)
47
109
  return { ok: false, issue: 'empty-command' };
48
110
  if (input.includes('\0'))
@@ -135,11 +197,16 @@ export function parseLiteralShellCommand(command) {
135
197
  return { ok: false, issue: 'invalid-quote' };
136
198
  finishToken();
137
199
  return argv.length > 0
138
- ? { ok: true, argv }
200
+ ? {
201
+ ok: true,
202
+ argv: options.allowManagedTmpDir
203
+ ? argv.map(value => restoreManagedTmpDirToken(value, options.managedTempDir))
204
+ : argv,
205
+ }
139
206
  : { ok: false, issue: 'empty-command' };
140
207
  }
141
- export function parseLiteralShellArgv(command) {
142
- const result = parseLiteralShellCommand(command);
208
+ export function parseLiteralShellArgv(command, options = {}) {
209
+ const result = parseLiteralShellCommand(command, options);
143
210
  return result.ok ? result.argv : null;
144
211
  }
145
212
  /**
@@ -157,12 +224,12 @@ export function unwrapCodexShellCommandArgv(argv) {
157
224
  * Codex app-server. This does not make shell composition safe: the nested
158
225
  * command still has to pass `parseLiteralShellCommand`.
159
226
  */
160
- export function parseCodexShellCommandArgv(command) {
161
- const outer = parseLiteralShellArgv(command);
227
+ export function parseCodexShellCommandArgv(command, options = {}) {
228
+ const outer = parseLiteralShellArgv(command, options);
162
229
  if (!outer)
163
230
  return null;
164
231
  const wrapped = unwrapCodexShellCommandArgv(outer);
165
- return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped);
232
+ return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped, options);
166
233
  }
167
234
  /**
168
235
  * Resolve a Codex command input while preserving the raw `commandArgv` form
@@ -173,10 +240,504 @@ export function parseCodexToolCommandArgv(input) {
173
240
  if (Array.isArray(input.commandArgv) && input.commandArgv.every(value => typeof value === 'string')) {
174
241
  const outer = input.commandArgv;
175
242
  const wrapped = unwrapCodexShellCommandArgv(outer);
176
- return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped);
243
+ return wrapped === undefined
244
+ ? outer
245
+ : parseLiteralShellArgv(wrapped, { allowManagedTmpDir: true });
177
246
  }
178
247
  const command = typeof input.command === 'string' ? input.command : '';
179
- return parseCodexShellCommandArgv(command);
248
+ return parseCodexShellCommandArgv(command, { allowManagedTmpDir: true });
249
+ }
250
+ function isSimpleExecStringLiteral(value) {
251
+ const quote = value[0];
252
+ if ((quote !== '"' && quote !== "'") || value.at(-1) !== quote)
253
+ return false;
254
+ let escaped = false;
255
+ for (let index = 1; index < value.length - 1; index++) {
256
+ const char = value[index];
257
+ if (escaped) {
258
+ escaped = false;
259
+ continue;
260
+ }
261
+ if (char === '\\') {
262
+ escaped = true;
263
+ continue;
264
+ }
265
+ if (char === '\r' || char === '\n')
266
+ return false;
267
+ }
268
+ return !escaped;
269
+ }
270
+ function splitTopLevelExecObjectProperties(body) {
271
+ const properties = [];
272
+ let start = 0;
273
+ let curlyDepth = 0;
274
+ let squareDepth = 0;
275
+ let quote = null;
276
+ let escaped = false;
277
+ for (let index = 0; index < body.length; index++) {
278
+ const char = body[index];
279
+ if (quote) {
280
+ if (escaped) {
281
+ escaped = false;
282
+ }
283
+ else if (char === '\\') {
284
+ escaped = true;
285
+ }
286
+ else if (char === quote) {
287
+ quote = null;
288
+ }
289
+ else if (char === '\r' || char === '\n') {
290
+ return null;
291
+ }
292
+ continue;
293
+ }
294
+ if (char === '"' || char === "'" || char === '`') {
295
+ quote = char;
296
+ continue;
297
+ }
298
+ if (char === '{')
299
+ curlyDepth++;
300
+ else if (char === '}') {
301
+ if (curlyDepth === 0)
302
+ return null;
303
+ curlyDepth--;
304
+ }
305
+ else if (char === '[')
306
+ squareDepth++;
307
+ else if (char === ']') {
308
+ if (squareDepth === 0)
309
+ return null;
310
+ squareDepth--;
311
+ }
312
+ else if (char === ',' && curlyDepth === 0 && squareDepth === 0) {
313
+ properties.push(body.slice(start, index));
314
+ start = index + 1;
315
+ }
316
+ }
317
+ if (quote || curlyDepth !== 0 || squareDepth !== 0 || escaped)
318
+ return null;
319
+ properties.push(body.slice(start));
320
+ return properties;
321
+ }
322
+ function topLevelExecPropertyColon(property) {
323
+ let curlyDepth = 0;
324
+ let squareDepth = 0;
325
+ let quote = null;
326
+ let escaped = false;
327
+ for (let index = 0; index < property.length; index++) {
328
+ const char = property[index];
329
+ if (quote) {
330
+ if (escaped)
331
+ escaped = false;
332
+ else if (char === '\\')
333
+ escaped = true;
334
+ else if (char === quote)
335
+ quote = null;
336
+ continue;
337
+ }
338
+ if (char === '"' || char === "'" || char === '`') {
339
+ quote = char;
340
+ continue;
341
+ }
342
+ if (char === '{')
343
+ curlyDepth++;
344
+ else if (char === '}')
345
+ curlyDepth--;
346
+ else if (char === '[')
347
+ squareDepth++;
348
+ else if (char === ']')
349
+ squareDepth--;
350
+ else if (char === ':' && curlyDepth === 0 && squareDepth === 0)
351
+ return index;
352
+ }
353
+ return -1;
354
+ }
355
+ function parseSimpleExecPropertyKey(raw) {
356
+ const value = raw.trim();
357
+ if (/^[A-Za-z_$][\w$]*$/.test(value))
358
+ return value;
359
+ if (value.startsWith('"') && value.endsWith('"')) {
360
+ try {
361
+ const parsed = JSON.parse(value);
362
+ return typeof parsed === 'string' ? parsed : null;
363
+ }
364
+ catch {
365
+ return null;
366
+ }
367
+ }
368
+ if (value.startsWith("'") && value.endsWith("'") && /^'[A-Za-z_$][\w$]*'$/.test(value)) {
369
+ return value.slice(1, -1);
370
+ }
371
+ return null;
372
+ }
373
+ function validateSimpleExecCarrierObject(source, objectStart, objectEnd) {
374
+ const properties = splitTopLevelExecObjectProperties(source.slice(objectStart + 1, objectEnd - 1));
375
+ if (!properties)
376
+ return false;
377
+ const seen = new Set();
378
+ for (const property of properties) {
379
+ if (!property.trim())
380
+ continue;
381
+ const colon = topLevelExecPropertyColon(property);
382
+ if (colon < 0)
383
+ return false;
384
+ const key = parseSimpleExecPropertyKey(property.slice(0, colon));
385
+ const value = property.slice(colon + 1).trim();
386
+ if (!key || seen.has(key) || !value)
387
+ return false;
388
+ seen.add(key);
389
+ // The command and all carrier options must be inert literals. In
390
+ // particular, reject calls, member expressions, template literals, and
391
+ // nested objects that could execute while the real tool is invoked.
392
+ if (key === 'cmd') {
393
+ if (!isSimpleExecStringLiteral(value))
394
+ return false;
395
+ }
396
+ else if (!isSimpleExecStringLiteral(value)
397
+ && !/^(?:true|false|null)$/.test(value)
398
+ && !/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(value)) {
399
+ return false;
400
+ }
401
+ }
402
+ return seen.has('cmd');
403
+ }
404
+ /**
405
+ * Extract the one literal command carried by Codex's custom `exec` tool.
406
+ *
407
+ * The desktop Codex tool is a JavaScript carrier around `tools.exec_command`.
408
+ * Treating the carrier as a send command is safe only when it contains one
409
+ * direct command call; a script containing a send plus any other command must
410
+ * remain blocked because its execution order cannot satisfy the gate.
411
+ */
412
+ export function parseCodexExecToolCommand(input) {
413
+ const source = typeof input.input === 'string' ? input.input : '';
414
+ if (!source)
415
+ return null;
416
+ // Count only source-level calls. A command body is itself a string literal,
417
+ // so a message such as `"mention tools.foo("` must not look like a second
418
+ // tool invocation. Comments are ignored for the same reason.
419
+ const toolCalls = [];
420
+ let sourceQuote = null;
421
+ let sourceEscaped = false;
422
+ let lineComment = false;
423
+ let blockComment = false;
424
+ for (let index = 0; index < source.length;) {
425
+ const char = source[index];
426
+ const next = source[index + 1];
427
+ if (lineComment) {
428
+ if (char === '\n' || char === '\r')
429
+ lineComment = false;
430
+ index++;
431
+ continue;
432
+ }
433
+ if (blockComment) {
434
+ if (char === '*' && next === '/') {
435
+ blockComment = false;
436
+ index += 2;
437
+ }
438
+ else {
439
+ index++;
440
+ }
441
+ continue;
442
+ }
443
+ if (sourceQuote) {
444
+ if (sourceEscaped) {
445
+ sourceEscaped = false;
446
+ }
447
+ else if (char === '\\') {
448
+ sourceEscaped = true;
449
+ }
450
+ else if (char === sourceQuote) {
451
+ sourceQuote = null;
452
+ }
453
+ index++;
454
+ continue;
455
+ }
456
+ if (char === '/' && next === '/') {
457
+ lineComment = true;
458
+ index += 2;
459
+ continue;
460
+ }
461
+ if (char === '/' && next === '*') {
462
+ blockComment = true;
463
+ index += 2;
464
+ continue;
465
+ }
466
+ if (char === '"' || char === "'" || char === '`') {
467
+ sourceQuote = char;
468
+ index++;
469
+ continue;
470
+ }
471
+ if (source.startsWith('tools.', index)
472
+ && (index === 0 || !/[A-Za-z0-9_$]/.test(source[index - 1]))) {
473
+ let nameEnd = index + 'tools.'.length;
474
+ const nameMatch = source.slice(nameEnd).match(/^[A-Za-z_$][\w$]*/);
475
+ if (nameMatch) {
476
+ nameEnd += nameMatch[0].length;
477
+ while (nameEnd < source.length && /\s/u.test(source[nameEnd]))
478
+ nameEnd++;
479
+ if (source[nameEnd] === '(') {
480
+ toolCalls.push({ name: nameMatch[0], start: index, openParen: nameEnd });
481
+ index = nameEnd + 1;
482
+ continue;
483
+ }
484
+ }
485
+ }
486
+ index++;
487
+ }
488
+ const calls = toolCalls.filter(call => call.name === 'exec_command');
489
+ if (toolCalls.length !== 1 || calls.length !== 1)
490
+ return null;
491
+ const callStart = calls[0].start;
492
+ const callOpenParen = calls[0].openParen;
493
+ const prefix = source.slice(0, callStart);
494
+ const binding = prefix.match(/^\s*(?:(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*await\s*|await\s*)$/);
495
+ if (!binding)
496
+ return null;
497
+ const outputBinding = binding[1];
498
+ let objectStart = callOpenParen + 1;
499
+ while (objectStart < source.length && /\s/u.test(source[objectStart]))
500
+ objectStart++;
501
+ if (source[objectStart] !== '{')
502
+ return null;
503
+ // Desktop Codex serializes the carrier object as JSON (`{"cmd":"..."}`),
504
+ // while hand-written calls may use a JavaScript identifier (`{ cmd: "..." }`).
505
+ // Walk the carrier object so a nested `{ cmd: ... }` or a string containing
506
+ // `cmd:` cannot be mistaken for the direct command property.
507
+ let curlyDepth = 0;
508
+ let squareDepth = 0;
509
+ let propertyStart = false;
510
+ let scanQuote = null;
511
+ let scanEscaped = false;
512
+ let valueStart = -1;
513
+ let objectEnd = -1;
514
+ for (let index = objectStart; index < source.length; index++) {
515
+ const char = source[index];
516
+ if (!scanQuote && curlyDepth === 1 && squareDepth === 0 && propertyStart) {
517
+ let keyStart = index;
518
+ while (keyStart < source.length && /\s/u.test(source[keyStart]))
519
+ keyStart++;
520
+ let key;
521
+ let keyEnd = keyStart;
522
+ const keyQuote = source[keyStart];
523
+ if (keyQuote === '"' || keyQuote === "'") {
524
+ keyEnd++;
525
+ let keyEscaped = false;
526
+ for (; keyEnd < source.length; keyEnd++) {
527
+ const keyChar = source[keyEnd];
528
+ if (keyEscaped) {
529
+ keyEscaped = false;
530
+ continue;
531
+ }
532
+ if (keyChar === '\\') {
533
+ keyEscaped = true;
534
+ continue;
535
+ }
536
+ if (keyChar === keyQuote)
537
+ break;
538
+ }
539
+ if (keyEnd >= source.length)
540
+ return null;
541
+ key = source.slice(keyStart + 1, keyEnd);
542
+ keyEnd++;
543
+ }
544
+ else {
545
+ const keyMatch = source.slice(keyStart).match(/^[A-Za-z_$][\w$]*/);
546
+ if (!keyMatch) {
547
+ propertyStart = false;
548
+ continue;
549
+ }
550
+ key = keyMatch[0];
551
+ keyEnd += key.length;
552
+ }
553
+ while (keyEnd < source.length && /\s/u.test(source[keyEnd]))
554
+ keyEnd++;
555
+ if (source[keyEnd] !== ':') {
556
+ propertyStart = false;
557
+ continue;
558
+ }
559
+ keyEnd++;
560
+ while (keyEnd < source.length && /\s/u.test(source[keyEnd]))
561
+ keyEnd++;
562
+ propertyStart = false;
563
+ if (key === 'cmd') {
564
+ const valueQuote = source[keyEnd];
565
+ if (valueQuote !== '"' && valueQuote !== "'")
566
+ return null;
567
+ if (valueStart >= 0)
568
+ return null;
569
+ valueStart = keyEnd;
570
+ index = keyEnd - 1;
571
+ continue;
572
+ }
573
+ index = keyEnd - 1;
574
+ continue;
575
+ }
576
+ if (scanQuote) {
577
+ if (scanEscaped) {
578
+ scanEscaped = false;
579
+ continue;
580
+ }
581
+ if (char === '\\') {
582
+ scanEscaped = true;
583
+ continue;
584
+ }
585
+ if (char === scanQuote)
586
+ scanQuote = null;
587
+ continue;
588
+ }
589
+ if (char === '"' || char === "'" || char === '`') {
590
+ scanQuote = char;
591
+ continue;
592
+ }
593
+ if (char === '{') {
594
+ curlyDepth++;
595
+ if (curlyDepth === 1)
596
+ propertyStart = true;
597
+ continue;
598
+ }
599
+ if (char === '}') {
600
+ if (curlyDepth === 1) {
601
+ objectEnd = index + 1;
602
+ break;
603
+ }
604
+ curlyDepth--;
605
+ continue;
606
+ }
607
+ if (char === '[') {
608
+ squareDepth++;
609
+ continue;
610
+ }
611
+ if (char === ']') {
612
+ squareDepth = Math.max(0, squareDepth - 1);
613
+ continue;
614
+ }
615
+ if (curlyDepth !== 1 || squareDepth !== 0)
616
+ continue;
617
+ if (char === ',') {
618
+ propertyStart = true;
619
+ continue;
620
+ }
621
+ }
622
+ if (valueStart < 0 || objectEnd < 0)
623
+ return null;
624
+ if (!validateSimpleExecCarrierObject(source, objectStart, objectEnd))
625
+ return null;
626
+ let callEnd = objectEnd;
627
+ while (callEnd < source.length && /\s/u.test(source[callEnd]))
628
+ callEnd++;
629
+ if (source[callEnd] !== ')')
630
+ return null;
631
+ callEnd++;
632
+ // The carrier is intentionally restricted to one top-level await. This
633
+ // rejects loops/conditionals that could invoke the same source call more
634
+ // than once, and prevents additional arguments from hiding another action.
635
+ const tail = source.slice(callEnd).replace(/\/\/[^\r\n]*|\/\*[\s\S]*?\*\//g, '');
636
+ const tailPattern = outputBinding
637
+ ? new RegExp(`^\\s*;?\\s*(?:text\\(\\s*${outputBinding}\\.output\\s*\\)\\s*;?)?\\s*$`)
638
+ : /^\s*;?\s*$/;
639
+ if (!tailPattern.test(tail))
640
+ return null;
641
+ const valueQuote = source[valueStart];
642
+ const quoteForValue = valueQuote === '"' || valueQuote === "'" ? valueQuote : null;
643
+ if (!quoteForValue)
644
+ return null;
645
+ const quote = quoteForValue;
646
+ let end = valueStart + 1;
647
+ let escaped = false;
648
+ for (; end < source.length; end++) {
649
+ const char = source[end];
650
+ if (escaped) {
651
+ escaped = false;
652
+ continue;
653
+ }
654
+ if (char === '\\') {
655
+ escaped = true;
656
+ continue;
657
+ }
658
+ if (char === quote)
659
+ break;
660
+ }
661
+ if (end >= source.length)
662
+ return null;
663
+ const raw = source.slice(valueStart, end + 1);
664
+ if (quote === '"') {
665
+ try {
666
+ return JSON.parse(raw);
667
+ }
668
+ catch {
669
+ return null;
670
+ }
671
+ }
672
+ // Decode the JS string escapes needed for command literals without
673
+ // evaluating model-provided source code. Unknown escapes are rejected:
674
+ // silently dropping the backslash could make the parsed command differ
675
+ // from the string that JavaScript will actually execute (for example
676
+ // `\x26` becoming `&`).
677
+ let decoded = '';
678
+ for (let index = 1; index < raw.length - 1; index++) {
679
+ const char = raw[index];
680
+ if (char !== '\\') {
681
+ decoded += char;
682
+ continue;
683
+ }
684
+ const next = raw[++index];
685
+ if (next === undefined)
686
+ return null;
687
+ if (next === 'n')
688
+ decoded += '\n';
689
+ else if (next === 'r')
690
+ decoded += '\r';
691
+ else if (next === 't')
692
+ decoded += '\t';
693
+ else if (next === 'b')
694
+ decoded += '\b';
695
+ else if (next === 'f')
696
+ decoded += '\f';
697
+ else if (next === 'v')
698
+ decoded += '\v';
699
+ else if (next === '0') {
700
+ if (/^[0-9]/.test(raw[index + 1] ?? ''))
701
+ return null;
702
+ decoded += '\0';
703
+ }
704
+ else if (next === 'x') {
705
+ const hex = raw.slice(index + 1, index + 3);
706
+ if (!/^[0-9a-fA-F]{2}$/.test(hex))
707
+ return null;
708
+ decoded += String.fromCharCode(parseInt(hex, 16));
709
+ index += 2;
710
+ }
711
+ else if (next === 'u') {
712
+ const hex = raw.slice(index + 1, index + 5);
713
+ if (!/^[0-9a-fA-F]{4}$/.test(hex))
714
+ return null;
715
+ decoded += String.fromCharCode(parseInt(hex, 16));
716
+ index += 4;
717
+ }
718
+ else if (next === '\n') {
719
+ // JavaScript line continuations contribute no command character.
720
+ }
721
+ else if (next === '\r') {
722
+ if (raw[index + 1] === '\n')
723
+ index++;
724
+ }
725
+ else if (next === '\\' || next === "'" || next === '"' || next === '`' || next === '/') {
726
+ decoded += next;
727
+ }
728
+ else {
729
+ return null;
730
+ }
731
+ }
732
+ return decoded;
733
+ }
734
+ function parseProactiveToolCommandArgv(toolName, input) {
735
+ if (toolName === 'Bash' || toolName === 'Shell')
736
+ return parseCodexToolCommandArgv(input);
737
+ if (toolName !== 'exec')
738
+ return null;
739
+ const command = parseCodexExecToolCommand(input);
740
+ return command ? parseCodexShellCommandArgv(command, { allowManagedTmpDir: true }) : null;
180
741
  }
181
742
  function scanBoundedOutputOperators(command) {
182
743
  const operators = [];
@@ -290,7 +851,7 @@ function parseStdinOnlyHeadArgs(segment) {
290
851
  * optional stderr merge, one bounded `head`, and one create-only output path.
291
852
  */
292
853
  export function parseBoundedOutputShellCommand(command) {
293
- const input = trimShellSpacing(command);
854
+ const input = trimShellSpacing(substituteManagedTmpDirToken(command));
294
855
  if (!input)
295
856
  return null;
296
857
  const operators = scanBoundedOutputOperators(input);
@@ -311,6 +872,10 @@ export function parseBoundedOutputShellCommand(command) {
311
872
  const argv = parseLiteralShellArgv(sourceText);
312
873
  if (!argv)
313
874
  return null;
875
+ // `$TMPDIR` is a transport-level path reference, never an argument
876
+ // expansion for the privileged command itself.
877
+ if (argv.some(value => value.includes(MANAGED_TMPDIR_TOKEN)))
878
+ return null;
314
879
  let mergeStderr = false;
315
880
  let pipe;
316
881
  let redirect;
@@ -345,7 +910,12 @@ export function parseBoundedOutputShellCommand(command) {
345
910
  const outputArgv = parseLiteralShellArgv(trimShellSpacing(input.slice(redirect.end)));
346
911
  if (!outputArgv || outputArgv.length !== 1)
347
912
  return null;
348
- outputPath = outputArgv[0];
913
+ // A single-quoted or escaped `$TMPDIR` is literal shell text, not the
914
+ // managed directory reference. Do not reinterpret it as a route to the
915
+ // session temp root.
916
+ if (outputArgv[0].includes('$') && !outputArgv[0].includes(MANAGED_TMPDIR_TOKEN))
917
+ return null;
918
+ outputPath = restoreManagedTmpDirToken(outputArgv[0]);
349
919
  }
350
920
  return { argv, mergeStderr, ...(headArgs ? { headArgs } : {}), ...(outputPath ? { outputPath } : {}) };
351
921
  }
@@ -355,13 +925,14 @@ export function parseBoundedOutputShellCommand(command) {
355
925
  * invocation may leave the sandbox, while shell composition may not.
356
926
  */
357
927
  export function classifyEvolcoreShellCommand(command) {
358
- const literal = parseLiteralShellCommand(command);
928
+ const parseOptions = { allowManagedTmpDir: true };
929
+ const literal = parseLiteralShellCommand(command, parseOptions);
359
930
  if (literal.ok) {
360
931
  if (literal.argv[0] === 'ec')
361
932
  return { kind: 'literal' };
362
933
  const wrapped = unwrapCodexShellCommandArgv(literal.argv);
363
934
  if (wrapped !== undefined) {
364
- const nested = parseLiteralShellCommand(wrapped);
935
+ const nested = parseLiteralShellCommand(wrapped, parseOptions);
365
936
  if (nested.ok && nested.argv[0] === 'ec')
366
937
  return { kind: 'literal' };
367
938
  if (!nested.ok && EVOLCORE_COMMAND_POSITION_RE.test(wrapped)) {
@@ -410,6 +981,66 @@ const PROACTIVE_SEND_VALUE_FLAGS = new Set([
410
981
  const PROACTIVE_SEND_BOOLEAN_FLAGS = new Set([
411
982
  '--encrypt', '--no-encrypt', '--mention-all',
412
983
  ]);
984
+ const SEND_CONTENT_VALUE_FLAGS = new Set([
985
+ '--payload', '--title', '--description', '--text', '--transcript',
986
+ ]);
987
+ const TRIGGER_CONTENT_VALUE_FLAGS = new Set(['--prompt']);
988
+ /**
989
+ * A shell-escaped `$TMPDIR` in a msg/group text body or trigger prompt is
990
+ * ordinary content, not a path reference. Keep every other unresolved
991
+ * occurrence fail-closed so file and control arguments cannot bypass
992
+ * managed-path checks.
993
+ */
994
+ export function containsLiteralManagedTmpDirOutsideSendContent(argv) {
995
+ const literalIndexes = argv
996
+ .map((value, index) => value.includes('$TMPDIR') ? index : -1)
997
+ .filter(index => index >= 0);
998
+ if (literalIndexes.length === 0)
999
+ return false;
1000
+ const isSendCommand = argv[0] === 'ec'
1001
+ && (argv[1] === 'msg' || argv[1] === 'group')
1002
+ && argv[2] === 'send';
1003
+ const isTriggerCommand = argv[0] === 'ec' && argv[1] === 'trigger';
1004
+ if (!isSendCommand && !isTriggerCommand)
1005
+ return true;
1006
+ const textIndexes = new Set();
1007
+ if (isTriggerCommand) {
1008
+ for (let index = 2; index < argv.length; index++) {
1009
+ if (!TRIGGER_CONTENT_VALUE_FLAGS.has(argv[index]))
1010
+ continue;
1011
+ if (argv[index + 1] !== undefined)
1012
+ textIndexes.add(index + 1);
1013
+ index++;
1014
+ }
1015
+ }
1016
+ if (!isSendCommand)
1017
+ return literalIndexes.some(index => !textIndexes.has(index));
1018
+ let endOfOptions = false;
1019
+ for (let index = 5; index < argv.length; index++) {
1020
+ const value = argv[index];
1021
+ if (endOfOptions) {
1022
+ textIndexes.add(index);
1023
+ continue;
1024
+ }
1025
+ if (value === '--') {
1026
+ endOfOptions = true;
1027
+ continue;
1028
+ }
1029
+ if (PROACTIVE_SEND_BOOLEAN_FLAGS.has(value))
1030
+ continue;
1031
+ if (PROACTIVE_SEND_VALUE_FLAGS.has(value)) {
1032
+ const operandIndex = ++index;
1033
+ if (SEND_CONTENT_VALUE_FLAGS.has(value) && argv[operandIndex] !== undefined) {
1034
+ textIndexes.add(operandIndex);
1035
+ }
1036
+ continue;
1037
+ }
1038
+ textIndexes.add(index);
1039
+ }
1040
+ if (textIndexes.size === 0)
1041
+ return true;
1042
+ return literalIndexes.some(index => !textIndexes.has(index));
1043
+ }
413
1044
  function hasExecutableProactiveSendPayload(argv, scope) {
414
1045
  if (!argv || argv[0] !== 'ec' || argv[1] !== scope || argv[2] !== 'send')
415
1046
  return false;
@@ -531,11 +1162,11 @@ export function isEvolcoreSendCommandForSession(toolName, input, channelId) {
531
1162
  }
532
1163
  /** Proactive first-send guard: exact channel kind and current target only. */
533
1164
  export function isExactEvolcoreSendCommandForSession(toolName, input, channelId, chatType, selfAid) {
534
- // Codex hooks use the canonical name `Bash`; the app-server event adapter
535
- // exposes the same commandExecution items as `Shell` to the renderer.
536
- if ((toolName !== 'Bash' && toolName !== 'Shell') || !selfAid)
1165
+ // Codex app-server exposes shell items as `Shell`; the desktop custom tool
1166
+ // is represented as `exec` and is handled only for one literal inner call.
1167
+ if (!selfAid)
537
1168
  return false;
538
- const argv = parseCodexToolCommandArgv(input);
1169
+ const argv = parseProactiveToolCommandArgv(toolName, input);
539
1170
  const parsed = argv ? parseEvolcoreSendArgv(argv) : null;
540
1171
  if (!parsed || parsed.action !== 'send')
541
1172
  return false;