@@ -27,7 +27,6 @@ import {
2727 type ExecCommandAnalysis ,
2828 type ExecCommandSegment ,
2929 type 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 = {
132131 segmentSatisfiedBy : ExecSegmentSatisfiedBy [ ] ;
133132} ;
134133
135- 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 = {
143136 name : string ;
144137 resolvedPath : string ;
@@ -232,163 +225,6 @@ function isSkillAutoAllowedSegment(params: {
232225 return Boolean ( params . skillBinTrust . get ( executableName ) ?. has ( resolvedPath ) ) ;
233226}
234227
235- 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 ;
393229
394230type InlineChainAllowlistEvaluation = {
@@ -1305,7 +1141,7 @@ export function evaluateShellAllowlist(
13051141 } ;
13061142 }
13071143
1308- const chainEvaluations = chainParts . map ( ( { part, opToNext } ) => {
1144+ const chainEvaluations = chainParts . map ( ( { part } ) => {
13091145 const analysis = analyzeShellCommand ( {
13101146 command : part ,
13111147 cwd : params . cwd ,
@@ -1318,7 +1154,6 @@ export function evaluateShellAllowlist(
13181154 return {
13191155 analysis,
13201156 evaluation : evaluateExecAllowlist ( { analysis, ...allowlistContext } ) ,
1321- opToNext,
13221157 } ;
13231158 } ) ;
13241159 if ( chainEvaluations . some ( ( entry ) => entry === null ) ) {
@@ -1328,60 +1163,18 @@ export function evaluateShellAllowlist(
13281163 const finalizedEvaluations = chainEvaluations as Array < {
13291164 analysis : ExecCommandAnalysis ;
13301165 evaluation : 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- }
13671167 const allowlistMatches : ExecAllowlistEntry [ ] = [ ] ;
13681168 const segments : ExecCommandSegment [ ] = [ ] ;
13691169 const segmentAllowlistEntries : Array < ExecAllowlistEntry | null > = [ ] ;
13701170 const segmentSatisfiedBy : ExecSegmentSatisfiedBy [ ] = [ ] ;
13711171
1372- 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 ) {
13801173 segments . push ( ...analysis . segments ) ;
13811174 allowlistMatches . 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 ) {
13851178 return {
13861179 analysisOk : true ,
13871180 allowlistSatisfied : false ,
0 commit comments