winx_code_agent/
diagnostics.rs1use serde_json::{json, Value};
4
5pub async fn doctor_report() -> Value {
7 let runtime = crate::runtime::configured_runtime_mode().map_or_else(
8 |error| format!("invalid: {error}"),
9 |mode| format!("{mode:?}").to_ascii_lowercase(),
10 );
11 let build = crate::build_info::BuildIdentity::current();
12 let mut report = json!({
13 "version": crate::build_info::package_version(),
14 "build": build,
15 "platform": {
16 "os": std::env::consts::OS,
17 "arch": std::env::consts::ARCH
18 },
19 "cwd": std::env::current_dir().ok().map(|path| path.display().to_string()),
20 "runtime": runtime,
21 "environment": {
22 "sandbox_requested": crate::config::env_flag("WINX_SANDBOX"),
23 "redaction_disabled": crate::config::env_flag("WINX_NO_REDACT"),
24 "compression_disabled": crate::config::env_flag("WINX_NO_COMPRESS"),
25 "http_token_configured": crate::config::env_text("WINX_HTTP_TOKEN").is_some(),
26 "usage_log_configured": crate::config::env_text("WINX_USAGE_LOG").is_some()
27 }
28 });
29
30 let allowed_roots = crate::utils::path::configured_allowed_roots();
31 let unconfined =
32 cfg!(unix) && allowed_roots.iter().any(|root| root == std::path::Path::new("/"));
33 let containment_mode = if unconfined {
34 "unconfined"
35 } else if allowed_roots.is_empty() {
36 "workspace"
37 } else {
38 "extended"
39 };
40 report["file_tool_containment"] = json!({
41 "mode": containment_mode,
42 "extra_roots": allowed_roots
43 .iter()
44 .map(|path| path.display().to_string())
45 .collect::<Vec<_>>()
46 });
47
48 #[cfg(unix)]
49 add_unix_runtime_report(&mut report).await;
50 report
51}
52
53#[cfg(unix)]
54async fn add_unix_runtime_report(report: &mut Value) {
55 use crate::daemon::{socket_candidates, DaemonClient};
56
57 let expected = crate::build_info::BuildIdentity::current();
58 let candidates = socket_candidates();
59 let selected = candidates
60 .iter()
61 .find(|candidate| candidate.selected)
62 .map(|candidate| candidate.path.display().to_string());
63 let mut entries = Vec::with_capacity(candidates.len());
64 let mut reachable = 0_u64;
65 let mut compatible = 0_u64;
66 let mut mixed_guardians = 0_u64;
67
68 for candidate in candidates {
69 let client = DaemonClient::new(&candidate.path);
70 let entry = match client.hello().await {
71 Ok(hello) => {
72 reachable += 1;
73 let build_compatible = hello.build_matches(&expected);
74 compatible += u64::from(build_compatible);
75 let sessions = match client.list_sessions().await {
76 Ok(sessions) => sessions,
77 Err(error) => {
78 entries.push(json!({
79 "path": candidate.path.display().to_string(),
80 "sources": candidate.sources,
81 "selected": candidate.selected,
82 "reachable": true,
83 "build_compatible": build_compatible,
84 "hello": hello,
85 "sessions_error": error.to_string()
86 }));
87 continue;
88 }
89 };
90 let session_entries = sessions
91 .into_iter()
92 .map(|session| {
93 let guardian_build_compatible = session
94 .runtime
95 .as_ref()
96 .is_some_and(|runtime| runtime.build_matches(&expected));
97 if session.runtime.is_some() && !guardian_build_compatible {
98 mixed_guardians += 1;
99 }
100 json!({
101 "thread_id": session.thread_id,
102 "cwd": session.cwd,
103 "running": session.running,
104 "guardian_build_compatible": guardian_build_compatible,
105 "runtime": session.runtime
106 })
107 })
108 .collect::<Vec<_>>();
109 json!({
110 "path": candidate.path.display().to_string(),
111 "sources": candidate.sources,
112 "selected": candidate.selected,
113 "reachable": true,
114 "build_compatible": build_compatible,
115 "hello": hello,
116 "session_count": session_entries.len(),
117 "sessions": session_entries
118 })
119 }
120 Err(error) => json!({
121 "path": candidate.path.display().to_string(),
122 "sources": candidate.sources,
123 "selected": candidate.selected,
124 "reachable": false,
125 "error": error.to_string()
126 }),
127 };
128 entries.push(entry);
129 }
130
131 report["daemon_topology"] = json!({
132 "selected_socket": selected,
133 "reachable_count": reachable,
134 "compatible_count": compatible,
135 "split_brain": reachable > 1,
136 "mixed_guardian_builds": mixed_guardians,
137 "healthy": reachable == 1 && mixed_guardians == 0 && compatible == reachable,
138 "candidates": entries
139 });
140 report["daemon_binary"] = match crate::runtime::configured_daemon_binary() {
141 Ok(path) => json!({"available": true, "path": path.display().to_string()}),
142 Err(error) => json!({"available": false, "error": error.to_string()}),
143 };
144}
145
146#[cfg(test)]
147mod tests {
148 #[tokio::test]
149 async fn doctor_report_never_exposes_secret_values() {
150 let report = super::doctor_report().await.to_string();
151 assert!(!report.contains("WINX_HTTP_TOKEN="));
152 assert!(report.contains("http_token_configured"));
153 assert!(report.contains("daemon_topology"));
154 }
155}