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)]
157mod tests {
158 use super::*;
159 use crate::types::RuntimePolicy;
160
161 #[test]
162 fn adapter_strategy_maps_to_expected_mode() {
163 assert_eq!(
164 map_adapter_strategy(AdapterStrategy::DirectHttp),
165 AcquisitionModeHint::Fast
166 );
167 assert_eq!(
168 map_adapter_strategy(AdapterStrategy::BrowserStealth),
169 AcquisitionModeHint::Resilient
170 );
171 assert_eq!(
172 map_adapter_strategy(AdapterStrategy::StickyProxy),
173 AcquisitionModeHint::Hostile
174 );
175 assert_eq!(
176 map_adapter_strategy(AdapterStrategy::InvestigateOnly),
177 AcquisitionModeHint::Investigate
178 );
179 }
180
181 #[test]
182 fn high_risk_biases_to_stronger_mode() {
183 let mapped = map_policy_hints(&RuntimePolicyHints {
184 execution_mode: Some(ExecutionMode::Http),
185 session_mode: Some(SessionMode::Stateless),
186 telemetry_level: Some(TelemetryLevel::Standard),
187 risk_score: Some(0.92),
188 max_retries: Some(2),
189 backoff_base_ms: Some(250),
190 enable_warmup: Some(false),
191 });
192
193 assert_eq!(mapped.mode, AcquisitionModeHint::Hostile);
194 assert!(mapped.risk_score >= 0.9);
195 }
196
197 #[test]
198 fn missing_fields_fall_back_to_defaults() {
199 let mapped = map_policy_hints(&RuntimePolicyHints::default());
200
201 assert_eq!(mapped.mode, AcquisitionModeHint::Resilient);
202 assert_eq!(mapped.retry_budget, 2);
203 assert_eq!(mapped.backoff_base_ms, 250);
204 assert!(!mapped.enable_warmup);
205 assert!(!mapped.sticky_session);
206 assert_eq!(mapped.telemetry_level, TelemetryLevel::Standard);
207 assert!((mapped.risk_score - 0.5).abs() < f64::EPSILON);
208 }
209
210 #[test]
211 fn runtime_policy_mapping_is_stable() {
212 let policy = RuntimePolicy {
213 execution_mode: ExecutionMode::Browser,
214 session_mode: SessionMode::Sticky,
215 telemetry_level: TelemetryLevel::Deep,
216 rate_limit_rps: 1.0,
217 max_retries: 5,
218 backoff_base_ms: 700,
219 enable_warmup: true,
220 enforce_webrtc_proxy_only: true,
221 sticky_session_ttl_secs: Some(300),
222 required_stygian_features: vec![],
223 config_hints: std::collections::BTreeMap::default(),
224 risk_score: 0.81,
225 };
226
227 let mapped = map_runtime_policy(&policy);
228 assert_eq!(mapped.mode, AcquisitionModeHint::Hostile);
229 assert!(mapped.sticky_session);
230 assert_eq!(mapped.retry_budget, 5);
231 assert_eq!(mapped.backoff_base_ms, 700);
232 assert!(mapped.enable_warmup);
233 }
234}