Skip to main content

batuta/serve/
backends.rs

1//! Backend Selection and Privacy Tiers
2//!
3//! Implements Toyota Way "Poka-Yoke" (Mistake Proofing) with privacy gates.
4//!
5//! ## Privacy Tiers
6//!
7//! - `Sovereign` - Local only, blocks all external API calls
8//! - `Private` - Dedicated/VPC endpoints only
9//! - `Standard` - Public APIs acceptable
10
11use serde::{Deserialize, Serialize};
12use std::collections::HashSet;
13
14// ============================================================================
15// SERVE-BKD-001: Backend Types
16// ============================================================================
17
18/// Supported serving backends
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub enum ServingBackend {
21    // Local backends
22    Realizar,
23    Ollama,
24    LlamaCpp,
25    Llamafile,
26    Candle,
27    Vllm,
28    Tgi,
29    LocalAI,
30
31    // Remote backends
32    HuggingFace,
33    Together,
34    Replicate,
35    Anyscale,
36    Modal,
37    Fireworks,
38    Groq,
39    OpenAI,
40    Anthropic,
41    AzureOpenAI,
42    AwsBedrock,
43    GoogleVertex,
44
45    // Serverless backends
46    AwsLambda,
47    CloudflareWorkers,
48}
49
50impl ServingBackend {
51    /// Check if this is a local backend (no network calls)
52    #[must_use]
53    pub const fn is_local(&self) -> bool {
54        matches!(
55            self,
56            Self::Realizar
57                | Self::Ollama
58                | Self::LlamaCpp
59                | Self::Llamafile
60                | Self::Candle
61                | Self::Vllm
62                | Self::Tgi
63                | Self::LocalAI
64        )
65    }
66
67    /// Check if this is a remote/cloud backend
68    #[must_use]
69    pub const fn is_remote(&self) -> bool {
70        !self.is_local()
71    }
72
73    /// Get the API endpoint host for remote backends
74    #[must_use]
75    pub const fn api_host(&self) -> Option<&'static str> {
76        match self {
77            Self::HuggingFace => Some("api-inference.huggingface.co"),
78            Self::Together => Some("api.together.xyz"),
79            Self::Replicate => Some("api.replicate.com"),
80            Self::Anyscale => Some("api.anyscale.com"),
81            Self::Modal => Some("api.modal.com"),
82            Self::Fireworks => Some("api.fireworks.ai"),
83            Self::Groq => Some("api.groq.com"),
84            Self::OpenAI => Some("api.openai.com"),
85            Self::Anthropic => Some("api.anthropic.com"),
86            Self::AzureOpenAI => Some("openai.azure.com"),
87            Self::AwsBedrock => Some("bedrock-runtime.amazonaws.com"),
88            Self::GoogleVertex => Some("aiplatform.googleapis.com"),
89            Self::AwsLambda => Some("lambda.amazonaws.com"),
90            Self::CloudflareWorkers => Some("workers.cloudflare.com"),
91            _ => None,
92        }
93    }
94
95    /// Check if this is a serverless backend
96    #[must_use]
97    pub const fn is_serverless(&self) -> bool {
98        matches!(self, Self::AwsLambda | Self::CloudflareWorkers | Self::Modal)
99    }
100}
101
102// ============================================================================
103// SERVE-BKD-002: Privacy Tiers
104// ============================================================================
105
106/// Privacy tier for backend selection
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
108pub enum PrivacyTier {
109    /// Local only - blocks ALL external API calls (Poka-Yoke)
110    Sovereign,
111    /// Dedicated/VPC endpoints only
112    Private,
113    /// Public APIs acceptable
114    #[default]
115    Standard,
116}
117
118impl PrivacyTier {
119    /// Check if a backend is allowed under this privacy tier
120    #[must_use]
121    pub fn allows(&self, backend: ServingBackend) -> bool {
122        match self {
123            Self::Sovereign => backend.is_local(),
124            Self::Private => {
125                backend.is_local()
126                    || matches!(
127                        backend,
128                        ServingBackend::AzureOpenAI
129                            | ServingBackend::AwsBedrock
130                            | ServingBackend::GoogleVertex
131                            | ServingBackend::AwsLambda // Your own Lambda functions
132                    )
133            }
134            Self::Standard => true,
135        }
136    }
137
138    /// Get all blocked API hosts for this tier (for network egress locking)
139    #[must_use]
140    pub fn blocked_hosts(&self) -> Vec<&'static str> {
141        match self {
142            Self::Sovereign => {
143                // Block ALL remote API hosts
144                vec![
145                    "api-inference.huggingface.co",
146                    "api.together.xyz",
147                    "api.replicate.com",
148                    "api.anyscale.com",
149                    "api.modal.com",
150                    "api.fireworks.ai",
151                    "api.groq.com",
152                    "api.openai.com",
153                    "api.anthropic.com",
154                    "openai.azure.com",
155                    "bedrock-runtime.amazonaws.com",
156                    "aiplatform.googleapis.com",
157                    "lambda.amazonaws.com",
158                    "workers.cloudflare.com",
159                ]
160            }
161            Self::Private => {
162                // Block public APIs but allow enterprise/owned endpoints
163                vec![
164                    "api-inference.huggingface.co",
165                    "api.together.xyz",
166                    "api.replicate.com",
167                    "api.anyscale.com",
168                    "api.modal.com",
169                    "api.fireworks.ai",
170                    "api.groq.com",
171                    "api.openai.com",
172                    "api.anthropic.com",
173                    "workers.cloudflare.com", // Cloudflare not in your control
174                ]
175            }
176            Self::Standard => vec![],
177        }
178    }
179}
180
181// ============================================================================
182// SERVE-BKD-003: Latency and Throughput Tiers
183// ============================================================================
184
185/// Latency requirement tier
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
187pub enum LatencyTier {
188    /// <100ms - requires local GPU or Groq
189    RealTime,
190    /// <1s - local or fast remote
191    #[default]
192    Interactive,
193    /// >1s acceptable - any backend
194    Batch,
195}
196
197/// Throughput requirement tier
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
199pub enum ThroughputTier {
200    /// Low volume (<10 req/s)
201    #[default]
202    Low,
203    /// Medium volume (10-100 req/s)
204    Medium,
205    /// High volume (>100 req/s)
206    High,
207}
208
209/// Cost sensitivity tier
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
211pub enum CostTier {
212    /// Minimize cost at all costs
213    Frugal,
214    /// Balance cost and performance
215    #[default]
216    Balanced,
217    /// Performance over cost
218    Premium,
219}
220
221// ============================================================================
222// SERVE-BKD-004: Backend Selector
223// ============================================================================
224
225/// Backend selection configuration
226#[derive(Debug, Clone, Default, Serialize, Deserialize)]
227pub struct BackendSelector {
228    pub privacy: PrivacyTier,
229    pub latency: LatencyTier,
230    pub throughput: ThroughputTier,
231    pub cost: CostTier,
232    /// Explicitly disabled backends
233    pub disabled: HashSet<ServingBackend>,
234    /// Preferred backends (tried first)
235    pub preferred: Vec<ServingBackend>,
236}
237
238impl BackendSelector {
239    /// Create a new backend selector with default settings
240    #[must_use]
241    pub fn new() -> Self {
242        Self::default()
243    }
244
245    /// Set privacy tier
246    #[must_use]
247    pub fn with_privacy(mut self, tier: PrivacyTier) -> Self {
248        self.privacy = tier;
249        self
250    }
251
252    /// Set latency tier
253    #[must_use]
254    pub fn with_latency(mut self, tier: LatencyTier) -> Self {
255        self.latency = tier;
256        self
257    }
258
259    /// Set throughput tier
260    #[must_use]
261    pub fn with_throughput(mut self, tier: ThroughputTier) -> Self {
262        self.throughput = tier;
263        self
264    }
265
266    /// Set cost tier
267    #[must_use]
268    pub fn with_cost(mut self, tier: CostTier) -> Self {
269        self.cost = tier;
270        self
271    }
272
273    /// Disable a specific backend
274    #[must_use]
275    pub fn disable(mut self, backend: ServingBackend) -> Self {
276        self.disabled.insert(backend);
277        self
278    }
279
280    /// Add a preferred backend
281    #[must_use]
282    pub fn prefer(mut self, backend: ServingBackend) -> Self {
283        self.preferred.push(backend);
284        self
285    }
286
287    /// Recommend backends based on requirements
288    #[must_use]
289    pub fn recommend(&self) -> Vec<ServingBackend> {
290        let mut candidates: Vec<ServingBackend> = Vec::new();
291
292        // Start with preferred backends
293        for backend in &self.preferred {
294            if self.is_valid(*backend) {
295                candidates.push(*backend);
296            }
297        }
298
299        // Add tier-appropriate backends
300        let tier_backends = self.get_tier_backends();
301        for backend in tier_backends {
302            if self.is_valid(backend) && !candidates.contains(&backend) {
303                candidates.push(backend);
304            }
305        }
306
307        candidates
308    }
309
310    /// Check if a backend is valid for current configuration
311    #[must_use]
312    pub fn is_valid(&self, backend: ServingBackend) -> bool {
313        !self.disabled.contains(&backend) && self.privacy.allows(backend)
314    }
315
316    /// Validate a request against privacy tier (Poka-Yoke gate)
317    ///
318    /// Returns `Err` if the backend would violate privacy constraints.
319    pub fn validate(&self, backend: ServingBackend) -> Result<(), PrivacyViolation> {
320        if self.disabled.contains(&backend) {
321            return Err(PrivacyViolation::BackendDisabled(backend));
322        }
323
324        if !self.privacy.allows(backend) {
325            return Err(PrivacyViolation::TierViolation { backend, tier: self.privacy });
326        }
327
328        Ok(())
329    }
330
331    fn get_tier_backends(&self) -> Vec<ServingBackend> {
332        match (self.latency, self.privacy) {
333            // Real-time + Sovereign: only local with GPU
334            (LatencyTier::RealTime, PrivacyTier::Sovereign) => {
335                vec![ServingBackend::Realizar, ServingBackend::LlamaCpp]
336            }
337            // Real-time + any: Groq is fastest, then local
338            (LatencyTier::RealTime, _) => {
339                vec![ServingBackend::Groq, ServingBackend::Realizar, ServingBackend::Fireworks]
340            }
341            // Interactive + Sovereign: local options
342            (LatencyTier::Interactive, PrivacyTier::Sovereign) => {
343                vec![ServingBackend::Realizar, ServingBackend::Ollama, ServingBackend::LlamaCpp]
344            }
345            // Interactive + Private: local + enterprise + Lambda
346            (LatencyTier::Interactive, PrivacyTier::Private) => {
347                vec![
348                    ServingBackend::Realizar,
349                    ServingBackend::Ollama,
350                    ServingBackend::AzureOpenAI,
351                    ServingBackend::AwsBedrock,
352                    ServingBackend::AwsLambda,
353                ]
354            }
355            // Interactive + Standard: mix of fast options
356            (LatencyTier::Interactive, PrivacyTier::Standard) => {
357                vec![
358                    ServingBackend::Realizar,
359                    ServingBackend::Groq,
360                    ServingBackend::Together,
361                    ServingBackend::Fireworks,
362                ]
363            }
364            // Batch + Sovereign: local only
365            (LatencyTier::Batch, PrivacyTier::Sovereign) => {
366                vec![ServingBackend::Realizar, ServingBackend::Ollama]
367            }
368            // Batch + Private: Lambda is excellent for batch (pay per use)
369            (LatencyTier::Batch, PrivacyTier::Private) => {
370                vec![
371                    ServingBackend::AwsLambda,
372                    ServingBackend::Realizar,
373                    ServingBackend::AwsBedrock,
374                ]
375            }
376            // Batch + Standard: prioritize cost
377            (LatencyTier::Batch, PrivacyTier::Standard) => {
378                vec![
379                    ServingBackend::AwsLambda,
380                    ServingBackend::Together,
381                    ServingBackend::HuggingFace,
382                    ServingBackend::Replicate,
383                ]
384            }
385        }
386    }
387}
388
389/// Privacy violation error
390#[derive(Debug, Clone, PartialEq, Eq)]
391pub enum PrivacyViolation {
392    /// Backend is explicitly disabled
393    BackendDisabled(ServingBackend),
394    /// Backend violates privacy tier
395    TierViolation { backend: ServingBackend, tier: PrivacyTier },
396}
397
398impl std::fmt::Display for PrivacyViolation {
399    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
400        match self {
401            Self::BackendDisabled(b) => write!(f, "Backend {:?} is disabled", b),
402            Self::TierViolation { backend, tier } => {
403                write!(f, "Backend {:?} violates {:?} privacy tier", backend, tier)
404            }
405        }
406    }
407}
408
409impl std::error::Error for PrivacyViolation {}
410
411// ============================================================================
412// Tests
413// ============================================================================
414
415#[cfg(test)]
416#[allow(non_snake_case)]
417mod tests {
418    use super::*;
419
420    // ========================================================================
421    // SERVE-BKD-001: Backend Type Tests
422    // ========================================================================
423
424    #[test]
425    fn test_SERVE_BKD_001_local_backends() {
426        assert!(ServingBackend::Realizar.is_local());
427        assert!(ServingBackend::Ollama.is_local());
428        assert!(ServingBackend::LlamaCpp.is_local());
429        assert!(ServingBackend::Llamafile.is_local());
430        assert!(ServingBackend::Candle.is_local());
431        assert!(ServingBackend::Vllm.is_local());
432        assert!(ServingBackend::Tgi.is_local());
433        assert!(ServingBackend::LocalAI.is_local());
434    }
435
436    #[test]
437    fn test_SERVE_BKD_001_remote_backends() {
438        assert!(ServingBackend::OpenAI.is_remote());
439        assert!(ServingBackend::Anthropic.is_remote());
440        assert!(ServingBackend::Together.is_remote());
441        assert!(ServingBackend::Groq.is_remote());
442        assert!(ServingBackend::HuggingFace.is_remote());
443    }
444
445    #[test]
446    fn test_SERVE_BKD_001_api_hosts() {
447        assert_eq!(ServingBackend::OpenAI.api_host(), Some("api.openai.com"));
448        assert_eq!(ServingBackend::Anthropic.api_host(), Some("api.anthropic.com"));
449        assert_eq!(ServingBackend::Realizar.api_host(), None);
450    }
451
452    // ========================================================================
453    // SERVE-BKD-002: Privacy Tier Tests
454    // ========================================================================
455
456    #[test]
457    fn test_SERVE_BKD_002_sovereign_blocks_remote() {
458        let tier = PrivacyTier::Sovereign;
459        assert!(tier.allows(ServingBackend::Realizar));
460        assert!(tier.allows(ServingBackend::Ollama));
461        assert!(!tier.allows(ServingBackend::OpenAI));
462        assert!(!tier.allows(ServingBackend::Anthropic));
463        assert!(!tier.allows(ServingBackend::AzureOpenAI));
464    }
465
466    #[test]
467    fn test_SERVE_BKD_002_private_allows_enterprise() {
468        let tier = PrivacyTier::Private;
469        assert!(tier.allows(ServingBackend::Realizar));
470        assert!(tier.allows(ServingBackend::AzureOpenAI));
471        assert!(tier.allows(ServingBackend::AwsBedrock));
472        assert!(tier.allows(ServingBackend::GoogleVertex));
473        assert!(!tier.allows(ServingBackend::OpenAI));
474        assert!(!tier.allows(ServingBackend::Together));
475    }
476
477    #[test]
478    fn test_SERVE_BKD_002_standard_allows_all() {
479        let tier = PrivacyTier::Standard;
480        assert!(tier.allows(ServingBackend::Realizar));
481        assert!(tier.allows(ServingBackend::OpenAI));
482        assert!(tier.allows(ServingBackend::Together));
483    }
484
485    #[test]
486    fn test_SERVE_BKD_002_sovereign_blocked_hosts() {
487        let hosts = PrivacyTier::Sovereign.blocked_hosts();
488        assert!(hosts.contains(&"api.openai.com"));
489        assert!(hosts.contains(&"api.anthropic.com"));
490        assert!(hosts.contains(&"api.together.xyz"));
491        assert!(hosts.contains(&"lambda.amazonaws.com"));
492        assert!(hosts.contains(&"workers.cloudflare.com"));
493        assert_eq!(hosts.len(), 14);
494    }
495
496    #[test]
497    fn test_SERVE_BKD_002_standard_no_blocked_hosts() {
498        let hosts = PrivacyTier::Standard.blocked_hosts();
499        assert!(hosts.is_empty());
500    }
501
502    // ========================================================================
503    // SERVE-BKD-003: Backend Selector Tests
504    // ========================================================================
505
506    #[test]
507    fn test_SERVE_BKD_003_default_selector() {
508        let selector = BackendSelector::new();
509        assert_eq!(selector.privacy, PrivacyTier::Standard);
510        assert_eq!(selector.latency, LatencyTier::Interactive);
511    }
512
513    #[test]
514    fn test_SERVE_BKD_003_sovereign_recommend() {
515        let selector = BackendSelector::new().with_privacy(PrivacyTier::Sovereign);
516        let backends = selector.recommend();
517        // All recommended backends should be local
518        for backend in &backends {
519            assert!(backend.is_local(), "{:?} should be local", backend);
520        }
521    }
522
523    #[test]
524    fn test_SERVE_BKD_003_realtime_recommend() {
525        let selector = BackendSelector::new().with_latency(LatencyTier::RealTime);
526        let backends = selector.recommend();
527        // Should include Groq (fastest)
528        assert!(backends.contains(&ServingBackend::Groq));
529    }
530
531    #[test]
532    fn test_SERVE_BKD_003_disabled_backend() {
533        let selector = BackendSelector::new().disable(ServingBackend::OpenAI);
534        assert!(!selector.is_valid(ServingBackend::OpenAI));
535        assert!(selector.is_valid(ServingBackend::Anthropic));
536    }
537
538    #[test]
539    fn test_SERVE_BKD_003_preferred_backend() {
540        let selector = BackendSelector::new().prefer(ServingBackend::Anthropic);
541        let backends = selector.recommend();
542        assert_eq!(backends[0], ServingBackend::Anthropic);
543    }
544
545    // ========================================================================
546    // SERVE-BKD-004: Validation Tests (Poka-Yoke)
547    // ========================================================================
548
549    #[test]
550    fn test_SERVE_BKD_004_validate_sovereign_blocks_openai() {
551        let selector = BackendSelector::new().with_privacy(PrivacyTier::Sovereign);
552        let result = selector.validate(ServingBackend::OpenAI);
553        assert!(result.is_err());
554        assert!(matches!(result.unwrap_err(), PrivacyViolation::TierViolation { .. }));
555    }
556
557    #[test]
558    fn test_SERVE_BKD_004_validate_sovereign_allows_local() {
559        let selector = BackendSelector::new().with_privacy(PrivacyTier::Sovereign);
560        assert!(selector.validate(ServingBackend::Realizar).is_ok());
561        assert!(selector.validate(ServingBackend::Ollama).is_ok());
562    }
563
564    #[test]
565    fn test_SERVE_BKD_004_validate_disabled() {
566        let selector = BackendSelector::new().disable(ServingBackend::Together);
567        let result = selector.validate(ServingBackend::Together);
568        assert!(result.is_err());
569        assert!(matches!(result.unwrap_err(), PrivacyViolation::BackendDisabled(_)));
570    }
571
572    #[test]
573    fn test_SERVE_BKD_004_privacy_violation_display() {
574        let err = PrivacyViolation::TierViolation {
575            backend: ServingBackend::OpenAI,
576            tier: PrivacyTier::Sovereign,
577        };
578        assert!(err.to_string().contains("OpenAI"));
579        assert!(err.to_string().contains("Sovereign"));
580    }
581
582    // ========================================================================
583    // SERVE-BKD-005: Builder Pattern Tests
584    // ========================================================================
585
586    #[test]
587    fn test_SERVE_BKD_005_builder_chain() {
588        let selector = BackendSelector::new()
589            .with_privacy(PrivacyTier::Private)
590            .with_latency(LatencyTier::RealTime)
591            .with_throughput(ThroughputTier::High)
592            .with_cost(CostTier::Premium)
593            .prefer(ServingBackend::AzureOpenAI)
594            .disable(ServingBackend::AwsBedrock);
595
596        assert_eq!(selector.privacy, PrivacyTier::Private);
597        assert_eq!(selector.latency, LatencyTier::RealTime);
598        assert_eq!(selector.throughput, ThroughputTier::High);
599        assert_eq!(selector.cost, CostTier::Premium);
600        assert!(selector.preferred.contains(&ServingBackend::AzureOpenAI));
601        assert!(selector.disabled.contains(&ServingBackend::AwsBedrock));
602    }
603
604    // ========================================================================
605    // SERVE-BKD-006: Edge Cases
606    // ========================================================================
607
608    #[test]
609    fn test_SERVE_BKD_006_empty_recommend() {
610        // If all tier-recommended backends are disabled, recommend returns empty
611        let selector = BackendSelector::new()
612            .with_privacy(PrivacyTier::Sovereign)
613            .with_latency(LatencyTier::Interactive)
614            .disable(ServingBackend::Realizar)
615            .disable(ServingBackend::Ollama)
616            .disable(ServingBackend::LlamaCpp);
617        let backends = selector.recommend();
618        // With Interactive + Sovereign tier, only these 3 are recommended
619        // When all are disabled, recommend returns empty
620        assert!(backends.is_empty());
621    }
622
623    #[test]
624    fn test_SERVE_BKD_006_batch_tier_prefers_cheap() {
625        let selector = BackendSelector::new()
626            .with_latency(LatencyTier::Batch)
627            .with_privacy(PrivacyTier::Standard);
628        let backends = selector.recommend();
629        // Should include Lambda (excellent for batch)
630        assert!(backends.contains(&ServingBackend::AwsLambda));
631    }
632
633    // ========================================================================
634    // SERVE-BKD-007: Lambda & Serverless Tests
635    // ========================================================================
636
637    #[test]
638    fn test_SERVE_BKD_007_lambda_is_serverless() {
639        assert!(ServingBackend::AwsLambda.is_serverless());
640        assert!(ServingBackend::CloudflareWorkers.is_serverless());
641        assert!(ServingBackend::Modal.is_serverless());
642        assert!(!ServingBackend::Realizar.is_serverless());
643        assert!(!ServingBackend::OpenAI.is_serverless());
644    }
645
646    #[test]
647    fn test_SERVE_BKD_007_lambda_api_host() {
648        assert_eq!(ServingBackend::AwsLambda.api_host(), Some("lambda.amazonaws.com"));
649        assert_eq!(ServingBackend::CloudflareWorkers.api_host(), Some("workers.cloudflare.com"));
650    }
651
652    #[test]
653    fn test_SERVE_BKD_007_lambda_privacy_tier() {
654        // Lambda should be allowed in Private tier (it's your own account)
655        assert!(PrivacyTier::Private.allows(ServingBackend::AwsLambda));
656        assert!(PrivacyTier::Standard.allows(ServingBackend::AwsLambda));
657        // But not Sovereign (no network calls)
658        assert!(!PrivacyTier::Sovereign.allows(ServingBackend::AwsLambda));
659    }
660
661    #[test]
662    fn test_SERVE_BKD_007_batch_private_includes_lambda() {
663        let selector = BackendSelector::new()
664            .with_latency(LatencyTier::Batch)
665            .with_privacy(PrivacyTier::Private);
666        let backends = selector.recommend();
667        // Lambda should be first for batch + private (pay per use)
668        assert!(backends.contains(&ServingBackend::AwsLambda));
669    }
670
671    #[test]
672    fn test_SERVE_BKD_007_interactive_private_includes_lambda() {
673        let selector = BackendSelector::new()
674            .with_latency(LatencyTier::Interactive)
675            .with_privacy(PrivacyTier::Private);
676        let backends = selector.recommend();
677        // Lambda should be available for interactive private
678        assert!(backends.contains(&ServingBackend::AwsLambda));
679    }
680
681    #[test]
682    fn test_SERVE_BKD_007_lambda_is_remote() {
683        assert!(ServingBackend::AwsLambda.is_remote());
684        assert!(ServingBackend::CloudflareWorkers.is_remote());
685    }
686
687    #[test]
688    fn test_SERVE_BKD_007_cloudflare_blocked_in_private() {
689        // Cloudflare Workers not allowed in Private (not your infrastructure)
690        assert!(!PrivacyTier::Private.allows(ServingBackend::CloudflareWorkers));
691    }
692
693    #[test]
694    fn test_SERVE_BKD_007_private_blocked_hosts_includes_cloudflare() {
695        let hosts = PrivacyTier::Private.blocked_hosts();
696        assert!(hosts.contains(&"workers.cloudflare.com"));
697        // But Lambda is NOT blocked
698        assert!(!hosts.contains(&"lambda.amazonaws.com"));
699    }
700}