stygian_charon/challenge_feedback/mod.rs
1//! Challenge-aware policy feedback loop (T83).
2//!
3//! ## What this module does
4//!
5//! Captures the **challenge outcome** of an acquisition attempt and
6//! feeds it back into the next policy planning cycle. Anti-bot vendors
7//! escalate their posture as they observe more challenges (more
8//! captchas, harder JS proofs, longer interstitials). A naïve
9//! scraper that replays the same strategy over and over teaches the
10//! vendor to escalate, eventually locking the scraper out.
11//!
12//! [`ChallengeMemory`] keeps a short-horizon record of the **last
13//! observed outcome** per `(domain, target_class)` key with a TTL
14//! and a max-entries cap (LRU eviction). [`adjust_runtime_policy`]
15//! (and [`build_runtime_policy_with_memory`]) consume the memory to
16//! nudge the risk score up (when the last outcome was adversarial)
17//! or down (when the last outcome was a clean pass).
18//!
19//! ## Why a clamp?
20//!
21//! Influence bounds are **critical** for this module. A feedback
22//! loop that can shift the risk score arbitrarily would amplify
23//! noise: a single transient captcha would cascade into a full
24//! browser-stealth escalation that the site is not actually
25//! demanding. To prevent runaway strategy escalation, every per-key
26//! adjustment is **clamped to** [`MAX_RISK_DELTA`] (a documented,
27//! conservative `0.20` ceiling) and the final risk score is
28//! re-clamped to `[0.0, 1.0]` after the adjustment. Callers can
29//! tighten the clamp with [`ChallengeFeedbackPolicy::with_max_delta`]
30//! but cannot raise it above [`MAX_RISK_DELTA`].
31//!
32//! ## Backing store
33//!
34//! The LRU+TTL store is **shared** with the existing investigation
35//! report cache
36//! ([`crate::cache::MemoryInvestigationCache`]). It is exposed here
37//! as the crate-private `LruTtlStore`
38//! helper so the challenge memory and the investigation cache
39//! share eviction + expiry semantics and we do not introduce a
40//! parallel "second cache store" with its own semantics.
41//!
42//! ## Feature flag
43//!
44//! The module is **default-on** (the `caching` feature is now part
45//! of `stygian-charon`'s default feature set, so the
46//! `LruTtlStore` is always available). No new feature gate is
47//! introduced.
48//!
49//! # Example
50//!
51//! ```
52//! use stygian_charon::challenge_feedback::{
53//! ChallengeMemory, ChallengeOutcome, adjust_runtime_policy, MAX_RISK_DELTA,
54//! };
55//! use stygian_charon::types::{
56//! ExecutionMode, RuntimePolicy, SessionMode, TargetClass, TelemetryLevel,
57//! };
58//! use std::collections::BTreeMap;
59//! use std::num::NonZeroUsize;
60//!
61//! let memory = ChallengeMemory::with_default_ttl(NonZeroUsize::new(64).expect("non-zero"));
62//! memory.record("example.com", TargetClass::ContentSite, ChallengeOutcome::Captcha);
63//!
64//! let policy = RuntimePolicy {
65//! execution_mode: ExecutionMode::Http,
66//! session_mode: SessionMode::Stateless,
67//! telemetry_level: TelemetryLevel::Standard,
68//! rate_limit_rps: 3.0,
69//! max_retries: 2,
70//! backoff_base_ms: 250,
71//! enable_warmup: false,
72//! enforce_webrtc_proxy_only: false,
73//! sticky_session_ttl_secs: None,
74//! required_stygian_features: Vec::new(),
75//! config_hints: BTreeMap::new(),
76//! risk_score: 0.20,
77//! };
78//!
79//! let adjusted =
80//! adjust_runtime_policy(&policy, &memory, "example.com", TargetClass::ContentSite);
81//! assert!(adjusted.risk_score >= policy.risk_score);
82//! assert!(adjusted.risk_score <= policy.risk_score + MAX_RISK_DELTA);
83//! ```
84
85mod memory;
86mod outcome;
87mod policy;
88
89pub use memory::{ChallengeMemory, ChallengeMemoryEntry, challenge_memory_key};
90pub use outcome::ChallengeOutcome;
91pub use policy::{
92 ChallengeFeedbackPolicy, MAX_RISK_DELTA, adjust_runtime_policy,
93 build_runtime_policy_with_memory, memory_adjustment_for,
94};