@@ -309,34 +309,36 @@ async function vatSize(rootKref: string): Promise<number> {
309309}
310310
311311/**
312- * Parse a tool invocation into a list of ParsedInvocation objects suitable for
313- * sheaf dispatch. For Bash, uses tree-sitter to decompose the pipeline into
314- * component commands. For other tools, treats the tool as a single command with
315- * string field values as argv .
312+ * Parse a tool invocation into clause arrays suitable for per-clause sheaf
313+ * dispatch. For Bash, uses tree-sitter to decompose the command into independent
314+ * clauses (split on &&/||/;), each of which is a pipeline of commands. For
315+ * other tools, wraps the tool as a single one-invocation clause .
316316 *
317317 * Returns null when the command is dynamic or unparseable (no provision possible).
318318 *
319319 * @param toolName - The Claude Code tool name.
320320 * @param toolInput - The raw tool input object.
321- * @returns Parsed invocations, or null for dynamic/unparseable Bash .
321+ * @returns Array of clauses (each clause is an array of ParsedInvocations), or null .
322322 */
323- function buildInvocations (
323+ function buildClauses (
324324 toolName : string ,
325325 toolInput : Record < string , unknown > ,
326- ) : ParsedInvocation [ ] | null {
326+ ) : ParsedInvocation [ ] [ ] | null {
327327 if ( toolName === 'Bash' ) {
328328 const command =
329329 typeof toolInput . command === 'string' ? toolInput . command : '' ;
330330 const result = decompose ( command ) ;
331331 if ( ! result . ok ) {
332332 return null ;
333333 }
334- return result . commands . map ( ( { name, argv } ) => ( { name, argv } ) ) ;
334+ return result . clauses . map ( ( clause ) =>
335+ clause . map ( ( { name, argv } ) => ( { name, argv } ) ) ,
336+ ) ;
335337 }
336338 const argv = Object . values ( toolInput ) . filter (
337339 ( val ) : val is string => typeof val === 'string' ,
338340 ) ;
339- return [ { name : toolName , argv } ] ;
341+ return [ [ { name : toolName , argv } ] ] ;
340342}
341343
342344// ─── Session initialization ──────────────────────────────────────────────────
@@ -518,7 +520,7 @@ async function onSessionStart(payload: SessionStartPayload): Promise<void> {
518520async function onPreToolUse ( payload : PreToolUsePayload ) : Promise < void > {
519521 const { session_id, tool_name, tool_input } = payload ;
520522 const sha = inputSha ( tool_input ) ;
521- const invocations = buildInvocations ( tool_name , tool_input ) ;
523+ const clauses = buildClauses ( tool_name , tool_input ) ;
522524
523525 const state = await getOrInitSession ( payload ) ;
524526 if ( ! state ) {
@@ -528,8 +530,16 @@ async function onPreToolUse(payload: PreToolUsePayload): Promise<void> {
528530
529531 let vatResponse = 'unknown' ;
530532 try {
531- if ( invocations !== null ) {
532- vatResponse = await vatRoute ( state . rootKref , tool_name , invocations ) ;
533+ if ( clauses !== null ) {
534+ let allAllow = true ;
535+ for ( const clause of clauses ) {
536+ const verdict = await vatRoute ( state . rootKref , tool_name , clause ) ;
537+ if ( verdict !== 'allow' ) {
538+ allAllow = false ;
539+ break ;
540+ }
541+ }
542+ vatResponse = allAllow ? 'allow' : 'ask' ;
533543 }
534544 } catch ( error ) {
535545 process . stderr . write ( `[caprock] vatRoute failed: ${ String ( error ) } \n` ) ;
@@ -545,27 +555,37 @@ async function onPreToolUse(payload: PreToolUsePayload): Promise<void> {
545555 } ) ;
546556
547557 if ( vatResponse === 'allow' ) {
548- if ( state . kernelSessionId && invocations !== null ) {
558+ if ( state . kernelSessionId !== undefined && clauses !== null ) {
549559 const autoDescription = `Allow ${ tool_name } (${ JSON . stringify ( tool_input ) } )` ;
550- vatFindMatch ( state . rootKref , tool_name , invocations )
551- . then ( async ( matched ) =>
552- recordProvisioned (
560+ Promise . all (
561+ clauses . map ( async ( clause ) =>
562+ vatFindMatch ( state . rootKref , tool_name , clause ) ,
563+ ) ,
564+ )
565+ . then ( async ( matches ) => {
566+ const provisions = matches . filter (
567+ ( matched ) : matched is Provision => matched !== null ,
568+ ) ;
569+ await recordProvisioned (
553570 SOCKET_PATH ,
554571 state . kernelSessionId ,
555572 autoDescription ,
556573 {
557- invocations,
558- ...( matched === null ? { } : { provision : matched } ) ,
574+ // invocations is clauses.flat() — non-null because clauses !== null here
575+ invocations : clauses . flat ( ) ,
576+ clauses,
577+ ...( provisions . length > 0 ? { provisions } : { } ) ,
559578 } ,
560- ) ,
561- )
579+ ) ;
580+ return undefined ;
581+ } )
562582 . catch ( ( ) => undefined ) ;
563583 }
564584 process . stdout . write ( JSON . stringify ( { continue : true } ) ) ;
565585 return ;
566586 }
567587
568- if ( ! state . kernelSessionId ) {
588+ if ( state . kernelSessionId === undefined ) {
569589 process . stdout . write ( JSON . stringify ( { continue : true } ) ) ;
570590 return ;
571591 }
@@ -578,7 +598,13 @@ async function onPreToolUse(payload: PreToolUsePayload): Promise<void> {
578598 SOCKET_PATH ,
579599 state . kernelSessionId ,
580600 description ,
581- invocations === null ? undefined : { invocations } ,
601+ clauses === null
602+ ? undefined
603+ : {
604+ // invocations is clauses.flat() — non-null because clauses !== null here
605+ invocations : clauses . flat ( ) ,
606+ clauses,
607+ } ,
582608 ) ;
583609 } catch ( error ) {
584610 const errorStr = String ( error ) ;
@@ -610,15 +636,18 @@ async function onPreToolUse(payload: PreToolUsePayload): Promise<void> {
610636 }
611637
612638 if ( decision . verdict === 'accept' ) {
613- if ( decision . provision !== undefined ) {
614- await vatAddSection ( state . rootKref , decision . provision ) . catch (
615- ( ) => undefined ,
616- ) ;
617- } else if ( invocations !== null ) {
618- await vatAddSection (
619- state . rootKref ,
620- invocationToProvision ( tool_name , invocations ) ,
621- ) . catch ( ( ) => undefined ) ;
639+ const decidedProvisions = decision . provisions ;
640+ if ( decidedProvisions !== undefined && decidedProvisions . length > 0 ) {
641+ for ( const prov of decidedProvisions ) {
642+ await vatAddSection ( state . rootKref , prov ) . catch ( ( ) => undefined ) ;
643+ }
644+ } else if ( clauses !== null ) {
645+ for ( const clause of clauses ) {
646+ await vatAddSection (
647+ state . rootKref ,
648+ invocationToProvision ( tool_name , clause ) ,
649+ ) . catch ( ( ) => undefined ) ;
650+ }
622651 }
623652 await appendEvent ( session_id , {
624653 t : now ( ) ,
@@ -658,13 +687,15 @@ async function onPostToolUse(payload: PostToolUsePayload): Promise<void> {
658687 return ;
659688 }
660689
661- const invocations = buildInvocations ( tool_name , tool_input ) ;
662- if ( invocations !== null ) {
690+ const postClauses = buildClauses ( tool_name , tool_input ) ;
691+ if ( postClauses !== null ) {
663692 try {
664- await vatAddSection (
665- state . rootKref ,
666- invocationToProvision ( tool_name , invocations ) ,
667- ) ;
693+ for ( const clause of postClauses ) {
694+ await vatAddSection (
695+ state . rootKref ,
696+ invocationToProvision ( tool_name , clause ) ,
697+ ) ;
698+ }
668699 } catch ( error ) {
669700 process . stderr . write (
670701 `[caprock] vatAddSection failed: ${ String ( error ) } \n` ,
@@ -707,15 +738,18 @@ async function onPermissionRequest(
707738 }
708739
709740 if ( tool_name && tool_input ) {
710- const invocations = buildInvocations ( tool_name , tool_input ) ;
711- if ( invocations !== null ) {
741+ const permClauses = buildClauses ( tool_name , tool_input ) ;
742+ if ( permClauses !== null ) {
712743 try {
713- const vatResponse = await vatRoute (
714- state . rootKref ,
715- tool_name ,
716- invocations ,
717- ) ;
718- if ( vatResponse === 'allow' ) {
744+ let allAllow = true ;
745+ for ( const clause of permClauses ) {
746+ const verdict = await vatRoute ( state . rootKref , tool_name , clause ) ;
747+ if ( verdict !== 'allow' ) {
748+ allAllow = false ;
749+ break ;
750+ }
751+ }
752+ if ( allAllow ) {
719753 process . stdout . write ( `${ permissionAllow ( ) } \n` ) ;
720754 }
721755 } catch {
0 commit comments