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 report["shell"] = match crate::utils::executable::bash_executable() {
49 Some(path) => json!({"available": true, "bash": path.display().to_string()}),
50 None => json!({
51 "available": false,
52 "hint": if cfg!(windows) {
53 "no bash.exe found: install Git for Windows (the System32 WSL launcher is not used), or set WINX_SHELL to an absolute bash.exe"
54 } else {
55 "bash not found on PATH"
56 }
57 }),
58 };
59
60 add_runtime_report(&mut report).await;
61 report
62}
63
64#[allow(clippy::too_many_lines)] async fn add_runtime_report(report: &mut Value) {
66 use std::time::Duration;
67
68 use crate::daemon::{socket_candidates, DaemonClient};
69
70 const PROBE_TIMEOUT: Duration = Duration::from_secs(2);
73
74 let expected = crate::build_info::BuildIdentity::current();
75 let candidates = socket_candidates();
76 let selected = candidates
77 .iter()
78 .find(|candidate| candidate.selected)
79 .map(|candidate| candidate.path.display().to_string());
80 let mut entries = Vec::with_capacity(candidates.len());
81 let mut reachable = 0_u64;
82 let mut compatible = 0_u64;
83 let mut mixed_guardians = 0_u64;
84
85 for candidate in candidates {
86 let client = DaemonClient::new(&candidate.path);
87 let entry = match tokio::time::timeout(PROBE_TIMEOUT, client.hello()).await {
88 Ok(Ok(hello)) => {
89 reachable += 1;
90 let build_compatible = hello.build_matches(&expected);
91 compatible += u64::from(build_compatible);
92 let sessions =
93 match tokio::time::timeout(PROBE_TIMEOUT, client.list_sessions()).await {
94 Ok(Ok(sessions)) => sessions,
95 Ok(Err(error)) => {
96 entries.push(json!({
97 "path": candidate.path.display().to_string(),
98 "sources": candidate.sources,
99 "selected": candidate.selected,
100 "reachable": true,
101 "build_compatible": build_compatible,
102 "hello": hello,
103 "sessions_error": error.to_string()
104 }));
105 continue;
106 }
107 Err(_) => {
108 entries.push(json!({
109 "path": candidate.path.display().to_string(),
110 "sources": candidate.sources,
111 "selected": candidate.selected,
112 "reachable": true,
113 "build_compatible": build_compatible,
114 "hello": hello,
115 "sessions_error": "timed out waiting for winxd"
116 }));
117 continue;
118 }
119 };
120 let session_entries = sessions
121 .into_iter()
122 .map(|session| {
123 let guardian_build_compatible = session
124 .runtime
125 .as_ref()
126 .is_some_and(|runtime| runtime.build_matches(&expected));
127 if session.runtime.is_some() && !guardian_build_compatible {
128 mixed_guardians += 1;
129 }
130 json!({
131 "thread_id": session.thread_id,
132 "cwd": session.cwd,
133 "running": session.running,
134 "guardian_build_compatible": guardian_build_compatible,
135 "runtime": session.runtime
136 })
137 })
138 .collect::<Vec<_>>();
139 json!({
140 "path": candidate.path.display().to_string(),
141 "sources": candidate.sources,
142 "selected": candidate.selected,
143 "reachable": true,
144 "build_compatible": build_compatible,
145 "hello": hello,
146 "session_count": session_entries.len(),
147 "sessions": session_entries
148 })
149 }
150 Ok(Err(error)) => json!({
151 "path": candidate.path.display().to_string(),
152 "sources": candidate.sources,
153 "selected": candidate.selected,
154 "reachable": false,
155 "error": error.to_string()
156 }),
157 Err(_) => json!({
158 "path": candidate.path.display().to_string(),
159 "sources": candidate.sources,
160 "selected": candidate.selected,
161 "reachable": false,
162 "error": "timed out waiting for winxd"
163 }),
164 };
165 entries.push(entry);
166 }
167
168 let last_exit = selected
169 .as_deref()
170 .map(std::path::Path::new)
171 .and_then(crate::daemon::exit::read_record)
172 .map_or(Value::Null, |record| serde_json::to_value(record).unwrap_or(Value::Null));
173 report["daemon_topology"] = json!({
174 "selected_socket": selected,
175 "last_exit": last_exit,
176 "reachable_count": reachable,
177 "compatible_count": compatible,
178 "split_brain": reachable > 1,
179 "mixed_guardian_builds": mixed_guardians,
180 "healthy": reachable == 1 && mixed_guardians == 0 && compatible == reachable,
181 "candidates": entries
182 });
183 report["daemon_binary"] = match crate::runtime::configured_daemon_binary() {
184 Ok(path) => json!({"available": true, "path": path.display().to_string()}),
185 Err(error) => json!({"available": false, "error": error.to_string()}),
186 };
187}
188
189#[cfg(test)]
190mod tests {
191 #[tokio::test]
192 async fn doctor_report_never_exposes_secret_values() {
193 let report = super::doctor_report().await.to_string();
194 assert!(!report.contains("WINX_HTTP_TOKEN="));
195 assert!(report.contains("http_token_configured"));
196 assert!(report.contains("daemon_topology"));
197 }
198}