Skip to main content

a3s_code_core/config/
search.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4// ============================================================================
5// Search / Browser / Document Configuration
6// ============================================================================
7
8/// Ordered cascade tiers for automatic web-search fallback.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum SearchCascadeTier {
12    /// JavaScript-rendered engines (Google, Baidu, Bing, Brave via headless).
13    Headless,
14    /// Public HTTP/RSS scrapers and feeds.
15    Http,
16    /// Native API providers (AnySearch, Tavily).
17    Api,
18}
19
20/// Search engine configuration (a3s-search integration)
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct SearchConfig {
24    /// Default timeout in seconds for all engines
25    #[serde(default = "default_search_timeout")]
26    pub timeout: u64,
27
28    /// Health monitor configuration
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub health: Option<SearchHealthConfig>,
31
32    /// Preferred cascade tier order for automatic fallback.
33    ///
34    /// When omitted, Code uses Headless → HTTP → API (or HTTP → API without
35    /// the `headless-search` feature). Invalid or empty lists fall back to that
36    /// default. Duplicate entries are ignored.
37    #[serde(
38        default,
39        alias = "cascade_order",
40        skip_serializing_if = "Option::is_none"
41    )]
42    pub cascade_order: Option<Vec<SearchCascadeTier>>,
43
44    /// Engine configurations
45    #[serde(default, rename = "engine")]
46    pub engines: std::collections::HashMap<String, SearchEngineConfig>,
47
48    /// Headless browser configuration for JS-rendered engines (Google, Baidu,
49    /// Bing, and Brave). When omitted, Moli is provisioned lazily from the
50    /// bundled sidecar or the shared per-user cache.
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub headless: Option<HeadlessConfig>,
53}
54
55/// Browser backend for JS-rendered search engines.
56#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "lowercase")]
58pub enum BrowserBackend {
59    /// Moli's standalone, JavaScript-capable headless browser and the default.
60    #[default]
61    Moli,
62    /// Chrome/Chromium headless browser for explicit compatibility use.
63    #[serde(alias = "chromium")]
64    Chrome,
65    /// Explicit Lightpanda backend (native Linux/macOS; WSL2 on Windows).
66    Lightpanda,
67}
68
69/// Headless browser configuration for JS-rendered search engines.
70///
71/// Moli is the default backend. When it is selected and no executable is
72/// configured or packaged, A3S Code downloads the pinned, digest-verified
73/// runtime into the user cache on first use.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(rename_all = "camelCase")]
76pub struct HeadlessConfig {
77    /// Browser backend to use.
78    #[serde(default)]
79    pub backend: BrowserBackend,
80
81    /// Maximum number of concurrent browser tabs.
82    #[serde(default = "default_headless_max_tabs")]
83    pub max_tabs: usize,
84
85    /// Path to the browser executable. If None, Moli is discovered/downloaded
86    /// or an explicit Chrome/Lightpanda runtime is discovered.
87    #[serde(
88        default,
89        alias = "chromePath",
90        alias = "lightpandaPath",
91        alias = "obscuraPath",
92        alias = "playwrightPath",
93        skip_serializing_if = "Option::is_none"
94    )]
95    pub browser_path: Option<String>,
96
97    /// Download Moli automatically when the selected backend is Moli and no
98    /// usable executable is already available.
99    #[serde(default = "default_auto_download_moli")]
100    pub auto_download_moli: bool,
101
102    /// Optional pinned Moli release version. Defaults to the version bundled
103    /// by this A3S Code release.
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub moli_version: Option<String>,
106
107    /// Optional SHA-256 digest for the pinned Moli archive. Supplying a version
108    /// without its digest is rejected by the runtime manager.
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub moli_sha256: Option<String>,
111
112    /// Optional cache directory for the managed Moli runtime.
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub moli_cache_dir: Option<PathBuf>,
115
116    /// Maximum time in seconds spent provisioning a missing Moli runtime.
117    /// This budget is separate from the per-request web-search timeout so a
118    /// first-use download does not make the default search path unusable.
119    #[serde(default = "default_moli_download_timeout_secs")]
120    pub moli_download_timeout_secs: u64,
121
122    /// Additional browser launch arguments.
123    #[serde(default, skip_serializing_if = "Vec::is_empty")]
124    pub launch_args: Vec<String>,
125
126    /// Proxy URL for the browser to use.
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub proxy_url: Option<String>,
129}
130
131impl BrowserBackend {
132    pub fn is_moli(self) -> bool {
133        matches!(self, Self::Moli)
134    }
135
136    pub fn is_lightpanda(self) -> bool {
137        matches!(self, Self::Lightpanda)
138    }
139}
140
141impl Default for HeadlessConfig {
142    fn default() -> Self {
143        Self {
144            backend: BrowserBackend::Moli,
145            max_tabs: 4,
146            browser_path: None,
147            auto_download_moli: true,
148            moli_version: None,
149            moli_sha256: None,
150            moli_cache_dir: None,
151            moli_download_timeout_secs: default_moli_download_timeout_secs(),
152            launch_args: Vec::new(),
153            proxy_url: None,
154        }
155    }
156}
157
158/// Default configuration for built-in document context extraction.
159#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct DocumentParserConfig {
162    /// Whether the default document extraction stack is registered in the parser registry.
163    #[serde(default = "default_enabled")]
164    pub enabled: bool,
165
166    /// Maximum file size accepted by the parser, in MiB.
167    #[serde(default = "default_document_parser_max_file_size_mb")]
168    pub max_file_size_mb: u64,
169
170    /// Optional cache settings for parsed / normalized document context.
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub cache: Option<DocumentCacheConfig>,
173}
174
175impl Default for DocumentParserConfig {
176    fn default() -> Self {
177        Self {
178            enabled: true,
179            max_file_size_mb: default_document_parser_max_file_size_mb(),
180            cache: Some(DocumentCacheConfig::default()),
181        }
182    }
183}
184
185impl DocumentParserConfig {
186    pub fn normalized(&self) -> Self {
187        Self {
188            enabled: self.enabled,
189            max_file_size_mb: self.max_file_size_mb.clamp(1, 1024),
190            cache: self.cache.as_ref().map(DocumentCacheConfig::normalized),
191        }
192    }
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct DocumentCacheConfig {
198    #[serde(default = "default_enabled")]
199    pub enabled: bool,
200
201    #[serde(default, skip_serializing_if = "Option::is_none")]
202    pub directory: Option<PathBuf>,
203}
204
205impl Default for DocumentCacheConfig {
206    fn default() -> Self {
207        Self {
208            enabled: true,
209            directory: None,
210        }
211    }
212}
213
214impl DocumentCacheConfig {
215    pub fn normalized(&self) -> Self {
216        Self {
217            enabled: self.enabled,
218            directory: self.directory.clone(),
219        }
220    }
221}
222
223/// Search health monitor configuration
224#[derive(Debug, Clone, Serialize, Deserialize)]
225#[serde(rename_all = "camelCase")]
226pub struct SearchHealthConfig {
227    /// Number of consecutive failures before suspending
228    #[serde(default = "default_max_failures")]
229    pub max_failures: u32,
230
231    /// Suspension duration in seconds
232    #[serde(default = "default_suspend_seconds")]
233    pub suspend_seconds: u64,
234}
235
236/// Per-engine search configuration
237#[derive(Debug, Clone, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct SearchEngineConfig {
240    /// Whether the engine is enabled
241    #[serde(default = "default_enabled")]
242    pub enabled: bool,
243
244    /// Weight for ranking (higher = more influence)
245    #[serde(default = "default_weight")]
246    pub weight: f64,
247
248    /// Per-engine timeout override in seconds
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub timeout: Option<u64>,
251
252    /// Optional API key for native providers (`anysearch`, `tavily`, and the
253    /// opt-in billed providers).
254    /// When set, overrides the provider's environment-variable default.
255    #[serde(default, skip_serializing_if = "Option::is_none")]
256    pub api_key: Option<String>,
257
258    /// Optional Tavily project identifier used with an authenticated key.
259    #[serde(default, skip_serializing_if = "Option::is_none")]
260    pub project: Option<String>,
261
262    /// Optional API provider endpoint override.
263    ///
264    /// Wired only for native API engines (`anysearch`, `tavily`). Validation
265    /// stays in `a3s-search`: HTTPS in production, loopback HTTP for fixtures.
266    /// This does not weaken `web_fetch` / `download` SSRF checks.
267    #[serde(default, skip_serializing_if = "Option::is_none")]
268    pub endpoint: Option<String>,
269}
270
271pub(crate) fn default_search_timeout() -> u64 {
272    20
273}
274
275pub(crate) fn default_headless_max_tabs() -> usize {
276    4
277}
278
279fn default_auto_download_moli() -> bool {
280    true
281}
282
283pub(crate) fn default_moli_download_timeout_secs() -> u64 {
284    120
285}
286
287fn default_max_failures() -> u32 {
288    3
289}
290
291fn default_suspend_seconds() -> u64 {
292    60
293}
294
295pub(crate) fn default_enabled() -> bool {
296    true
297}
298
299fn default_weight() -> f64 {
300    1.0
301}
302
303pub(crate) fn default_document_parser_max_file_size_mb() -> u64 {
304    50
305}