返回 CodeWhale
scope.rs
根目录 / crates / config / src / cloud_facts / scope.rs
1 //! Scope a verified payload to the running binary: per-item `applies_to`,
2 //! announcement windows, and the provider `base_url` allowlist all live here so
3 //! consumers see one already-filtered view.
4
5 use std::collections::BTreeMap;
6
7 use super::types::{
8 Announcement, MAX_ANNOUNCEMENT_CHARS, ModelFact, ModelOp, ProviderDefaultFact, ReleaseFact,
9 };
10 use super::verify::{VerifiedFacts, item_applies, parse_rfc3339_utc};
11 use crate::provider_kind::ProviderKind;
12
13 /// A verified payload filtered to what applies to this binary right now.
14 #[derive(Debug, Clone, PartialEq, Default)]
15 pub struct ScopedFacts {
16 pub channel: String,
17 pub facts_version: u64,
18 pub key_id: String,
19 pub sha256: String,
20 pub published_at: String,
21 pub stale: bool,
22 /// Signed expiry plus the verification grace window; never extended by 304.
23 pub valid_until: Option<u64>,
24 pub models: Vec<ModelFact>,
25 pub provider_defaults: BTreeMap<String, ProviderDefaultFact>,
26 pub release: Option<ReleaseFact>,
27 pub announcements: Vec<Announcement>,
28 /// Human-readable receipts for every item that was dropped and why.
29 pub dropped: Vec<String>,
30 }
31
32 impl ScopedFacts {
33 #[must_use]
34 pub fn is_current_at(&self, now: u64) -> bool {
35 !self.stale && self.valid_until.is_none_or(|expires| now <= expires)
36 }
37
38 /// Count of applied patch/default/announcement items, for `/status`.
39 #[must_use]
40 pub fn item_counts(&self) -> (usize, usize, usize) {
41 (
42 self.models.len(),
43 self.provider_defaults.len(),
44 self.announcements.len(),
45 )
46 }
47 }
48
49 fn cloud_provider(provider: &str) -> bool {
50 // Catalog aliases collapse wire variants; signed route facts retain the
51 // exact canonical config identity and its distinct endpoint contract.
52 ProviderKind::parse_config_identity(provider).is_some_and(|kind| {
53 kind.as_str() == provider
54 && !matches!(
55 kind,
56 ProviderKind::Custom
57 | ProviderKind::Ollama
58 | ProviderKind::Sglang
59 | ProviderKind::Vllm
60 | ProviderKind::OpenaiCodex
61 | ProviderKind::Antigravity
62 )
63 })
64 }
65
66 fn model_id_valid(id: &str) -> bool {
67 !id.is_empty()
68 && id.len() <= 256
69 && id.bytes().all(|byte| {
70 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'/' | b'-')
71 })
72 }
73
74 /// Accept only an existing official HTTPS endpoint contract. Signing a fact
75 /// cannot grant a new host, neighboring API path, userinfo or credential scope.
76 #[must_use]
77 pub fn base_url_allowed(provider: &str, candidate: &str) -> bool {
78 if !cloud_provider(provider) {
79 return false;
80 }
81 let Some(kind) = ProviderKind::parse_config_identity(provider) else {
82 return false;
83 };
84 let normalized = candidate.trim().trim_end_matches('/').to_ascii_lowercase();
85 if !normalized.starts_with("https://")
86 || normalized
87 .chars()
88 .any(|ch| ch.is_whitespace() || ch.is_control() || matches!(ch, '?' | '#' | '@' | '\\'))
89 {
90 return false;
91 }
92 match kind {
93 ProviderKind::Codewhale => normalized == crate::DEFAULT_CODEWHALE_BASE_URL,
94 ProviderKind::Moonshot => matches!(
95 normalized.as_str(),
96 crate::DEFAULT_MOONSHOT_BASE_URL
97 | crate::MOONSHOT_CN_BASE_URL
98 | "https://api.kimi.com/coding"
99 | "https://api.kimi.com/coding/v1"
100 ),
101 _ => crate::provider_base_url_is_official(kind, &normalized),
102 }
103 }
104
105 /// Build the scoped view for `current` at `now_unix`.
106 #[must_use]
107 pub fn scoped_view(
108 verified: &VerifiedFacts,
109 current: &semver::Version,
110 now_unix: u64,
111 ) -> ScopedFacts {
112 let facts = &verified.facts;
113 let mut out = ScopedFacts {
114 channel: facts.channel.clone(),
115 facts_version: facts.facts_version,
116 key_id: verified.key_id.clone(),
117 sha256: verified.sha256.clone(),
118 published_at: facts.published_at.clone(),
119 stale: verified.stale,
120 valid_until: facts
121 .not_after
122 .as_deref()
123 .and_then(parse_rfc3339_utc)
124 .map(|expires| expires.saturating_add(super::verify::NOT_AFTER_GRACE_SECS)),
125 ..ScopedFacts::default()
126 };
127
128 for model in &facts.models {
129 if !cloud_provider(&model.provider)
130 || !model_id_valid(&model.id)
131 || model.context_window == Some(0)
132 || model.max_output == Some(0)
133 {
134 out.dropped
135 .push("model patch has unsupported provider, identity, or limits".into());
136 continue;
137 }
138 if !item_applies(model.applies_to.as_deref(), current) {
139 out.dropped.push(format!(
140 "model {}/{}: applies_to {:?} does not match",
141 model.provider,
142 model.id,
143 model.applies_to.as_deref().unwrap_or("")
144 ));
145 continue;
146 }
147 let mut kept = model.clone();
148 // An unlisted assertion overrides a provider roster's own omission, so
149 // it must be an `Upsert` and it must expire: without `not_after` the
150 // claim would outlive any ability to withdraw it by publishing. The
151 // rest of the patch still applies; only the assertion is discarded.
152 if kept.allow_unlisted && (kept.op != ModelOp::Upsert || out.valid_until.is_none()) {
153 kept.allow_unlisted = false;
154 out.dropped.push(format!(
155 "model {}/{}: allow_unlisted needs an upsert in a payload with not_after",
156 model.provider, model.id
157 ));
158 }
159 out.models.push(kept);
160 }
161
162 for (provider, fact) in &facts.provider_defaults {
163 if !cloud_provider(provider) {
164 out.dropped.push("unsupported provider default".into());
165 continue;
166 }
167 if !item_applies(fact.applies_to.as_deref(), current) {
168 out.dropped.push(format!(
169 "provider_defaults.{provider}: applies_to {:?} does not match",
170 fact.applies_to.as_deref().unwrap_or("")
171 ));
172 continue;
173 }
174 let mut kept = ProviderDefaultFact {
175 default_model: fact
176 .default_model
177 .as_deref()
178 .map(str::trim)
179 .filter(|m| {
180 model_id_valid(m)
181 && !m.eq_ignore_ascii_case("auto")
182 && !m.eq_ignore_ascii_case("unknown")
183 })
184 .map(str::to_string),
185 base_url: None,
186 applies_to: None,
187 };
188 if let Some(url) = fact.base_url.as_deref().map(str::trim) {
189 if base_url_allowed(provider, url) {
190 kept.base_url = Some(url.to_string());
191 } else {
192 out.dropped.push(format!(
193 "provider_defaults.{provider}.base_url: outside the official HTTPS endpoint contract"
194 ));
195 }
196 }
197 if kept.default_model.is_none() && kept.base_url.is_none() {
198 continue;
199 }
200 out.provider_defaults.insert(provider.clone(), kept);
201 }
202
203 if let Some(release) = &facts.release {
204 if item_applies(release.applies_to.as_deref(), current) {
205 out.release = Some(release.clone());
206 } else {
207 out.dropped.push(format!(
208 "release: applies_to {:?} does not match",
209 release.applies_to.as_deref().unwrap_or("")
210 ));
211 }
212 }
213
214 for announcement in &facts.announcements {
215 let id = announcement.id.trim();
216 if id.is_empty() || announcement.text.trim().is_empty() {
217 out.dropped.push("announcement without id/text".into());
218 continue;
219 }
220 if announcement.text.chars().count() > MAX_ANNOUNCEMENT_CHARS {
221 out.dropped
222 .push(format!("announcement {id}: text too long"));
223 continue;
224 }
225 if !item_applies(announcement.applies_to.as_deref(), current) {
226 out.dropped
227 .push(format!("announcement {id}: applies_to does not match"));
228 continue;
229 }
230 if [
231 announcement.starts_at.as_deref(),
232 announcement.expires_at.as_deref(),
233 ]
234 .into_iter()
235 .flatten()
236 .any(|value| parse_rfc3339_utc(value).is_none())
237 {
238 out.dropped
239 .push(format!("announcement {id}: invalid time window"));
240 continue;
241 }
242 if let Some(starts) = announcement
243 .starts_at
244 .as_deref()
245 .and_then(parse_rfc3339_utc)
246 && now_unix < starts
247 {
248 out.dropped.push(format!("announcement {id}: not started"));
249 continue;
250 }
251 if let Some(expires) = announcement
252 .expires_at
253 .as_deref()
254 .and_then(parse_rfc3339_utc)
255 && now_unix >= expires
256 {
257 out.dropped.push(format!("announcement {id}: expired"));
258 continue;
259 }
260 out.announcements.push(announcement.clone());
261 }
262
263 out
264 }
265
265 lines RUST