clawft_kernel/model_manifest/
advertise.rs1use std::path::{Path, PathBuf};
14
15use clawft_types::placement::{AttrValue, Capability, CapabilityState, Provenance};
16
17use super::body::MODEL_MARKER_PREFIX;
18use super::check::{CheckMode, ModelState};
19use super::registry::ModelRegistry;
20use crate::node_facts::probe::{cap, str_list};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum StoreTier {
25 Internal,
27 External,
29}
30
31#[derive(Debug, Clone)]
36pub struct TierResolver {
37 external_roots: Vec<PathBuf>,
38}
39
40impl TierResolver {
41 pub fn system_default() -> Self {
43 let roots: &[&str] = if cfg!(target_os = "macos") {
44 &["/Volumes"]
45 } else {
46 &["/media", "/mnt", "/run/media"]
47 };
48 Self::with_external_roots(roots.iter().map(PathBuf::from).collect())
49 }
50
51 pub fn with_external_roots(external_roots: Vec<PathBuf>) -> Self {
53 Self { external_roots }
54 }
55
56 pub fn classify(&self, path: &Path) -> (StoreTier, Option<PathBuf>) {
58 for root in &self.external_roots {
59 if let Ok(rest) = path.strip_prefix(root)
60 && let Some(first) = rest.components().next()
61 {
62 return (StoreTier::External, Some(root.join(first)));
63 }
64 }
65 (StoreTier::Internal, None)
66 }
67
68 pub fn mounted(&self, mount: &Path) -> bool {
71 mount.is_dir()
72 }
73}
74
75impl Default for TierResolver {
76 fn default() -> Self {
77 Self::system_default()
78 }
79}
80
81pub fn model_capabilities(registry: &ModelRegistry, tiers: &TierResolver) -> Vec<Capability> {
87 let mut out = Vec::new();
88 let mut detached = false;
89 for (id, entry) in registry.entries() {
90 if entry.hidden {
91 continue;
93 }
94 let Ok(check) = registry.check(&id, CheckMode::Stat) else {
95 continue;
96 };
97 let untrusted = registry.untrusted(&id, &entry);
101 if matches!(check.state, ModelState::Refused { .. }) || entry.refused.is_some() || untrusted {
102 if let Some(c) = cap("model.present", Provenance::Probed) {
103 out.push(
104 c.with_attr("shards", str_list::<&str>(&[]))
105 .with_state(CapabilityState::Degraded),
106 );
107 }
108 continue;
109 }
110 let (tier, mount) = tiers.classify(&entry.root);
111 let drive_gone = tier == StoreTier::External
112 && mount.as_deref().is_some_and(|m| !tiers.mounted(m));
113 detached |= drive_gone;
114 let complete = check.present_shards.len() == check.total_shards;
115 let state = match (&check.state, complete) {
116 (ModelState::Ready, true) if !drive_gone => CapabilityState::Available,
117 _ => CapabilityState::Degraded,
118 };
119 let mut shards: Vec<String> = Vec::new();
120 if state == CapabilityState::Available || drive_gone {
121 shards.push(format!("{MODEL_MARKER_PREFIX}{id}"));
122 }
123 if drive_gone && let Ok(body) = entry.body() {
124 shards.extend(body.shard_hashes().map(String::from));
126 } else {
127 shards.extend(check.present_shards.iter().cloned());
128 }
129 if shards.is_empty() {
130 continue;
131 }
132 if let Some(c) = cap("model.present", Provenance::Probed) {
133 out.push(c.with_attr("shards", str_list(&shards)).with_state(state));
134 }
135 }
136 if detached
137 && let Some(c) = cap("store.tier.external", Provenance::Probed)
138 {
139 out.push(
140 c.with_attr("mounted", AttrValue::Bool(false))
141 .with_state(CapabilityState::Degraded),
142 );
143 }
144 out
145}