@@ -6,15 +6,16 @@ use std::{
66
77use agentdesktop_core:: model:: {
88 AccessCapability , AccessCategory , AccessCoverage , AccessCoverageStatus , AccessDecision ,
9- AccessEnforcement , AccessFinding , AccessOperation , AccessSeverity , AccessSourceKind ,
9+ AccessEnforcement , AccessFinding , AccessOperation , AccessRuleMechanism , AccessSeverity ,
10+ AccessSourceKind ,
1011} ;
1112use serde_json:: Value ;
1213
1314use super :: {
1415 CollectedAccess ,
1516 configuration:: { capability, default_capability} ,
1617 history_scan:: { HistoryAdapter , RuntimeCollector , permission_mode, runtime_capability} ,
17- host_pattern, plural_suffix, safe_command_identifier, source,
18+ host_pattern, network_rule_ref , plural_suffix, safe_command_identifier, source,
1819} ;
1920
2021pub ( super ) fn history_adapter ( home : & Path ) -> HistoryAdapter {
@@ -52,29 +53,41 @@ pub(super) fn inspect_configuration(home: &Path) -> CollectedAccess {
5253 let state_path = home. join ( ".claude.json" ) ;
5354 let project_roots = inspect_state ( & state_path, & mut collected) ;
5455 let mut settings = BTreeSet :: new ( ) ;
55- settings. insert ( ( config_dir ( home) . join ( "settings.json" ) , None ) ) ;
56- settings. extend ( managed_settings ( ) . into_iter ( ) . map ( |path| ( path, None ) ) ) ;
56+ settings. insert ( ( config_dir ( home) . join ( "settings.json" ) , None , true ) ) ;
57+ settings. extend (
58+ managed_settings ( )
59+ . into_iter ( )
60+ . map ( |path| ( path, None , false ) ) ,
61+ ) ;
5762 for workspace in project_roots {
5863 settings. insert ( (
5964 workspace. join ( ".claude/settings.json" ) ,
6065 Some ( workspace. clone ( ) ) ,
66+ true ,
6167 ) ) ;
6268 settings. insert ( (
6369 workspace. join ( ".claude/settings.local.json" ) ,
6470 Some ( workspace) ,
71+ true ,
6572 ) ) ;
6673 }
6774
6875 let mut inspected = usize:: from ( state_path. is_file ( ) ) ;
69- for ( path, workspace) in settings {
76+ for ( path, workspace, editable ) in settings {
7077 let Ok ( contents) = fs:: read ( & path) else {
7178 continue ;
7279 } ;
7380 let Ok ( document) = serde_json:: from_slice :: < Value > ( & contents) else {
7481 continue ;
7582 } ;
7683 inspected += 1 ;
77- parse_settings ( & document, & path, workspace. as_deref ( ) , & mut collected) ;
84+ parse_settings (
85+ & document,
86+ & path,
87+ workspace. as_deref ( ) ,
88+ editable,
89+ & mut collected,
90+ ) ;
7891 }
7992 collected. capabilities . extend ( [
8093 default_capability (
@@ -168,6 +181,7 @@ fn inspect_state(path: &Path, collected: &mut CollectedAccess) -> Vec<PathBuf> {
168181 enforcement : AccessEnforcement :: Harness ,
169182 workspace : Some ( workspace. clone ( ) ) ,
170183 source : source. clone ( ) ,
184+ rule : None ,
171185 detail : Some ( "Trusted Claude workspace" . to_owned ( ) ) ,
172186 } ,
173187 AccessCapability {
@@ -178,6 +192,7 @@ fn inspect_state(path: &Path, collected: &mut CollectedAccess) -> Vec<PathBuf> {
178192 enforcement : AccessEnforcement :: Harness ,
179193 workspace : Some ( workspace. clone ( ) ) ,
180194 source,
195+ rule : None ,
181196 detail : Some (
182197 "Trusted workspace; edit gating depends on permission mode" . to_owned ( ) ,
183198 ) ,
@@ -188,8 +203,10 @@ fn inspect_state(path: &Path, collected: &mut CollectedAccess) -> Vec<PathBuf> {
188203 parse_rule (
189204 rule,
190205 AccessDecision :: Allow ,
206+ None ,
191207 path,
192208 Some ( & workspace) ,
209+ false ,
193210 collected,
194211 ) ;
195212 }
@@ -203,6 +220,7 @@ fn parse_settings(
203220 document : & Value ,
204221 path : & Path ,
205222 workspace : Option < & Path > ,
223+ editable : bool ,
206224 collected : & mut CollectedAccess ,
207225) {
208226 if let Some ( permissions) = document. get ( "permissions" ) . and_then ( Value :: as_object) {
@@ -213,7 +231,15 @@ fn parse_settings(
213231 ] {
214232 if let Some ( rules) = permissions. get ( key) . and_then ( Value :: as_array) {
215233 for rule in rules. iter ( ) . filter_map ( Value :: as_str) {
216- parse_rule ( rule, decision. clone ( ) , path, workspace, collected) ;
234+ parse_rule (
235+ rule,
236+ decision. clone ( ) ,
237+ Some ( key) ,
238+ path,
239+ workspace,
240+ editable,
241+ collected,
242+ ) ;
217243 }
218244 }
219245 }
@@ -277,15 +303,17 @@ fn parse_settings(
277303 }
278304 }
279305 if let Some ( sandbox) = document. get ( "sandbox" ) . and_then ( Value :: as_object) {
280- parse_sandbox ( sandbox, path, workspace, collected) ;
306+ parse_sandbox ( sandbox, path, workspace, editable , collected) ;
281307 }
282308}
283309
284310fn parse_rule (
285311 rule : & str ,
286312 decision : AccessDecision ,
313+ list : Option < & str > ,
287314 path : & Path ,
288315 workspace : Option < & Path > ,
316+ editable : bool ,
289317 collected : & mut CollectedAccess ,
290318) {
291319 let ( tool, specifier) = rule
@@ -325,7 +353,7 @@ fn parse_rule(
325353 ) ,
326354 _ => return ,
327355 } ;
328- collected . capabilities . push ( capability (
356+ let mut capability = capability (
329357 category,
330358 & resource,
331359 operations,
@@ -334,13 +362,26 @@ fn parse_rule(
334362 path,
335363 workspace,
336364 "Claude permission rule" ,
337- ) ) ;
365+ ) ;
366+ if editable
367+ && tool == "WebFetch"
368+ && let Some ( list) = list
369+ && rule == format ! ( "WebFetch(domain:{resource})" )
370+ {
371+ capability. rule = Some ( network_rule_ref (
372+ AccessRuleMechanism :: ClaudePermission ,
373+ path,
374+ & format ! ( "permissions.{list}\0 {rule}" ) ,
375+ ) ) ;
376+ }
377+ collected. capabilities . push ( capability) ;
338378}
339379
340380fn parse_sandbox (
341381 sandbox : & serde_json:: Map < String , Value > ,
342382 path : & Path ,
343383 workspace : Option < & Path > ,
384+ editable : bool ,
344385 collected : & mut CollectedAccess ,
345386) {
346387 if sandbox. get ( "enabled" ) . and_then ( Value :: as_bool) == Some ( true ) {
@@ -414,7 +455,7 @@ fn parse_sandbox(
414455 ( "deniedDomains" , AccessDecision :: Deny ) ,
415456 ] {
416457 for resource in string_array ( network. get ( key) ) {
417- collected . capabilities . push ( capability (
458+ let mut capability = capability (
418459 AccessCategory :: Network ,
419460 resource,
420461 vec ! [ AccessOperation :: Connect ] ,
@@ -423,7 +464,15 @@ fn parse_sandbox(
423464 path,
424465 workspace,
425466 "Claude sandbox network rule" ,
426- ) ) ;
467+ ) ;
468+ if editable && host_pattern ( resource) . as_deref ( ) == Some ( resource) {
469+ capability. rule = Some ( network_rule_ref (
470+ AccessRuleMechanism :: ClaudeSandboxDomain ,
471+ path,
472+ & format ! ( "sandbox.network.{key}\0 {resource}" ) ,
473+ ) ) ;
474+ }
475+ collected. capabilities . push ( capability) ;
427476 }
428477 }
429478 }
@@ -473,7 +522,7 @@ mod tests {
473522 use std:: path:: { Path , PathBuf } ;
474523
475524 use agentdesktop_core:: model:: {
476- AccessCategory , AccessDecision , AccessEnforcement , AccessSourceKind ,
525+ AccessCategory , AccessDecision , AccessEnforcement , AccessRuleMechanism , AccessSourceKind ,
477526 } ;
478527 use serde_json:: json;
479528
@@ -497,6 +546,7 @@ mod tests {
497546 } ) ,
498547 Path :: new ( "settings.json" ) ,
499548 Some ( Path :: new ( "/workspace" ) ) ,
549+ true ,
500550 & mut collected,
501551 ) ;
502552
@@ -511,6 +561,20 @@ mod tests {
511561 capability. enforcement == AccessEnforcement :: Sandbox
512562 && capability. resource == "docs.example.com"
513563 } ) ) ;
564+ let editable_network_rules: Vec < _ > = collected
565+ . capabilities
566+ . iter ( )
567+ . filter ( |capability| capability. resource == "docs.example.com" )
568+ . filter_map ( |capability| capability. rule . as_ref ( ) )
569+ . collect ( ) ;
570+ assert_eq ! ( editable_network_rules. len( ) , 2 ) ;
571+ assert ! ( editable_network_rules. iter( ) . any( |rule| {
572+ rule. mechanism == AccessRuleMechanism :: ClaudePermission && rule. id. len( ) == 64
573+ } ) ) ;
574+ assert ! ( editable_network_rules. iter( ) . any( |rule| {
575+ rule. mechanism == AccessRuleMechanism :: ClaudeSandboxDomain && rule. id. len( ) == 64
576+ } ) ) ;
577+ assert_ne ! ( editable_network_rules[ 0 ] . id, editable_network_rules[ 1 ] . id) ;
514578 assert ! ( !format!( "{:?}" , collected. capabilities) . contains( "super-secret" ) ) ;
515579 }
516580
0 commit comments