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