1use serde::{Deserialize, Serialize};
4
5use crate::types::{AdapterStrategy, ExecutionMode, RuntimePolicy, SessionMode, TelemetryLevel};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum AcquisitionModeHint {
11 Fast,
13 Resilient,
15 Hostile,
17 Investigate,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum AcquisitionStartHint {
25 DirectHttp,
27 TlsProfiledHttp,
29 BrowserLightStealth,
31 StickyProxyBrowser,
33}
34
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct AcquisitionPolicy {
38 pub mode: AcquisitionModeHint,
40 pub investigate_start: Option<AcquisitionStartHint>,
42 pub retry_budget: u32,
44 pub backoff_base_ms: u64,
46 pub enable_warmup: bool,
48 pub sticky_session: bool,
50 pub telemetry_level: TelemetryLevel,
52 pub risk_score: f64,
54}
55
56#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
58pub struct RuntimePolicyHints {
59 pub execution_mode: Option<ExecutionMode>,
61 pub session_mode: Option<SessionMode>,
63 pub telemetry_level: Option<TelemetryLevel>,
65 pub risk_score: Option<f64>,
67 pub max_retries: Option<u32>,
69 pub backoff_base_ms: Option<u64>,
71 pub enable_warmup: Option<bool>,
73}
74
75#[must_use]
77pub const fn map_adapter_strategy(strategy: AdapterStrategy) -> AcquisitionModeHint {
78 match strategy {
79 AdapterStrategy::DirectHttp => AcquisitionModeHint::Fast,
80 AdapterStrategy::BrowserStealth | AdapterStrategy::SessionWarmup => {
81 AcquisitionModeHint::Resilient
82 }
83 AdapterStrategy::StickyProxy => AcquisitionModeHint::Hostile,
84 AdapterStrategy::InvestigateOnly => AcquisitionModeHint::Investigate,
85 }
86}
87
88#[must_use]
90pub fn map_runtime_policy(policy: &RuntimePolicy) -> AcquisitionPolicy {
91 map_policy_hints(&RuntimePolicyHints {
92 execution_mode: Some(policy.execution_mode),
93 session_mode: Some(policy.session_mode),
94 telemetry_level: Some(policy.telemetry_level),
95 risk_score: Some(policy.risk_score),
96 max_retries: Some(policy.max_retries),
97 backoff_base_ms: Some(policy.backoff_base_ms),
98 enable_warmup: Some(policy.enable_warmup),
99 })
100}
101
102#[must_use]
104pub fn map_policy_hints(hints: &RuntimePolicyHints) -> AcquisitionPolicy {
105 let execution_mode = hints.execution_mode.unwrap_or(ExecutionMode::Http);
106 let session_mode = hints.session_mode.unwrap_or(SessionMode::Stateless);
107 let telemetry_level = hints.telemetry_level.unwrap_or(TelemetryLevel::Standard);
108 let risk_score = clamp_unit(hints.risk_score.unwrap_or(0.5));
109 let retry_budget = hints.max_retries.unwrap_or(2);
110 let backoff_base_ms = hints.backoff_base_ms.unwrap_or(250);
111 let enable_warmup = hints.enable_warmup.unwrap_or(false);
112
113 let mode = if telemetry_level == TelemetryLevel::Deep && execution_mode == ExecutionMode::Http {
114 AcquisitionModeHint::Investigate
115 } else if session_mode == SessionMode::Sticky || risk_score >= 0.8 {
116 AcquisitionModeHint::Hostile
117 } else if execution_mode == ExecutionMode::Http && risk_score <= 0.35 && retry_budget <= 2 {
118 AcquisitionModeHint::Fast
119 } else {
120 AcquisitionModeHint::Resilient
121 };
122
123 let investigate_start = if mode == AcquisitionModeHint::Investigate {
124 Some(match (execution_mode, session_mode, risk_score) {
125 (_, SessionMode::Sticky, _) => AcquisitionStartHint::StickyProxyBrowser,
126 (ExecutionMode::Browser, _, _) => AcquisitionStartHint::BrowserLightStealth,
127 (_, _, r) if r >= 0.7 => AcquisitionStartHint::TlsProfiledHttp,
128 _ => AcquisitionStartHint::DirectHttp,
129 })
130 } else {
131 None
132 };
133
134 AcquisitionPolicy {
135 mode,
136 investigate_start,
137 retry_budget,
138 backoff_base_ms,
139 enable_warmup,
140 sticky_session: session_mode == SessionMode::Sticky,
141 telemetry_level,
142 risk_score,
143 }
144}
145
146const fn clamp_unit(value: f64) -> f64 {
147 if value < 0.0 {
148 0.0
149 } else if value > 1.0 {
150 1.0
151 } else {
152 value
153 }
154}
155
156#[cfg(test)]
157#[allow(
158 clippy::unwrap_used,
159 clippy::expect_used,
160 clippy::panic,
161 clippy::indexing_slicing
162)]
163mod tests {
164 use super::*;
165 use crate::types::RuntimePolicy;
166
167 #[test]
168 fn adapter_strategy_maps_to_expected_mode() {
169 assert_eq!(
170 map_adapter_strategy(AdapterStrategy::DirectHttp),
171 AcquisitionModeHint::Fast
172 );
173 assert_eq!(
174 map_adapter_strategy(AdapterStrategy::BrowserStealth),
175 AcquisitionModeHint::Resilient
176 );
177 assert_eq!(
178 map_adapter_strategy(AdapterStrategy::StickyProxy),
179 AcquisitionModeHint::Hostile
180 );
181 assert_eq!(
182 map_adapter_strategy(AdapterStrategy::InvestigateOnly),
183 AcquisitionModeHint::Investigate
184 );
185 }
186
187 #[test]
188 fn high_risk_biases_to_stronger_mode() {
189 let mapped = map_policy_hints(&RuntimePolicyHints {
190 execution_mode: Some(ExecutionMode::Http),
191 session_mode: Some(SessionMode::Stateless),
192 telemetry_level: Some(TelemetryLevel::Standard),
193 risk_score: Some(0.92),
194 max_retries: Some(2),
195 backoff_base_ms: Some(250),
196 enable_warmup: Some(false),
197 });
198
199 assert_eq!(mapped.mode, AcquisitionModeHint::Hostile);
200 assert!(mapped.risk_score >= 0.9);
201 }
202
203 #[test]
204 fn missing_fields_fall_back_to_defaults() {
205 let mapped = map_policy_hints(&RuntimePolicyHints::default());
206
207 assert_eq!(mapped.mode, AcquisitionModeHint::Resilient);
208 assert_eq!(mapped.retry_budget, 2);
209 assert_eq!(mapped.backoff_base_ms, 250);
210 assert!(!mapped.enable_warmup);
211 assert!(!mapped.sticky_session);
212 assert_eq!(mapped.telemetry_level, TelemetryLevel::Standard);
213 assert!((mapped.risk_score - 0.5).abs() < f64::EPSILON);
214 }
215
216 #[test]
217 fn runtime_policy_mapping_is_stable() {
218 let policy = RuntimePolicy {
219 execution_mode: ExecutionMode::Browser,
220 session_mode: SessionMode::Sticky,
221 telemetry_level: TelemetryLevel::Deep,
222 rate_limit_rps: 1.0,
223 max_retries: 5,
224 backoff_base_ms: 700,
225 enable_warmup: true,
226 enforce_webrtc_proxy_only: true,
227 sticky_session_ttl_secs: Some(300),
228 required_stygian_features: vec![],
229 config_hints: std::collections::BTreeMap::default(),
230 risk_score: 0.81,
231 };
232
233 let mapped = map_runtime_policy(&policy);
234 assert_eq!(mapped.mode, AcquisitionModeHint::Hostile);
235 assert!(mapped.sticky_session);
236 assert_eq!(mapped.retry_budget, 5);
237 assert_eq!(mapped.backoff_base_ms, 700);
238 assert!(mapped.enable_warmup);
239 }
240}