evolcore 0.0.13 → 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 (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/bin/codex-managed-hook.mjs +4 -1
  3. package/bin/install-codex-managed-hooks.mjs +201 -0
  4. package/dist/agents/claude-runner.js +53 -5
  5. package/dist/agents/codex-app-server-client.js +123 -2
  6. package/dist/agents/codex-runner.js +149 -30
  7. package/dist/agents/ecagent-runner.js +17 -1
  8. package/dist/agents/gemini-runner.js +9 -4
  9. package/dist/aun/msg/managed-operation.js +63 -3
  10. package/dist/channels/aun.js +144 -15
  11. package/dist/channels/daemon.js +2 -0
  12. package/dist/cli/aun-commands.js +1 -1
  13. package/dist/cli/fs-command.js +46 -9
  14. package/dist/cli/task-context.js +172 -0
  15. package/dist/config/builtin-roles.js +2 -0
  16. package/dist/config/config-manager.js +6 -2
  17. package/dist/config/contact-book-store.js +7 -2
  18. package/dist/core/auth/auth-gateway.js +1 -0
  19. package/dist/core/auth/authorization-audit.js +32 -0
  20. package/dist/core/auth/operation-catalog.js +3 -3
  21. package/dist/core/bootstrap-service.js +7 -1
  22. package/dist/core/command/command-handler.js +3 -0
  23. package/dist/core/event-catalog.js +2 -0
  24. package/dist/core/message/im-renderer.js +15 -1
  25. package/dist/core/message/message-bridge.js +5 -2
  26. package/dist/core/message/response-engine.js +138 -10
  27. package/dist/core/permission/ec-command-parser.js +556 -4
  28. package/dist/core/permission/tool-policy.js +17 -29
  29. package/dist/core/runtime-lock.js +101 -0
  30. package/dist/index.js +30 -3
  31. package/dist/response-system/engines/v1/proactive-flow.js +92 -8
  32. package/dist/response-system/modes/single-session/index.js +3 -0
  33. package/dist/trigger/history.js +42 -7
  34. package/dist/utils/error-utils.js +7 -0
  35. package/dist/utils/logger.js +37 -4
  36. package/kits/templates/roles/admin.json +2 -0
  37. package/kits/templates/roles/member.json +1 -0
  38. package/package.json +1 -1
@@ -247,6 +247,498 @@ export function parseCodexToolCommandArgv(input) {
247
247
  const command = typeof input.command === 'string' ? input.command : '';
248
248
  return parseCodexShellCommandArgv(command, { allowManagedTmpDir: true });
249
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;
741
+ }
250
742
  function scanBoundedOutputOperators(command) {
251
743
  const operators = [];
252
744
  let quote = null;
@@ -489,6 +981,66 @@ const PROACTIVE_SEND_VALUE_FLAGS = new Set([
489
981
  const PROACTIVE_SEND_BOOLEAN_FLAGS = new Set([
490
982
  '--encrypt', '--no-encrypt', '--mention-all',
491
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
+ }
492
1044
  function hasExecutableProactiveSendPayload(argv, scope) {
493
1045
  if (!argv || argv[0] !== 'ec' || argv[1] !== scope || argv[2] !== 'send')
494
1046
  return false;
@@ -610,11 +1162,11 @@ export function isEvolcoreSendCommandForSession(toolName, input, channelId) {
610
1162
  }
611
1163
  /** Proactive first-send guard: exact channel kind and current target only. */
612
1164
  export function isExactEvolcoreSendCommandForSession(toolName, input, channelId, chatType, selfAid) {
613
- // Codex hooks use the canonical name `Bash`; the app-server event adapter
614
- // exposes the same commandExecution items as `Shell` to the renderer.
615
- 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)
616
1168
  return false;
617
- const argv = parseCodexToolCommandArgv(input);
1169
+ const argv = parseProactiveToolCommandArgv(toolName, input);
618
1170
  const parsed = argv ? parseEvolcoreSendArgv(argv) : null;
619
1171
  if (!parsed || parsed.action !== 'send')
620
1172
  return false;
@@ -1348,40 +1348,28 @@ export function evaluateToolPreflight(toolName, input, context) {
1348
1348
  return prepareBoundedOutputInput(input, ecCommand.command, 'ec', context);
1349
1349
  }
1350
1350
  if (ecCommand.kind === 'composite') {
1351
- // Older app-server approval callbacks may provide only the rendered
1352
- // `/bin/bash -lc ...` command string. A composite inside that carrier
1353
- // is not eligible for a delegation ticket, but owner bypass still
1354
- // handles it as an ordinary shell request. Structured `commandArgv`
1355
- // callbacks remain fail-closed below because their command boundary is
1356
- // authoritative.
1357
- const legacyRenderedCarrier = !explicitCommandArgv && wrappedCommand !== undefined;
1358
- const allowLegacyBypassComposition = legacyRenderedCarrier
1359
- && context.permissionMode === 'bypass'
1360
- && ecCommand.issue === 'shell-composition';
1361
- if (!allowLegacyBypassComposition) {
1362
- if (ecCommand.issue === 'unsafe-expansion') {
1363
- return {
1364
- behavior: 'deny',
1365
- input,
1366
- message: '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量',
1367
- policyCode: 'ec_shell_unsafe_expansion',
1368
- };
1369
- }
1370
- if (ecCommand.issue === 'invalid-quote') {
1371
- return {
1372
- behavior: 'deny',
1373
- input,
1374
- message: '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"',
1375
- policyCode: 'ec_shell_invalid_quote',
1376
- };
1377
- }
1351
+ if (ecCommand.issue === 'unsafe-expansion') {
1378
1352
  return {
1379
1353
  behavior: 'deny',
1380
1354
  input,
1381
- message: '🔒 EC 命令调用被拒绝:一次 Bash/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要先用 `command -v`/`ec aid` 探测,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开、子 shell、`sh -c` 或 `bash -lc` 拼接其它命令。',
1382
- policyCode: 'ec_shell_composite_command',
1355
+ message: '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量',
1356
+ policyCode: 'ec_shell_unsafe_expansion',
1383
1357
  };
1384
1358
  }
1359
+ if (ecCommand.issue === 'invalid-quote') {
1360
+ return {
1361
+ behavior: 'deny',
1362
+ input,
1363
+ message: '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"',
1364
+ policyCode: 'ec_shell_invalid_quote',
1365
+ };
1366
+ }
1367
+ return {
1368
+ behavior: 'deny',
1369
+ input,
1370
+ message: '🔒 EC 命令调用被拒绝:一次 Bash/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要先用 `command -v`/`ec aid` 探测,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开、子 shell、`sh -c` 或 `bash -lc` 拼接其它命令。',
1371
+ policyCode: 'ec_shell_composite_command',
1372
+ };
1385
1373
  }
1386
1374
  const boundedOutput = parseBoundedOutputShellCommand(command);
1387
1375
  const hostProcessCommand = boundedOutput