@@ -339,6 +339,12 @@ await withMockedModule(
339339 ) ;
340340 return real . trustPathPlugins ( paths , home ?? addPathProbe . home ) ;
341341 } ,
342+ // revokeTrust in the backend calls without a home (production default).
343+ // Redirect here too so an add→revoke round trip in this file reads back
344+ // the same store the grant went to.
345+ revokePathPlugin : async ( path : string , home ?: string ) => {
346+ return real . revokePathPlugin ( path , home ?? addPathProbe . home ) ;
347+ } ,
342348 } ) ,
343349) ;
344350
@@ -356,6 +362,7 @@ async function makeAddPathAdmin(base: string): Promise<{
356362 addPath : (
357363 path : string ,
358364 ) => Promise < { ok : boolean ; message : string ; id ?: string } > ;
365+ revokeTrust : ( id : string ) => Promise < { ok : boolean ; message : string } > ;
359366} > {
360367 const backend = await import ( "../../src/tui/plugins-admin-backend.js" ) ;
361368 const emptyProjectTrust : ProjectTrustStore = {
@@ -439,7 +446,7 @@ describe("addPath grants path trust before importing plugin code", () => {
439446 const result = await admin . addPath ( "marketplace" ) ;
440447 expect ( result ) . toEqual ( {
441448 ok : true ,
442- message : " Added mkt-root" ,
449+ message : ` Added mkt-root (trusted 2 marketplace members: ${ alpha } , ${ beta } )` ,
443450 id : "mkt-root" ,
444451 } ) ;
445452
@@ -517,4 +524,89 @@ describe("addPath grants path trust before importing plugin code", () => {
517524 await rm ( base , { recursive : true , force : true } ) ;
518525 }
519526 } ) ;
527+
528+ test ( "file-path add grants the containing dir so revokeTrust clears it and reload stays metadata-only" , async ( ) => {
529+ const base = await mkdtemp ( join ( tmpdir ( ) , "corbits-addpath-file-" ) ) ;
530+ try {
531+ const home = join ( base , "home" ) ;
532+ await mkdir ( home , { recursive : true } ) ;
533+ const pluginDir = join ( base , "p" ) ;
534+ const marker = join ( base , "FILE_MARKER" ) ;
535+ await writeCommandPlugin ( pluginDir , "file-plugin" , marker ) ;
536+ resetAddPathProbe ( home , marker ) ;
537+
538+ const admin = await makeAddPathAdmin ( base ) ;
539+ // Operator points at the file, not the directory.
540+ const result = await admin . addPath ( join ( pluginDir , "index.ts" ) ) ;
541+ expect ( result ) . toEqual ( {
542+ ok : true ,
543+ message : "Added file-plugin" ,
544+ id : "file-plugin" ,
545+ } ) ;
546+ // The grant is the normalized dir — the identity loadPluginEntry stamps
547+ // and revokeTrust removes — never the raw file path.
548+ expect ( addPathProbe . trustCalls ) . toEqual ( [ [ pluginDir ] ] ) ;
549+ expect ( ( await loadPathTrust ( home ) ) . trustedPluginPaths ) . toEqual ( [
550+ pluginDir ,
551+ ] ) ;
552+ expect ( await Bun . file ( marker ) . exists ( ) ) . toBe ( true ) ;
553+
554+ const revoked = await admin . revokeTrust ( "file-plugin" ) ;
555+ expect ( revoked . ok ) . toBe ( true ) ;
556+ expect ( ( await loadPathTrust ( home ) ) . trustedPluginPaths ) . toEqual ( [ ] ) ;
557+
558+ // Next boot resolves the persisted entry against the emptied store: the
559+ // module stays metadata-only and its code never re-executes.
560+ await rm ( marker , { force : true } ) ;
561+ const store = await loadPathTrust ( home ) ;
562+ const mods = await loadPluginsFromPaths ( [ pluginDir ] , base , {
563+ isPluginTrusted : ( p ) => isPathPluginTrusted ( store , p ) ,
564+ } ) ;
565+ expect (
566+ mods . find ( ( m ) => m . manifest ?. id === "file-plugin" ) ?. metadataOnly ,
567+ ) . toBe ( true ) ;
568+ expect ( await Bun . file ( marker ) . exists ( ) ) . toBe ( false ) ;
569+ } finally {
570+ await rm ( base , { recursive : true , force : true } ) ;
571+ }
572+ } ) ;
573+
574+ test ( "hybrid root add surfaces exactly the expanded member set" , async ( ) => {
575+ const base = await mkdtemp ( join ( tmpdir ( ) , "corbits-addpath-hybrid-" ) ) ;
576+ try {
577+ const home = join ( base , "home" ) ;
578+ await mkdir ( home , { recursive : true } ) ;
579+ const root = join ( base , "hybrid" ) ;
580+ const rootMarker = join ( base , "ROOT_MARKER" ) ;
581+ await writeCommandPlugin ( root , "hybrid-root" , rootMarker ) ;
582+ const sibling = join ( base , "agents" , "evil-sibling" ) ;
583+ const siblingMarker = join ( base , "SIBLING_MARKER" ) ;
584+ await writeCommandPlugin ( sibling , "evil-sibling" , siblingMarker ) ;
585+ await mkdir ( join ( root , ".claude-plugin" ) , { recursive : true } ) ;
586+ await writeFile (
587+ join ( root , ".claude-plugin" , "marketplace.json" ) ,
588+ JSON . stringify ( {
589+ plugins : [ { name : "evil-sibling" , source : "../agents/evil-sibling" } ] ,
590+ } ) ,
591+ "utf8" ,
592+ ) ;
593+ resetAddPathProbe ( home , rootMarker ) ;
594+
595+ const admin = await makeAddPathAdmin ( base ) ;
596+ const result = await admin . addPath ( "hybrid" ) ;
597+ // Only the expanded member set is granted, and the result names it so
598+ // the operator sees the sibling consent covers.
599+ expect ( addPathProbe . trustCalls ) . toEqual ( [ [ sibling ] ] ) ;
600+ expect ( ( await loadPathTrust ( home ) ) . trustedPluginPaths ) . toEqual ( [ sibling ] ) ;
601+ expect ( result ) . toEqual ( {
602+ ok : true ,
603+ message : `Added hybrid-root (trusted 1 marketplace member: ${ sibling } )` ,
604+ id : "hybrid-root" ,
605+ } ) ;
606+ // The sibling is granted but never imported by the add itself.
607+ expect ( await Bun . file ( siblingMarker ) . exists ( ) ) . toBe ( false ) ;
608+ } finally {
609+ await rm ( base , { recursive : true , force : true } ) ;
610+ }
611+ } ) ;
520612} ) ;
0 commit comments