


























@@ -27,7 +27,6 @@ import {
2727type ExecCommandAnalysis,
2828type ExecCommandSegment,
2929type ExecutableResolution,
30-type ShellChainOperator,
3130} from "./exec-approvals-analysis.js";
3231import type { ExecAllowlistEntry } from "./exec-approvals.types.js";
3332import {
@@ -132,13 +131,7 @@ export type ExecAllowlistEvaluation = {
132131segmentSatisfiedBy: ExecSegmentSatisfiedBy[];
133132};
134133135-export type ExecSegmentSatisfiedBy =
136-| "allowlist"
137-| "safeBins"
138-| "inlineChain"
139-| "skills"
140-| "skillPrelude"
141-| null;
134+export type ExecSegmentSatisfiedBy = "allowlist" | "safeBins" | "inlineChain" | "skills" | null;
142135export type SkillBinTrustEntry = {
143136name: string;
144137resolvedPath: string;
@@ -232,163 +225,6 @@ function isSkillAutoAllowedSegment(params: {
232225return Boolean(params.skillBinTrust.get(executableName)?.has(resolvedPath));
233226}
234227235-function resolveSkillPreludePath(rawPath: string, cwd?: string): string {
236-const expanded = rawPath.startsWith("~") ? expandHomePrefix(rawPath) : rawPath;
237-if (path.isAbsolute(expanded)) {
238-return path.resolve(expanded);
239-}
240-return path.resolve(cwd?.trim() || process.cwd(), expanded);
241-}
242-243-function isSkillMarkdownPreludePath(filePath: string): boolean {
244-const normalized = filePath.replace(/\\/g, "/");
245-const lowerNormalized = normalizeLowercaseStringOrEmpty(normalized);
246-if (!lowerNormalized.endsWith("/skill.md")) {
247-return false;
248-}
249-const parts = lowerNormalized.split("/").filter(Boolean);
250-if (parts.length < 2) {
251-return false;
252-}
253-for (let index = parts.length - 2; index >= 0; index -= 1) {
254-if (parts[index] !== "skills") {
255-continue;
256-}
257-const segmentsAfterSkills = parts.length - index - 1;
258-if (segmentsAfterSkills === 1 || segmentsAfterSkills === 2) {
259-return true;
260-}
261-}
262-return false;
263-}
264-265-function resolveSkillMarkdownPreludeId(filePath: string): string | null {
266-const normalized = filePath.replace(/\\/g, "/");
267-const lowerNormalized = normalizeLowercaseStringOrEmpty(normalized);
268-if (!lowerNormalized.endsWith("/skill.md")) {
269-return null;
270-}
271-const parts = lowerNormalized.split("/").filter(Boolean);
272-if (parts.length < 3) {
273-return null;
274-}
275-for (let index = parts.length - 2; index >= 0; index -= 1) {
276-if (parts[index] !== "skills") {
277-continue;
278-}
279-if (parts.length - index - 1 !== 2) {
280-continue;
281-}
282-const skillId = parts[index + 1]?.trim();
283-return skillId || null;
284-}
285-return null;
286-}
287-288-function isSkillPreludeReadSegment(segment: ExecCommandSegment, cwd?: string): boolean {
289-const execution = resolveExecutionTargetResolution(segment.resolution);
290-if (normalizeLowercaseStringOrEmpty(execution?.executableName) !== "cat") {
291-return false;
292-}
293-// Keep the display-prelude exception narrow: only a plain `cat <...>/SKILL.md`
294-// qualifies, not extra argv forms or arbitrary file reads.
295-if (segment.argv.length !== 2) {
296-return false;
297-}
298-const rawPath = segment.argv[1]?.trim();
299-if (!rawPath) {
300-return false;
301-}
302-return isSkillMarkdownPreludePath(resolveSkillPreludePath(rawPath, cwd));
303-}
304-305-function isSkillPreludeMarkerSegment(segment: ExecCommandSegment): boolean {
306-const execution = resolveExecutionTargetResolution(segment.resolution);
307-if (normalizeLowercaseStringOrEmpty(execution?.executableName) !== "printf") {
308-return false;
309-}
310-if (segment.argv.length !== 2) {
311-return false;
312-}
313-const marker = segment.argv[1];
314-return marker === "\\n---CMD---\\n" || marker === "\n---CMD---\n";
315-}
316-317-function isSkillPreludeSegment(segment: ExecCommandSegment, cwd?: string): boolean {
318-return isSkillPreludeReadSegment(segment, cwd) || isSkillPreludeMarkerSegment(segment);
319-}
320-321-function isSkillPreludeOnlyEvaluation(
322-segments: ExecCommandSegment[],
323-cwd: string | undefined,
324-): boolean {
325-return segments.length > 0 && segments.every((segment) => isSkillPreludeSegment(segment, cwd));
326-}
327-328-function resolveSkillPreludeIds(
329-segments: ExecCommandSegment[],
330-cwd: string | undefined,
331-): ReadonlySet<string> {
332-const skillIds = new Set<string>();
333-for (const segment of segments) {
334-if (!isSkillPreludeReadSegment(segment, cwd)) {
335-continue;
336-}
337-const rawPath = segment.argv[1]?.trim();
338-if (!rawPath) {
339-continue;
340-}
341-const skillId = resolveSkillMarkdownPreludeId(resolveSkillPreludePath(rawPath, cwd));
342-if (skillId) {
343-skillIds.add(skillId);
344-}
345-}
346-return skillIds;
347-}
348-349-function resolveAllowlistedSkillWrapperId(segment: ExecCommandSegment): string | null {
350-const execution = resolveExecutionTargetResolution(segment.resolution);
351-const executableName = normalizeExecutableToken(
352-execution?.executableName ?? segment.argv[0] ?? "",
353-);
354-if (!executableName.endsWith("-wrapper")) {
355-return null;
356-}
357-const skillId = executableName.slice(0, -"-wrapper".length).trim();
358-return skillId || null;
359-}
360-361-function resolveTrustedSkillExecutionIds(params: {
362-analysis: ExecCommandAnalysis;
363-evaluation: ExecAllowlistEvaluation;
364-}): ReadonlySet<string> {
365-const skillIds = new Set<string>();
366-if (!params.evaluation.allowlistSatisfied) {
367-return skillIds;
368-}
369-for (const [index, segment] of params.analysis.segments.entries()) {
370-const satisfiedBy = params.evaluation.segmentSatisfiedBy[index];
371-if (satisfiedBy === "skills") {
372-const execution = resolveExecutionTargetResolution(segment.resolution);
373-const executableName = normalizeExecutableToken(
374-execution?.executableName ?? execution?.rawExecutable ?? segment.argv[0] ?? "",
375-);
376-if (executableName) {
377-skillIds.add(executableName);
378-}
379-continue;
380-}
381-if (satisfiedBy !== "allowlist") {
382-continue;
383-}
384-const wrapperSkillId = resolveAllowlistedSkillWrapperId(segment);
385-if (wrapperSkillId) {
386-skillIds.add(wrapperSkillId);
387-}
388-}
389-return skillIds;
390-}
391-392228const MAX_SHELL_WRAPPER_INLINE_EVAL_DEPTH = 3;
393229394230type InlineChainAllowlistEvaluation = {
@@ -1305,7 +1141,7 @@ export function evaluateShellAllowlist(
13051141};
13061142}
130711431308-const chainEvaluations = chainParts.map(({ part, opToNext }) => {
1144+const chainEvaluations = chainParts.map(({ part }) => {
13091145const analysis = analyzeShellCommand({
13101146command: part,
13111147cwd: params.cwd,
@@ -1318,7 +1154,6 @@ export function evaluateShellAllowlist(
13181154return {
13191155 analysis,
13201156evaluation: evaluateExecAllowlist({ analysis, ...allowlistContext }),
1321- opToNext,
13221157};
13231158});
13241159if (chainEvaluations.some((entry) => entry === null)) {
@@ -1328,60 +1163,18 @@ export function evaluateShellAllowlist(
13281163const finalizedEvaluations = chainEvaluations as Array<{
13291164analysis: ExecCommandAnalysis;
13301165evaluation: ExecAllowlistEvaluation;
1331-opToNext: ShellChainOperator | null;
13321166}>;
1333-const allowSkillPreludeAtIndex = new Set<number>();
1334-const reachableSkillIds = new Set<string>();
1335-// Only allow the `cat SKILL.md && printf ...` display prelude when it sits on a
1336-// contiguous `&&` chain that actually reaches a later trusted skill-wrapper execution.
1337-for (let index = finalizedEvaluations.length - 1; index >= 0; index -= 1) {
1338-const { analysis, evaluation, opToNext } = finalizedEvaluations[index];
1339-const trustedSkillIds = resolveTrustedSkillExecutionIds({
1340- analysis,
1341- evaluation,
1342-});
1343-if (trustedSkillIds.size > 0) {
1344-for (const skillId of trustedSkillIds) {
1345-reachableSkillIds.add(skillId);
1346-}
1347-continue;
1348-}
1349-1350-const isPreludeOnly =
1351-!evaluation.allowlistSatisfied && isSkillPreludeOnlyEvaluation(analysis.segments, params.cwd);
1352-const preludeSkillIds = isPreludeOnly
1353- ? resolveSkillPreludeIds(analysis.segments, params.cwd)
1354- : new Set<string>();
1355-const reachesTrustedSkillExecution =
1356-opToNext === "&&" &&
1357-(preludeSkillIds.size === 0
1358- ? reachableSkillIds.size > 0
1359- : [...preludeSkillIds].some((skillId) => reachableSkillIds.has(skillId)));
1360-if (isPreludeOnly && reachesTrustedSkillExecution) {
1361-allowSkillPreludeAtIndex.add(index);
1362-continue;
1363-}
1364-1365-reachableSkillIds.clear();
1366-}
13671167const allowlistMatches: ExecAllowlistEntry[] = [];
13681168const segments: ExecCommandSegment[] = [];
13691169const segmentAllowlistEntries: Array<ExecAllowlistEntry | null> = [];
13701170const segmentSatisfiedBy: ExecSegmentSatisfiedBy[] = [];
137111711372-for (const [index, { analysis, evaluation }] of finalizedEvaluations.entries()) {
1373-const effectiveSegmentSatisfiedBy = allowSkillPreludeAtIndex.has(index)
1374- ? analysis.segments.map(() => "skillPrelude" as const)
1375- : evaluation.segmentSatisfiedBy;
1376-const effectiveSegmentAllowlistEntries = allowSkillPreludeAtIndex.has(index)
1377- ? analysis.segments.map(() => null)
1378- : evaluation.segmentAllowlistEntries;
1379-1172+for (const { analysis, evaluation } of finalizedEvaluations) {
13801173segments.push(...analysis.segments);
13811174allowlistMatches.push(...evaluation.allowlistMatches);
1382-segmentAllowlistEntries.push(...effectiveSegmentAllowlistEntries);
1383-segmentSatisfiedBy.push(...effectiveSegmentSatisfiedBy);
1384-if (!evaluation.allowlistSatisfied && !allowSkillPreludeAtIndex.has(index)) {
1175+segmentAllowlistEntries.push(...evaluation.segmentAllowlistEntries);
1176+segmentSatisfiedBy.push(...evaluation.segmentSatisfiedBy);
1177+if (!evaluation.allowlistSatisfied) {
13851178return {
13861179analysisOk: true,
13871180allowlistSatisfied: false,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。