@guilz-dev/belay 0.7.0 → 0.8.0
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/README.md +19 -19
- package/dist/adapters/shared/gate-runtime.js +23 -5
- package/dist/bundle/claude-runtime.mjs +4909 -3504
- package/dist/bundle/codex-runtime.mjs +5050 -3645
- package/dist/bundle/cursor-runtime.mjs +5050 -3645
- package/dist/commands/doctor.js +7 -0
- package/dist/commands/dogfood.js +1 -1
- package/dist/commands/explain.js +2 -2
- package/dist/commands/harvest.d.ts +0 -1
- package/dist/commands/harvest.js +1 -4
- package/dist/commands/metrics.js +6 -0
- package/dist/commands/quality.js +1 -3
- package/dist/conformance/guarantee-table.js +133 -11
- package/dist/conformance/types.d.ts +1 -0
- package/dist/core/audit-metrics.d.ts +1 -0
- package/dist/core/audit-metrics.js +12 -4
- package/dist/core/audit-types.d.ts +3 -0
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/policy-engine.d.ts +31 -1
- package/dist/core/capability/policy-engine.js +186 -0
- package/dist/core/capability/request.d.ts +9 -0
- package/dist/core/capability/resolver.d.ts +0 -1
- package/dist/core/capability/resolver.js +0 -1
- package/dist/core/config.js +0 -2
- package/dist/core/effect-ir/audit.js +1 -0
- package/dist/core/effect-ir/build.js +5 -1
- package/dist/core/effect-ir/index.d.ts +6 -1
- package/dist/core/effect-ir/index.js +2 -0
- package/dist/core/effect-ir/normalize.js +9 -3
- package/dist/core/effect-ir/policy.d.ts +2 -1
- package/dist/core/effect-ir/policy.js +93 -6
- package/dist/core/effect-ir/shell-build.d.ts +43 -0
- package/dist/core/effect-ir/shell-build.js +77 -0
- package/dist/core/effect-ir/shell-lower.d.ts +14 -0
- package/dist/core/effect-ir/shell-lower.js +1509 -0
- package/dist/core/effect-ir/types.d.ts +9 -2
- package/dist/core/gate-engine.js +63 -23
- package/dist/core/git-resource-identity.d.ts +26 -0
- package/dist/core/git-resource-identity.js +284 -0
- package/dist/core/harvest.d.ts +3 -7
- package/dist/core/harvest.js +4 -27
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +1 -0
- package/dist/core/path-utils.js +42 -23
- package/dist/core/shell-substitution.d.ts +1 -0
- package/dist/core/shell-substitution.js +61 -0
- package/dist/core/standing-allow.d.ts +1 -2
- package/dist/core/standing-allow.js +3 -20
- package/dist/core/types.d.ts +4 -1
- package/dist/core/verdict/adapter.js +15 -15
- package/dist/core/verdict/containment.js +4 -0
- package/dist/core/verdict/egress-classify.d.ts +12 -0
- package/dist/core/verdict/egress-classify.js +746 -0
- package/dist/core/verdict/git-classifier.d.ts +12 -0
- package/dist/core/verdict/git-classifier.js +320 -0
- package/dist/core/verdict/launcher-resolve.js +77 -17
- package/dist/core/verdict/parser.d.ts +2 -0
- package/dist/core/verdict/parser.js +95 -1
- package/dist/core/verdict/types.d.ts +2 -3
- package/dist/core/verdict/verdict.js +103 -952
- package/dist/corpus/must-allow-commands.d.ts +2 -1
- package/dist/corpus/must-allow-commands.js +2 -1
- package/dist/corpus/runtime-match.d.ts +2 -1
- package/dist/corpus/runtime-match.js +2 -1
- package/dist/corpus/types.d.ts +2 -2
- package/dist/templates.js +7 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/dist/core/verdict/overrides.d.ts +0 -7
- package/dist/core/verdict/overrides.js +0 -37
- package/dist/core/verdict/shell-policy.d.ts +0 -26
- package/dist/core/verdict/shell-policy.js +0 -40
- package/dist/corpus/standing-allow-catalog.generated.d.ts +0 -13
- package/dist/corpus/standing-allow-catalog.generated.js +0 -99
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ShellEffectRequirement } from '../effect-ir/shell-build.js';
|
|
1
2
|
import type { VerdictEffect } from './types.js';
|
|
2
3
|
export interface NormalizedGitInvocation {
|
|
3
4
|
subcommand: string;
|
|
@@ -21,6 +22,17 @@ export interface GitCommandSemantics {
|
|
|
21
22
|
normalizedKey: string;
|
|
22
23
|
isReadOnly: boolean;
|
|
23
24
|
}
|
|
25
|
+
export interface DecodeGitEffectsParams {
|
|
26
|
+
tokens: string[];
|
|
27
|
+
cwd: string;
|
|
28
|
+
repoRoot: string;
|
|
29
|
+
segment: string;
|
|
30
|
+
}
|
|
24
31
|
export declare function normalizeGitInvocation(tokens: string[], baseCwd: string): NormalizedGitInvocation | null;
|
|
25
32
|
export declare function isGitReadOnlyInvocation(tokens: string[], baseCwd: string): boolean;
|
|
26
33
|
export declare function classifyGitCommand(tokens: string[], baseCwd: string): GitCommandSemantics | null;
|
|
34
|
+
/**
|
|
35
|
+
* Decode git CLI grammar to typed effects without making an authorization
|
|
36
|
+
* decision. Legacy classification remains separate until the authority cutover.
|
|
37
|
+
*/
|
|
38
|
+
export declare function decodeGitEffects(params: DecodeGitEffectsParams): ShellEffectRequirement[] | null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { parseNetworkEndpoint } from '../network-endpoint.js';
|
|
2
3
|
import { extractRedirectTargets } from '../shell-tokenizer.js';
|
|
3
4
|
const GIT_BRANCH_MUTATION_FLAGS = new Set([
|
|
4
5
|
'--copy',
|
|
@@ -531,3 +532,322 @@ export function classifyGitCommand(tokens, baseCwd) {
|
|
|
531
532
|
isReadOnly: false,
|
|
532
533
|
};
|
|
533
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* Decode git CLI grammar to typed effects without making an authorization
|
|
537
|
+
* decision. Legacy classification remains separate until the authority cutover.
|
|
538
|
+
*/
|
|
539
|
+
export function decodeGitEffects(params) {
|
|
540
|
+
const normalized = normalizeGitInvocation(params.tokens, params.cwd);
|
|
541
|
+
if (!normalized) {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
const { subcommand, args } = normalized;
|
|
545
|
+
const signals = [
|
|
546
|
+
`git.${subcommand.replaceAll(' ', '.')}`,
|
|
547
|
+
...(subcommand === 'push' ? ['tier0_external'] : []),
|
|
548
|
+
];
|
|
549
|
+
const effectiveCwd = normalized.effectiveCwd ?? params.cwd;
|
|
550
|
+
const workTreeRoot = normalized.workTree
|
|
551
|
+
? path.resolve(normalized.effectiveCwd ?? params.cwd, normalized.workTree)
|
|
552
|
+
: (normalized.effectiveCwd ?? params.repoRoot);
|
|
553
|
+
const gitRefRoot = normalized.gitDir
|
|
554
|
+
? path.resolve(effectiveCwd, normalized.gitDir)
|
|
555
|
+
: workTreeRoot;
|
|
556
|
+
const gitControlRoot = normalized.gitDir ? gitRefRoot : path.join(gitRefRoot, '.git');
|
|
557
|
+
const requirements = [];
|
|
558
|
+
if (subcommand === 'fetch' || subcommand === 'pull') {
|
|
559
|
+
const positionals = gitRemotePositionals(args);
|
|
560
|
+
if (subcommand === 'fetch' && args.includes('--all')) {
|
|
561
|
+
return [
|
|
562
|
+
gitRequirement('network.connect', 'network.connect', {
|
|
563
|
+
kind: 'network',
|
|
564
|
+
host: 'unknown',
|
|
565
|
+
protocol: 'git-remote',
|
|
566
|
+
mode: 'read',
|
|
567
|
+
payload: 'none',
|
|
568
|
+
}, params.segment, [...signals, 'git.fetch_all_remotes_unknown']),
|
|
569
|
+
gitRequirement('git.ref.write', 'git.ref.write', { kind: 'git-ref', ref: 'refs/remotes/*', scope: 'local', repoPath: gitRefRoot }, params.segment, [...signals, 'git.ref.local']),
|
|
570
|
+
gitRequirement('indeterminate', 'indeterminate', { kind: 'unknown' }, params.segment, [
|
|
571
|
+
...signals,
|
|
572
|
+
'git.fetch_all_remotes_unknown',
|
|
573
|
+
]),
|
|
574
|
+
];
|
|
575
|
+
}
|
|
576
|
+
const multiple = subcommand === 'fetch' && args.includes('--multiple');
|
|
577
|
+
const remoteTokens = multiple ? positionals : [positionals[0]];
|
|
578
|
+
for (const remoteToken of remoteTokens) {
|
|
579
|
+
const remote = gitRemoteResource(remoteToken);
|
|
580
|
+
requirements.push(gitRequirement('network.connect', 'network.connect', {
|
|
581
|
+
kind: 'network',
|
|
582
|
+
...remote.endpoint,
|
|
583
|
+
mode: 'read',
|
|
584
|
+
payload: 'none',
|
|
585
|
+
}, params.segment, [...signals, 'git.network.read']), gitRequirement('git.ref.write', 'git.ref.write', {
|
|
586
|
+
kind: 'git-ref',
|
|
587
|
+
ref: remote.ref ? `refs/remotes/${remote.ref}/*` : 'refs/remotes/*',
|
|
588
|
+
scope: 'local',
|
|
589
|
+
repoPath: gitRefRoot,
|
|
590
|
+
}, params.segment, [...signals, 'git.ref.local']));
|
|
591
|
+
}
|
|
592
|
+
const hazardousFetchSignals = gitFetchControlSignals(params.tokens, args, remoteTokens);
|
|
593
|
+
if (hazardousFetchSignals.length > 0) {
|
|
594
|
+
requirements.push(gitRequirement('indeterminate', 'indeterminate', { kind: 'unknown' }, params.segment, [
|
|
595
|
+
...signals,
|
|
596
|
+
...hazardousFetchSignals,
|
|
597
|
+
]));
|
|
598
|
+
}
|
|
599
|
+
if (multiple && positionals.length === 0) {
|
|
600
|
+
requirements.push(gitRequirement('indeterminate', 'indeterminate', { kind: 'unknown' }, params.segment, [
|
|
601
|
+
...signals,
|
|
602
|
+
'git.fetch_multiple_remote_missing',
|
|
603
|
+
]));
|
|
604
|
+
}
|
|
605
|
+
if (subcommand === 'pull') {
|
|
606
|
+
requirements.push(gitRequirement('fs.write', 'fs.write', { kind: 'path', path: workTreeRoot }, params.segment, [...signals, 'git.worktree.update']));
|
|
607
|
+
}
|
|
608
|
+
return requirements;
|
|
609
|
+
}
|
|
610
|
+
if (subcommand === 'push') {
|
|
611
|
+
const positionals = gitRemotePositionals(args);
|
|
612
|
+
const repositoryOption = gitOptionValue(args, '--repo');
|
|
613
|
+
const remote = gitRemoteResource(repositoryOption.value ?? positionals[0]);
|
|
614
|
+
const refs = repositoryOption.value ? positionals : positionals.slice(1);
|
|
615
|
+
const lowered = [
|
|
616
|
+
gitRequirement('network.connect', 'network.connect', {
|
|
617
|
+
kind: 'network',
|
|
618
|
+
...remote.endpoint,
|
|
619
|
+
mode: 'mutate',
|
|
620
|
+
payload: 'present',
|
|
621
|
+
}, params.segment, [...signals, 'git.network.mutate']),
|
|
622
|
+
];
|
|
623
|
+
for (const ref of refs.length > 0 ? refs : ['*']) {
|
|
624
|
+
lowered.push(gitRequirement('git.ref.write', 'git.ref.write', {
|
|
625
|
+
kind: 'git-ref',
|
|
626
|
+
ref: ref.startsWith('refs/') ? ref : `refs/heads/${ref}`,
|
|
627
|
+
scope: 'remote',
|
|
628
|
+
}, params.segment, [...signals, 'git.ref.remote']));
|
|
629
|
+
}
|
|
630
|
+
if (repositoryOption.missing) {
|
|
631
|
+
lowered.push(gitRequirement('indeterminate', 'indeterminate', { kind: 'unknown' }, params.segment, [
|
|
632
|
+
...signals,
|
|
633
|
+
'git.push_repository_missing',
|
|
634
|
+
]));
|
|
635
|
+
}
|
|
636
|
+
return lowered;
|
|
637
|
+
}
|
|
638
|
+
if (subcommand === 'reflog') {
|
|
639
|
+
const operation = args[0];
|
|
640
|
+
const mutationFlags = [
|
|
641
|
+
'--expire',
|
|
642
|
+
'--expire-unreachable',
|
|
643
|
+
'--rewrite',
|
|
644
|
+
'--stale-fix',
|
|
645
|
+
'--updateref',
|
|
646
|
+
];
|
|
647
|
+
const mutating = operation === 'expire' ||
|
|
648
|
+
operation === 'delete' ||
|
|
649
|
+
operation === 'drop' ||
|
|
650
|
+
args.some((arg) => mutationFlags.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
|
|
651
|
+
if (mutating) {
|
|
652
|
+
return [
|
|
653
|
+
gitRequirement('process.exec', 'process.exec', { kind: 'executable', command: 'git', operation: 'spawn' }, params.segment, [...signals, 'git_history_destructive', 'git.reflog.mutate']),
|
|
654
|
+
gitRequirement('control_plane.write', 'control_plane.write', { kind: 'path', path: path.join(gitControlRoot, 'logs') }, params.segment, [...signals, 'git_history_destructive', 'git.reflog.mutate']),
|
|
655
|
+
];
|
|
656
|
+
}
|
|
657
|
+
if (isReadOnlyReflogInvocation(args)) {
|
|
658
|
+
return [
|
|
659
|
+
gitRequirement('process.exec', 'process.exec', { kind: 'executable', command: 'git', operation: 'inspect' }, params.segment, [...signals, 'git.inspect']),
|
|
660
|
+
gitRequirement('fs.read', 'fs.read', { kind: 'path', path: workTreeRoot }, params.segment, [
|
|
661
|
+
...signals,
|
|
662
|
+
'git.repository.read',
|
|
663
|
+
]),
|
|
664
|
+
];
|
|
665
|
+
}
|
|
666
|
+
return [
|
|
667
|
+
gitRequirement('process.exec', 'process.exec', { kind: 'executable', command: 'git', operation: 'spawn' }, params.segment, signals),
|
|
668
|
+
gitRequirement('indeterminate', 'indeterminate', { kind: 'unknown' }, params.segment, [
|
|
669
|
+
...signals,
|
|
670
|
+
'git.reflog.grammar_incomplete',
|
|
671
|
+
]),
|
|
672
|
+
];
|
|
673
|
+
}
|
|
674
|
+
const semantics = classifyGitCommand(params.tokens, params.cwd);
|
|
675
|
+
if (!semantics) {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
if (semantics.isReadOnly) {
|
|
679
|
+
requirements.push(gitRequirement('process.exec', 'process.exec', { kind: 'executable', command: 'git', operation: 'inspect' }, params.segment, [...signals, 'git.inspect']), gitRequirement('fs.read', 'fs.read', { kind: 'path', path: workTreeRoot }, params.segment, [
|
|
680
|
+
...signals,
|
|
681
|
+
'git.repository.read',
|
|
682
|
+
]));
|
|
683
|
+
for (const operand of semantics.pathTargets) {
|
|
684
|
+
requirements.push(gitRequirement('fs.read', 'fs.read', { kind: 'path', path: path.resolve(workTreeRoot, operand) }, params.segment, [...signals, 'git.path.read']));
|
|
685
|
+
}
|
|
686
|
+
return requirements;
|
|
687
|
+
}
|
|
688
|
+
if (semantics.effect === 'local_mutation') {
|
|
689
|
+
requirements.push(gitRequirement('process.exec', 'process.exec', { kind: 'executable', command: 'git', operation: 'spawn' }, params.segment, signals), gitRequirement('git.ref.write', 'git.ref.write', {
|
|
690
|
+
kind: 'git-ref',
|
|
691
|
+
ref: localGitRef(subcommand, args),
|
|
692
|
+
scope: 'local',
|
|
693
|
+
repoPath: gitRefRoot,
|
|
694
|
+
}, params.segment, [...signals, 'git.ref.local']));
|
|
695
|
+
for (const operand of semantics.pathTargets) {
|
|
696
|
+
requirements.push(gitRequirement('fs.write', 'fs.write', { kind: 'path', path: path.resolve(workTreeRoot, operand) }, params.segment, [...signals, 'git.path.write']));
|
|
697
|
+
}
|
|
698
|
+
if (semantics.requiresAsk) {
|
|
699
|
+
requirements.push(gitRequirement('fs.write', 'fs.write', { kind: 'path', path: workTreeRoot }, params.segment, [...signals, ...semantics.requiresAsk.signals, 'git.destructive_worktree_write']), gitRequirement('control_plane.write', 'control_plane.write', { kind: 'path', path: gitControlRoot }, params.segment, [...signals, ...semantics.requiresAsk.signals, 'git.destructive_history_write']));
|
|
700
|
+
}
|
|
701
|
+
return requirements;
|
|
702
|
+
}
|
|
703
|
+
return [
|
|
704
|
+
gitRequirement('process.exec', 'process.exec', { kind: 'executable', command: 'git', operation: 'spawn' }, params.segment, signals),
|
|
705
|
+
gitRequirement('indeterminate', 'indeterminate', { kind: 'unknown' }, params.segment, [
|
|
706
|
+
...signals,
|
|
707
|
+
'git.grammar_incomplete',
|
|
708
|
+
]),
|
|
709
|
+
];
|
|
710
|
+
}
|
|
711
|
+
function isReadOnlyReflogInvocation(args) {
|
|
712
|
+
if (args[0] === 'exists') {
|
|
713
|
+
return args.length === 2 && Boolean(args[1]) && !(args[1] ?? '').startsWith('-');
|
|
714
|
+
}
|
|
715
|
+
let index = args[0] === 'show' || args[0] === 'list' ? 1 : 0;
|
|
716
|
+
let positionalCount = 0;
|
|
717
|
+
while (index < args.length) {
|
|
718
|
+
const arg = args[index] ?? '';
|
|
719
|
+
if (arg === '--all' || /^-\d+$/.test(arg)) {
|
|
720
|
+
index += 1;
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (arg === '-n' || arg === '--max-count') {
|
|
724
|
+
const count = args[index + 1];
|
|
725
|
+
if (!count || !/^\d+$/.test(count)) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
index += 2;
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (/^-n\d+$/.test(arg) || /^--(?:max-count|date|format|pretty)=.+/.test(arg)) {
|
|
732
|
+
index += 1;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (arg.startsWith('-')) {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
positionalCount += 1;
|
|
739
|
+
if (args[0] !== 'show' && args[0] !== 'list') {
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
if (positionalCount > 1) {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
index += 1;
|
|
746
|
+
}
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
const GIT_REMOTE_OPTIONS_WITH_VALUES = new Set([
|
|
750
|
+
'--depth',
|
|
751
|
+
'--exec',
|
|
752
|
+
'--filter',
|
|
753
|
+
'--jobs',
|
|
754
|
+
'--negotiation-tip',
|
|
755
|
+
'--push-option',
|
|
756
|
+
'--repo',
|
|
757
|
+
'--receive-pack',
|
|
758
|
+
'--recurse-submodules',
|
|
759
|
+
'--server-option',
|
|
760
|
+
'--shallow-exclude',
|
|
761
|
+
'--shallow-since',
|
|
762
|
+
'--upload-pack',
|
|
763
|
+
'-j',
|
|
764
|
+
'-o',
|
|
765
|
+
]);
|
|
766
|
+
function gitFetchControlSignals(tokens, args, remoteTokens) {
|
|
767
|
+
const signals = [];
|
|
768
|
+
if (tokens.some((token) => token === '-c' ||
|
|
769
|
+
(token.startsWith('-c') && token.length > 2) ||
|
|
770
|
+
token === '--config-env' ||
|
|
771
|
+
token.startsWith('--config-env=') ||
|
|
772
|
+
token === '--exec-path' ||
|
|
773
|
+
token.startsWith('--exec-path='))) {
|
|
774
|
+
signals.push('git.fetch.global_execution_override');
|
|
775
|
+
}
|
|
776
|
+
if (args.some((arg) => ['--exec', '--server-option', '--upload-pack'].some((option) => arg === option || arg.startsWith(`${option}=`)))) {
|
|
777
|
+
signals.push('git.fetch.caller_controlled_execution_or_payload');
|
|
778
|
+
}
|
|
779
|
+
if (remoteTokens.some((remote) => remote?.startsWith('ext::'))) {
|
|
780
|
+
signals.push('git.fetch.external_remote_helper');
|
|
781
|
+
}
|
|
782
|
+
return signals;
|
|
783
|
+
}
|
|
784
|
+
function gitOptionValue(args, option) {
|
|
785
|
+
const exactIndex = args.indexOf(option);
|
|
786
|
+
if (exactIndex >= 0) {
|
|
787
|
+
const value = args[exactIndex + 1];
|
|
788
|
+
return value && !value.startsWith('-') ? { value, missing: false } : { missing: true };
|
|
789
|
+
}
|
|
790
|
+
const prefix = `${option}=`;
|
|
791
|
+
const inline = args.find((arg) => arg.startsWith(prefix));
|
|
792
|
+
if (inline !== undefined) {
|
|
793
|
+
const value = inline.slice(prefix.length);
|
|
794
|
+
return value ? { value, missing: false } : { missing: true };
|
|
795
|
+
}
|
|
796
|
+
return { missing: false };
|
|
797
|
+
}
|
|
798
|
+
function gitRemotePositionals(args) {
|
|
799
|
+
const positionals = [];
|
|
800
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
801
|
+
const arg = args[index] ?? '';
|
|
802
|
+
if (arg === '--') {
|
|
803
|
+
positionals.push(...args.slice(index + 1));
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
if (arg.startsWith('-')) {
|
|
807
|
+
if (GIT_REMOTE_OPTIONS_WITH_VALUES.has(arg) && !arg.includes('=')) {
|
|
808
|
+
index += args[index + 1] ? 1 : 0;
|
|
809
|
+
}
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
positionals.push(arg);
|
|
813
|
+
}
|
|
814
|
+
return positionals;
|
|
815
|
+
}
|
|
816
|
+
function gitRemoteResource(remoteToken) {
|
|
817
|
+
const token = remoteToken && !remoteToken.startsWith('-') ? remoteToken : undefined;
|
|
818
|
+
const endpoint = token
|
|
819
|
+
? parseNetworkEndpoint(token, { allowHostedGitShorthand: true, allowScpStyle: true })
|
|
820
|
+
: null;
|
|
821
|
+
if (endpoint) {
|
|
822
|
+
return { endpoint };
|
|
823
|
+
}
|
|
824
|
+
const ref = token && !token.includes('/') ? token : undefined;
|
|
825
|
+
return {
|
|
826
|
+
endpoint: { host: ref ?? 'origin', protocol: 'git-remote' },
|
|
827
|
+
...(ref ? { ref } : {}),
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function localGitRef(subcommand, args) {
|
|
831
|
+
if (subcommand === 'branch') {
|
|
832
|
+
const name = args.find((arg) => arg && !arg.startsWith('-'));
|
|
833
|
+
return name ? `refs/heads/${name}` : 'refs/heads/*';
|
|
834
|
+
}
|
|
835
|
+
if (subcommand === 'tag') {
|
|
836
|
+
const name = args.find((arg) => arg && !arg.startsWith('-'));
|
|
837
|
+
return name ? `refs/tags/${name}` : 'refs/tags/*';
|
|
838
|
+
}
|
|
839
|
+
return 'refs/local/*';
|
|
840
|
+
}
|
|
841
|
+
function gitRequirement(tag, action, resource, segment, signals) {
|
|
842
|
+
return {
|
|
843
|
+
tag,
|
|
844
|
+
action,
|
|
845
|
+
resource,
|
|
846
|
+
evidence: {
|
|
847
|
+
level: tag === 'indeterminate' ? 'indeterminate' : 'certain',
|
|
848
|
+
signals: [...new Set(signals)].sort(),
|
|
849
|
+
basis: ['git_grammar'],
|
|
850
|
+
},
|
|
851
|
+
provenance: { segment },
|
|
852
|
+
};
|
|
853
|
+
}
|
|
@@ -123,32 +123,47 @@ function resolveNpmRecipe(cwd, repoRoot, scriptName, extraArgs) {
|
|
|
123
123
|
if (!scripts || typeof scripts !== 'object') {
|
|
124
124
|
return { recipes: [], opaque: true, reason: 'package_scripts_missing' };
|
|
125
125
|
}
|
|
126
|
-
const
|
|
126
|
+
const scriptMap = scripts;
|
|
127
|
+
const recipe = scriptMap[scriptName];
|
|
127
128
|
if (!recipe || typeof recipe !== 'string') {
|
|
128
129
|
if (/deploy|publish|release|ship|prod/i.test(scriptName)) {
|
|
129
130
|
return { recipes: [], opaque: true, reason: 'external_script' };
|
|
130
131
|
}
|
|
131
132
|
return { recipes: [], opaque: true, reason: 'npm_script_undefined' };
|
|
132
133
|
}
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
const lifecycleRecipes = [
|
|
135
|
+
scriptMap[`pre${scriptName}`],
|
|
136
|
+
applyForwardedArgs(recipe, extraArgs),
|
|
137
|
+
scriptMap[`post${scriptName}`],
|
|
138
|
+
].filter((entry) => typeof entry === 'string' && entry.trim().length > 0);
|
|
139
|
+
if (lifecycleRecipes.some((entry) => /\$\(/.test(entry) || /\$\{/.test(entry))) {
|
|
140
|
+
return { recipes: lifecycleRecipes, opaque: true, reason: 'npm_script_dynamic' };
|
|
135
141
|
}
|
|
136
142
|
return {
|
|
137
|
-
recipes:
|
|
143
|
+
recipes: lifecycleRecipes,
|
|
138
144
|
opaque: false,
|
|
139
145
|
reason: 'npm_script_resolved',
|
|
140
146
|
};
|
|
141
147
|
}
|
|
142
148
|
function parseMakefileRecipes(makefilePath) {
|
|
143
|
-
const
|
|
149
|
+
const targets = new Map();
|
|
144
150
|
try {
|
|
145
151
|
const content = readFileSync(makefilePath, 'utf8');
|
|
146
152
|
const lines = content.split('\n');
|
|
147
153
|
let currentTarget = null;
|
|
148
154
|
let recipeLines = [];
|
|
149
155
|
const flush = () => {
|
|
150
|
-
if (currentTarget
|
|
151
|
-
|
|
156
|
+
if (currentTarget) {
|
|
157
|
+
const current = targets.get(currentTarget) ?? {
|
|
158
|
+
prerequisites: [],
|
|
159
|
+
recipes: [],
|
|
160
|
+
opaquePrerequisites: false,
|
|
161
|
+
};
|
|
162
|
+
targets.set(currentTarget, {
|
|
163
|
+
prerequisites: current.prerequisites,
|
|
164
|
+
recipes: recipeLines.map((line) => line.trim()).filter((line) => line.length > 0),
|
|
165
|
+
opaquePrerequisites: current.opaquePrerequisites,
|
|
166
|
+
});
|
|
152
167
|
}
|
|
153
168
|
currentTarget = null;
|
|
154
169
|
recipeLines = [];
|
|
@@ -157,13 +172,26 @@ function parseMakefileRecipes(makefilePath) {
|
|
|
157
172
|
if (line.trim().startsWith('#')) {
|
|
158
173
|
continue;
|
|
159
174
|
}
|
|
160
|
-
const targetMatch = /^([A-Za-z0-9_.-]+)\s*:(?!=)/.exec(line);
|
|
175
|
+
const targetMatch = /^([A-Za-z0-9_.-]+)\s*:(?!=)\s*([^#]*)/.exec(line);
|
|
161
176
|
if (targetMatch) {
|
|
162
177
|
flush();
|
|
163
178
|
currentTarget = targetMatch[1] ?? null;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
179
|
+
if (currentTarget) {
|
|
180
|
+
const targetBody = (targetMatch[2] ?? '').trim();
|
|
181
|
+
const inlineRecipeIndex = targetBody.indexOf(';');
|
|
182
|
+
const prerequisiteText = inlineRecipeIndex >= 0 ? targetBody.slice(0, inlineRecipeIndex).trim() : targetBody;
|
|
183
|
+
const inlineRecipe = inlineRecipeIndex >= 0 ? targetBody.slice(inlineRecipeIndex + 1).trim() : '';
|
|
184
|
+
const prerequisiteTokens = prerequisiteText
|
|
185
|
+
.split(/\s+/)
|
|
186
|
+
.filter((token) => token && token !== '|');
|
|
187
|
+
targets.set(currentTarget, {
|
|
188
|
+
prerequisites: prerequisiteTokens.filter((token) => !token.includes('$(') && !token.includes('${')),
|
|
189
|
+
recipes: [],
|
|
190
|
+
opaquePrerequisites: prerequisiteTokens.some((token) => token.includes('$(') || token.includes('${')),
|
|
191
|
+
});
|
|
192
|
+
if (inlineRecipe) {
|
|
193
|
+
recipeLines.push(inlineRecipe);
|
|
194
|
+
}
|
|
167
195
|
}
|
|
168
196
|
continue;
|
|
169
197
|
}
|
|
@@ -174,9 +202,9 @@ function parseMakefileRecipes(makefilePath) {
|
|
|
174
202
|
flush();
|
|
175
203
|
}
|
|
176
204
|
catch {
|
|
177
|
-
return
|
|
205
|
+
return targets;
|
|
178
206
|
}
|
|
179
|
-
return
|
|
207
|
+
return targets;
|
|
180
208
|
}
|
|
181
209
|
function resolveMakeRecipe(cwd, repoRoot, target) {
|
|
182
210
|
const candidates = ['Makefile', 'makefile', 'GNUmakefile'];
|
|
@@ -199,16 +227,48 @@ function resolveMakeRecipe(cwd, repoRoot, target) {
|
|
|
199
227
|
if (!makefilePath) {
|
|
200
228
|
return { recipes: [], opaque: true, reason: 'unknown_local_effect' };
|
|
201
229
|
}
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
if (!recipeLines || recipeLines.length === 0) {
|
|
230
|
+
const targets = parseMakefileRecipes(makefilePath);
|
|
231
|
+
if (!targets.has(target)) {
|
|
205
232
|
return { recipes: [], opaque: true, reason: 'make_target_undefined' };
|
|
206
233
|
}
|
|
234
|
+
const recipeLines = [];
|
|
235
|
+
const visiting = new Set();
|
|
236
|
+
const visited = new Set();
|
|
237
|
+
let opaquePrerequisites = false;
|
|
238
|
+
const collect = (name) => {
|
|
239
|
+
if (visited.has(name)) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
if (visiting.has(name)) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
const entry = targets.get(name);
|
|
246
|
+
if (!entry) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
visiting.add(name);
|
|
250
|
+
opaquePrerequisites ||= entry.opaquePrerequisites;
|
|
251
|
+
for (const prerequisite of entry.prerequisites) {
|
|
252
|
+
if (targets.has(prerequisite) && !collect(prerequisite)) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
recipeLines.push(...entry.recipes);
|
|
257
|
+
visiting.delete(name);
|
|
258
|
+
visited.add(name);
|
|
259
|
+
return true;
|
|
260
|
+
};
|
|
261
|
+
if (!collect(target)) {
|
|
262
|
+
return { recipes: recipeLines, opaque: true, reason: 'make_dependency_cycle' };
|
|
263
|
+
}
|
|
207
264
|
for (const line of recipeLines) {
|
|
208
265
|
if (/\$\(/.test(line) || /\$\{/.test(line)) {
|
|
209
|
-
return { recipes:
|
|
266
|
+
return { recipes: recipeLines, opaque: true, reason: 'make_recipe_dynamic' };
|
|
210
267
|
}
|
|
211
268
|
}
|
|
269
|
+
if (opaquePrerequisites) {
|
|
270
|
+
return { recipes: recipeLines, opaque: true, reason: 'make_prerequisite_dynamic' };
|
|
271
|
+
}
|
|
212
272
|
return { recipes: recipeLines, opaque: false, reason: 'make_recipe_resolved' };
|
|
213
273
|
}
|
|
214
274
|
export function resolveLauncherRecipe(params) {
|
|
@@ -15,7 +15,9 @@ export declare function extractEvalBody(tokens: string[]): string | null;
|
|
|
15
15
|
export declare function extractRecursiveScript(tokens: string[]): string | null;
|
|
16
16
|
export declare function isBareInterpreter(tokens: string[]): boolean;
|
|
17
17
|
export declare function splitTopLevelSegments(command: string): string[];
|
|
18
|
+
export declare function splitStructuralShellSegments(command: string): string[];
|
|
18
19
|
export declare function parseSegment(command: string): ParsedSegment;
|
|
19
20
|
export declare function segmentOpacity(command: string): VerdictOpacity;
|
|
20
21
|
export declare function substitutionInners(command: string): string[];
|
|
22
|
+
export declare function structuralSubstitutionInners(command: string): string[];
|
|
21
23
|
export declare function redactCommand(command: string): string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { findCommandSubstitutions } from '../shell-substitution.js';
|
|
2
|
+
import { findCommandSubstitutions, findStructuralCommandSubstitutions, } from '../shell-substitution.js';
|
|
3
3
|
import { commandKey, tokenizeShell } from '../shell-tokenizer.js';
|
|
4
4
|
import { detectUnparseableShell } from '../shell-unparseable.js';
|
|
5
5
|
const ENV_PREFIX_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S+)$/;
|
|
@@ -174,6 +174,97 @@ export function splitTopLevelSegments(command) {
|
|
|
174
174
|
flush();
|
|
175
175
|
return segments.filter((segment) => segment.trim().length > 0);
|
|
176
176
|
}
|
|
177
|
+
export function splitStructuralShellSegments(command) {
|
|
178
|
+
const segments = [];
|
|
179
|
+
let current = '';
|
|
180
|
+
const outer = { quote: null, escaping: false, backtick: false };
|
|
181
|
+
const substitutions = [];
|
|
182
|
+
const flush = () => {
|
|
183
|
+
const segment = current.trim();
|
|
184
|
+
if (segment) {
|
|
185
|
+
segments.push(segment);
|
|
186
|
+
}
|
|
187
|
+
current = '';
|
|
188
|
+
};
|
|
189
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
190
|
+
const char = command[index] ?? '';
|
|
191
|
+
const next = command[index + 1] ?? '';
|
|
192
|
+
const frame = substitutions.at(-1) ?? outer;
|
|
193
|
+
if (frame.escaping) {
|
|
194
|
+
current += char;
|
|
195
|
+
frame.escaping = false;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (char === '\\' && frame.quote !== "'") {
|
|
199
|
+
current += char;
|
|
200
|
+
frame.escaping = true;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (frame.quote === "'") {
|
|
204
|
+
current += char;
|
|
205
|
+
if (char === "'") {
|
|
206
|
+
frame.quote = null;
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (char === '`') {
|
|
211
|
+
frame.backtick = !frame.backtick;
|
|
212
|
+
current += char;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (frame.backtick) {
|
|
216
|
+
current += char;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (char === '$' && next === '(') {
|
|
220
|
+
current += '$(';
|
|
221
|
+
substitutions.push({ quote: null, escaping: false, backtick: false });
|
|
222
|
+
index += 1;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (frame.quote) {
|
|
226
|
+
current += char;
|
|
227
|
+
if (char === frame.quote) {
|
|
228
|
+
frame.quote = null;
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (char === "'" || char === '"') {
|
|
233
|
+
frame.quote = char;
|
|
234
|
+
current += char;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (substitutions.length > 0 && char === ')') {
|
|
238
|
+
substitutions.pop();
|
|
239
|
+
current += char;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (substitutions.length > 0) {
|
|
243
|
+
current += char;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (char === '&' &&
|
|
247
|
+
(command[index - 1] === '>' || command[index - 1] === '<' || next === '>')) {
|
|
248
|
+
current += char;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const twoCharacterOperator = (char === '&' && next === '&') ||
|
|
252
|
+
(char === '|' && next === '|') ||
|
|
253
|
+
(char === '|' && next === '&');
|
|
254
|
+
if (twoCharacterOperator) {
|
|
255
|
+
flush();
|
|
256
|
+
index += 1;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (char === ';' || char === '|' || char === '&' || char === '\n' || char === '\r') {
|
|
260
|
+
flush();
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
current += char;
|
|
264
|
+
}
|
|
265
|
+
flush();
|
|
266
|
+
return segments;
|
|
267
|
+
}
|
|
177
268
|
export function parseSegment(command) {
|
|
178
269
|
const tokens = tokenizeShell(command);
|
|
179
270
|
const { tokens: peeled } = peelTransparentWrappers(tokens);
|
|
@@ -210,6 +301,9 @@ export function segmentOpacity(command) {
|
|
|
210
301
|
export function substitutionInners(command) {
|
|
211
302
|
return findCommandSubstitutions(command);
|
|
212
303
|
}
|
|
304
|
+
export function structuralSubstitutionInners(command) {
|
|
305
|
+
return findStructuralCommandSubstitutions(command);
|
|
306
|
+
}
|
|
213
307
|
export function redactCommand(command) {
|
|
214
308
|
return command
|
|
215
309
|
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [REDACTED]')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PolicyDecision } from '../capability/policy-types.js';
|
|
2
2
|
import type { CapabilityRequestV1 } from '../capability/request.js';
|
|
3
3
|
import type { BelayConfigV4 } from '../config.js';
|
|
4
|
-
import type { EffectPlan } from '../effect-ir/types.js';
|
|
4
|
+
import type { EffectPlan, EffectPlanPolicyProjection } from '../effect-ir/types.js';
|
|
5
5
|
export type VerdictPermission = 'allow' | 'ask';
|
|
6
6
|
export type VerdictLocation = 'repo_local' | 'repo_outside' | 'external' | 'mixed' | 'unknown';
|
|
7
7
|
export type VerdictOpacity = 'transparent' | 'recursive' | 'opaque' | 'unparseable';
|
|
@@ -22,6 +22,7 @@ export interface VerdictResult {
|
|
|
22
22
|
authorizationDecision?: PolicyDecision;
|
|
23
23
|
effectPlan?: EffectPlan;
|
|
24
24
|
effectPlanPolicyDecisions?: PolicyDecision[];
|
|
25
|
+
effectPlanProjection?: EffectPlanPolicyProjection;
|
|
25
26
|
}
|
|
26
27
|
export interface Tier1Verdict {
|
|
27
28
|
/** true = undoable via git/fs snapshot or trivial local revert (ADR-002 local-recoverable). */
|
|
@@ -75,8 +76,6 @@ export interface VerdictContext {
|
|
|
75
76
|
trustedWorkspaceRoots?: string[];
|
|
76
77
|
sensitivePaths: string[];
|
|
77
78
|
protectedArtifactRoots?: string[];
|
|
78
|
-
customAllowCommands?: string[];
|
|
79
|
-
customExternalCommands?: string[];
|
|
80
79
|
mode: VerdictMode;
|
|
81
80
|
unknownLocalEffect: 'allow_flagged' | 'deny';
|
|
82
81
|
unparseableShell: 'allow_flagged' | 'deny';
|