oxios_kernel/host_tools/
mod.rs1pub mod oauth;
12pub mod provisioner;
13pub mod registry;
14pub mod scanner;
15pub use oauth::{DeviceCodeResponse, OAuthBroker, PollOutcome, PollResponse};
16pub use provisioner::{InstallOutput, install as install_spec};
17pub use registry::{CredentialResolver, CredentialStatus, Integration, IntegrationRegistry};
18pub use scanner::{DetectedTool, HostProbe, HostToolScanner, RealProbe, ToolSource};
19
20use std::sync::Arc;
21
22pub struct HostToolsApi {
26 scanner: Arc<HostToolScanner>,
27 registry: IntegrationRegistry,
28 oauth: OAuthBroker,
29}
30
31impl HostToolsApi {
32 pub fn new() -> Self {
35 let registry = Self::load_registry().unwrap_or_else(|e| {
36 tracing::warn!(error = %e, "failed to load integration registry; using empty");
37 IntegrationRegistry::default()
38 });
39 Self {
40 scanner: Arc::new(HostToolScanner::real()),
41 registry,
42 oauth: OAuthBroker::new(),
43 }
44 }
45
46 pub fn with_scanner(scanner: Arc<HostToolScanner>) -> Self {
48 Self {
49 scanner,
50 registry: IntegrationRegistry::default(),
51 oauth: OAuthBroker::new(),
52 }
53 }
54
55 fn load_registry() -> anyhow::Result<IntegrationRegistry> {
60 const EMBEDDED: &str = include_str!("../../share/default-integrations.toml");
61 let home = crate::config::expand_home("~/.oxios");
62 let fs_defaults = home.join("share/default-integrations.toml");
63 let defaults_text = if fs_defaults.exists() {
64 std::fs::read_to_string(&fs_defaults).unwrap_or_else(|_| EMBEDDED.to_string())
65 } else {
66 EMBEDDED.to_string()
67 };
68 let overrides = crate::config::expand_home("~/.oxios/integrations.d");
69 IntegrationRegistry::load_text(&defaults_text, &overrides)
70 }
71
72 pub async fn detect(&self, name: &str) -> Option<DetectedTool> {
74 self.scanner.detect(name).await
75 }
76
77 pub async fn detect_many(&self, names: &[String]) -> Vec<DetectedTool> {
79 self.scanner.detect_many(names).await
80 }
81
82 pub fn invalidate(&self) {
84 self.scanner.invalidate();
85 }
86
87 pub fn integrations(&self) -> Vec<&Integration> {
89 self.registry.all()
90 }
91
92 pub fn integration(&self, id: &str) -> Option<&Integration> {
94 self.registry.get(id)
95 }
96
97 pub fn integration_cli_names(&self) -> Vec<String> {
99 self.registry.cli_names()
100 }
101
102 pub fn credential_status(&self, id: &str) -> Option<CredentialStatus> {
105 self.registry
106 .get(id)
107 .map(|i| CredentialStatus::resolve(&i.credential))
108 }
109
110 pub async fn install(&self, id: &str) -> anyhow::Result<InstallOutput> {
114 let it = self
115 .registry
116 .get(id)
117 .ok_or_else(|| anyhow::anyhow!("integration '{id}' not found"))?;
118 anyhow::ensure!(
119 !it.install.is_empty(),
120 "integration '{id}' has no install specs"
121 );
122 install_spec(&it.install).await
123 }
124
125 pub async fn oauth_start(&self, id: &str) -> anyhow::Result<DeviceCodeResponse> {
128 let it = self
129 .registry
130 .get(id)
131 .ok_or_else(|| anyhow::anyhow!("integration '{id}' not found"))?;
132 let (provider, store_key, scopes) = match &it.credential {
133 CredentialResolver::OAuth {
134 provider,
135 store_key,
136 scopes,
137 } => (provider.clone(), store_key.clone(), scopes.clone()),
138 other => anyhow::bail!("integration '{id}' is {:?}, not oauth", other),
139 };
140 self.oauth.start(&provider, &store_key, &scopes).await
141 }
142
143 pub async fn oauth_poll(&self, handle: &str) -> anyhow::Result<PollResponse> {
145 self.oauth.poll(handle).await
146 }
147}
148
149impl Default for HostToolsApi {
150 fn default() -> Self {
151 Self::new()
152 }
153}
154
155impl std::fmt::Debug for HostToolsApi {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 f.debug_struct("HostToolsApi").finish_non_exhaustive()
158 }
159}