refactor(infra): dedupe exec approval allowlist evaluation flow

This commit is contained in:
Peter Steinberger
2026-03-03 02:43:57 +00:00
parent b8181e5944
commit 8b4cdbb21d

View File

@@ -109,6 +109,29 @@ export type SkillBinTrustEntry = {
name: string; name: string;
resolvedPath: string; resolvedPath: string;
}; };
type ExecAllowlistContext = {
allowlist: ExecAllowlistEntry[];
safeBins: Set<string>;
safeBinProfiles?: Readonly<Record<string, SafeBinProfile>>;
cwd?: string;
platform?: string | null;
trustedSafeBinDirs?: ReadonlySet<string>;
skillBins?: readonly SkillBinTrustEntry[];
autoAllowSkills?: boolean;
};
function pickExecAllowlistContext(params: ExecAllowlistContext): ExecAllowlistContext {
return {
allowlist: params.allowlist,
safeBins: params.safeBins,
safeBinProfiles: params.safeBinProfiles,
cwd: params.cwd,
platform: params.platform,
trustedSafeBinDirs: params.trustedSafeBinDirs,
skillBins: params.skillBins,
autoAllowSkills: params.autoAllowSkills,
};
}
function normalizeSkillBinName(value: string | undefined): string | null { function normalizeSkillBinName(value: string | undefined): string | null {
const trimmed = value?.trim().toLowerCase(); const trimmed = value?.trim().toLowerCase();
@@ -173,16 +196,7 @@ function isSkillAutoAllowedSegment(params: {
function evaluateSegments( function evaluateSegments(
segments: ExecCommandSegment[], segments: ExecCommandSegment[],
params: { params: ExecAllowlistContext,
allowlist: ExecAllowlistEntry[];
safeBins: Set<string>;
safeBinProfiles?: Readonly<Record<string, SafeBinProfile>>;
cwd?: string;
platform?: string | null;
trustedSafeBinDirs?: ReadonlySet<string>;
skillBins?: readonly SkillBinTrustEntry[];
autoAllowSkills?: boolean;
},
): { ): {
satisfied: boolean; satisfied: boolean;
matches: ExecAllowlistEntry[]; matches: ExecAllowlistEntry[];
@@ -245,35 +259,21 @@ function resolveAnalysisSegmentGroups(analysis: ExecCommandAnalysis): ExecComman
return [analysis.segments]; return [analysis.segments];
} }
export function evaluateExecAllowlist(params: { export function evaluateExecAllowlist(
params: {
analysis: ExecCommandAnalysis; analysis: ExecCommandAnalysis;
allowlist: ExecAllowlistEntry[]; } & ExecAllowlistContext,
safeBins: Set<string>; ): ExecAllowlistEvaluation {
safeBinProfiles?: Readonly<Record<string, SafeBinProfile>>;
cwd?: string;
platform?: string | null;
trustedSafeBinDirs?: ReadonlySet<string>;
skillBins?: readonly SkillBinTrustEntry[];
autoAllowSkills?: boolean;
}): ExecAllowlistEvaluation {
const allowlistMatches: ExecAllowlistEntry[] = []; const allowlistMatches: ExecAllowlistEntry[] = [];
const segmentSatisfiedBy: ExecSegmentSatisfiedBy[] = []; const segmentSatisfiedBy: ExecSegmentSatisfiedBy[] = [];
if (!params.analysis.ok || params.analysis.segments.length === 0) { if (!params.analysis.ok || params.analysis.segments.length === 0) {
return { allowlistSatisfied: false, allowlistMatches, segmentSatisfiedBy }; return { allowlistSatisfied: false, allowlistMatches, segmentSatisfiedBy };
} }
const allowlistContext = pickExecAllowlistContext(params);
const hasChains = Boolean(params.analysis.chains); const hasChains = Boolean(params.analysis.chains);
for (const group of resolveAnalysisSegmentGroups(params.analysis)) { for (const group of resolveAnalysisSegmentGroups(params.analysis)) {
const result = evaluateSegments(group, { const result = evaluateSegments(group, allowlistContext);
allowlist: params.allowlist,
safeBins: params.safeBins,
safeBinProfiles: params.safeBinProfiles,
cwd: params.cwd,
platform: params.platform,
trustedSafeBinDirs: params.trustedSafeBinDirs,
skillBins: params.skillBins,
autoAllowSkills: params.autoAllowSkills,
});
if (!result.satisfied) { if (!result.satisfied) {
if (!hasChains) { if (!hasChains) {
return { return {
@@ -339,16 +339,12 @@ function collectAllowAlwaysPatterns(params: {
return; return;
} }
if (isDispatchWrapperSegment(params.segment)) { const recurseWithArgv = (argv: string[]): void => {
const dispatchUnwrap = unwrapKnownDispatchWrapperInvocation(params.segment.argv);
if (dispatchUnwrap.kind !== "unwrapped" || dispatchUnwrap.argv.length === 0) {
return;
}
collectAllowAlwaysPatterns({ collectAllowAlwaysPatterns({
segment: { segment: {
raw: dispatchUnwrap.argv.join(" "), raw: argv.join(" "),
argv: dispatchUnwrap.argv, argv,
resolution: resolveCommandResolutionFromArgv(dispatchUnwrap.argv, params.cwd, params.env), resolution: resolveCommandResolutionFromArgv(argv, params.cwd, params.env),
}, },
cwd: params.cwd, cwd: params.cwd,
env: params.env, env: params.env,
@@ -356,6 +352,14 @@ function collectAllowAlwaysPatterns(params: {
depth: params.depth + 1, depth: params.depth + 1,
out: params.out, out: params.out,
}); });
};
if (isDispatchWrapperSegment(params.segment)) {
const dispatchUnwrap = unwrapKnownDispatchWrapperInvocation(params.segment.argv);
if (dispatchUnwrap.kind !== "unwrapped" || dispatchUnwrap.argv.length === 0) {
return;
}
recurseWithArgv(dispatchUnwrap.argv);
return; return;
} }
@@ -364,22 +368,7 @@ function collectAllowAlwaysPatterns(params: {
return; return;
} }
if (shellMultiplexerUnwrap.kind === "unwrapped") { if (shellMultiplexerUnwrap.kind === "unwrapped") {
collectAllowAlwaysPatterns({ recurseWithArgv(shellMultiplexerUnwrap.argv);
segment: {
raw: shellMultiplexerUnwrap.argv.join(" "),
argv: shellMultiplexerUnwrap.argv,
resolution: resolveCommandResolutionFromArgv(
shellMultiplexerUnwrap.argv,
params.cwd,
params.env,
),
},
cwd: params.cwd,
env: params.env,
platform: params.platform,
depth: params.depth + 1,
out: params.out,
});
return; return;
} }
@@ -444,18 +433,13 @@ export function resolveAllowAlwaysPatterns(params: {
/** /**
* Evaluates allowlist for shell commands (including &&, ||, ;) and returns analysis metadata. * Evaluates allowlist for shell commands (including &&, ||, ;) and returns analysis metadata.
*/ */
export function evaluateShellAllowlist(params: { export function evaluateShellAllowlist(
params: {
command: string; command: string;
allowlist: ExecAllowlistEntry[];
safeBins: Set<string>;
safeBinProfiles?: Readonly<Record<string, SafeBinProfile>>;
cwd?: string;
env?: NodeJS.ProcessEnv; env?: NodeJS.ProcessEnv;
trustedSafeBinDirs?: ReadonlySet<string>; } & ExecAllowlistContext,
skillBins?: readonly SkillBinTrustEntry[]; ): ExecAllowlistAnalysis {
autoAllowSkills?: boolean; const allowlistContext = pickExecAllowlistContext(params);
platform?: string | null;
}): ExecAllowlistAnalysis {
const analysisFailure = (): ExecAllowlistAnalysis => ({ const analysisFailure = (): ExecAllowlistAnalysis => ({
analysisOk: false, analysisOk: false,
allowlistSatisfied: false, allowlistSatisfied: false,
@@ -481,17 +465,7 @@ export function evaluateShellAllowlist(params: {
if (!analysis.ok) { if (!analysis.ok) {
return analysisFailure(); return analysisFailure();
} }
const evaluation = evaluateExecAllowlist({ const evaluation = evaluateExecAllowlist({ analysis, ...allowlistContext });
analysis,
allowlist: params.allowlist,
safeBins: params.safeBins,
safeBinProfiles: params.safeBinProfiles,
cwd: params.cwd,
platform: params.platform,
trustedSafeBinDirs: params.trustedSafeBinDirs,
skillBins: params.skillBins,
autoAllowSkills: params.autoAllowSkills,
});
return { return {
analysisOk: true, analysisOk: true,
allowlistSatisfied: evaluation.allowlistSatisfied, allowlistSatisfied: evaluation.allowlistSatisfied,
@@ -517,17 +491,7 @@ export function evaluateShellAllowlist(params: {
} }
segments.push(...analysis.segments); segments.push(...analysis.segments);
const evaluation = evaluateExecAllowlist({ const evaluation = evaluateExecAllowlist({ analysis, ...allowlistContext });
analysis,
allowlist: params.allowlist,
safeBins: params.safeBins,
safeBinProfiles: params.safeBinProfiles,
cwd: params.cwd,
platform: params.platform,
trustedSafeBinDirs: params.trustedSafeBinDirs,
skillBins: params.skillBins,
autoAllowSkills: params.autoAllowSkills,
});
allowlistMatches.push(...evaluation.allowlistMatches); allowlistMatches.push(...evaluation.allowlistMatches);
segmentSatisfiedBy.push(...evaluation.segmentSatisfiedBy); segmentSatisfiedBy.push(...evaluation.segmentSatisfiedBy);
if (!evaluation.allowlistSatisfied) { if (!evaluation.allowlistSatisfied) {