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.
- package/CHANGELOG.md +22 -0
- package/README.md +58 -9
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +379 -108
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +402 -135
- package/dist/agents/ecagent-runner.js +171 -61
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +182 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +409 -88
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +69 -115
- package/dist/cli/init.js +27 -15
- package/dist/cli/task-context.js +46 -0
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +16 -5
- package/dist/config/config-manager.js +135 -17
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +31 -1
- package/dist/core/auth/auth-gateway.js +33 -4
- package/dist/core/auth/authorization-audit.js +150 -2
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-service.js +6 -2
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +290 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +279 -80
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +350 -32
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +4 -0
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +10 -0
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +83 -84
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +124 -10
- package/dist/core/message/message-log.js +14 -7
- package/dist/core/message/message-queue.js +206 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +486 -68
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +410 -54
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +159 -1
- package/dist/core/permission/tool-policy.js +575 -21
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +299 -30
- package/dist/core/session/session-renew.js +19 -12
- package/dist/core/session/session-turn-coordinator.js +11 -4
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/index.js +253 -46
- package/dist/ipc.js +374 -24
- package/dist/paths.js +64 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +56 -5
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +3 -2
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +21 -2
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/schemas/_meta.json +4 -2
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/package.json +6 -3
- package/skills/eclink/SKILL.md +2 -0
- 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 '
|
|
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:
|
|
490
|
-
lClass:
|
|
491
|
-
pathHClass:
|
|
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
|
|
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
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
|
1459
|
-
?
|
|
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 =
|
|
2017
|
+
const message = shellDialect === 'powershell'
|
|
1470
2018
|
? '🔒 EC 双引号正文包含未转义的 PowerShell 展开;纯文字请优先使用单引号,或用 PowerShell 反引号转义 $ 和正文内的双引号。PowerShell 传给 ec 时会还原为原文字面量'
|
|
1471
|
-
:
|
|
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 =
|
|
2030
|
+
const message = shellDialect === 'powershell'
|
|
1481
2031
|
? '🔒 EC 命令引号未闭合或存在错误嵌套;PowerShell 正文内的双引号请用反引号转义,或改用单引号包裹纯文字'
|
|
1482
|
-
:
|
|
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 =
|
|
2042
|
+
const message = shellDialect === 'powershell'
|
|
1491
2043
|
? '🔒 EC 命令调用被拒绝:一次 PowerShell/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要拼接探测命令,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开或子表达式追加其它命令。'
|
|
1492
|
-
:
|
|
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,
|