| 1 | //! One process-wide publication gate for verified facts, status and reader generation. |
| 2 | //! Fetchers capture a settings ticket before work; stale settings cannot publish or |
| 3 | //! write their cache after a newer disable or source change has been admitted. |
| 4 | |
| 5 | use std::collections::BTreeMap; |
| 6 | use std::sync::{Arc, LazyLock, RwLock}; |
| 7 | |
| 8 | use super::provenance::{CloudFactsState, CloudFactsStatus}; |
| 9 | use super::scope::ScopedFacts; |
| 10 | |
| 11 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 12 | pub struct OverlayTicket { |
| 13 | epoch: u64, |
| 14 | source_identity: String, |
| 15 | } |
| 16 | |
| 17 | #[derive(Debug, Clone, Default)] |
| 18 | pub struct OverlaySnapshot { |
| 19 | pub generation: u64, |
| 20 | pub fetched_at: Option<u64>, |
| 21 | pub facts: Option<Arc<ScopedFacts>>, |
| 22 | pub status: CloudFactsStatus, |
| 23 | } |
| 24 | |
| 25 | #[derive(Default)] |
| 26 | struct State { |
| 27 | epoch: u64, |
| 28 | enabled: bool, |
| 29 | source_identity: String, |
| 30 | highest_seen: BTreeMap<String, u64>, |
| 31 | snapshot: OverlaySnapshot, |
| 32 | } |
| 33 | |
| 34 | static STATE: LazyLock<RwLock<State>> = LazyLock::new(|| RwLock::new(State::default())); |
| 35 | |
| 36 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 37 | pub enum DefaultSource { |
| 38 | Compiled, |
| 39 | CloudFacts { facts_version: u64 }, |
| 40 | } |
| 41 | |
| 42 | #[must_use] |
| 43 | pub fn hard_disabled() -> bool { |
| 44 | std::env::var("CODEWHALE_DISABLE_CLOUD_FACTS").is_ok_and(|value| { |
| 45 | matches!( |
| 46 | value.trim().to_ascii_lowercase().as_str(), |
| 47 | "1" | "true" | "yes" | "on" |
| 48 | ) |
| 49 | }) |
| 50 | } |
| 51 | |
| 52 | fn disable(state: &mut State) { |
| 53 | if state.enabled |
| 54 | || state.snapshot.facts.is_some() |
| 55 | || state.snapshot.status.state != CloudFactsState::Off |
| 56 | { |
| 57 | state.epoch = state.epoch.saturating_add(1); |
| 58 | state.snapshot.generation = state.snapshot.generation.saturating_add(1); |
| 59 | } |
| 60 | state.enabled = false; |
| 61 | state.snapshot.facts = None; |
| 62 | state.snapshot.status = CloudFactsStatus::default(); |
| 63 | } |
| 64 | |
| 65 | /// Admit effective settings synchronously. Refreshes must use `current_ticket` |
| 66 | /// instead: an old settings object must never implicitly re-enable this layer. |
| 67 | pub fn configure(enabled: bool, source_identity: &str) -> Option<OverlayTicket> { |
| 68 | let mut state = STATE.write().ok()?; |
| 69 | if !enabled || hard_disabled() { |
| 70 | disable(&mut state); |
| 71 | return None; |
| 72 | } |
| 73 | if !state.enabled || state.source_identity != source_identity { |
| 74 | state.epoch = state.epoch.saturating_add(1); |
| 75 | state.snapshot.generation = state.snapshot.generation.saturating_add(1); |
| 76 | state.snapshot.facts = None; |
| 77 | state.snapshot.status = CloudFactsStatus { |
| 78 | state: CloudFactsState::BundledOnly, |
| 79 | ..Default::default() |
| 80 | }; |
| 81 | state.source_identity = source_identity.to_string(); |
| 82 | state.enabled = true; |
| 83 | } |
| 84 | Some(OverlayTicket { |
| 85 | epoch: state.epoch, |
| 86 | source_identity: state.source_identity.clone(), |
| 87 | }) |
| 88 | } |
| 89 | |
| 90 | /// Accepted in-process rollback floors survive settings and trust changes. |
| 91 | #[must_use] |
| 92 | pub fn highest_seen(channel: &str) -> Option<u64> { |
| 93 | STATE.read().ok()?.highest_seen.get(channel).copied() |
| 94 | } |
| 95 | |
| 96 | #[must_use] |
| 97 | pub fn current_ticket(source_identity: &str) -> Option<OverlayTicket> { |
| 98 | let mut state = STATE.write().ok()?; |
| 99 | if hard_disabled() { |
| 100 | disable(&mut state); |
| 101 | } |
| 102 | (state.enabled && state.source_identity == source_identity).then(|| OverlayTicket { |
| 103 | epoch: state.epoch, |
| 104 | source_identity: state.source_identity.clone(), |
| 105 | }) |
| 106 | } |
| 107 | |
| 108 | #[must_use] |
| 109 | pub fn is_current(ticket: &OverlayTicket) -> bool { |
| 110 | let Ok(mut state) = STATE.write() else { |
| 111 | return false; |
| 112 | }; |
| 113 | if hard_disabled() { |
| 114 | disable(&mut state); |
| 115 | } |
| 116 | state.enabled && ticket.epoch == state.epoch && ticket.source_identity == state.source_identity |
| 117 | } |
| 118 | |
| 119 | pub fn publish( |
| 120 | ticket: &OverlayTicket, |
| 121 | facts: Option<ScopedFacts>, |
| 122 | status: CloudFactsStatus, |
| 123 | ) -> bool { |
| 124 | publish_with(ticket, facts, status, || {}) |
| 125 | } |
| 126 | |
| 127 | /// The callback may save/delete the bounded cache but must not reenter this |
| 128 | /// module. Its write and the state publication share the disable/source gate. |
| 129 | pub fn publish_with( |
| 130 | ticket: &OverlayTicket, |
| 131 | facts: Option<ScopedFacts>, |
| 132 | status: CloudFactsStatus, |
| 133 | before_publish: impl FnOnce(), |
| 134 | ) -> bool { |
| 135 | let Ok(mut state) = STATE.write() else { |
| 136 | return false; |
| 137 | }; |
| 138 | if hard_disabled() { |
| 139 | disable(&mut state); |
| 140 | } |
| 141 | if !state.enabled |
| 142 | || ticket.epoch != state.epoch |
| 143 | || ticket.source_identity != state.source_identity |
| 144 | { |
| 145 | return false; |
| 146 | } |
| 147 | if let Some(facts) = &facts { |
| 148 | if state |
| 149 | .highest_seen |
| 150 | .get(&facts.channel) |
| 151 | .is_some_and(|version| facts.facts_version < *version) |
| 152 | { |
| 153 | return false; |
| 154 | } |
| 155 | state |
| 156 | .highest_seen |
| 157 | .insert(facts.channel.clone(), facts.facts_version); |
| 158 | } |
| 159 | before_publish(); |
| 160 | state.snapshot.fetched_at = if facts.is_none() { |
| 161 | None |
| 162 | } else { |
| 163 | match &status.state { |
| 164 | CloudFactsState::Verified { fetched_at, .. } => Some(*fetched_at), |
| 165 | _ => state.snapshot.fetched_at, |
| 166 | } |
| 167 | }; |
| 168 | state.snapshot.facts = facts.map(Arc::new); |
| 169 | state.snapshot.status = status; |
| 170 | state.snapshot.generation = state.snapshot.generation.saturating_add(1); |
| 171 | true |
| 172 | } |
| 173 | |
| 174 | fn snapshot_at(now: u64) -> OverlaySnapshot { |
| 175 | let Ok(mut state) = STATE.write() else { |
| 176 | return OverlaySnapshot::default(); |
| 177 | }; |
| 178 | if hard_disabled() { |
| 179 | disable(&mut state); |
| 180 | } |
| 181 | if state |
| 182 | .snapshot |
| 183 | .facts |
| 184 | .as_ref() |
| 185 | .is_some_and(|facts| !facts.is_current_at(now)) |
| 186 | { |
| 187 | state.snapshot.facts = None; |
| 188 | state.snapshot.generation = state.snapshot.generation.saturating_add(1); |
| 189 | if let CloudFactsState::Verified { stale, .. } = &mut state.snapshot.status.state { |
| 190 | *stale = true; |
| 191 | } |
| 192 | } |
| 193 | state.snapshot.clone() |
| 194 | } |
| 195 | |
| 196 | #[must_use] |
| 197 | pub fn snapshot() -> OverlaySnapshot { |
| 198 | snapshot_at(crate::catalog::now_unix()) |
| 199 | } |
| 200 | |
| 201 | #[must_use] |
| 202 | pub fn overlay() -> Option<Arc<ScopedFacts>> { |
| 203 | snapshot().facts |
| 204 | } |
| 205 | |
| 206 | #[must_use] |
| 207 | pub fn status() -> CloudFactsStatus { |
| 208 | snapshot().status |
| 209 | } |
| 210 | |
| 211 | #[must_use] |
| 212 | pub fn is_verified() -> bool { |
| 213 | overlay().is_some() |
| 214 | } |
| 215 | |
| 216 | #[must_use] |
| 217 | pub fn cloud_default_model(provider: &str) -> Option<(String, DefaultSource)> { |
| 218 | let overlay = overlay()?; |
| 219 | let model = overlay |
| 220 | .provider_defaults |
| 221 | .get(provider)? |
| 222 | .default_model |
| 223 | .clone()?; |
| 224 | Some(( |
| 225 | model, |
| 226 | DefaultSource::CloudFacts { |
| 227 | facts_version: overlay.facts_version, |
| 228 | }, |
| 229 | )) |
| 230 | } |
| 231 | |
| 232 | /// Cloud defaults are a floor for official hosted endpoints, never an override |
| 233 | /// for named compatible routes, local machines, or a Codex account roster. |
| 234 | #[must_use] |
| 235 | pub fn cloud_default_model_for_route( |
| 236 | provider: crate::ProviderKind, |
| 237 | base_url: &str, |
| 238 | ) -> Option<(String, DefaultSource)> { |
| 239 | if matches!( |
| 240 | provider, |
| 241 | crate::ProviderKind::Custom |
| 242 | | crate::ProviderKind::Ollama |
| 243 | | crate::ProviderKind::Sglang |
| 244 | | crate::ProviderKind::Vllm |
| 245 | | crate::ProviderKind::OpenaiCodex |
| 246 | ) || !super::scope::base_url_allowed(provider.as_str(), base_url) |
| 247 | { |
| 248 | return None; |
| 249 | } |
| 250 | cloud_default_model(provider.as_str()) |
| 251 | } |
| 252 | |
| 253 | #[must_use] |
| 254 | pub fn cloud_default_base_url(provider: &str) -> Option<(String, DefaultSource)> { |
| 255 | let overlay = overlay()?; |
| 256 | let url = overlay.provider_defaults.get(provider)?.base_url.clone()?; |
| 257 | Some(( |
| 258 | url, |
| 259 | DefaultSource::CloudFacts { |
| 260 | facts_version: overlay.facts_version, |
| 261 | }, |
| 262 | )) |
| 263 | } |
| 264 | |
| 265 | pub fn clear() { |
| 266 | if let Ok(mut state) = STATE.write() { |
| 267 | disable(&mut state); |
| 268 | } |
| 269 | } |
| 270 |