qualia_client_core/api/
qapp_vault.rs1#![allow(non_snake_case)]
4
5use super::*;
6
7use crate::qapp_paths::qapps_dir;
8use crate::qapp_registry;
9use crate::state::*;
10use serde::Serialize;
11use std::path::PathBuf;
12use std::sync::atomic::Ordering;
13use sysinfo::Disks;
14
15pub fn list_installed_qapps() -> Vec<String> {
16 let state = crate::state::APP_STATE.get().unwrap();
17 let data_dir = state.config.lock().unwrap().storage_path.clone();
18 let qapps_dir = qapps_dir(&data_dir);
19 let mut qapps = Vec::new();
20 if let Ok(entries) = std::fs::read_dir(qapps_dir) {
21 for entry in entries.filter_map(Result::ok) {
22 if entry.path().is_dir() {
23 qapps.push(entry.file_name().to_string_lossy().to_string());
24 }
25 }
26 }
27 qapps
28}
29
30pub fn mcp_list_qapps() -> Result<String, String> {
31 crate::qapp_mcp::list_qapp_catalogue_json()
32}
33
34pub fn mcp_get_qapp_manifest(qapp_name: String) -> Result<String, String> {
35 crate::qapp_mcp::get_qapp_manifest_json(&qapp_name)
36}
37
38pub fn mcp_inspect_qapp_readiness(qapp_name: String) -> Result<String, String> {
39 crate::qapp_mcp::inspect_qapp_readiness_json(&qapp_name)
40}
41
42pub fn mcp_list_qapp_updates() -> Result<String, String> {
43 crate::qapp_mcp::list_qapp_updates_json()
44}
45
46pub fn mcp_describe_qapp_surface_schema() -> Result<String, String> {
47 crate::qapp_mcp::describe_qapp_surface_schema_json()
48}
49
50pub fn generate_qapp_credential(qapp_name: String) -> String {
51 format!("did:qualia:qapp:{}:signed_vc", qapp_name)
52}
53
54pub fn verify_and_install_qapp(target_path: String) -> Result<String, String> {
55 let state = crate::state::APP_STATE.get().unwrap();
56 let storage = state.config.lock().unwrap().storage_path.clone();
57 let storage_path = std::path::PathBuf::from(&storage);
58
59 if let Ok(port) = target_path.parse::<u16>() {
60 return Err(format!(
61 "Dev proxy port registration ({port}) requires a package directory path, not a bare port"
62 ));
63 }
64
65 let source_dir = std::path::PathBuf::from(&target_path);
66 if !source_dir.is_dir() {
67 return Err(format!("Qapp source directory not found: {target_path}"));
68 }
69
70 let entry = crate::qapp_install::install_package_atomic(
71 &storage_path,
72 &source_dir,
73 crate::qapp_install::InstallPolicy::Development,
74 None,
75 )
76 .map_err(|e| e.to_string())?;
77
78 let package_dir =
79 crate::qapp_paths::resolve_active_package_dir(&storage_path, &entry.package_id);
80 let manifest = load_qapp_package_from_dir(&package_dir)?;
81 let qapp_did = format!(
82 "did:qualia:qapp:{}",
83 manifest.name.to_lowercase().replace(" ", "-")
84 );
85
86 let registered_qapp = qapp_registry::RegisteredQapp {
87 did: qapp_did.clone(),
88 manifest: manifest.clone(),
89 target: qapp_registry::QappTarget::IsolatedVault(entry.package_id.clone()),
90 };
91
92 let qapp_id_hash = crate::qapp_manifest::install_qapp_capabilities(&manifest)
93 .map_err(|e| format!("Qapp capability compile failed: {e:?}"))?;
94
95 {
96 let mut installed = state.installed_qapps.lock().unwrap();
97 installed.retain(|q| q.manifest.name != entry.package_id && q.did != qapp_did);
98 installed.push(registered_qapp);
99 }
100 save_directory_state();
101
102 Ok(format!(
103 "{qapp_did} v{} hash={} content={}",
104 entry.active_version, qapp_id_hash, entry.content_hash
105 ))
106}
107
108#[derive(Serialize)]
109pub struct WalletStatus {
110 pub xec_sats: i64,
112 pub ilp_dispatched_microcents: u64,
114 pub nym_connected: bool,
116 pub sync_status: String,
118}
119
120pub fn get_wallet_status() -> WalletStatus {
121 let nym_connected = crate::state::APP_STATE
122 .get()
123 .map(|s| s.nym_relay_active.load(Ordering::Relaxed))
124 .unwrap_or(false);
125
126 let state = crate::state::APP_STATE.get();
127 let storage_path = state
128 .map(|s| s.config.lock().unwrap().storage_path.clone())
129 .unwrap_or_default();
130
131 let ilp_dispatched =
133 crate::wallet::ledger::total_ilp_sent_micro_cents(&std::path::Path::new(&storage_path));
134
135 let id = read_identity();
137 let (xec_sats, sync_status) = match id
138 .as_ref()
139 .and_then(|v| v.get("ecash_hash160"))
140 .and_then(|v| v.as_str())
141 {
142 Some(hash160) => {
143 let client = crate::wallet::chronik::ChronikClient::new("https://chronik.be.cash");
144 match client.fetch_utxos_p2pkh(hash160) {
145 Ok(utxos) => {
146 let sats: i64 = utxos
147 .iter()
148 .filter(|u| u.slp_meta.is_none())
149 .map(|u| u.value)
150 .sum();
151 (sats, "synced".to_string())
152 }
153 Err(_) => (0, "offline".to_string()),
154 }
155 }
156 None => (0, "no_identity".to_string()),
157 };
158
159 WalletStatus {
160 xec_sats,
161 ilp_dispatched_microcents: ilp_dispatched,
162 nym_connected,
163 sync_status,
164 }
165}
166
167pub fn nym_mixnet_opted_in() -> bool {
168 crate::state::APP_STATE
169 .get()
170 .map(|s| s.nym_relay_active.load(Ordering::Relaxed))
171 .unwrap_or(false)
172}
173
174pub fn get_config() -> AgentConfig {
175 let state = crate::state::APP_STATE.get().unwrap();
176 state.config.lock().unwrap().clone()
177}
178
179pub fn save_config(new_config: AgentConfig) -> Result<(), String> {
180 let state = crate::state::APP_STATE.get().unwrap();
181 let disks = Disks::new_with_refreshed_list();
182 let path = PathBuf::from(&new_config.storage_path);
183 let mut available = u64::MAX;
184 for disk in disks.list() {
185 if path.starts_with(disk.mount_point()) {
186 available = disk.available_space();
187 break;
188 }
189 }
190 let margin: u64 = 15 * 1024 * 1024 * 1024;
191 let requested = new_config.storage_quota_gb * 1024 * 1024 * 1024;
192 if available.saturating_sub(requested) < margin {
193 return Err(
194 "OS_SAFETY_VIOLATION: Would leave the host OS with less than the 15 GB safety margin."
195 .to_string(),
196 );
197 }
198 let meta = app_meta_dir();
200 std::fs::create_dir_all(&meta).map_err(|e| e.to_string())?;
201 let json = serde_json::to_string_pretty(&new_config).map_err(|e| e.to_string())?;
202 std::fs::write(config_file_path(), json).map_err(|e| e.to_string())?;
203 init_data_directories(&new_config.storage_path);
205 let mut ib = crate::inference_backend::load_inference_backend_settings();
207 ib.apply_agent_config_backend_string(&new_config.inference_backend);
208 let _ = crate::inference_backend::save_inference_backend_settings(&ib);
209 *state.config.lock().unwrap() = new_config;
210 Ok(())
211}