55 */
66import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
77import { setEmbeddedMode } from "../infra/embedded-mode.js" ;
8- import { getGlobalHookRunner } from "../plugins/hook-runner-global.js" ;
8+ import {
9+ getGlobalHookRunner ,
10+ initializeGlobalHookRunner ,
11+ resetGlobalHookRunner ,
12+ } from "../plugins/hook-runner-global.js" ;
913import type { HookRunner } from "../plugins/hooks.js" ;
1014import { createEmptyPluginRegistry } from "../plugins/registry-empty.js" ;
11- import { setActivePluginRegistry } from "../plugins/runtime.js" ;
15+ import {
16+ pinActivePluginChannelRegistry ,
17+ releasePinnedPluginChannelRegistry ,
18+ setActivePluginRegistry ,
19+ } from "../plugins/runtime.js" ;
1220import { PluginApprovalResolutions } from "../plugins/types.js" ;
1321import { runBeforeToolCallHook } from "./agent-tools.before-tool-call.js" ;
1422import { callGatewayTool } from "./tools/gateway.js" ;
@@ -69,6 +77,7 @@ describe("runBeforeToolCallHook — embedded mode approvals", () => {
6977 let runBeforeToolCallMock : ReturnType < typeof vi . fn < HookRunner [ "runBeforeToolCall" ] > > ;
7078
7179 beforeEach ( ( ) => {
80+ resetGlobalHookRunner ( ) ;
7281 runBeforeToolCallMock = vi . fn < HookRunner [ "runBeforeToolCall" ] > ( ) ;
7382 hookRunner = {
7483 hasHooks : vi . fn < HookRunner [ "hasHooks" ] > ( ) . mockReturnValue ( true ) ,
@@ -82,6 +91,7 @@ describe("runBeforeToolCallHook — embedded mode approvals", () => {
8291 afterEach ( ( ) => {
8392 setEmbeddedMode ( false ) ;
8493 setActivePluginRegistry ( createEmptyPluginRegistry ( ) ) ;
94+ resetGlobalHookRunner ( ) ;
8595 } ) ;
8696
8797 it ( "blocks approval-required tools in embedded mode when no gateway approval route exists" , async ( ) => {
@@ -445,6 +455,94 @@ describe("runBeforeToolCallHook — embedded mode approvals", () => {
445455 expect ( runBeforeToolCallMock ) . not . toHaveBeenCalled ( ) ;
446456 } ) ;
447457
458+ it ( "runs trusted policies from the global hook registry after the active registry changes" , async ( ) => {
459+ const evaluatePolicy = vi . fn ( ( ) => ( {
460+ block : true ,
461+ blockReason : "gateway registry policy blocked" ,
462+ } ) ) ;
463+ const gatewayRegistry = createEmptyPluginRegistry ( ) ;
464+ gatewayRegistry . trustedToolPolicies = [
465+ {
466+ pluginId : "gateway-policy" ,
467+ pluginName : "Gateway Policy" ,
468+ source : "test" ,
469+ policy : {
470+ id : "gateway-block" ,
471+ description : "Gateway policy" ,
472+ evaluate : evaluatePolicy ,
473+ } ,
474+ } ,
475+ ] ;
476+ initializeGlobalHookRunner ( gatewayRegistry ) ;
477+ setActivePluginRegistry ( createEmptyPluginRegistry ( ) ) ;
478+ runBeforeToolCallMock . mockResolvedValue ( undefined ) ;
479+
480+ const result = await runBeforeToolCallHook ( {
481+ toolName : "bash" ,
482+ params : { command : "deploy" } ,
483+ toolCallId : "call-gateway-policy" ,
484+ ctx : { agentId : "main" , sessionKey : "main" } ,
485+ } ) ;
486+
487+ expect ( result ) . toEqual ( {
488+ blocked : true ,
489+ kind : "veto" ,
490+ deniedReason : "plugin-before-tool-call" ,
491+ reason : "gateway registry policy blocked" ,
492+ params : { command : "deploy" } ,
493+ } ) ;
494+ expect ( evaluatePolicy ) . toHaveBeenCalledTimes ( 1 ) ;
495+ expect ( runBeforeToolCallMock ) . not . toHaveBeenCalled ( ) ;
496+ } ) ;
497+
498+ it ( "runs pinned gateway trusted policies after a later global runner initialization" , async ( ) => {
499+ const evaluatePolicy = vi . fn ( ( ) => ( {
500+ block : true ,
501+ blockReason : "pinned gateway policy blocked" ,
502+ } ) ) ;
503+ const gatewayRegistry = createEmptyPluginRegistry ( ) ;
504+ gatewayRegistry . trustedToolPolicies = [
505+ {
506+ pluginId : "gateway-policy" ,
507+ pluginName : "Gateway Policy" ,
508+ source : "test" ,
509+ policy : {
510+ id : "gateway-block" ,
511+ description : "Gateway policy" ,
512+ evaluate : evaluatePolicy ,
513+ } ,
514+ } ,
515+ ] ;
516+ setActivePluginRegistry ( gatewayRegistry ) ;
517+ initializeGlobalHookRunner ( gatewayRegistry ) ;
518+ pinActivePluginChannelRegistry ( gatewayRegistry ) ;
519+ try {
520+ const laterRegistry = createEmptyPluginRegistry ( ) ;
521+ setActivePluginRegistry ( laterRegistry ) ;
522+ initializeGlobalHookRunner ( laterRegistry ) ;
523+ runBeforeToolCallMock . mockResolvedValue ( undefined ) ;
524+
525+ const result = await runBeforeToolCallHook ( {
526+ toolName : "bash" ,
527+ params : { command : "deploy" } ,
528+ toolCallId : "call-pinned-gateway-policy" ,
529+ ctx : { agentId : "main" , sessionKey : "main" } ,
530+ } ) ;
531+
532+ expect ( result ) . toEqual ( {
533+ blocked : true ,
534+ kind : "veto" ,
535+ deniedReason : "plugin-before-tool-call" ,
536+ reason : "pinned gateway policy blocked" ,
537+ params : { command : "deploy" } ,
538+ } ) ;
539+ expect ( evaluatePolicy ) . toHaveBeenCalledTimes ( 1 ) ;
540+ expect ( runBeforeToolCallMock ) . not . toHaveBeenCalled ( ) ;
541+ } finally {
542+ releasePinnedPluginChannelRegistry ( gatewayRegistry ) ;
543+ }
544+ } ) ;
545+
448546 it ( "does not require skill_workshop lifecycle approval in auto mode" , async ( ) => {
449547 ( hookRunner . hasHooks as ReturnType < typeof vi . fn > ) . mockReturnValue ( false ) ;
450548
0 commit comments