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)]
54#[allow(clippy::too_many_lines)] async fn add_unix_runtime_report(report: &mut Value) {
56 use std::time::Duration;
57
58 use crate::daemon::{socket_candidates, DaemonClient};
59
60 const PROBE_TIMEOUT: Duration = Duration::from_secs(2);
63
64 let expected = crate::build_info::BuildIdentity::current();
65 let candidates = socket_candidates();
66 let selected = candidates
67 .iter()
68 .find(|candidate| candidate.selected)
69 .map(|candidate| candidate.path.display().to_string());
70 let mut entries = Vec::with_capacity(candidates.len());
71 let mut reachable = 0_u64;
72 let mut compatible = 0_u64;
73 let mut mixed_guardians = 0_u64;
74
75 for candidate in candidates {
76 let client = DaemonClient::new(&candidate.path);
77 let entry = match tokio::time::timeout(PROBE_TIMEOUT, client.hello()).await {
78 Ok(Ok(hello)) => {
79 reachable += 1;
80 let build_compatible = hello.build_matches(&expected);
81 compatible += u64::from(build_compatible);
82 let sessions =
83 match tokio::time::timeout(PROBE_TIMEOUT, client.list_sessions()).await {
84 Ok(Ok(sessions)) => sessions,
85 Ok(Err(error)) => {
86 entries.push(json!({
87 "path": candidate.path.display().to_string(),
88 "sources": candidate.sources,
89 "selected": candidate.selected,
90 "reachable": true,
91 "build_compatible": build_compatible,
92 "hello": hello,
93 "sessions_error": error.to_string()
94 }));
95 continue;
96 }
97 Err(_) => {
98 entries.push(json!({
99 "path": candidate.path.display().to_string(),
100 "sources": candidate.sources,
101 "selected": candidate.selected,
102 "reachable": true,
103 "build_compatible": build_compatible,
104 "hello": hello,
105 "sessions_error": "timed out waiting for winxd"
106 }));
107 continue;
108 }
109 };
110 let session_entries = sessions
111 .into_iter()
112 .map(|session| {
113 let guardian_build_compatible = session
114 .runtime
115 .as_ref()
116 .is_some_and(|runtime| runtime.build_matches(&expected));
117 if session.runtime.is_some() && !guardian_build_compatible {
118 mixed_guardians += 1;
119 }
120 json!({
121 "thread_id": session.thread_id,
122 "cwd": session.cwd,
123 "running": session.running,
124 "guardian_build_compatible": guardian_build_compatible,
125 "runtime": session.runtime
126 })
127 })
128 .collect::<Vec<_>>();
129 json!({
130 "path": candidate.path.display().to_string(),
131 "sources": candidate.sources,
132 "selected": candidate.selected,
133 "reachable": true,
134 "build_compatible": build_compatible,
135 "hello": hello,
136 "session_count": session_entries.len(),
137 "sessions": session_entries
138 })
139 }
140 Ok(Err(error)) => json!({
141 "path": candidate.path.display().to_string(),
142 "sources": candidate.sources,
143 "selected": candidate.selected,
144 "reachable": false,
145 "error": error.to_string()
146 }),
147 Err(_) => json!({
148 "path": candidate.path.display().to_string(),
149 "sources": candidate.sources,
150 "selected": candidate.selected,
151 "reachable": false,
152 "error": "timed out waiting for winxd"
153 }),
154 };
155 entries.push(entry);
156 }
157
158 report["daemon_topology"] = json!({
159 "selected_socket": selected,
160 "reachable_count": reachable,
161 "compatible_count": compatible,
162 "split_brain": reachable > 1,
163 "mixed_guardian_builds": mixed_guardians,
164 "healthy": reachable == 1 && mixed_guardians == 0 && compatible == reachable,
165 "candidates": entries
166 });
167 report["daemon_binary"] = match crate::runtime::configured_daemon_binary() {
168 Ok(path) => json!({"available": true, "path": path.display().to_string()}),
169 Err(error) => json!({"available": false, "error": error.to_string()}),
170 };
171}
172
173#[cfg(test)]
174mod tests {
175 #[tokio::test]
176 async fn doctor_report_never_exposes_secret_values() {
177 let report = super::doctor_report().await.to_string();
178 assert!(!report.contains("WINX_HTTP_TOKEN="));
179 assert!(report.contains("http_token_configured"));
180 assert!(report.contains("daemon_topology"));
181 }
182}