@@ -29,17 +29,21 @@ pub fn evaluate_path_based_detector(
2929 let config_probe =
3030 probe_config_path ( config. config_override_env_var , config. config_fallback_paths ) ;
3131
32- let ( config_path, override_invalid_path ) = match config_probe {
32+ let ( config_path, probe_issue ) = match config_probe {
3333 ConfigProbe :: Resolved ( path) => ( Some ( path) , None ) ,
34- ConfigProbe :: OverrideInvalid ( path) => ( None , Some ( path) ) ,
34+ ConfigProbe :: OverrideMissing ( path) => ( None , Some ( ProbeIssue :: OverrideMissing ( path) ) ) ,
35+ ConfigProbe :: OverridePermissionDenied ( path) => {
36+ ( None , Some ( ProbeIssue :: OverridePermissionDenied ( path) ) )
37+ }
38+ ConfigProbe :: PermissionDenied ( path) => ( None , Some ( ProbeIssue :: PermissionDenied ( path) ) ) ,
3539 ConfigProbe :: Missing => ( None , None ) ,
3640 } ;
3741
3842 let ( status, confidence, note) = resolve_status_and_note (
3943 config,
4044 binary_path. is_some ( ) ,
4145 config_path. is_some ( ) ,
42- override_invalid_path . as_deref ( ) ,
46+ probe_issue . as_ref ( ) ,
4347 ) ;
4448
4549 let evidence = DetectionEvidence {
@@ -61,21 +65,46 @@ pub fn evaluate_path_based_detector(
6165 }
6266}
6367
68+ #[ derive( Debug , Clone ) ]
69+ enum ProbeIssue {
70+ OverrideMissing ( String ) ,
71+ OverridePermissionDenied ( String ) ,
72+ PermissionDenied ( String ) ,
73+ }
74+
6475fn resolve_status_and_note (
6576 config : & PathBasedDetectorConfig ,
6677 binary_found : bool ,
6778 config_found : bool ,
68- override_invalid_path : Option < & str > ,
79+ probe_issue : Option < & ProbeIssue > ,
6980) -> ( DetectionStatus , u8 , String ) {
70- if let Some ( invalid_path) = override_invalid_path {
71- return (
72- DetectionStatus :: Partial ,
73- 20 ,
74- format ! (
75- "[config_override_invalid] {} override '{}' is set but unreadable: {}" ,
76- config. display_name, config. config_override_env_var, invalid_path
81+ if let Some ( issue) = probe_issue {
82+ return match issue {
83+ ProbeIssue :: OverrideMissing ( path) => (
84+ DetectionStatus :: Partial ,
85+ 20 ,
86+ format ! (
87+ "[config_override_missing] {} override '{}' points to missing config: {}" ,
88+ config. display_name, config. config_override_env_var, path
89+ ) ,
7790 ) ,
78- ) ;
91+ ProbeIssue :: OverridePermissionDenied ( path) => (
92+ DetectionStatus :: Error ,
93+ 0 ,
94+ format ! (
95+ "[config_permission_denied] {} override '{}' is not readable: {}" ,
96+ config. display_name, config. config_override_env_var, path
97+ ) ,
98+ ) ,
99+ ProbeIssue :: PermissionDenied ( path) => (
100+ DetectionStatus :: Error ,
101+ 0 ,
102+ format ! (
103+ "[config_permission_denied] {} fallback config is not readable: {}" ,
104+ config. display_name, path
105+ ) ,
106+ ) ,
107+ } ;
79108 }
80109
81110 match config. kind {
@@ -150,10 +179,10 @@ fn resolve_status_and_note(
150179mod tests {
151180 use crate :: contracts:: { common:: ClientKind , detect:: DetectionStatus } ;
152181
153- use super :: { DetectorKind , PathBasedDetectorConfig , resolve_status_and_note} ;
182+ use super :: { DetectorKind , PathBasedDetectorConfig , ProbeIssue , resolve_status_and_note} ;
154183
155184 #[ test]
156- fn invalid_override_resolves_to_partial_state ( ) {
185+ fn missing_override_resolves_to_partial_state ( ) {
157186 let config = PathBasedDetectorConfig {
158187 client : ClientKind :: CodexCli ,
159188 display_name : "Test CLI" ,
@@ -163,13 +192,19 @@ mod tests {
163192 config_fallback_paths : & [ ] ,
164193 } ;
165194
166- let ( status, confidence, note) =
167- resolve_status_and_note ( & config, false , false , Some ( "/definitely/not/a/file.json" ) ) ;
195+ let ( status, confidence, note) = resolve_status_and_note (
196+ & config,
197+ false ,
198+ false ,
199+ Some ( & ProbeIssue :: OverrideMissing (
200+ "/definitely/not/a/file.json" . to_string ( ) ,
201+ ) ) ,
202+ ) ;
168203
169204 assert ! ( matches!( status, DetectionStatus :: Partial ) ) ;
170205 assert_eq ! ( confidence, 20 ) ;
171206 assert ! ( note. contains( "AI_MANAGER_TEST_INVALID_OVERRIDE" ) ) ;
172- assert ! ( note. contains( "[config_override_invalid ]" ) ) ;
207+ assert ! ( note. contains( "[config_override_missing ]" ) ) ;
173208 }
174209
175210 #[ test]
@@ -202,4 +237,29 @@ mod tests {
202237 assert ! ( note. contains( reason_code) ) ;
203238 }
204239 }
240+
241+ #[ test]
242+ fn desktop_detector_surfaces_permission_failures_as_error ( ) {
243+ let config = PathBasedDetectorConfig {
244+ client : ClientKind :: Cursor ,
245+ display_name : "Cursor" ,
246+ kind : DetectorKind :: Desktop ,
247+ binary_candidates : & [ ] ,
248+ config_override_env_var : "AI_MANAGER_CURSOR_MCP_CONFIG" ,
249+ config_fallback_paths : & [ ] ,
250+ } ;
251+
252+ let ( status, confidence, note) = resolve_status_and_note (
253+ & config,
254+ false ,
255+ false ,
256+ Some ( & ProbeIssue :: PermissionDenied (
257+ "/tmp/unreadable/mcp.json" . to_string ( ) ,
258+ ) ) ,
259+ ) ;
260+
261+ assert ! ( matches!( status, DetectionStatus :: Error ) ) ;
262+ assert_eq ! ( confidence, 0 ) ;
263+ assert ! ( note. contains( "[config_permission_denied]" ) ) ;
264+ }
205265}
0 commit comments