evolcore 0.0.20 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +58 -9
  3. package/dist/agents/baseagent.js +10 -6
  4. package/dist/agents/claude-runner.js +379 -108
  5. package/dist/agents/codex-app-server-client.js +10 -2
  6. package/dist/agents/codex-runner.js +402 -135
  7. package/dist/agents/ecagent-runner.js +171 -61
  8. package/dist/agents/gemini-runner.js +130 -30
  9. package/dist/agents/request-identity.js +25 -0
  10. package/dist/agents/runner-types.js +19 -0
  11. package/dist/aun/aid/agentmd.js +59 -2
  12. package/dist/aun/aid/identity.js +4 -1
  13. package/dist/aun/aid/index.js +1 -1
  14. package/dist/aun/msg/group.js +72 -6
  15. package/dist/aun/msg/history.js +213 -36
  16. package/dist/aun/msg/managed-operation.js +58 -9
  17. package/dist/aun/msg/p2p.js +5 -0
  18. package/dist/aun/outbox.js +182 -80
  19. package/dist/aun/service-proxy.js +43 -25
  20. package/dist/channels/aun.js +409 -88
  21. package/dist/channels/daemon.js +6 -1
  22. package/dist/cli/agent-command.js +4 -3
  23. package/dist/cli/agent.js +66 -56
  24. package/dist/cli/aun-commands.js +177 -42
  25. package/dist/cli/command-log.js +10 -11
  26. package/dist/cli/contact.js +1 -0
  27. package/dist/cli/daemon-commands.js +69 -115
  28. package/dist/cli/init.js +27 -15
  29. package/dist/cli/task-context.js +46 -0
  30. package/dist/cli/trigger-command.js +1 -1
  31. package/dist/cli/watch-logs.js +10 -3
  32. package/dist/config/builtin-roles.js +1 -0
  33. package/dist/config/config-field-policy.js +16 -5
  34. package/dist/config/config-manager.js +135 -17
  35. package/dist/config/contact-operation-service.js +32 -1
  36. package/dist/config/contact-request-service.js +44 -0
  37. package/dist/config/daemon-services.js +186 -0
  38. package/dist/config/gateway-config.js +20 -9
  39. package/dist/config/role-service.js +54 -3
  40. package/dist/config/schema-migration.js +550 -0
  41. package/dist/config-store.js +151 -9
  42. package/dist/core/agent-application-service.js +279 -0
  43. package/dist/core/audit/log-integrity.js +102 -0
  44. package/dist/core/auth/agent-delegation.js +31 -1
  45. package/dist/core/auth/auth-gateway.js +33 -4
  46. package/dist/core/auth/authorization-audit.js +150 -2
  47. package/dist/core/auth/operation-authorizer.js +41 -1
  48. package/dist/core/auth/operation-catalog.js +9 -1
  49. package/dist/core/bootstrap-service.js +6 -2
  50. package/dist/core/causation/aun-association.js +7 -4
  51. package/dist/core/command/agent-control.js +56 -16
  52. package/dist/core/command/command-handler.js +290 -44
  53. package/dist/core/command/connect-menu.js +3 -4
  54. package/dist/core/command/group-menu.js +5 -7
  55. package/dist/core/command/menu-handler.js +279 -80
  56. package/dist/core/command/role-menu.js +21 -11
  57. package/dist/core/command/slash-gate.js +85 -18
  58. package/dist/core/command/slash-handler.js +350 -32
  59. package/dist/core/event-catalog.js +5 -0
  60. package/dist/core/evolagent.js +4 -0
  61. package/dist/core/handoff/dispatcher.js +4 -0
  62. package/dist/core/handoff/runtime.js +10 -0
  63. package/dist/core/handoff/store.js +32 -9
  64. package/dist/core/inference/text-inference.js +7 -15
  65. package/dist/core/message/im-renderer.js +83 -84
  66. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  67. package/dist/core/message/message-bridge.js +124 -10
  68. package/dist/core/message/message-log.js +14 -7
  69. package/dist/core/message/message-queue.js +206 -16
  70. package/dist/core/message/message-utils.js +12 -5
  71. package/dist/core/message/response-engine.js +486 -68
  72. package/dist/core/message/send-receipt.js +1 -0
  73. package/dist/core/message/stream-debouncer.js +9 -2
  74. package/dist/core/model/model-catalog.js +23 -15
  75. package/dist/core/model/model-diagnostics.js +28 -10
  76. package/dist/core/permission/approval-gateway.js +180 -6
  77. package/dist/core/permission/ec-command-parser.js +410 -54
  78. package/dist/core/permission/mode.js +18 -3
  79. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  80. package/dist/core/permission/readonly-shell-query.js +263 -9
  81. package/dist/core/permission/sandbox-runtime.js +159 -1
  82. package/dist/core/permission/tool-policy.js +575 -21
  83. package/dist/core/session/session-fs-store.js +154 -5
  84. package/dist/core/session/session-manager.js +299 -30
  85. package/dist/core/session/session-renew.js +19 -12
  86. package/dist/core/session/session-turn-coordinator.js +11 -4
  87. package/dist/eck/kit-renderer.js +1 -1
  88. package/dist/index.js +253 -46
  89. package/dist/ipc.js +374 -24
  90. package/dist/paths.js +64 -7
  91. package/dist/response-system/context-builder.js +1 -7
  92. package/dist/trigger/anomaly-store.js +1 -0
  93. package/dist/trigger/feedback.js +56 -5
  94. package/dist/trigger/history.js +79 -4
  95. package/dist/trigger/legacy-session-history.js +2 -2
  96. package/dist/trigger/parser.js +3 -2
  97. package/dist/trigger/validation.js +6 -1
  98. package/dist/utils/atomic-write.js +27 -0
  99. package/dist/utils/ecweb-utils.js +16 -2
  100. package/dist/utils/error-utils.js +4 -1
  101. package/dist/utils/logger.js +21 -2
  102. package/dist/utils/process-tree-stats.js +24 -4
  103. package/dist/utils/process-tree-worker.js +31 -0
  104. package/dist/utils/project-path.js +1 -2
  105. package/kits/docs/INDEX.md +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -1
  107. package/kits/docs/evolcore/contact.md +7 -1
  108. package/kits/docs/evolcore/msg.md +16 -0
  109. package/kits/schemas/_meta.json +4 -2
  110. package/kits/schemas/agent-config.schema.11.json +13 -0
  111. package/kits/schemas/daemon.schema.5.json +0 -1
  112. package/kits/schemas/daemon.schema.6.json +131 -0
  113. package/kits/schemas/defaults.schema.5.json +15 -3
  114. package/kits/schemas/migrations/README.md +3 -1
  115. package/kits/schemas/relation-config.schema.8.json +13 -0
  116. package/kits/schemas/role-config.schema.1.json +1 -2
  117. package/kits/schemas/single-session.schema.3.json +32 -0
  118. package/kits/templates/roles/admin.json +1 -0
  119. package/kits/templates/roles/member.json +1 -0
  120. package/kits/templates/roles/visitor.json +1 -0
  121. package/package.json +6 -3
  122. package/skills/eclink/SKILL.md +2 -0
  123. package/dist/config/aun-gateway-config.js +0 -2
