Skip to main content

agentctl/diag/
capabilities.rs

1use crate::diag::{
2    AutomationToolSpec, DIAG_SCHEMA_VERSION, EXIT_OK, EXIT_USAGE, OutputFormat, ProbeMode,
3    ProbeModeArg, ReadinessSection, automation_tools, current_platform, doctor, emit_json,
4    resolve_probe_mode,
5};
6use crate::provider::registry::ProviderRegistry;
7use agent_runtime_core::schema::CapabilitiesRequest;
8use clap::Args;
9use serde::Serialize;
10
11#[derive(Debug, Args)]
12pub struct CapabilitiesArgs {
13    /// Optional provider filter (defaults to querying all registered providers)
14    #[arg(long)]
15    pub provider: Option<String>,
16
17    /// Include experimental capability flags from provider adapters
18    #[arg(long)]
19    pub include_experimental: bool,
20
21    /// Healthcheck timeout passed to readiness checks
22    #[arg(long)]
23    pub timeout_ms: Option<u64>,
24
25    /// Render format
26    #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
27    pub format: OutputFormat,
28
29    /// Probe execution mode (`test` enables deterministic CI probe behavior)
30    #[arg(long, value_enum, default_value_t = ProbeModeArg::Auto)]
31    pub probe_mode: ProbeModeArg,
32}
33
34#[derive(Debug, Serialize)]
35struct CapabilitiesReport {
36    schema_version: &'static str,
37    command: &'static str,
38    probe_mode: ProbeMode,
39    readiness: ReadinessSection,
40    providers: Vec<ProviderCapabilities>,
41    automation_tools: Vec<AutomationToolCapabilities>,
42}
43
44#[derive(Debug, Serialize)]
45struct ProviderCapabilities {
46    id: String,
47    contract_version: String,
48    capabilities: Vec<CapabilityEntry>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    error: Option<ProviderCapabilitiesError>,
51}
52
53#[derive(Debug, Serialize)]
54struct CapabilityEntry {
55    name: String,
56    available: bool,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    description: Option<String>,
59}
60
61#[derive(Debug, Serialize)]
62struct ProviderCapabilitiesError {
63    category: String,
64    code: String,
65    message: String,
66}
67
68#[derive(Debug, Serialize)]
69struct AutomationToolCapabilities {
70    id: String,
71    command: String,
72    capabilities: Vec<String>,
73    supported_platforms: Vec<String>,
74    supports_current_platform: bool,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    test_mode_env: Option<String>,
77}
78
79pub fn run(args: CapabilitiesArgs) -> i32 {
80    let probe_mode = resolve_probe_mode(args.probe_mode);
81    let readiness =
82        match doctor::collect_readiness(args.provider.as_deref(), args.timeout_ms, probe_mode) {
83            Ok(readiness) => readiness,
84            Err(error) => {
85                eprintln!("agentctl diag capabilities: {error}");
86                return EXIT_USAGE;
87            }
88        };
89
90    let providers =
91        match collect_provider_capabilities(args.provider.as_deref(), args.include_experimental) {
92            Ok(providers) => providers,
93            Err(error) => {
94                eprintln!("agentctl diag capabilities: {error}");
95                return EXIT_USAGE;
96            }
97        };
98
99    let automation_tools = collect_automation_capabilities();
100    let report = CapabilitiesReport {
101        schema_version: DIAG_SCHEMA_VERSION,
102        command: "capabilities",
103        probe_mode,
104        readiness,
105        providers,
106        automation_tools,
107    };
108
109    match args.format {
110        OutputFormat::Json => emit_json(&report),
111        OutputFormat::Text => emit_text(&report),
112    }
113}
114
115fn collect_provider_capabilities(
116    provider_filter: Option<&str>,
117    include_experimental: bool,
118) -> Result<Vec<ProviderCapabilities>, String> {
119    let registry = ProviderRegistry::with_builtins();
120    let provider_ids = doctor::resolve_provider_ids(&registry, provider_filter)?;
121    let mut providers = Vec::with_capacity(provider_ids.len());
122
123    for provider_id in provider_ids {
124        let Some(adapter) = registry.get(provider_id.as_str()) else {
125            continue;
126        };
127        let metadata = adapter.metadata();
128        match adapter.capabilities(CapabilitiesRequest {
129            include_experimental,
130        }) {
131            Ok(response) => {
132                let capabilities = response
133                    .capabilities
134                    .into_iter()
135                    .map(|capability| CapabilityEntry {
136                        name: capability.name,
137                        available: capability.available,
138                        description: capability.description,
139                    })
140                    .collect::<Vec<_>>();
141                providers.push(ProviderCapabilities {
142                    id: provider_id,
143                    contract_version: metadata.contract_version.as_str().to_string(),
144                    capabilities,
145                    error: None,
146                });
147            }
148            Err(error) => {
149                let category = serde_json::to_value(error.category)
150                    .ok()
151                    .and_then(|value| value.as_str().map(ToOwned::to_owned))
152                    .unwrap_or_else(|| "unknown".to_string());
153                providers.push(ProviderCapabilities {
154                    id: provider_id,
155                    contract_version: metadata.contract_version.as_str().to_string(),
156                    capabilities: Vec::new(),
157                    error: Some(ProviderCapabilitiesError {
158                        category,
159                        code: error.code,
160                        message: error.message,
161                    }),
162                });
163            }
164        }
165    }
166
167    Ok(providers)
168}
169
170fn collect_automation_capabilities() -> Vec<AutomationToolCapabilities> {
171    automation_tools()
172        .iter()
173        .map(|spec| AutomationToolCapabilities {
174            id: spec.id.to_string(),
175            command: spec.command.to_string(),
176            capabilities: spec
177                .capabilities
178                .iter()
179                .map(|capability| capability.to_string())
180                .collect(),
181            supported_platforms: spec
182                .supported_platforms
183                .iter()
184                .map(|platform| platform.to_string())
185                .collect(),
186            supports_current_platform: supports_current_platform(spec),
187            test_mode_env: spec.test_mode_env.map(ToOwned::to_owned),
188        })
189        .collect()
190}
191
192fn supports_current_platform(spec: &AutomationToolSpec) -> bool {
193    if spec.supported_platforms.is_empty() {
194        return true;
195    }
196
197    spec.supported_platforms.contains(&current_platform())
198}
199
200fn emit_text(report: &CapabilitiesReport) -> i32 {
201    println!("schema_version: {}", report.schema_version);
202    println!("command: {}", report.command);
203    println!("probe_mode: {}", report.probe_mode.as_str());
204    println!(
205        "overall_status: {}",
206        report.readiness.overall_status.as_str()
207    );
208    println!(
209        "summary: total={} ready={} degraded={} not_ready={} unknown={}",
210        report.readiness.summary.total_checks,
211        report.readiness.summary.ready,
212        report.readiness.summary.degraded,
213        report.readiness.summary.not_ready,
214        report.readiness.summary.unknown
215    );
216    println!("providers:");
217    for provider in &report.providers {
218        println!("- {} ({})", provider.id, provider.contract_version);
219        if let Some(error) = provider.error.as_ref() {
220            println!(
221                "  error: {} [{}:{}]",
222                error.message, error.category, error.code
223            );
224            continue;
225        }
226        for capability in &provider.capabilities {
227            if let Some(description) = capability.description.as_deref() {
228                println!(
229                    "  - {} [{}] {}",
230                    capability.name,
231                    if capability.available {
232                        "available"
233                    } else {
234                        "unavailable"
235                    },
236                    description
237                );
238            } else {
239                println!(
240                    "  - {} [{}]",
241                    capability.name,
242                    if capability.available {
243                        "available"
244                    } else {
245                        "unavailable"
246                    }
247                );
248            }
249        }
250    }
251    println!("automation_tools:");
252    for tool in &report.automation_tools {
253        println!(
254            "- {} ({}) [{}]",
255            tool.id,
256            tool.command,
257            if tool.supports_current_platform {
258                "supported"
259            } else {
260                "unsupported"
261            }
262        );
263        println!("  capabilities: {}", tool.capabilities.join(", "));
264        if let Some(test_mode_env) = tool.test_mode_env.as_deref() {
265            println!("  test_mode_env: {test_mode_env}");
266        }
267    }
268
269    EXIT_OK
270}