evolcore 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -1
- package/README.md +60 -9
- package/bin/install-codex-managed-hooks.mjs +61 -0
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +383 -111
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +405 -137
- package/dist/agents/ecagent-runner.js +174 -63
- 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 +66 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +86 -9
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +26 -11
- package/dist/aun/outbox.js +295 -68
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +1004 -273
- 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 +81 -121
- package/dist/cli/index.js +1 -0
- package/dist/cli/init.js +76 -24
- package/dist/cli/restart-monitor.js +3 -3
- 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 +226 -24
- package/dist/config/config-operation-service.js +1 -2
- 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 +29 -16
- package/dist/config/lifecycle.js +16 -5
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +161 -12
- 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 +155 -4
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +2 -2
- package/dist/core/bootstrap-service.js +27 -38
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/channel-loader.js +0 -2
- 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 +9 -4
- 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 +495 -72
- 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 +465 -56
- 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 +205 -13
- package/dist/core/permission/tool-policy.js +673 -62
- package/dist/core/relation/peer-identity.js +18 -0
- 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 +18 -9
- package/dist/index.js +283 -65
- 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/dist/utils/stats.js +52 -18
- package/dist/utils/welcome.js +2 -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/rules/01-overview.md +1 -1
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/schemas/_meta.json +10 -5
- package/kits/schemas/agent-config.schema.10.json +2 -1
- package/kits/schemas/agent-config.schema.11.json +421 -0
- package/kits/schemas/daemon.schema.5.json +135 -0
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +119 -0
- 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 +7 -3
- package/skills/eclink/SKILL.md +2 -0
|
@@ -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
|
|
@@ -425,7 +948,7 @@ function shellOperandMatchesClass(operand, className, options, accessKind = oper
|
|
|
425
948
|
const workspace = options.cwd ? resolveProtectedCandidate(options.cwd) : undefined;
|
|
426
949
|
if (isSameOrDescendant(candidate, managedTemp)
|
|
427
950
|
&& (!workspace || !isSameOrDescendant(candidate, workspace))) {
|
|
428
|
-
return
|
|
951
|
+
return undefined;
|
|
429
952
|
}
|
|
430
953
|
}
|
|
431
954
|
if (className === 'h' && accessKind === 'metadata' && operand.access === 'exact') {
|
|
@@ -439,27 +962,31 @@ function shellOperandMatchesClass(operand, className, options, accessKind = oper
|
|
|
439
962
|
followFinalSymlink: false,
|
|
440
963
|
});
|
|
441
964
|
if (options.allowProtectedMetadata === false) {
|
|
442
|
-
return metadata.pathClass === 'h-file' || metadata.pathClass === 'h-directory'
|
|
965
|
+
return metadata.pathClass === 'h-file' || metadata.pathClass === 'h-directory'
|
|
966
|
+
? resolveProtectedCandidate(operand.value, options.cwd)
|
|
967
|
+
: undefined;
|
|
443
968
|
}
|
|
444
969
|
// The no-follow decision is authoritative for metadata. In particular,
|
|
445
970
|
// an ordinary symlink whose target is H-class must remain inspectable as a
|
|
446
971
|
// link; falling through to isHClassPath() would follow it and reject the
|
|
447
972
|
// otherwise safe lstat/stat operation.
|
|
448
973
|
if (metadata.pathClass !== 'unknown' && metadata.allowed) {
|
|
449
|
-
return
|
|
974
|
+
return undefined;
|
|
450
975
|
}
|
|
451
976
|
}
|
|
452
977
|
const reference = className === 'h' ? containsHClassReference(operand.value) : containsLClassReference(operand.value);
|
|
453
978
|
if (reference)
|
|
454
|
-
return
|
|
979
|
+
return resolveProtectedCandidate(operand.value, options.cwd);
|
|
455
980
|
if (operand.access === 'recursive') {
|
|
456
|
-
|
|
981
|
+
const matched = className === 'h'
|
|
457
982
|
? hClassReadScopeIncludesProtectedPath(operand.value, options)
|
|
458
983
|
: lClassGrantIncludesProtectedPath(operand.value, options);
|
|
984
|
+
return matched ? resolveProtectedCandidate(operand.value, options.cwd) : undefined;
|
|
459
985
|
}
|
|
460
|
-
|
|
986
|
+
const matched = className === 'h'
|
|
461
987
|
? isHClassPath(operand.value, options)
|
|
462
988
|
: isLClassPath(operand.value, options);
|
|
989
|
+
return matched ? resolveProtectedCandidate(operand.value, options.cwd) : undefined;
|
|
463
990
|
}
|
|
464
991
|
/**
|
|
465
992
|
* A read-only recursive query only needs to cover H-class targets that exist
|
|
@@ -480,25 +1007,55 @@ function hClassReadScopeIncludesProtectedPath(value, options) {
|
|
|
480
1007
|
function analyzeShellProtectedOperands(command, options) {
|
|
481
1008
|
const analysis = analyzeReadonlyShellQuery(command, { managedTempDir: options.managedTempDir });
|
|
482
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);
|
|
483
1013
|
return {
|
|
484
1014
|
analysis,
|
|
485
|
-
hClass:
|
|
486
|
-
lClass:
|
|
487
|
-
pathHClass:
|
|
1015
|
+
hClass: !!hClassPath,
|
|
1016
|
+
lClass: !!lClassPath,
|
|
1017
|
+
pathHClass: !!hClassPath,
|
|
488
1018
|
lockDiagnostic: false,
|
|
1019
|
+
hClassPath,
|
|
1020
|
+
lClassPath,
|
|
489
1021
|
};
|
|
490
1022
|
}
|
|
491
1023
|
const operands = queryPathOperands(analysis);
|
|
492
|
-
|
|
493
|
-
|
|
1024
|
+
// Only parsed path operands can establish a lock-file diagnostic. Searching
|
|
1025
|
+
// the raw command would mistake a grep/rg pattern or report text for an
|
|
1026
|
+
// actual protected filesystem target.
|
|
1027
|
+
const lockDiagnostic = operands.some(({ operand }) => containsLockPathReference(operand.value))
|
|
1028
|
+
|| hasFindLockPattern(analysis);
|
|
1029
|
+
const hClassPath = operands.map(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'h', options, accessKind)).find(Boolean);
|
|
1030
|
+
const lClassPath = operands.map(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'l', options, accessKind)).find(Boolean);
|
|
494
1031
|
return {
|
|
495
1032
|
analysis,
|
|
496
|
-
hClass: lockDiagnostic ||
|
|
497
|
-
lClass:
|
|
498
|
-
pathHClass,
|
|
1033
|
+
hClass: lockDiagnostic || !!hClassPath,
|
|
1034
|
+
lClass: !!lClassPath,
|
|
1035
|
+
pathHClass: !!hClassPath,
|
|
499
1036
|
lockDiagnostic,
|
|
1037
|
+
hClassPath,
|
|
1038
|
+
lClassPath,
|
|
500
1039
|
};
|
|
501
1040
|
}
|
|
1041
|
+
/** Detect a find pattern such as `-name '*.lock'` from parsed argv only. */
|
|
1042
|
+
function hasFindLockPattern(analysis) {
|
|
1043
|
+
if (analysis.kind !== 'proven-readonly')
|
|
1044
|
+
return false;
|
|
1045
|
+
return analysis.ir.pipelines.some(pipeline => pipeline.some(command => {
|
|
1046
|
+
if (command.executable !== 'find')
|
|
1047
|
+
return false;
|
|
1048
|
+
for (let index = 0; index < command.argv.length - 1; index++) {
|
|
1049
|
+
if (!['-name', '-iname', '-path', '-ipath', '-wholename'].includes(command.argv[index]))
|
|
1050
|
+
continue;
|
|
1051
|
+
if (/(?:^|[\\/])[^\\/]*\.lock(?:$|[\\/])/i.test(command.argv[index + 1]))
|
|
1052
|
+
return true;
|
|
1053
|
+
if (/^EVOLCORE_RUNTIME_LOCK_DIR$/i.test(command.argv[index + 1]))
|
|
1054
|
+
return true;
|
|
1055
|
+
}
|
|
1056
|
+
return false;
|
|
1057
|
+
}));
|
|
1058
|
+
}
|
|
502
1059
|
function protectedReadToolPaths(toolName, input) {
|
|
503
1060
|
if (toolName === 'Read') {
|
|
504
1061
|
return typeof input.file_path === 'string' ? [input.file_path] : [];
|
|
@@ -513,33 +1070,43 @@ function protectedReadToolPaths(toolName, input) {
|
|
|
513
1070
|
}
|
|
514
1071
|
function protectedReadToolMatchesLClass(toolName, input, options) {
|
|
515
1072
|
const paths = protectedReadToolPaths(toolName, input);
|
|
516
|
-
|
|
517
|
-
|
|
1073
|
+
const direct = paths.find(filePath => containsLClassReference(filePath) || isLClassPath(filePath, options));
|
|
1074
|
+
if (direct) {
|
|
1075
|
+
return resolveProtectedCandidate(direct, options.cwd);
|
|
518
1076
|
}
|
|
519
1077
|
if (toolName === 'Grep') {
|
|
520
1078
|
const scope = typeof input.path === 'string' && input.path ? input.path : '.';
|
|
521
|
-
return lClassGrantIncludesProtectedPath(scope, options)
|
|
1079
|
+
return lClassGrantIncludesProtectedPath(scope, options)
|
|
1080
|
+
? resolveProtectedCandidate(scope, options.cwd)
|
|
1081
|
+
: undefined;
|
|
522
1082
|
}
|
|
523
1083
|
if (toolName === 'Glob') {
|
|
524
1084
|
const scope = globReadScope(input, options);
|
|
525
|
-
return lClassGrantIncludesProtectedPath(scope, { root: options.root })
|
|
1085
|
+
return lClassGrantIncludesProtectedPath(scope, { root: options.root })
|
|
1086
|
+
? resolveProtectedCandidate(scope, options.cwd)
|
|
1087
|
+
: undefined;
|
|
526
1088
|
}
|
|
527
|
-
return
|
|
1089
|
+
return undefined;
|
|
528
1090
|
}
|
|
529
1091
|
function protectedReadToolMatchesHClass(toolName, input, options) {
|
|
530
1092
|
const paths = protectedReadToolPaths(toolName, input);
|
|
531
|
-
|
|
532
|
-
|
|
1093
|
+
const direct = paths.find(filePath => containsHClassReference(filePath) || isHClassPath(filePath, options));
|
|
1094
|
+
if (direct) {
|
|
1095
|
+
return resolveProtectedCandidate(direct, options.cwd);
|
|
533
1096
|
}
|
|
534
1097
|
if (toolName === 'Grep') {
|
|
535
1098
|
const scope = typeof input.path === 'string' && input.path ? input.path : '.';
|
|
536
|
-
return hClassReadScopeIncludesProtectedPath(scope, options)
|
|
1099
|
+
return hClassReadScopeIncludesProtectedPath(scope, options)
|
|
1100
|
+
? resolveProtectedCandidate(scope, options.cwd)
|
|
1101
|
+
: undefined;
|
|
537
1102
|
}
|
|
538
1103
|
if (toolName === 'Glob') {
|
|
539
1104
|
const scope = globReadScope(input, options);
|
|
540
|
-
return hClassReadScopeIncludesProtectedPath(scope, options)
|
|
1105
|
+
return hClassReadScopeIncludesProtectedPath(scope, options)
|
|
1106
|
+
? resolveProtectedCandidate(scope, options.cwd)
|
|
1107
|
+
: undefined;
|
|
541
1108
|
}
|
|
542
|
-
return
|
|
1109
|
+
return undefined;
|
|
543
1110
|
}
|
|
544
1111
|
function globLiteralPrefix(pattern) {
|
|
545
1112
|
const normalized = pattern.replace(/\\/g, '/');
|
|
@@ -646,14 +1213,17 @@ export function checkReadonly(toolName, input, projectPath, context) {
|
|
|
646
1213
|
if (!protectedReadToolStaysInWorkspace(toolName, input, projectPath)) {
|
|
647
1214
|
return { behavior: 'deny', message: '🔒 只读模式:文件读取范围不能越出当前项目目录', policyCode: 'readonly_workspace_escape' };
|
|
648
1215
|
}
|
|
649
|
-
|
|
650
|
-
|
|
1216
|
+
const directHPath = paths.find(filePath => containsHClassReference(filePath) || isHClassPath(filePath, pathOptions));
|
|
1217
|
+
if (directHPath) {
|
|
1218
|
+
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read', matchedPath: resolveProtectedCandidate(directHPath, projectPath) };
|
|
651
1219
|
}
|
|
652
|
-
|
|
653
|
-
|
|
1220
|
+
const hPath = protectedReadToolMatchesHClass(toolName, input, pathOptions);
|
|
1221
|
+
if (hPath) {
|
|
1222
|
+
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read', matchedPath: hPath };
|
|
654
1223
|
}
|
|
655
|
-
|
|
656
|
-
|
|
1224
|
+
const lPath = protectedReadToolMatchesLClass(toolName, input, pathOptions);
|
|
1225
|
+
if (lPath) {
|
|
1226
|
+
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', policyCode: 'readonly_l_class_read', matchedPath: lPath };
|
|
657
1227
|
}
|
|
658
1228
|
return { behavior: 'allow' };
|
|
659
1229
|
}
|
|
@@ -691,11 +1261,15 @@ export function checkReadonly(toolName, input, projectPath, context) {
|
|
|
691
1261
|
}
|
|
692
1262
|
if (protectedOperands.hClass) {
|
|
693
1263
|
logger.warn(`[ReadonlyCheck] 🔒 H-class path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
694
|
-
|
|
1264
|
+
if (protectedOperands.analysis.kind === 'proven-readonly') {
|
|
1265
|
+
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read', matchedPath: protectedOperands.hClassPath };
|
|
1266
|
+
}
|
|
695
1267
|
}
|
|
696
1268
|
if (protectedOperands.lClass) {
|
|
697
1269
|
logger.warn(`[ReadonlyCheck] 🔒 L-class path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
698
|
-
|
|
1270
|
+
if (protectedOperands.analysis.kind === 'proven-readonly') {
|
|
1271
|
+
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', policyCode: 'readonly_l_class_read', matchedPath: protectedOperands.lClassPath };
|
|
1272
|
+
}
|
|
699
1273
|
}
|
|
700
1274
|
if (protectedOperands.analysis.kind === 'proven-readonly') {
|
|
701
1275
|
return { behavior: 'deny', message: '🔒 只读模式:Shell 查询范围不能越出当前项目目录', policyCode: 'readonly_workspace_escape' };
|
|
@@ -967,6 +1541,7 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
967
1541
|
return {
|
|
968
1542
|
behavior: 'deny',
|
|
969
1543
|
message: '🔒 临时文件必须位于当前会话的 $TMPDIR 内',
|
|
1544
|
+
matchedPath: tempCheck.paths[0],
|
|
970
1545
|
};
|
|
971
1546
|
}
|
|
972
1547
|
// A managed temp write is intentionally allowed to contain names such as
|
|
@@ -992,6 +1567,7 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
992
1567
|
return {
|
|
993
1568
|
behavior: 'deny',
|
|
994
1569
|
message: '🔒 Shell 命令涉及受保护的 H 类配置/证书/快照路径,agent 不可直接操作',
|
|
1570
|
+
matchedPath: protectedOperands.hClassPath,
|
|
995
1571
|
};
|
|
996
1572
|
}
|
|
997
1573
|
collectFilesystemGrantPaths(input.additionalPermissions, grantPaths, projectRootGrantSubpaths);
|
|
@@ -1010,14 +1586,17 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
1010
1586
|
// filesystem operation is checked again when it reaches Read/Write/Bash,
|
|
1011
1587
|
// FileChange, or PermissionGrant above.
|
|
1012
1588
|
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
1013
|
-
if (['Read', 'Glob', 'Grep'].includes(toolName)
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
`
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1589
|
+
if (['Read', 'Glob', 'Grep'].includes(toolName)) {
|
|
1590
|
+
const matchedReadPath = protectedReadToolMatchesHClass(toolName, input, pathOptions);
|
|
1591
|
+
if (matchedReadPath) {
|
|
1592
|
+
logger.warn(`[H-Class Protection] 🔒 Protected filesystem read scope blocked: tool=${toolName} ` +
|
|
1593
|
+
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
1594
|
+
return {
|
|
1595
|
+
behavior: 'deny',
|
|
1596
|
+
message: '🔒 文件读取范围包含受保护的 H 类配置/证书/快照路径,agent 不可直接操作',
|
|
1597
|
+
matchedPath: matchedReadPath,
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1021
1600
|
}
|
|
1022
1601
|
if (requestsFilesystemRoot(input.permissions) || requestsFilesystemRoot(input.additionalPermissions)) {
|
|
1023
1602
|
logger.warn(`[H-Class Protection] 🔒 Permission grant covers filesystem root: tool=${toolName} ` +
|
|
@@ -1036,6 +1615,7 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
1036
1615
|
return {
|
|
1037
1616
|
behavior: 'deny',
|
|
1038
1617
|
message: '🔒 请求的文件系统权限范围包含受保护的 H 类路径,已拒绝授权',
|
|
1618
|
+
matchedPath: resolveProtectedCandidate(grantPath, pathOptions.cwd),
|
|
1039
1619
|
};
|
|
1040
1620
|
}
|
|
1041
1621
|
}
|
|
@@ -1059,6 +1639,7 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
1059
1639
|
return {
|
|
1060
1640
|
behavior: 'deny',
|
|
1061
1641
|
message: '🔒 请求的 project_roots 权限范围包含受保护的 H 类路径,已拒绝授权',
|
|
1642
|
+
matchedPath: resolveProtectedCandidate(candidate, permissionWorkspace),
|
|
1062
1643
|
};
|
|
1063
1644
|
}
|
|
1064
1645
|
}
|
|
@@ -1093,7 +1674,8 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
1093
1674
|
behavior: 'deny',
|
|
1094
1675
|
message: `🔒 此文件受保护,agent 不可直接写入\n\n` +
|
|
1095
1676
|
`文件:${filePath}\n` +
|
|
1096
|
-
`类型:配置/快照/证书等系统关键文件${suggestion}
|
|
1677
|
+
`类型:配置/快照/证书等系统关键文件${suggestion}`,
|
|
1678
|
+
matchedPath: resolveProtectedCandidate(filePath, pathOptions.cwd),
|
|
1097
1679
|
};
|
|
1098
1680
|
}
|
|
1099
1681
|
}
|
|
@@ -1108,7 +1690,7 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
1108
1690
|
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
1109
1691
|
const lClassRead = protectedReadToolMatchesLClass(toolName, input, pathOptions);
|
|
1110
1692
|
if (lClassRead && context?.permissionMode === 'readonly') {
|
|
1111
|
-
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径' };
|
|
1693
|
+
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', matchedPath: lClassRead };
|
|
1112
1694
|
}
|
|
1113
1695
|
return { behavior: 'allow' };
|
|
1114
1696
|
}
|
|
@@ -1149,6 +1731,7 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
1149
1731
|
message: context?.permissionMode === 'readonly'
|
|
1150
1732
|
? '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径'
|
|
1151
1733
|
: '🔒 L-class 路径允许读取,但当前 Bash 复合命令无法证明为只读,已拒绝执行。请改用受控的 EvolCore 读取命令或白名单只读命令',
|
|
1734
|
+
matchedPath: protectedOperands.lClassPath,
|
|
1152
1735
|
};
|
|
1153
1736
|
}
|
|
1154
1737
|
collectFilesystemWriteGrantPaths(input.additionalPermissions, writeGrantPaths, projectRootWriteSubpaths);
|
|
@@ -1167,6 +1750,7 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
1167
1750
|
return {
|
|
1168
1751
|
behavior: 'deny',
|
|
1169
1752
|
message: '🔒 请求的文件系统写权限包含 L-class 只读路径,已拒绝授权',
|
|
1753
|
+
matchedPath: resolveProtectedCandidate(grantPath, pathOptions.cwd),
|
|
1170
1754
|
};
|
|
1171
1755
|
}
|
|
1172
1756
|
}
|
|
@@ -1188,6 +1772,7 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
1188
1772
|
return {
|
|
1189
1773
|
behavior: 'deny',
|
|
1190
1774
|
message: `🔒 L-class 路径允许读取但禁止 Agent 直接修改、删除或移动\n\n文件:${filePath}\n\n💡 请使用对应的 EvolCore 受控命令修改`,
|
|
1775
|
+
matchedPath: resolveProtectedCandidate(filePath, pathOptions.cwd),
|
|
1191
1776
|
};
|
|
1192
1777
|
}
|
|
1193
1778
|
return { behavior: 'allow' };
|
|
@@ -1338,7 +1923,25 @@ function prepareBoundedOutputInput(input, command, kind, context) {
|
|
|
1338
1923
|
export function evaluateToolPreflight(toolName, input, context) {
|
|
1339
1924
|
if (toolName === 'Bash') {
|
|
1340
1925
|
const command = typeof input.command === 'string' ? input.command : '';
|
|
1341
|
-
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));
|
|
1342
1945
|
if (tempCheck.kind === 'deny') {
|
|
1343
1946
|
return {
|
|
1344
1947
|
behavior: 'deny',
|
|
@@ -1347,14 +1950,14 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1347
1950
|
policyCode: 'temporary_path_outside_tmpdir',
|
|
1348
1951
|
};
|
|
1349
1952
|
}
|
|
1350
|
-
const explicitCommandArgv = Array.isArray(input.commandArgv)
|
|
1351
|
-
&& input.commandArgv.every(value => typeof value === 'string')
|
|
1352
|
-
? input.commandArgv
|
|
1353
|
-
: undefined;
|
|
1354
1953
|
const outerArgv = explicitCommandArgv ?? parseLiteralShellArgv(command) ?? undefined;
|
|
1355
|
-
const shellCarrier = outerArgv
|
|
1356
|
-
|
|
1357
|
-
|
|
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;
|
|
1358
1961
|
if (context.sessionId && /^\s*(?:command\s+-v|which|type)\s+ec\s*$/i.test(policyCommand)) {
|
|
1359
1962
|
return {
|
|
1360
1963
|
behavior: 'deny',
|
|
@@ -1398,8 +2001,10 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1398
2001
|
policyCode: 'managed_ec_aid_scope_forbidden',
|
|
1399
2002
|
};
|
|
1400
2003
|
}
|
|
1401
|
-
const
|
|
1402
|
-
?
|
|
2004
|
+
const shellDialect = stringShellCarrier?.dialect
|
|
2005
|
+
?? (hasCodexCmdCarrierEcIntent(input) ? 'cmd' : undefined);
|
|
2006
|
+
const ecCommand = classifyEvolcoreShellCommand(policyCommand, shellDialect
|
|
2007
|
+
? { dialect: shellDialect }
|
|
1403
2008
|
: {});
|
|
1404
2009
|
if (ecCommand.kind === 'literal') {
|
|
1405
2010
|
return { behavior: 'allow', input, reason: 'ec-command' };
|
|
@@ -1409,9 +2014,11 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1409
2014
|
}
|
|
1410
2015
|
if (ecCommand.kind === 'composite') {
|
|
1411
2016
|
if (ecCommand.issue === 'unsafe-expansion') {
|
|
1412
|
-
const message =
|
|
2017
|
+
const message = shellDialect === 'powershell'
|
|
1413
2018
|
? '🔒 EC 双引号正文包含未转义的 PowerShell 展开;纯文字请优先使用单引号,或用 PowerShell 反引号转义 $ 和正文内的双引号。PowerShell 传给 ec 时会还原为原文字面量'
|
|
1414
|
-
:
|
|
2019
|
+
: shellDialect === 'cmd'
|
|
2020
|
+
? '🔒 EC 命令包含未转义的 cmd.exe 展开;请不要使用 %VAR%、!VAR! 或 ^ 转义。cmd.exe 传给 ec 时必须保持原文字面量'
|
|
2021
|
+
: '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量';
|
|
1415
2022
|
return {
|
|
1416
2023
|
behavior: 'deny',
|
|
1417
2024
|
input,
|
|
@@ -1420,9 +2027,11 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1420
2027
|
};
|
|
1421
2028
|
}
|
|
1422
2029
|
if (ecCommand.issue === 'invalid-quote') {
|
|
1423
|
-
const message =
|
|
2030
|
+
const message = shellDialect === 'powershell'
|
|
1424
2031
|
? '🔒 EC 命令引号未闭合或存在错误嵌套;PowerShell 正文内的双引号请用反引号转义,或改用单引号包裹纯文字'
|
|
1425
|
-
:
|
|
2032
|
+
: shellDialect === 'cmd'
|
|
2033
|
+
? '🔒 EC 命令引号未闭合或存在错误嵌套;cmd.exe carrier 的命令正文请保持一个完整参数,不要用未配对的双引号'
|
|
2034
|
+
: '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"';
|
|
1426
2035
|
return {
|
|
1427
2036
|
behavior: 'deny',
|
|
1428
2037
|
input,
|
|
@@ -1430,9 +2039,11 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1430
2039
|
policyCode: 'ec_shell_invalid_quote',
|
|
1431
2040
|
};
|
|
1432
2041
|
}
|
|
1433
|
-
const message =
|
|
2042
|
+
const message = shellDialect === 'powershell'
|
|
1434
2043
|
? '🔒 EC 命令调用被拒绝:一次 PowerShell/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要拼接探测命令,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开或子表达式追加其它命令。'
|
|
1435
|
-
:
|
|
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` 拼接其它命令。';
|
|
1436
2047
|
return {
|
|
1437
2048
|
behavior: 'deny',
|
|
1438
2049
|
input,
|
|
@@ -1472,7 +2083,7 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1472
2083
|
allowProtectedMetadata: context.allowProtectedMetadata,
|
|
1473
2084
|
});
|
|
1474
2085
|
if (hClass.behavior === 'deny') {
|
|
1475
|
-
return { behavior: 'deny', input: checkedInput, message: hClass.message, policyCode: 'h_class_protection' };
|
|
2086
|
+
return { behavior: 'deny', input: checkedInput, message: hClass.message, policyCode: 'h_class_protection', matchedPath: hClass.matchedPath };
|
|
1476
2087
|
}
|
|
1477
2088
|
const lClass = checkLClassWrite(toolName, checkedInput, {
|
|
1478
2089
|
sessionId: context.sessionId,
|
|
@@ -1486,7 +2097,7 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1486
2097
|
managedTempDir: context.managedTempDir,
|
|
1487
2098
|
});
|
|
1488
2099
|
if (lClass.behavior === 'deny') {
|
|
1489
|
-
return { behavior: 'deny', input: checkedInput, message: lClass.message, policyCode: 'l_class_protection' };
|
|
2100
|
+
return { behavior: 'deny', input: checkedInput, message: lClass.message, policyCode: 'l_class_protection', matchedPath: lClass.matchedPath };
|
|
1490
2101
|
}
|
|
1491
2102
|
const dangerous = checkDangerousCommand(toolName, checkedInput);
|
|
1492
2103
|
if (dangerous.isDangerous
|