Skip to main content

relay_knowledge/env/
config.rs

1//! Process environment capture and typed configuration assembly.
2
3use std::{env as process_env, ffi::OsString};
4
5pub use super::overrides::{EnvironmentConfig, RemoteCliEnvironmentConfig};
6use super::{
7    AgentEnvOverrides, EnvError, FileIndexEnvOverrides, NetworkEnvOverrides, PathEnvOverrides,
8    PlatformKind, RemoteCliEnvOverrides, RetrievalEnvOverrides, TelemetryEnvOverrides,
9    UpdateEnvOverrides, WatcherEnvOverrides, WorkerEnvOverrides,
10    platform::{normalize_key, platform_environment},
11    value_parser::{
12        EnvironmentValues, bool_var, first_bool_var, first_string_var, path_var, positive_u64_var,
13        positive_usize_var, string_var,
14    },
15    variables::*,
16};
17
18impl RemoteCliEnvironmentConfig {
19    /// Reads the current process environment for remote CLI dispatch only.
20    pub fn from_process() -> Result<Self, EnvError> {
21        Self::from_pairs(PlatformKind::current(), process_env::vars_os())
22    }
23
24    /// Parses a deterministic remote CLI environment snapshot.
25    pub fn from_pairs<I, K, V>(platform: PlatformKind, pairs: I) -> Result<Self, EnvError>
26    where
27        I: IntoIterator<Item = (K, V)>,
28        K: Into<OsString>,
29        V: Into<OsString>,
30    {
31        let values = values_from_pairs(platform, pairs);
32
33        Ok(Self {
34            network: parse_network_overrides(&values)?,
35            remote_cli: parse_remote_cli_overrides(&values)?,
36        })
37    }
38}
39
40impl EnvironmentConfig {
41    /// Reads and validates the current process environment.
42    pub fn from_process() -> Result<Self, EnvError> {
43        Self::from_pairs(PlatformKind::current(), process_env::vars_os())
44    }
45
46    /// Parses a deterministic environment snapshot.
47    pub fn from_pairs<I, K, V>(platform: PlatformKind, pairs: I) -> Result<Self, EnvError>
48    where
49        I: IntoIterator<Item = (K, V)>,
50        K: Into<OsString>,
51        V: Into<OsString>,
52    {
53        let values = values_from_pairs(platform, pairs);
54
55        Ok(Self {
56            platform: platform_environment(&values, platform)?,
57            paths: parse_path_overrides(&values)?,
58            network: parse_network_overrides(&values)?,
59            remote_cli: parse_remote_cli_overrides(&values)?,
60            agent: parse_agent_overrides(&values)?,
61            retrieval: parse_retrieval_overrides(&values)?,
62            workers: parse_worker_overrides(&values)?,
63            file_index: parse_file_index_overrides(&values)?,
64            updates: parse_update_overrides(&values)?,
65            telemetry: parse_telemetry_overrides(&values)?,
66            watcher: parse_watcher_overrides(&values)?,
67            storage_topology: string_var(&values, RELAY_KNOWLEDGE_STORAGE_TOPOLOGY)?,
68        })
69    }
70}
71
72fn values_from_pairs<I, K, V>(platform: PlatformKind, pairs: I) -> EnvironmentValues
73where
74    I: IntoIterator<Item = (K, V)>,
75    K: Into<OsString>,
76    V: Into<OsString>,
77{
78    pairs
79        .into_iter()
80        .map(|(key, value)| (normalize_key(platform, key.into()), value.into()))
81        .collect()
82}
83
84fn parse_path_overrides(values: &EnvironmentValues) -> Result<PathEnvOverrides, EnvError> {
85    Ok(PathEnvOverrides {
86        home: path_var(values, RELAY_KNOWLEDGE_HOME)?,
87        config_dir: path_var(values, RELAY_KNOWLEDGE_CONFIG_DIR)?,
88        data_dir: path_var(values, RELAY_KNOWLEDGE_DATA_DIR)?,
89        state_dir: path_var(values, RELAY_KNOWLEDGE_STATE_DIR)?,
90        cache_dir: path_var(values, RELAY_KNOWLEDGE_CACHE_DIR)?,
91        log_dir: path_var(values, RELAY_KNOWLEDGE_LOG_DIR)?,
92        temp_dir: path_var(values, RELAY_KNOWLEDGE_TEMP_DIR)?,
93        runtime_dir: path_var(values, RELAY_KNOWLEDGE_RUNTIME_DIR)?,
94        service_dir: path_var(values, RELAY_KNOWLEDGE_SERVICE_DIR)?,
95    })
96}
97
98fn parse_network_overrides(values: &EnvironmentValues) -> Result<NetworkEnvOverrides, EnvError> {
99    Ok(NetworkEnvOverrides {
100        http_bind: string_var(values, RELAY_KNOWLEDGE_HTTP_BIND)?,
101        http_request_timeout_ms: positive_u64_var(values, RELAY_KNOWLEDGE_HTTP_REQUEST_TIMEOUT_MS)?,
102        http_shutdown_timeout_ms: positive_u64_var(
103            values,
104            RELAY_KNOWLEDGE_HTTP_SHUTDOWN_TIMEOUT_MS,
105        )?,
106        http_max_body_bytes: positive_u64_var(values, RELAY_KNOWLEDGE_HTTP_MAX_BODY_BYTES)?,
107        proxy: first_string_var(
108            values,
109            &[
110                HTTPS_PROXY,
111                HTTPS_PROXY_LOWER,
112                HTTP_PROXY,
113                HTTP_PROXY_LOWER,
114                ALL_PROXY,
115                ALL_PROXY_LOWER,
116            ],
117        )?,
118        no_proxy: first_string_var(values, &[NO_PROXY, NO_PROXY_LOWER])?,
119        ssl_verify: first_bool_var(values, &[SSL_VERIFY, SSL_VERIFY_LOWER])?,
120        qos_max_connections: positive_usize_var(values, RELAY_KNOWLEDGE_QOS_MAX_CONNECTIONS)?,
121        qos_max_in_flight_requests: positive_usize_var(
122            values,
123            RELAY_KNOWLEDGE_QOS_MAX_IN_FLIGHT_REQUESTS,
124        )?,
125        qos_max_queue_depth: positive_usize_var(values, RELAY_KNOWLEDGE_QOS_MAX_QUEUE_DEPTH)?,
126    })
127}
128
129fn parse_remote_cli_overrides(
130    values: &EnvironmentValues,
131) -> Result<RemoteCliEnvOverrides, EnvError> {
132    Ok(RemoteCliEnvOverrides {
133        base_url: string_var(values, RELAY_KNOWLEDGE_REMOTE_BASE_URL)?,
134    })
135}
136
137fn parse_agent_overrides(values: &EnvironmentValues) -> Result<AgentEnvOverrides, EnvError> {
138    Ok(AgentEnvOverrides {
139        mcp_streamable_http_enabled: bool_var(values, RELAY_KNOWLEDGE_MCP_STREAMABLE_HTTP_ENABLED)?,
140        mcp_endpoint: string_var(values, RELAY_KNOWLEDGE_MCP_ENDPOINT)?,
141        mcp_allowed_origins: string_var(values, RELAY_KNOWLEDGE_MCP_ALLOWED_ORIGINS)?,
142        mcp_allowed_scopes: string_var(values, RELAY_KNOWLEDGE_MCP_ALLOWED_SCOPES)?,
143        mcp_allow_unspecified_scope: bool_var(values, RELAY_KNOWLEDGE_MCP_ALLOW_UNSPECIFIED_SCOPE)?,
144        mcp_max_limit: positive_usize_var(values, RELAY_KNOWLEDGE_MCP_MAX_LIMIT)?,
145        mcp_max_context_bytes: positive_usize_var(values, RELAY_KNOWLEDGE_MCP_MAX_CONTEXT_BYTES)?,
146        mcp_allow_remote_clients: bool_var(values, RELAY_KNOWLEDGE_MCP_ALLOW_REMOTE_CLIENTS)?,
147        audit_sink_enabled: bool_var(values, RELAY_KNOWLEDGE_AGENT_AUDIT_SINK_ENABLED)?,
148        audit_queue_depth: positive_usize_var(values, RELAY_KNOWLEDGE_AGENT_AUDIT_QUEUE_DEPTH)?,
149    })
150}
151
152fn parse_retrieval_overrides(
153    values: &EnvironmentValues,
154) -> Result<RetrievalEnvOverrides, EnvError> {
155    Ok(RetrievalEnvOverrides {
156        semantic_backend: string_var(values, RELAY_KNOWLEDGE_SEMANTIC_BACKEND)?,
157        vector_backend: string_var(values, RELAY_KNOWLEDGE_VECTOR_BACKEND)?,
158        llm_provider: string_var(values, RELAY_KNOWLEDGE_LLM_PROVIDER)?,
159        embedding_base_url: string_var(values, RELAY_KNOWLEDGE_EMBEDDING_BASE_URL)?,
160        embedding_api_key: string_var(values, RELAY_KNOWLEDGE_EMBEDDING_API_KEY)?,
161        text_embedding_model: string_var(values, RELAY_KNOWLEDGE_TEXT_EMBEDDING_MODEL)?,
162        image_embedding_model: string_var(values, RELAY_KNOWLEDGE_IMAGE_EMBEDDING_MODEL)?,
163        embedding_dimension: positive_usize_var(values, RELAY_KNOWLEDGE_EMBEDDING_DIMENSION)?,
164        embedding_batch_size: positive_usize_var(values, RELAY_KNOWLEDGE_EMBEDDING_BATCH_SIZE)?,
165        embedding_timeout_ms: positive_u64_var(values, RELAY_KNOWLEDGE_EMBEDDING_TIMEOUT_MS)?,
166        embedding_max_concurrency: positive_usize_var(
167            values,
168            RELAY_KNOWLEDGE_EMBEDDING_MAX_CONCURRENCY,
169        )?,
170        rerank_backend: string_var(values, RELAY_KNOWLEDGE_RERANK_BACKEND)?,
171        rerank_model: string_var(values, RELAY_KNOWLEDGE_RERANK_MODEL)?,
172        rerank_timeout_ms: positive_u64_var(values, RELAY_KNOWLEDGE_RERANK_TIMEOUT_MS)?,
173        rerank_candidate_multiplier: positive_usize_var(
174            values,
175            RELAY_KNOWLEDGE_RERANK_CANDIDATE_MULTIPLIER,
176        )?,
177        rerank_max_candidates: positive_usize_var(values, RELAY_KNOWLEDGE_RERANK_MAX_CANDIDATES)?,
178    })
179}
180
181fn parse_worker_overrides(values: &EnvironmentValues) -> Result<WorkerEnvOverrides, EnvError> {
182    Ok(WorkerEnvOverrides {
183        embedding_endpoint: string_var(values, RELAY_KNOWLEDGE_WORKER_EMBEDDING_ENDPOINT)?,
184        ocr_endpoint: string_var(values, RELAY_KNOWLEDGE_WORKER_OCR_ENDPOINT)?,
185        vision_endpoint: string_var(values, RELAY_KNOWLEDGE_WORKER_VISION_ENDPOINT)?,
186        extractor_endpoint: string_var(values, RELAY_KNOWLEDGE_WORKER_EXTRACTOR_ENDPOINT)?,
187        max_in_flight: positive_usize_var(values, RELAY_KNOWLEDGE_WORKER_MAX_IN_FLIGHT)?,
188        code_index_max_in_flight: positive_usize_var(
189            values,
190            RELAY_KNOWLEDGE_CODE_INDEX_MAX_IN_FLIGHT,
191        )?,
192        silent_updates_enabled: bool_var(values, RELAY_KNOWLEDGE_SILENT_UPDATES_ENABLED)?,
193    })
194}
195
196fn parse_file_index_overrides(
197    values: &EnvironmentValues,
198) -> Result<FileIndexEnvOverrides, EnvError> {
199    Ok(FileIndexEnvOverrides {
200        enabled: bool_var(values, RELAY_KNOWLEDGE_FILE_INDEX_ENABLED)?,
201        roots: string_var(values, RELAY_KNOWLEDGE_FILE_INDEX_ROOTS)?,
202        excludes: string_var(values, RELAY_KNOWLEDGE_FILE_INDEX_EXCLUDES)?,
203        max_depth: positive_usize_var(values, RELAY_KNOWLEDGE_FILE_INDEX_MAX_DEPTH)?,
204        max_file_bytes: positive_u64_var(values, RELAY_KNOWLEDGE_FILE_INDEX_MAX_FILE_BYTES)?,
205        scan_interval_ms: positive_u64_var(values, RELAY_KNOWLEDGE_FILE_INDEX_SCAN_INTERVAL_MS)?,
206        scan_timeout_ms: positive_u64_var(values, RELAY_KNOWLEDGE_FILE_INDEX_SCAN_TIMEOUT_MS)?,
207        max_files_per_root: positive_usize_var(
208            values,
209            RELAY_KNOWLEDGE_FILE_INDEX_MAX_FILES_PER_ROOT,
210        )?,
211        query_timeout_ms: positive_u64_var(values, RELAY_KNOWLEDGE_FILE_QUERY_TIMEOUT_MS)?,
212    })
213}
214
215fn parse_update_overrides(values: &EnvironmentValues) -> Result<UpdateEnvOverrides, EnvError> {
216    let enabled = bool_var(values, RELAY_KNOWLEDGE_UPDATE_CHECK_ENABLED)?;
217    let check_interval_ms = if enabled == Some(false) {
218        None
219    } else {
220        positive_u64_var(values, RELAY_KNOWLEDGE_UPDATE_CHECK_INTERVAL_MS)?
221    };
222
223    Ok(UpdateEnvOverrides {
224        enabled,
225        sources: string_var(values, RELAY_KNOWLEDGE_UPDATE_SOURCES)?,
226        check_interval_ms,
227        github_repo: string_var(values, RELAY_KNOWLEDGE_UPDATE_GITHUB_REPO)?,
228    })
229}
230
231fn parse_telemetry_overrides(
232    values: &EnvironmentValues,
233) -> Result<TelemetryEnvOverrides, EnvError> {
234    Ok(TelemetryEnvOverrides {
235        otel_endpoint: string_var(values, RELAY_OTEL_ENDPOINT)?,
236        otel_traces: bool_var(values, RELAY_OTEL_TRACES)?,
237        otel_metrics: bool_var(values, RELAY_OTEL_METRICS)?,
238        export_timeout_ms: positive_u64_var(values, RELAY_OTEL_EXPORT_TIMEOUT_MS)?,
239        service_environment: string_var(values, RELAY_OTEL_SERVICE_ENVIRONMENT)?,
240    })
241}
242
243fn parse_watcher_overrides(values: &EnvironmentValues) -> Result<WatcherEnvOverrides, EnvError> {
244    Ok(WatcherEnvOverrides {
245        enabled: bool_var(values, RELAY_KNOWLEDGE_WATCHER_ENABLED)?,
246        debounce_ms: positive_u64_var(values, RELAY_KNOWLEDGE_WATCHER_DEBOUNCE_MS)?,
247        max_watch_dirs: positive_usize_var(values, RELAY_KNOWLEDGE_WATCHER_MAX_WATCH_DIRS)?,
248        hash_cache_capacity: positive_usize_var(
249            values,
250            RELAY_KNOWLEDGE_WATCHER_HASH_CACHE_CAPACITY,
251        )?,
252    })
253}
254
255#[cfg(test)]
256#[path = "config_tests.rs"]
257mod config_tests;