Skip to main content

stygian_charon/
acquisition.rs

1//! Pure mapping from Charon policy outputs to acquisition-runner input hints.
2
3use serde::{Deserialize, Serialize};
4
5use crate::types::{AdapterStrategy, ExecutionMode, RuntimePolicy, SessionMode, TelemetryLevel};
6
7/// Strategy mode hint for downstream acquisition runners.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum AcquisitionModeHint {
11    /// Lowest-latency path, minimal escalation.
12    Fast,
13    /// Balanced reliability path.
14    Resilient,
15    /// Strong anti-bot posture first.
16    Hostile,
17    /// Investigation-first entry.
18    Investigate,
19}
20
21/// Optional starting stage hint for investigation mode.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum AcquisitionStartHint {
25    /// Start from direct HTTP.
26    DirectHttp,
27    /// Start from TLS-profiled HTTP.
28    TlsProfiledHttp,
29    /// Start from browser-backed stage.
30    BrowserLightStealth,
31    /// Start from sticky-session browser stage.
32    StickyProxyBrowser,
33}
34
35/// Deterministic runner input derived from runtime policy.
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct AcquisitionPolicy {
38    /// Recommended acquisition mode.
39    pub mode: AcquisitionModeHint,
40    /// Optional investigation entry point.
41    pub investigate_start: Option<AcquisitionStartHint>,
42    /// Retry budget hint for transient failures.
43    pub retry_budget: u32,
44    /// Base retry backoff in milliseconds.
45    pub backoff_base_ms: u64,
46    /// Warmup recommendation.
47    pub enable_warmup: bool,
48    /// Sticky-session recommendation.
49    pub sticky_session: bool,
50    /// Telemetry intensity carried through for logging/diagnostics.
51    pub telemetry_level: TelemetryLevel,
52    /// Risk score clamped to [0.0, 1.0].
53    pub risk_score: f64,
54}
55
56/// Optional runtime-policy hints for partial policy inputs.
57#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
58pub struct RuntimePolicyHints {
59    /// Optional execution mode.
60    pub execution_mode: Option<ExecutionMode>,
61    /// Optional session mode.
62    pub session_mode: Option<SessionMode>,
63    /// Optional telemetry level.
64    pub telemetry_level: Option<TelemetryLevel>,
65    /// Optional risk score.
66    pub risk_score: Option<f64>,
67    /// Optional retry count.
68    pub max_retries: Option<u32>,
69    /// Optional backoff base.
70    pub backoff_base_ms: Option<u64>,
71    /// Optional warmup flag.
72    pub enable_warmup: Option<bool>,
73}
74
75/// Map a Charon strategy recommendation into an acquisition mode.
76#[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/// Map a full runtime policy into deterministic acquisition hints.
89#[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/// Map partial runtime-policy hints with documented defaults.
103#[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}