@@ -3,8 +3,8 @@ import path from 'path';
3
3
  import { randomUUID } from 'crypto';
4
4
  import { logger } from '../../utils/logger.js';
5
5
  import { resolveRoot } from '../../paths.js';
6
- import { containsHClassReference, containsLClassReference, checkProtectedPathAccess, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, resolveProtectedCandidateWithoutFinalSymlink, } from '../protected-paths.js';
7
- import { classifyEvolcoreShellCommand, parseCodexToolCommandArgv, parseBoundedOutputShellCommand, parseLiteralShellArgv, resolveCodexShellCarrierArgv, } from './ec-command-parser.js';
6
+ import { containsHClassReference, containsLClassReference, checkProtectedPathAccess, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, resolveProtectedCandidateWithoutFinalSymlink, } from './protected-paths.js';
7
+ import { classifyEvolcoreShellCommand, hasCodexCmdCarrierEcIntent, parseCodexToolCommandArgv, parseBoundedOutputShellCommand, parseLiteralShellArgv, resolveCodexShellCarrierArgv, resolveCodexShellCarrierString, } from './ec-command-parser.js';
8
8
  import { analyzeReadonlyShellQuery, } from './readonly-shell-query.js';
9
9
  /** Resolve the session-owned temporary root supplied by the execution host. */
10
10
  export function resolveManagedTempDir(env = process.env) {
@@ -412,6 +412,529 @@ function queryPathOperands(analysis) {
412
412
  accessKind: command.accessKind ?? (operand.access === 'recursive' ? 'enumerate' : 'content'),
413
413
  }))));
414
414
  }
