3030use crate :: agent:: AgentConfig ;
3131use crate :: hitl:: ConfirmationProvider ;
3232use crate :: hooks:: HookExecutor ;
33- use crate :: permissions:: { PermissionChecker , PermissionPolicy } ;
33+ use crate :: permissions:: { PermissionChecker , PermissionDecision , PermissionPolicy } ;
3434use crate :: security:: SecurityProvider ;
3535use crate :: skills:: SkillRegistry ;
3636use std:: sync:: Arc ;
@@ -60,6 +60,56 @@ pub struct ChildRunContext {
6060 pub budget_guard : Option < Arc < dyn crate :: budget:: BudgetGuard > > ,
6161}
6262
63+ struct DelegatedPermissionChecker {
64+ child : Arc < dyn PermissionChecker > ,
65+ child_policy : Option < PermissionPolicy > ,
66+ parent : Arc < dyn PermissionChecker > ,
67+ parent_policy : Option < PermissionPolicy > ,
68+ }
69+
70+ impl PermissionChecker for DelegatedPermissionChecker {
71+ fn expose_to_model ( & self , tool_name : & str ) -> bool {
72+ if !self . child . expose_to_model ( tool_name) {
73+ return false ;
74+ }
75+
76+ if self
77+ . child_policy
78+ . as_ref ( )
79+ . is_some_and ( |policy| policy. declares_tool_access ( tool_name) )
80+ {
81+ // A worker's explicitly declared capability may cross a host's
82+ // ordinary parent-only visibility filter. A serializable parent
83+ // deny remains authoritative and keeps the tool hidden.
84+ return self
85+ . parent_policy
86+ . as_ref ( )
87+ . map ( |policy| policy. expose_to_model ( tool_name) )
88+ . unwrap_or_else ( || self . parent . expose_to_model ( tool_name) ) ;
89+ }
90+
91+ self . parent . expose_to_model ( tool_name)
92+ }
93+
94+ fn check ( & self , tool_name : & str , args : & serde_json:: Value ) -> PermissionDecision {
95+ stricter_decision (
96+ self . child . check ( tool_name, args) ,
97+ self . parent . check ( tool_name, args) ,
98+ )
99+ }
100+ }
101+
102+ const fn stricter_decision (
103+ left : PermissionDecision ,
104+ right : PermissionDecision ,
105+ ) -> PermissionDecision {
106+ match ( left, right) {
107+ ( PermissionDecision :: Deny , _) | ( _, PermissionDecision :: Deny ) => PermissionDecision :: Deny ,
108+ ( PermissionDecision :: Ask , _) | ( _, PermissionDecision :: Ask ) => PermissionDecision :: Ask ,
109+ ( PermissionDecision :: Allow , PermissionDecision :: Allow ) => PermissionDecision :: Allow ,
110+ }
111+ }
112+
63113impl ChildRunContext {
64114 /// Apply inherited capabilities to a child AgentConfig.
65115 ///
@@ -75,10 +125,26 @@ impl ChildRunContext {
75125 if config. skill_registry . is_none ( ) {
76126 config. skill_registry = self . skill_registry . clone ( ) ;
77127 }
78- if config. permission_checker . is_none ( ) {
79- config. permission_checker = self . permission_checker . clone ( ) ;
80- config. permission_policy = self . permission_policy . clone ( ) ;
81- } else if config. permission_policy . is_none ( ) {
128+ match (
129+ config. permission_checker . take ( ) ,
130+ self . permission_checker . clone ( ) ,
131+ ) {
132+ ( Some ( child) , Some ( parent) ) => {
133+ config. permission_checker = Some ( Arc :: new ( DelegatedPermissionChecker {
134+ child,
135+ child_policy : config. permission_policy . clone ( ) ,
136+ parent,
137+ parent_policy : self . permission_policy . clone ( ) ,
138+ } ) ) ;
139+ }
140+ ( Some ( child) , None ) => config. permission_checker = Some ( child) ,
141+ ( None , Some ( parent) ) => {
142+ config. permission_checker = Some ( parent) ;
143+ config. permission_policy = self . permission_policy . clone ( ) ;
144+ }
145+ ( None , None ) => { }
146+ }
147+ if config. permission_policy . is_none ( ) {
82148 config. permission_policy = self . permission_policy . clone ( ) ;
83149 }
84150 if config. tool_timeout_ms . is_none ( ) {
@@ -110,3 +176,77 @@ impl ChildRunContext {
110176 }
111177 }
112178}
179+
180+ #[ cfg( test) ]
181+ mod tests {
182+ use super :: * ;
183+
184+ #[ derive( Clone ) ]
185+ struct ParentVisibility {
186+ policy : PermissionPolicy ,
187+ hide_use_from_primary : bool ,
188+ }
189+
190+ impl PermissionChecker for ParentVisibility {
191+ fn expose_to_model ( & self , tool_name : & str ) -> bool {
192+ !( self . hide_use_from_primary && tool_name. starts_with ( "mcp__use_" ) )
193+ && self . policy . expose_to_model ( tool_name)
194+ }
195+
196+ fn check ( & self , tool_name : & str , args : & serde_json:: Value ) -> PermissionDecision {
197+ self . policy . check ( tool_name, args)
198+ }
199+ }
200+
201+ fn delegated (
202+ child_policy : PermissionPolicy ,
203+ parent_policy : PermissionPolicy ,
204+ ) -> DelegatedPermissionChecker {
205+ DelegatedPermissionChecker {
206+ child : Arc :: new ( child_policy. clone ( ) ) ,
207+ child_policy : Some ( child_policy) ,
208+ parent : Arc :: new ( ParentVisibility {
209+ policy : parent_policy. clone ( ) ,
210+ hide_use_from_primary : true ,
211+ } ) ,
212+ parent_policy : Some ( parent_policy) ,
213+ }
214+ }
215+
216+ #[ test]
217+ fn explicitly_scoped_worker_can_see_parent_hidden_tool ( ) {
218+ let mut child = PermissionPolicy :: new ( ) . allow ( "mcp__use_*" ) ;
219+ child. default_decision = PermissionDecision :: Deny ;
220+ let parent = PermissionPolicy :: new ( ) . allow ( "mcp__use_*" ) ;
221+ let checker = delegated ( child, parent) ;
222+
223+ assert ! ( checker. expose_to_model( "mcp__use_browser__browser_snapshot" ) ) ;
224+ assert_eq ! (
225+ checker. check( "mcp__use_browser__browser_snapshot" , & serde_json:: json!( { } ) ) ,
226+ PermissionDecision :: Allow
227+ ) ;
228+ }
229+
230+ #[ test]
231+ fn unrelated_worker_does_not_inherit_parent_hidden_use_tools ( ) {
232+ let child = PermissionPolicy :: new ( ) . allow ( "read(*)" ) ;
233+ let parent = PermissionPolicy :: new ( ) . allow ( "mcp__use_*" ) ;
234+ let checker = delegated ( child, parent) ;
235+
236+ assert ! ( !checker. expose_to_model( "mcp__use_browser__browser_snapshot" ) ) ;
237+ }
238+
239+ #[ test]
240+ fn parent_deny_remains_authoritative_for_explicit_worker_capability ( ) {
241+ let mut child = PermissionPolicy :: new ( ) . allow ( "mcp__use_*" ) ;
242+ child. default_decision = PermissionDecision :: Deny ;
243+ let parent = PermissionPolicy :: new ( ) . deny ( "mcp__use_ocr__ocr_extract" ) ;
244+ let checker = delegated ( child, parent) ;
245+
246+ assert ! ( !checker. expose_to_model( "mcp__use_ocr__ocr_extract" ) ) ;
247+ assert_eq ! (
248+ checker. check( "mcp__use_ocr__ocr_extract" , & serde_json:: json!( { } ) ) ,
249+ PermissionDecision :: Deny
250+ ) ;
251+ }
252+ }
0 commit comments