415
+ const PROTECTED_BASENAME_LITERAL_RE = /^(?:daemon\.json|defaults\.json|config\.json|contact\.json|contact-operations\.jsonl|\.device_id|\.env|\.lock|daemon\.pid)$/i;
416
+ const SHELL_PATH_LITERAL_META_RE = /[\0\r\n$`*?\[\]{}<>|;&(),]/;
417
+ /**
418
+ * Extract literal operands immediately following an unquoted shell
419
+ * redirection operator. The full command remains unproven, but a redirect
420
+ * target is an explicit filesystem write/read operand and must retain H/L
421
+ * protection. Quoted prose and command text are deliberately ignored.
422
+ */
423
+ function literalShellRedirectOperands(command) {
424
+ const operands = [];
425
+ let quote = null;
426
+ let escaped = false;
427
+ let redirectPending = false;
428
+ let token = '';
429
+ const finishRedirect = () => {
430
+ if (redirectPending && token)
431
+ operands.push(token);
432
+ token = '';
433
+ redirectPending = false;
434
+ };
435
+ for (let index = 0; index < command.length; index++) {
436
+ const char = command[index];
437
+ if (escaped) {
438
+ if (redirectPending)
439
+ token += char;
440
+ escaped = false;
441
+ continue;
442
+ }
443
+ if (char === '\\') {
444
+ if (redirectPending)
445
+ token += char;
446
+ escaped = true;
447
+ continue;
448
+ }
449
+ if (quote === 'single') {
450
+ if (char === "'")
451
+ quote = null;
452
+ else if (redirectPending)
453
+ token += char;
454
+ continue;
455
+ }
456
+ if (quote === 'double') {
457
+ if (char === '"')
458
+ quote = null;
459
+ else if (redirectPending)
460
+ token += char;
461
+ continue;
462
+ }
463
+ if (char === "'" || char === '"') {
464
+ quote = char === "'" ? 'single' : 'double';
465
+ continue;
466
+ }
467
+ if (char === '>' || char === '<') {
468
+ // A preceding numeric token is an optional file descriptor, not part
469
+ // of the target. Repeated operators (`>>`) share one target.
470
+ token = '';
471
+ redirectPending = true;
472
+ if (command[index + 1] === char)
473
+ index++;
474
+ continue;
475
+ }
476
+ if (/\s/.test(char)) {
477
+ if (redirectPending && token)
478
+ finishRedirect();
479
+ continue;
480
+ }
481
+ if (/[;&|()]/.test(char)) {
482
+ finishRedirect();
483
+ continue;
484
+ }
485
+ if (redirectPending)
486
+ token += char;
487
+ }
488
+ finishRedirect();
489
+ return operands;
490
+ }
491
+ function looksLikeLiteralShellPath(value) {
492
+ return !!value
493
+ && !value.startsWith('-')
494
+ && !/\s/.test(value)
495
+ && !SHELL_PATH_LITERAL_META_RE.test(value)
496
+ && (value === '.' || value === '..' || value.includes('/') || value.includes('\\') || PROTECTED_BASENAME_LITERAL_RE.test(value));
497
+ }
498
+ function protectedClassPath(value, className, options) {
499
+ if (!looksLikeLiteralShellPath(value))
500
+ return undefined;
501
+ const matched = className === 'h'
502
+ ? (containsHClassReference(value) || isHClassPath(value, options))
503
+ : (containsLClassReference(value) || isLClassPath(value, options));
504
+ return matched ? resolveProtectedCandidate(value, options.cwd) : undefined;
505
+ }
506
+ function literalSedOperands(args) {
507
+ const values = [];
508
+ let hasExplicitProgram = false;
509
+ let skippedPositionalProgram = false;
510
+ let optionsEnded = false;
511
+ for (let index = 0; index < args.length; index++) {
512
+ const argument = args[index];
513
+ if (!optionsEnded && argument === '--') {
514
+ optionsEnded = true;
515
+ continue;
516
+ }
517
+ if (!optionsEnded && argument.startsWith('--')) {
518
+ if (argument === '--expression') {
519
+ hasExplicitProgram = true;
520
+ index++;
521
+ continue;
522
+ }
523
+ if (argument.startsWith('--expression=')) {
524
+ hasExplicitProgram = true;
525
+ continue;
526
+ }
527
+ if (argument === '--file') {
528
+ hasExplicitProgram = true;
529
+ if (args[index + 1])
530
+ values.push(args[++index]);
531
+ continue;
532
+ }
533
+ if (argument.startsWith('--file=')) {
534
+ hasExplicitProgram = true;
535
+ values.push(argument.slice(argument.indexOf('=') + 1));
536
+ continue;
537
+ }
538
+ continue;
539
+ }
540
+ if (!optionsEnded && argument.startsWith('-') && argument !== '-') {
541
+ const body = argument.slice(1);
542
+ for (let cursor = 0; cursor < body.length; cursor++) {
543
+ const option = body[cursor];
544
+ if (option !== 'e' && option !== 'f')
545
+ continue;
546
+ hasExplicitProgram = true;
547
+ const attached = body.slice(cursor + 1);
548
+ const value = attached || args[index + 1];
549
+ if (option === 'f' && value)
550
+ values.push(value);
551
+ if (!attached && value)
552
+ index++;
553
+ break;
554
+ }
555
+ continue;
556
+ }
557
+ if (!hasExplicitProgram && !skippedPositionalProgram) {
558
+ skippedPositionalProgram = true;
559
+ continue;
560
+ }
561
+ values.push(argument);
562
+ }
563
+ return values;
564
+ }
565
+ /**
566
+ * Recover paths from a small set of commands whose ordinary argv is a write
567
+ * target list. This is intentionally separate from grep/rg/git parsing: their
568
+ * first operands are patterns or revisions and must never be treated as
569
+ * filesystem paths merely because they contain a protected-looking string.
570
+ */
571
+ function literalMutationOperands(argv) {
572
+ const executable = commandName(argv[0]);
573
+ const args = argv.slice(1);
574
+ if (['mkdir', 'mktemp', 'touch', 'tee', 'cp', 'mv', 'install', 'chmod', 'chown', 'ln', 'rm'].includes(executable)) {
575
+ return args.filter(argument => argument !== '--' && !argument.startsWith('-'));
576
+ }
577
+ if (executable === 'sed')
578
+ return literalSedOperands(args);
579
+ if (executable === 'git') {
580
+ return literalGitOperands(argv);
581
+ }
582
+ return [];
583
+ }
584
+ /**
585
+ * Recover Git operands that are unambiguously filesystem paths. Git has many
586
+ * positional revision/search arguments, so scanning every argv value would
587
+ * turn a pickaxe string into a protected-path access. Keep global path options,
588
+ * explicit pathspecs after `--`, and the small set of subcommands/options that
589
+ * name an output or working directory.
590
+ */
591
+ function literalGitOperands(argv) {
592
+ const args = argv.slice(1);
593
+ const values = [];
594
+ const globalPathOptions = new Set(['-C', '--git-dir', '--work-tree', '--exec-path']);
595
+ const globalValueOptions = new Set(['-C', '--git-dir', '--work-tree', '--namespace', '--super-prefix']);
596
+ let index = 0;
597
+ let subcommand;
598
+ // Git global options precede the subcommand. Capture only options whose
599
+ // values are directories/files; skip the rest so config values are not
600
+ // mistaken for paths.
601
+ const pathConfigKey = (key) => /^(?:include(?:if\..+)?\.path|core\.(?:gitdir|worktree|excludesfile|attributesfile|hookspath))$/i.test(key);
602
+ while (index < args.length) {
603
+ const argument = args[index];
604
+ if (!argument.startsWith('-')) {
605
+ subcommand = argument.toLowerCase();
606
+ index++;
607
+ break;
608
+ }
609
+ const option = argument.split('=', 1)[0];
610
+ const shortAttached = !argument.startsWith('--') && argument.length > 2 && argument.startsWith('-C');
611
+ if (option === '-c') {
612
+ const config = argument.includes('=') ? argument.slice(argument.indexOf('=') + 1) : args[index + 1];
613
+ if (config) {
614
+ const separator = config.indexOf('=');
615
+ if (separator < 0 || pathConfigKey(config.slice(0, separator))) {
616
+ values.push(separator < 0 ? config : config.slice(separator + 1));
617
+ }
618
+ }
619
+ index += argument.includes('=') ? 1 : 2;
620
+ continue;
621
+ }
622
+ if (option === '--config-env') {
623
+ const config = argument.includes('=') ? argument.slice(argument.indexOf('=') + 1) : args[index + 1];
624
+ const separator = config?.indexOf('=') ?? -1;
625
+ const key = separator >= 0 ? config.slice(0, separator) : '';
626
+ const envName = separator >= 0 ? config.slice(separator + 1) : undefined;
627
+ if (envName && pathConfigKey(key) && process.env[envName])
628
+ values.push(process.env[envName]);
629
+ index += argument.includes('=') ? 1 : 2;
630
+ continue;
631
+ }
632
+ const execPathWithValue = option === '--exec-path' && argument.includes('=');
633
+ if ((globalPathOptions.has(option) && option !== '--exec-path') || execPathWithValue || shortAttached) {
634
+ const value = argument.includes('=')
635
+ ? argument.slice(argument.indexOf('=') + 1)
636
+ : shortAttached
637
+ ? argument.slice(2)
638
+ : args[index + 1];
639
+ if (value)
640
+ values.push(value);
641
+ }
642
+ if (!argument.includes('=') && globalValueOptions.has(option) && !shortAttached)
643
+ index++;
644
+ index++;
645
+ }
646
+ if (!subcommand)
647
+ return values;
648
+ const rest = args.slice(index);
649
+ const delimiter = rest.indexOf('--');
650
+ const beforeDelimiter = delimiter >= 0 ? rest.slice(0, delimiter) : rest;
651
+ if (delimiter >= 0) {
652
+ values.push(...rest.slice(delimiter + 1).filter(argument => !argument.startsWith('-')));
653
+ }
654
+ const positionalValues = (argsToScan, valueOptions, attachedShortOptions = new Set()) => {
655
+ const positionals = [];
656
+ for (let cursor = 0; cursor < argsToScan.length; cursor++) {
657
+ const argument = argsToScan[cursor];
658
+ if (!argument.startsWith('-')) {
659
+ positionals.push(argument);
660
+ continue;
661
+ }
662
+ const option = argument.split('=', 1)[0];
663
+ const attached = [...attachedShortOptions].some(entry => argument.startsWith(entry) && argument.length > entry.length);
664
+ if (valueOptions.has(option) && !argument.includes('=') && !attached)
665
+ cursor++;
666
+ }
667
+ return positionals;
668
+ };
669
+ const outputSubcommands = new Set(['archive', 'diff', 'log', 'show', 'format-patch', 'range-diff', 'shortlog']);
670
+ if (outputSubcommands.has(subcommand)) {
671
+ for (let cursor = 0; cursor < beforeDelimiter.length; cursor++) {
672
+ const argument = beforeDelimiter[cursor];
673
+ const option = argument.split('=', 1)[0];
674
+ const shortOutput = argument === '-o' || (!argument.startsWith('--') && argument.startsWith('-o') && argument.length > 2);
675
+ const longOutput = option === '--output' || option === '--output-directory';
676
+ if (!shortOutput && !longOutput)
677
+ continue;
678
+ const value = argument.includes('=')
679
+ ? argument.slice(argument.indexOf('=') + 1)
680
+ : shortOutput && argument.length > 2
681
+ ? argument.slice(2)
682
+ : beforeDelimiter[cursor + 1];
683
+ if (value)
684
+ values.push(value);
685
+ if (!argument.includes('=') && !(shortOutput && argument.length > 2))
686
+ cursor++;
687
+ }
688
+ }
689
+ // These subcommands have positional filesystem targets rather than
690
+ // revision/search operands. Conservative over-collection here preserves
691
+ // H/L protection for source and destination paths alike.
692
+ const positionalPathSubcommands = new Set(['clone', 'init', 'apply', 'am']);
693
+ if (positionalPathSubcommands.has(subcommand)) {
694
+ values.push(...beforeDelimiter.filter(argument => !argument.startsWith('-')));
695
+ }
696
+ else if (subcommand === 'worktree' || subcommand === 'bundle') {
697
+ const control = subcommand === 'worktree'
698
+ ? new Set(['add', 'move', 'remove', 'prune', 'lock', 'unlock', 'repair'])
699
+ : new Set(['create', 'verify-heads', 'list-heads', 'unbundle']);
700
+ values.push(...beforeDelimiter.filter(argument => !argument.startsWith('-') && !control.has(argument)));
701
+ }
702
+ // `git diff --no-index` compares two filesystem paths without the usual
703
+ // revision/pathspec ambiguity. Capture both operands even when the caller
704
+ // omitted the `--` delimiter.
705
+ if (subcommand === 'diff' && beforeDelimiter.includes('--no-index')) {
706
+ values.push(...beforeDelimiter.filter(argument => !argument.startsWith('-')));
707
+ }
708
+ else if (subcommand === 'diff' && delimiter < 0) {
709
+ // Without `--`, a path can be parsed by Git in the same position as a
710
+ // revision. Keep likely filesystem-shaped positionals in the protection
711
+ // pass, while skipping regex/text option values.
712
+ const textValueOptions = new Set([
713
+ '-I', '--ignore-matching-lines', '--anchored', '--word-diff-regex',
714
+ '--diff-algorithm', '--submodule', '--stat-width', '--stat-name-width',
715
+ '--stat-graph-width', '--stat-count', '--inter-hunk-context',
716
+ '--output', '--output-directory',
717
+ ]);
718
+ values.push(...positionalValues(beforeDelimiter, textValueOptions, new Set(['-I']))
719
+ .filter(looksLikeLiteralShellPath));
720
+ }
721
+ else if (subcommand === 'log' && delimiter < 0) {
722
+ const textValueOptions = new Set([
723
+ '-S', '-G', '-n', '--grep', '--author', '--committer', '--since', '--after',
724
+ '--until', '--before', '--format', '--pretty', '--max-count', '--skip',
725
+ '--decorate-refs',
726
+ ]);
727
+ values.push(...positionalValues(beforeDelimiter, textValueOptions, new Set(['-S', '-G', '-n']))
728
+ .filter(looksLikeLiteralShellPath));
729
+ }
730
+ // Several mutating Git subcommands accept directory/file options instead
731
+ // of positional targets. Keep these values in the H/L operand pass so a
732
+ // protected path cannot hide behind an option spelling.
733
+ const optionPathValues = new Set([
734
+ '--directory', '--include', '--build-fake-ancestor', '--separate-git-dir',
735
+ '--template', '--reference', '--reference-if-able', '--git-dir', '--work-tree',
736
+ ]);
737
+ if (['clone', 'init', 'apply', 'am', 'format-patch'].includes(subcommand)) {
738
+ for (let cursor = 0; cursor < beforeDelimiter.length; cursor++) {
739
+ const argument = beforeDelimiter[cursor];
740
+ const option = argument.split('=', 1)[0];
741
+ if (!optionPathValues.has(option))
742
+ continue;
743
+ const value = argument.includes('=') ? argument.slice(argument.indexOf('=') + 1) : beforeDelimiter[cursor + 1];
744
+ if (value)
745
+ values.push(value);
746
+ if (!argument.includes('='))
747
+ cursor++;
748
+ }
749
+ }
750
+ return values;
751
+ }
752
+ function literalReadonlyOperands(argv) {
753
+ const executable = commandName(argv[0]);
754
+ const args = argv.slice(1);
755
+ if (['cat', 'ls', 'head', 'sort', 'wc', 'stat', 'lstat'].includes(executable)) {
756
+ const valueOptions = new Set(['-c', '--format', '--printf', '-I', '-T', '-w', '--block-size', '--color', '--format', '--hide', '--ignore', '--key', '--parallel', '--sort']);
757
+ const values = [];
758
+ for (let index = 0; index < args.length; index++) {
759
+ const argument = args[index];
760
+ if (argument === '--') {
761
+ values.push(...args.slice(index + 1));
762
+ break;
763
+ }
764
+ if (argument.startsWith('-')) {
765
+ const option = argument.split('=', 1)[0];
766
+ if (valueOptions.has(option) && !argument.includes('=') && index + 1 < args.length)
767
+ index++;
768
+ continue;
769
+ }
770
+ values.push(argument);
771
+ }
772
+ return values;
773
+ }
774
+ if (['grep', 'rg'].includes(executable)) {
775
+ const values = [];
776
+ let patternSeen = false;
777
+ const textValueOptions = new Set(['-A', '-B', '-C', '-m', '-d', '-g', '--after-context', '--before-context', '--context', '--directories', '--exclude', '--exclude-dir', '--include', '--glob', '--pre-glob']);
778
+ const patternOptions = new Set(['-e', '--regexp']);
779
+ const pathValueOptions = new Set(['-f', '--file', '--ignore-file', '--exclude-from', '--pre', '--hostname-bin']);
780
+ const shortTextValueOptions = executable === 'grep'
781
+ ? new Set(['A', 'B', 'C', 'd', 'D', 'm'])
782
+ : new Set(['A', 'B', 'C', 'E', 'g', 'j', 'm', 'M', 'r', 't', 'T']);
783
+ for (let index = 0; index < args.length; index++) {
784
+ const argument = args[index];
785
+ if (argument === '--') {
786
+ values.push(...args.slice(index + 1));
787
+ break;
788
+ }
789
+ if (argument.startsWith('-')) {
790
+ if (!argument.startsWith('--') && argument !== '-') {
791
+ const body = argument.slice(1);
792
+ let consumedValue = false;
793
+ for (let cursor = 0; cursor < body.length; cursor++) {
794
+ const option = body[cursor];
795
+ if (option !== 'e' && option !== 'f' && !shortTextValueOptions.has(option))
796
+ continue;
797
+ const attached = body.slice(cursor + 1);
798
+ const value = attached || args[index + 1];
799
+ if (option === 'e')
800
+ patternSeen = true;
801
+ if (option === 'f') {
802
+ patternSeen = true;
803
+ if (value)
804
+ values.push(value);
805
+ }
806
+ if (!attached && value)
807
+ index++;
808
+ consumedValue = true;
809
+ break;
810
+ }
811
+ if (consumedValue)
812
+ continue;
813
+ }
814
+ const option = argument.split('=', 1)[0];
815
+ if (patternOptions.has(option)) {
816
+ patternSeen = true;
817
+ if (!argument.includes('=') && index + 1 < args.length)
818
+ index++;
819
+ continue;
820
+ }
821
+ if (pathValueOptions.has(option)) {
822
+ patternSeen ||= option === '-f' || option === '--file';
823
+ const value = argument.includes('=') ? argument.slice(argument.indexOf('=') + 1) : args[++index];
824
+ if (value)
825
+ values.push(value);
826
+ continue;
827
+ }
828
+ if (textValueOptions.has(option) && !argument.includes('=') && index + 1 < args.length)
829
+ index++;
830
+ continue;
831
+ }
832
+ if (!patternSeen)
833
+ patternSeen = true;
834
+ else
835
+ values.push(argument);
836
+ }
837
+ return values;
838
+ }
839
+ if (executable === 'find') {
840
+ const values = [];
841
+ for (const argument of args) {
842
+ if (argument === '--' || argument.startsWith('-') || ['!', '(', ')'].includes(argument))
843
+ break;
844
+ values.push(argument);
845
+ }
846
+ return values;
847
+ }
848
+ if (executable === 'sed')
849
+ return literalSedOperands(args);
850
+ return [];
851
+ }
852
+ /** Extract literal path strings from an explicit node/python-style eval only
853
+ * when the script calls a filesystem API. The script itself is not a shell
854
+ * path operand; this narrow check preserves H/L protection for common
855
+ * `node -e fs.writeFileSync/readFileSync('...')` carriers without scanning
856
+ * prose or arbitrary string arguments. */
857
+ function literalScriptFilesystemOperands(argv) {
858
+ const executable = commandName(argv[0]);
859
+ if (!['node', 'nodejs', 'python', 'python3', 'perl'].includes(executable))
860
+ return [];
861
+ const args = argv.slice(1);
862
+ const evalIndex = args.findIndex(argument => argument === '-e' || argument === '--eval' || argument === '-c');
863
+ const script = evalIndex >= 0 ? args[evalIndex + 1] : undefined;
864
+ if (!script || !/(?:writeFile|appendFile|truncate|unlink|rename|mkdir|rmdir|chmod|chown|symlink|link|copyFile|createWriteStream|readFile|readdir|realpath|lstat|stat|open\s*\()/.test(script)) {
865
+ return [];
866
+ }
867
+ const values = [];
868
+ const literalRe = /(['"])([^'"\\\r\n]+)\1/g;
869
+ for (const match of script.matchAll(literalRe)) {
870
+ if (match[2])
871
+ values.push(match[2]);
872
+ }
873
+ return values;
874
+ }
875
+ /**
876
+ * Recover only literal path-looking argv values when the structured readonly
877
+ * query parser cannot prove the complete command. This is deliberately not a
878
+ * raw-text search: regexps, log prose, comments and shell syntax are ignored.
879
+ * The normal readonly path still fails closed for an unproven command; this
880
+ * helper exists only so a non-readonly H/L check can retain protection for an
881
+ * explicit literal path in an otherwise unsupported command.
882
+ */
883
+ function literalProtectedShellOperand(command, className, options, depth = 0) {
884
+ for (const segment of splitShellCommandSegments(command)) {
885
+ for (const value of literalShellRedirectOperands(segment)) {
886
+ const matched = protectedClassPath(value, className, options);
887
+ if (matched)
888
+ return matched;
889
+ }
890
+ // A segment that is itself a proven readonly query already has precise
891
+ // path operands. Reusing that IR avoids treating patterns/revisions as
892
+ // paths and also lets a valid segment coexist with a protected one.
893
+ const segmentAnalysis = analyzeReadonlyShellQuery(segment);
894
+ if (segmentAnalysis.kind === 'proven-readonly') {
895
+ for (const operand of queryPathOperands(segmentAnalysis)) {
896
+ const matched = protectedClassPath(operand.operand.value, className, options);
897
+ if (matched)
898
+ return matched;
899
+ }
900
+ continue;
901
+ }
902
+ const argv = parseLiteralShellArgv(segment);
903
+ if (!argv || argv.length < 2)
904
+ continue;
905
+ for (const value of literalMutationOperands(argv)) {
906
+ const matched = protectedClassPath(value, className, options);
907
+ if (matched)
908
+ return matched;
909
+ }
910
+ for (const value of literalReadonlyOperands(argv)) {
911
+ const matched = protectedClassPath(value, className, options);
912
+ if (matched)
913
+ return matched;
914
+ }
915
+ for (const value of literalScriptFilesystemOperands(argv)) {
916
+ const matched = protectedClassPath(value, className, options);
917
+ if (matched)
918
+ return matched;
919
+ }
920
+ // Explicit nested shell carriers are parsed recursively. A nested query
921
+ // still goes through the same segment-aware rules; arbitrary unsupported
922
+ // shell composition remains unproven and is never authorized here.
923
+ if (depth < 2) {
924
+ const executable = path.posix.basename(argv[0].replace(/\\/g, '/'));
925
+ if (['bash', 'sh', 'dash', 'zsh'].includes(executable)) {
926
+ const commandIndex = argv.findIndex(value => value === '-c' || value === '-lc');
927
+ const nested = commandIndex >= 0 ? argv[commandIndex + 1] : undefined;
928
+ if (nested) {
929
+ const matched = literalProtectedShellOperand(nested, className, options, depth + 1);
930
+ if (matched)
931
+ return matched;
932
+ }
933
+ }
934
+ }
935
+ }
936
+ return undefined;
937
+ }
415
938
  function shellOperandMatchesClass(operand, className, options, accessKind = operand.access === 'recursive' ? 'enumerate' : 'content') {
416
939
  // `$TMPDIR` is a private session namespace, so names such as `CA` or
417
940
  // `config.json` inside it are ordinary temporary data rather than paths in
@@ -484,12 +1007,17 @@ function hClassReadScopeIncludesProtectedPath(value, options) {
484
1007
  function analyzeShellProtectedOperands(command, options) {
485
1008
  const analysis = analyzeReadonlyShellQuery(command, { managedTempDir: options.managedTempDir });
486
1009
  if (analysis.kind === 'unproven') {
1010
+ const pathOptions = { cwd: options.cwd, root: options.root };
1011
+ const hClassPath = literalProtectedShellOperand(command, 'h', pathOptions);
1012
+ const lClassPath = literalProtectedShellOperand(command, 'l', pathOptions);
487
1013
  return {
488
1014
  analysis,
489
- hClass: containsHClassReference(command),
490
- lClass: containsLClassReference(command),
491
- pathHClass: false,
1015
+ hClass: !!hClassPath,
1016
+ lClass: !!lClassPath,
1017
+ pathHClass: !!hClassPath,
492
1018
  lockDiagnostic: false,
1019
+ hClassPath,
1020
+ lClassPath,
493
1021
  };
494
1022
  }
495
1023
  const operands = queryPathOperands(analysis);
@@ -1395,7 +1923,25 @@ function prepareBoundedOutputInput(input, command, kind, context) {
1395
1923
  export function evaluateToolPreflight(toolName, input, context) {
1396
1924
  if (toolName === 'Bash') {
1397
1925
  const command = typeof input.command === 'string' ? input.command : '';
1398
- const tempCheck = checkManagedTempShellCommand(command, managedTempOptions(context));
1926
+ const explicitCommandArgv = Array.isArray(input.commandArgv)
1927
+ && input.commandArgv.every(value => typeof value === 'string')
1928
+ ? input.commandArgv
1929
+ : undefined;
1930
+ if (Array.isArray(input.commandArgv) && !explicitCommandArgv) {
1931
+ return {
1932
+ behavior: 'deny',
1933
+ input,
1934
+ message: '🔒 Codex commandArgv 必须是完整的字符串 argv,无法安全解析时拒绝执行。',
1935
+ policyCode: 'ec_command_not_canonical',
1936
+ };
1937
+ }
1938
+ // When both forms are present, commandArgv is the app-server's structured
1939
+ // source of truth. Do not let a conflicting display string hide a path or
1940
+ // shell operator that is present in the argv payload.
1941
+ const commandForPolicy = explicitCommandArgv
1942
+ ? explicitCommandArgv.join(' ')
1943
+ : command;
1944
+ const tempCheck = checkManagedTempShellCommand(commandForPolicy, managedTempOptions(context));
1399
1945
  if (tempCheck.kind === 'deny') {
1400
1946
  return {
1401
1947
  behavior: 'deny',
@@ -1404,14 +1950,14 @@ export function evaluateToolPreflight(toolName, input, context) {
1404
1950
  policyCode: 'temporary_path_outside_tmpdir',
1405
1951
  };
1406
1952
  }
1407
- const explicitCommandArgv = Array.isArray(input.commandArgv)
1408
- && input.commandArgv.every(value => typeof value === 'string')
1409
- ? input.commandArgv
1410
- : undefined;
1411
1953
  const outerArgv = explicitCommandArgv ?? parseLiteralShellArgv(command) ?? undefined;
1412
- const shellCarrier = outerArgv ? resolveCodexShellCarrierArgv(outerArgv) : undefined;
1413
- const wrappedCommand = shellCarrier?.command;
1414
- const policyCommand = wrappedCommand ?? command;
1954
+ const shellCarrier = outerArgv
1955
+ ? resolveCodexShellCarrierArgv(outerArgv)
1956
+ : undefined;
1957
+ const stringShellCarrier = shellCarrier
1958
+ ?? (explicitCommandArgv ? undefined : resolveCodexShellCarrierString(command));
1959
+ const wrappedCommand = stringShellCarrier?.command;
1960
+ const policyCommand = wrappedCommand ?? commandForPolicy;
1415
1961
  if (context.sessionId && /^\s*(?:command\s+-v|which|type)\s+ec\s*$/i.test(policyCommand)) {
1416
1962
  return {
1417
1963
  behavior: 'deny',
@@ -1455,8 +2001,10 @@ export function evaluateToolPreflight(toolName, input, context) {
1455
2001
  policyCode: 'managed_ec_aid_scope_forbidden',
1456
2002
  };
1457
2003
  }
1458
- const ecCommand = classifyEvolcoreShellCommand(policyCommand, shellCarrier
1459
- ? { dialect: shellCarrier.dialect }
2004
+ const shellDialect = stringShellCarrier?.dialect
2005
+ ?? (hasCodexCmdCarrierEcIntent(input) ? 'cmd' : undefined);
2006
+ const ecCommand = classifyEvolcoreShellCommand(policyCommand, shellDialect
2007
+ ? { dialect: shellDialect }
1460
2008
  : {});
1461
2009
  if (ecCommand.kind === 'literal') {
1462
2010
  return { behavior: 'allow', input, reason: 'ec-command' };
@@ -1466,9 +2014,11 @@ export function evaluateToolPreflight(toolName, input, context) {
1466
2014
  }
1467
2015
  if (ecCommand.kind === 'composite') {
1468
2016
  if (ecCommand.issue === 'unsafe-expansion') {
1469
- const message = shellCarrier?.dialect === 'powershell'
2017
+ const message = shellDialect === 'powershell'
1470
2018
  ? '🔒 EC 双引号正文包含未转义的 PowerShell 展开;纯文字请优先使用单引号,或用 PowerShell 反引号转义 $ 和正文内的双引号。PowerShell 传给 ec 时会还原为原文字面量'
1471
- : '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量';
2019
+ : shellDialect === 'cmd'
2020
+ ? '🔒 EC 命令包含未转义的 cmd.exe 展开;请不要使用 %VAR%、!VAR! 或 ^ 转义。cmd.exe 传给 ec 时必须保持原文字面量'
2021
+ : '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量';
1472
2022
  return {
1473
2023
  behavior: 'deny',
1474
2024
  input,
@@ -1477,9 +2027,11 @@ export function evaluateToolPreflight(toolName, input, context) {
1477
2027
  };
1478
2028
  }
1479
2029
  if (ecCommand.issue === 'invalid-quote') {
1480
- const message = shellCarrier?.dialect === 'powershell'
2030
+ const message = shellDialect === 'powershell'
1481
2031
  ? '🔒 EC 命令引号未闭合或存在错误嵌套;PowerShell 正文内的双引号请用反引号转义,或改用单引号包裹纯文字'
1482
- : '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"';
2032
+ : shellDialect === 'cmd'
2033
+ ? '🔒 EC 命令引号未闭合或存在错误嵌套;cmd.exe carrier 的命令正文请保持一个完整参数,不要用未配对的双引号'
2034
+ : '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"';
1483
2035
  return {
1484
2036
  behavior: 'deny',
1485
2037
  input,
@@ -1487,9 +2039,11 @@ export function evaluateToolPreflight(toolName, input, context) {
1487
2039
  policyCode: 'ec_shell_invalid_quote',
1488
2040
  };
1489
2041
  }
1490
- const message = shellCarrier?.dialect === 'powershell'
2042
+ const message = shellDialect === 'powershell'
1491
2043
  ? '🔒 EC 命令调用被拒绝:一次 PowerShell/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要拼接探测命令,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开或子表达式追加其它命令。'
1492
- : '🔒 EC 命令调用被拒绝:一次 Bash/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要先用 `command -v`/`ec aid` 探测,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开、子 shell、`sh -c` 或 `bash -lc` 拼接其它命令。';
2044
+ : shellDialect === 'cmd'
2045
+ ? '🔒 EC 命令调用被拒绝:一次 cmd.exe/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要拼接其它命令,不要使用 &、|、重定向、变量展开或 ^ 转义。'
2046
+ : '🔒 EC 命令调用被拒绝:一次 Bash/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要先用 `command -v`/`ec aid` 探测,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开、子 shell、`sh -c` 或 `bash -lc` 拼接其它命令。';
1493
2047
  return {
1494
2048
  behavior: 'deny',
1495
2049
  input,