1use super::{
4 AgentRegistrationMode, Config, DEFAULT_AGENT_REGISTRATION_MODE, DEFAULT_API_HOST,
5 DEFAULT_API_PORT, DEFAULT_CHALLENGE_PRIVATE_ASSET_BYTES_PER_REVIEW_RECORD,
6 DEFAULT_CHALLENGE_PRIVATE_ASSET_PENDING_TIMEOUT_MINUTES,
7 DEFAULT_CHALLENGE_REVIEW_RECORD_PUBLISH_TIMEOUT_MINUTES,
8 DEFAULT_CHALLENGE_REVIEW_RECORD_TTL_DAYS,
9 DEFAULT_CHALLENGE_REVIEW_RECORD_VALIDATION_TIMEOUT_MINUTES,
10 DEFAULT_CHALLENGE_REVIEW_RECORD_VALIDATIONS_PER_DAY, DEFAULT_HOST_PROBE_COMMAND,
11 DEFAULT_HOST_PROBE_MODE, DEFAULT_LOG_LEVEL, DEFAULT_MAX_ACTIVE_AGENTS,
12 DEFAULT_MAX_ACTIVE_CHALLENGE_REVIEW_RECORDS_PER_HUMAN, DEFAULT_MAX_ACTIVE_OFFICIAL_JOBS,
13 DEFAULT_OFFICIAL_LOG_REDACTION_MODE, DEFAULT_OFFICIAL_RUNS_PER_AGENT_CHALLENGE_DAY,
14 DEFAULT_POSTGRES_PORT, DEFAULT_REQUIRE_DIGEST_PINNED_IMAGES, DEFAULT_RUNNER_DOCKER_LAYER_QUOTA,
15 DEFAULT_RUNNER_INTERACTION_SHUTDOWN_GRACE_SECS,
16 DEFAULT_RUNNER_MAX_INTERACTION_BYTES_PER_DIRECTION, DEFAULT_RUNNER_MAX_OUTPUT_DEPTH,
17 DEFAULT_RUNNER_MAX_OUTPUT_DIRS, DEFAULT_RUNNER_MAX_OUTPUT_FILES,
18 DEFAULT_RUNNER_MAX_PUBLIC_RESULTS, DEFAULT_RUNNER_MAX_RESULT_JSON_BYTES,
19 DEFAULT_RUNNER_MAX_RESULT_LOG_BYTES, DEFAULT_RUNNER_MAX_RUNS, DEFAULT_RUNNER_SECURITY_PROFILE,
20 DEFAULT_RUNNER_WRITABLE_SLOT_CLASSES_MB, DEFAULT_RUNNER_WRITABLE_STORAGE_MODE,
21 DEFAULT_S3_BUCKET, DEFAULT_S3_FORCE_PATH_STYLE, DEFAULT_S3_REGION, DEFAULT_STORAGE_BACKEND,
22 DEFAULT_STORAGE_ROOT, DEFAULT_UNPUBLISHED_CHALLENGE_ASSET_GRACE_DAYS,
23 DEFAULT_VALIDATION_RUNS_PER_AGENT_CHALLENGE_DAY, DEFAULT_WEB_CSRF_COOKIE_NAME,
24 DEFAULT_WEB_PORT, DEFAULT_WEB_SESSION_COOKIE_NAME, DEFAULT_WEB_SESSION_COOKIE_SECURE,
25 DEFAULT_WEB_SESSION_TTL_HOURS, DEFAULT_WORKER_ACCELERATORS, DEFAULT_WORKER_POLL_INTERVAL_MS,
26 DEFAULT_WORKER_STALE_JOB_MINUTES, ENV_AGENTICS_BOOTSTRAP_ADMIN_GITHUB_USER_IDS,
27 ENV_AGENTICS_HOST_PROBE_COMMAND, ENV_AGENTICS_MOLTBOOK_SUBMOLT_NAME,
28 ENV_AGENTICS_MOLTBOOK_SUBMOLT_URL, ENV_AGENTICS_RUNNER_NAMESPACE, ENV_AGENTICS_S3_ENDPOINT_URL,
29 ENV_AGENTICS_S3_REGION, ENV_AGENTICS_STORAGE_ROOT, GithubApiUserUrl, GithubAppAuthorizeUrl,
30 GithubAppRedirectUrl, GithubAppTokenUrl, HostProbeMode, MoltbookSubmoltName,
31 MoltbookSubmoltUrl, OfficialLogRedactionMode, RunnerNamespace, RunnerSecurityProfile,
32 RunnerWritableStorageMode, StorageBackend, WorkerAccelerators, builtin_github_api_user_url,
33 builtin_github_app_authorize_url, builtin_github_app_token_url, builtin_moltbook_submolt_name,
34 builtin_moltbook_submolt_url, builtin_runner_namespace, builtin_s3_endpoint_url,
35 local_cors_allowed_origins, local_database_url, storage_config,
36};
37use agentics_domain::models::auth::GithubUserId;
38use secrecy::SecretString;
39use serde::{Deserialize, de::DeserializeOwned};
40
41const ENV_PREFIX: &str = "AGENTICS_";
42
43#[derive(Debug, Clone, Default)]
45pub struct RawAppEnv {
46 pub database: RawDatabaseEnv,
47 pub api_web: RawApiWebEnv,
48 pub storage: RawStorageEnv,
49 pub auth: RawAuthEnv,
50 pub moltbook: RawMoltbookEnv,
51 pub worker: RawWorkerEnv,
52 pub quotas: RawQuotaEnv,
53 pub github_app: RawGithubAppEnv,
54 pub runner: RawRunnerEnv,
55 pub logging: RawLoggingEnv,
56}
57
58impl RawAppEnv {
59 pub fn from_env() -> envy::Result<Self> {
61 Self::from_env_iter(std::env::vars())
62 }
63
64 pub fn from_env_iter<Iter>(iter: Iter) -> envy::Result<Self>
66 where
67 Iter: IntoIterator<Item = (String, String)>,
68 {
69 let vars: Vec<_> = iter.into_iter().collect();
70 Ok(Self {
71 database: load_group(&vars)?,
72 api_web: load_group(&vars)?,
73 storage: load_group(&vars)?,
74 auth: load_group(&vars)?,
75 moltbook: load_group(&vars)?,
76 worker: load_group(&vars)?,
77 quotas: load_group(&vars)?,
78 github_app: load_group(&vars)?,
79 runner: load_group(&vars)?,
80 logging: load_group(&vars)?,
81 })
82 }
83}
84
85fn load_group<T>(vars: &[(String, String)]) -> envy::Result<T>
86where
87 T: DeserializeOwned,
88{
89 envy::prefixed(ENV_PREFIX).from_iter(vars.iter().cloned())
90}
91
92#[derive(Debug, Clone, Default, Deserialize)]
94pub struct RawDatabaseEnv {
95 pub database_url: Option<String>,
96 pub postgres_port: Option<u16>,
97}
98
99#[derive(Debug, Clone, Default, Deserialize)]
101pub struct RawApiWebEnv {
102 pub api_host: Option<String>,
103 pub api_port: Option<u16>,
104 pub web_port: Option<u16>,
105 pub cors_allowed_origins: Option<String>,
106 pub web_session_cookie_name: Option<String>,
107 pub web_csrf_cookie_name: Option<String>,
108 pub web_session_ttl_hours: Option<i64>,
109 pub web_session_cookie_secure: Option<bool>,
110}
111
112#[derive(Debug, Clone, Default, Deserialize)]
114pub struct RawStorageEnv {
115 pub storage_root: Option<String>,
116 pub storage_backend: Option<StorageBackend>,
117 pub storage_work_root: Option<String>,
118 pub s3_bucket: Option<String>,
119 pub s3_prefix: Option<String>,
120 pub s3_region: Option<String>,
121 pub s3_endpoint_url: Option<String>,
122 pub s3_force_path_style: Option<bool>,
123 pub challenges_root: Option<String>,
124 pub storage_max_bundle_archive_bytes: Option<u64>,
125 pub storage_max_statement_bytes: Option<u64>,
126 pub storage_max_json_artifact_bytes: Option<u64>,
127 pub storage_tmp_object_grace_hours: Option<u64>,
128}
129
130#[derive(Debug, Clone, Default, Deserialize)]
132pub struct RawAuthEnv {
133 pub bootstrap_admin_github_user_ids: Option<String>,
134 pub agent_registration_mode: Option<AgentRegistrationMode>,
135}
136
137#[derive(Debug, Clone, Default, Deserialize)]
139pub struct RawMoltbookEnv {
140 pub moltbook_submolt_name: Option<String>,
141 pub moltbook_submolt_url: Option<String>,
142}
143
144#[derive(Debug, Clone, Default, Deserialize)]
146pub struct RawWorkerEnv {
147 pub worker_poll_interval_ms: Option<u64>,
148 pub worker_stale_job_minutes: Option<i32>,
149 pub worker_accelerators: Option<WorkerAccelerators>,
150 pub worker_gpu_probe_image: Option<String>,
151}
152
153#[derive(Debug, Clone, Default, Deserialize)]
155pub struct RawQuotaEnv {
156 pub validation_runs_per_agent_challenge_day: Option<u32>,
157 pub official_runs_per_agent_challenge_day: Option<u32>,
158 pub max_active_official_jobs: Option<u32>,
159 pub max_active_agents: Option<u32>,
160 pub max_active_challenge_review_records_per_human: Option<u32>,
161 pub challenge_private_asset_bytes_per_review_record: Option<u64>,
162 pub challenge_review_record_validations_per_day: Option<u32>,
163 pub challenge_review_record_validation_timeout_minutes: Option<i32>,
164 pub challenge_private_asset_pending_timeout_minutes: Option<i32>,
165 pub challenge_review_record_publish_timeout_minutes: Option<i32>,
166 pub challenge_review_record_ttl_days: Option<i64>,
167 pub unpublished_challenge_asset_grace_days: Option<i64>,
168}
169
170#[derive(Debug, Clone, Default, Deserialize)]
172pub struct RawGithubAppEnv {
173 pub github_app_client_id: Option<String>,
174 pub github_app_client_secret: Option<String>,
175 pub github_app_redirect_url: Option<String>,
176 pub github_app_authorize_url: Option<String>,
177 pub github_app_token_url: Option<String>,
178 pub github_api_user_url: Option<String>,
179}
180
181#[derive(Debug, Clone, Default, Deserialize)]
183pub struct RawRunnerEnv {
184 pub docker_host: Option<String>,
185 pub host_probe_mode: Option<HostProbeMode>,
186 pub host_probe_command: Option<String>,
187 pub runner_security_profile: Option<RunnerSecurityProfile>,
188 pub official_log_redaction: Option<OfficialLogRedactionMode>,
189 pub require_digest_pinned_images: Option<bool>,
190 pub runner_writable_storage_mode: Option<RunnerWritableStorageMode>,
191 pub runner_namespace: Option<String>,
192 pub runner_runtime_root: Option<String>,
193 pub runner_phase_mount_root: Option<String>,
194 pub runner_writable_slot_classes_mb: Option<String>,
195 pub runner_docker_layer_quota: Option<bool>,
196 pub runner_max_output_files: Option<u64>,
197 pub runner_max_output_dirs: Option<u64>,
198 pub runner_max_output_depth: Option<u64>,
199 pub runner_max_runs: Option<u64>,
200 pub runner_max_result_json_bytes: Option<u64>,
201 pub runner_max_public_results: Option<u64>,
202 pub runner_max_result_log_bytes: Option<u64>,
203 pub runner_max_interaction_bytes_per_direction: Option<u64>,
204 pub runner_interaction_shutdown_grace_secs: Option<u64>,
205}
206
207#[derive(Debug, Clone, Default, Deserialize)]
209pub struct RawLoggingEnv {
210 pub log_level: Option<String>,
211}
212
213impl TryFrom<RawAppEnv> for Config {
214 type Error = anyhow::Error;
215
216 fn try_from(raw: RawAppEnv) -> anyhow::Result<Self> {
218 let mut config = Self::default();
219 let postgres_port = raw.database.postgres_port.unwrap_or(DEFAULT_POSTGRES_PORT);
220 let web_port = raw.api_web.web_port.unwrap_or(DEFAULT_WEB_PORT);
221
222 config.database.url = match raw.database.database_url {
223 Some(value) => {
224 SecretString::from(required_trimmed_string("AGENTICS_DATABASE_URL", value)?)
225 }
226 None => local_database_url(postgres_port),
227 };
228 config.api_web.api_host =
229 string_or_default("AGENTICS_API_HOST", raw.api_web.api_host, DEFAULT_API_HOST)?;
230 config.api_web.api_port = raw.api_web.api_port.unwrap_or(DEFAULT_API_PORT);
231 config.api_web.cors_allowed_origins = match raw.api_web.cors_allowed_origins {
232 Some(value) => required_trimmed_string("AGENTICS_CORS_ALLOWED_ORIGINS", value)?,
233 None => local_cors_allowed_origins(web_port),
234 };
235 config.api_web.web_session_cookie_name = string_or_default(
236 "AGENTICS_WEB_SESSION_COOKIE_NAME",
237 raw.api_web.web_session_cookie_name,
238 DEFAULT_WEB_SESSION_COOKIE_NAME,
239 )?;
240 config.api_web.web_csrf_cookie_name = string_or_default(
241 "AGENTICS_WEB_CSRF_COOKIE_NAME",
242 raw.api_web.web_csrf_cookie_name,
243 DEFAULT_WEB_CSRF_COOKIE_NAME,
244 )?;
245 config.api_web.web_session_ttl_hours = raw
246 .api_web
247 .web_session_ttl_hours
248 .unwrap_or(DEFAULT_WEB_SESSION_TTL_HOURS);
249 config.api_web.web_session_cookie_secure = raw
250 .api_web
251 .web_session_cookie_secure
252 .unwrap_or(DEFAULT_WEB_SESSION_COOKIE_SECURE);
253
254 apply_storage_env(&mut config, raw.storage)?;
255 apply_auth_env(&mut config, raw.auth)?;
256 apply_moltbook_env(&mut config, raw.moltbook)?;
257 apply_worker_env(&mut config, raw.worker)?;
258 apply_quota_env(&mut config, raw.quotas)?;
259 apply_github_app_env(&mut config, raw.github_app)?;
260 apply_runner_env(&mut config, raw.runner)?;
261 config.logging.log_level = string_or_default(
262 "AGENTICS_LOG_LEVEL",
263 raw.logging.log_level,
264 DEFAULT_LOG_LEVEL,
265 )?;
266
267 Ok(config)
268 }
269}
270
271fn apply_storage_env(config: &mut Config, raw: RawStorageEnv) -> anyhow::Result<()> {
272 config.storage.root = string_or_default(
273 ENV_AGENTICS_STORAGE_ROOT,
274 raw.storage_root,
275 DEFAULT_STORAGE_ROOT,
276 )?;
277 config.storage.backend = raw.storage_backend.unwrap_or(DEFAULT_STORAGE_BACKEND);
278 config.storage.work_root = optional_non_empty_string(raw.storage_work_root);
279 config.storage.s3_bucket = raw
280 .s3_bucket
281 .map(trimmed_string)
282 .or_else(|| Some(DEFAULT_S3_BUCKET.to_string()));
283 config.storage.s3_prefix = optional_non_empty_string(raw.s3_prefix);
284 config.storage.s3_region =
285 string_or_default(ENV_AGENTICS_S3_REGION, raw.s3_region, DEFAULT_S3_REGION)?;
286 config.storage.s3_endpoint_url = match raw.s3_endpoint_url {
287 Some(value) => Some(parse_url_env(ENV_AGENTICS_S3_ENDPOINT_URL, value)?),
288 None => Some(builtin_s3_endpoint_url()),
289 };
290 config.storage.s3_force_path_style = raw
291 .s3_force_path_style
292 .unwrap_or(DEFAULT_S3_FORCE_PATH_STYLE);
293 if let Some(value) = raw.challenges_root {
294 config.storage.challenges_root =
295 required_trimmed_string("AGENTICS_CHALLENGES_ROOT", value)?;
296 }
297 config.storage.max_bundle_archive_bytes = raw
298 .storage_max_bundle_archive_bytes
299 .unwrap_or(storage_config::DEFAULT_STORAGE_MAX_BUNDLE_ARCHIVE_BYTES);
300 config.storage.max_statement_bytes = raw
301 .storage_max_statement_bytes
302 .unwrap_or(storage_config::DEFAULT_STORAGE_MAX_STATEMENT_BYTES);
303 config.storage.max_json_artifact_bytes = raw
304 .storage_max_json_artifact_bytes
305 .unwrap_or(storage_config::DEFAULT_STORAGE_MAX_JSON_ARTIFACT_BYTES);
306 config.storage.tmp_object_grace_hours = raw
307 .storage_tmp_object_grace_hours
308 .unwrap_or(storage_config::DEFAULT_STORAGE_TMP_OBJECT_GRACE_HOURS);
309 Ok(())
310}
311
312fn apply_auth_env(config: &mut Config, raw: RawAuthEnv) -> anyhow::Result<()> {
313 config.auth.bootstrap_admin_github_user_ids = match raw.bootstrap_admin_github_user_ids {
314 Some(value) => {
315 parse_github_user_id_list(ENV_AGENTICS_BOOTSTRAP_ADMIN_GITHUB_USER_IDS, &value)?
316 }
317 None => Vec::new(),
318 };
319 config.auth.agent_registration_mode = raw
320 .agent_registration_mode
321 .unwrap_or(DEFAULT_AGENT_REGISTRATION_MODE);
322 Ok(())
323}
324
325fn apply_moltbook_env(config: &mut Config, raw: RawMoltbookEnv) -> anyhow::Result<()> {
326 config.moltbook.submolt_name = match raw.moltbook_submolt_name {
327 Some(value) => MoltbookSubmoltName::try_new(required_trimmed_string(
328 ENV_AGENTICS_MOLTBOOK_SUBMOLT_NAME,
329 value,
330 )?)?,
331 None => builtin_moltbook_submolt_name(),
332 };
333 config.moltbook.submolt_url = match raw.moltbook_submolt_url {
334 Some(value) => MoltbookSubmoltUrl::try_new(required_trimmed_string(
335 ENV_AGENTICS_MOLTBOOK_SUBMOLT_URL,
336 value,
337 )?)?,
338 None => builtin_moltbook_submolt_url(),
339 };
340 Ok(())
341}
342
343fn apply_worker_env(config: &mut Config, raw: RawWorkerEnv) -> anyhow::Result<()> {
344 config.worker.poll_interval_ms = raw
345 .worker_poll_interval_ms
346 .unwrap_or(DEFAULT_WORKER_POLL_INTERVAL_MS);
347 config.worker.stale_job_minutes = raw
348 .worker_stale_job_minutes
349 .unwrap_or(DEFAULT_WORKER_STALE_JOB_MINUTES);
350 config.worker.accelerators = raw
351 .worker_accelerators
352 .unwrap_or(DEFAULT_WORKER_ACCELERATORS);
353 config.worker.gpu_probe_image = optional_non_empty_string(raw.worker_gpu_probe_image);
354 Ok(())
355}
356
357fn apply_quota_env(config: &mut Config, raw: RawQuotaEnv) -> anyhow::Result<()> {
358 config.quotas.validation_runs_per_agent_challenge_day = raw
359 .validation_runs_per_agent_challenge_day
360 .unwrap_or(DEFAULT_VALIDATION_RUNS_PER_AGENT_CHALLENGE_DAY);
361 config.quotas.official_runs_per_agent_challenge_day = raw
362 .official_runs_per_agent_challenge_day
363 .unwrap_or(DEFAULT_OFFICIAL_RUNS_PER_AGENT_CHALLENGE_DAY);
364 config.quotas.max_active_official_jobs = raw
365 .max_active_official_jobs
366 .unwrap_or(DEFAULT_MAX_ACTIVE_OFFICIAL_JOBS);
367 config.quotas.max_active_agents = raw.max_active_agents.unwrap_or(DEFAULT_MAX_ACTIVE_AGENTS);
368 config.quotas.max_active_challenge_review_records_per_human = raw
369 .max_active_challenge_review_records_per_human
370 .unwrap_or(DEFAULT_MAX_ACTIVE_CHALLENGE_REVIEW_RECORDS_PER_HUMAN);
371 config
372 .quotas
373 .challenge_private_asset_bytes_per_review_record = raw
374 .challenge_private_asset_bytes_per_review_record
375 .unwrap_or(DEFAULT_CHALLENGE_PRIVATE_ASSET_BYTES_PER_REVIEW_RECORD);
376 config.quotas.challenge_review_record_validations_per_day = raw
377 .challenge_review_record_validations_per_day
378 .unwrap_or(DEFAULT_CHALLENGE_REVIEW_RECORD_VALIDATIONS_PER_DAY);
379 config
380 .quotas
381 .challenge_review_record_validation_timeout_minutes = raw
382 .challenge_review_record_validation_timeout_minutes
383 .unwrap_or(DEFAULT_CHALLENGE_REVIEW_RECORD_VALIDATION_TIMEOUT_MINUTES);
384 config
385 .quotas
386 .challenge_private_asset_pending_timeout_minutes = raw
387 .challenge_private_asset_pending_timeout_minutes
388 .unwrap_or(DEFAULT_CHALLENGE_PRIVATE_ASSET_PENDING_TIMEOUT_MINUTES);
389 config
390 .quotas
391 .challenge_review_record_publish_timeout_minutes = raw
392 .challenge_review_record_publish_timeout_minutes
393 .unwrap_or(DEFAULT_CHALLENGE_REVIEW_RECORD_PUBLISH_TIMEOUT_MINUTES);
394 config.quotas.challenge_review_record_ttl_days = raw
395 .challenge_review_record_ttl_days
396 .unwrap_or(DEFAULT_CHALLENGE_REVIEW_RECORD_TTL_DAYS);
397 config.quotas.unpublished_challenge_asset_grace_days = raw
398 .unpublished_challenge_asset_grace_days
399 .unwrap_or(DEFAULT_UNPUBLISHED_CHALLENGE_ASSET_GRACE_DAYS);
400 Ok(())
401}
402
403fn apply_github_app_env(config: &mut Config, raw: RawGithubAppEnv) -> anyhow::Result<()> {
404 config.github_app.client_id = optional_non_empty_string(raw.github_app_client_id);
405 config.github_app.client_secret =
406 optional_non_empty_string(raw.github_app_client_secret).map(SecretString::from);
407 config.github_app.redirect_url = raw
408 .github_app_redirect_url
409 .map(|value| -> anyhow::Result<GithubAppRedirectUrl> {
410 let value = required_trimmed_string("AGENTICS_GITHUB_APP_REDIRECT_URL", value)?;
411 Ok(GithubAppRedirectUrl::try_new(value)?)
412 })
413 .transpose()?;
414 config.github_app.authorize_url = match raw.github_app_authorize_url {
415 Some(value) => GithubAppAuthorizeUrl::try_new(required_trimmed_string(
416 "AGENTICS_GITHUB_APP_AUTHORIZE_URL",
417 value,
418 )?)?,
419 None => builtin_github_app_authorize_url(),
420 };
421 config.github_app.token_url = match raw.github_app_token_url {
422 Some(value) => GithubAppTokenUrl::try_new(required_trimmed_string(
423 "AGENTICS_GITHUB_APP_TOKEN_URL",
424 value,
425 )?)?,
426 None => builtin_github_app_token_url(),
427 };
428 config.github_app.api_user_url = match raw.github_api_user_url {
429 Some(value) => GithubApiUserUrl::try_new(required_trimmed_string(
430 "AGENTICS_GITHUB_API_USER_URL",
431 value,
432 )?)?,
433 None => builtin_github_api_user_url(),
434 };
435 Ok(())
436}
437
438fn apply_runner_env(config: &mut Config, raw: RawRunnerEnv) -> anyhow::Result<()> {
439 config.runner.docker_host = optional_non_empty_string(raw.docker_host);
440 config.runner.host_probe_mode = raw.host_probe_mode.unwrap_or(DEFAULT_HOST_PROBE_MODE);
441 config.runner.host_probe_command = string_or_default(
442 ENV_AGENTICS_HOST_PROBE_COMMAND,
443 raw.host_probe_command,
444 DEFAULT_HOST_PROBE_COMMAND,
445 )?;
446 config.runner.security_profile = raw
447 .runner_security_profile
448 .unwrap_or(DEFAULT_RUNNER_SECURITY_PROFILE);
449 config.runner.official_log_redaction = raw
450 .official_log_redaction
451 .unwrap_or(DEFAULT_OFFICIAL_LOG_REDACTION_MODE);
452 config.runner.require_digest_pinned_images = raw
453 .require_digest_pinned_images
454 .unwrap_or(DEFAULT_REQUIRE_DIGEST_PINNED_IMAGES);
455 config.runner.writable_storage_mode = raw
456 .runner_writable_storage_mode
457 .unwrap_or(DEFAULT_RUNNER_WRITABLE_STORAGE_MODE);
458 config.runner.namespace = match raw.runner_namespace {
459 Some(value) => RunnerNamespace::try_new(required_trimmed_string(
460 ENV_AGENTICS_RUNNER_NAMESPACE,
461 value,
462 )?)?,
463 None => builtin_runner_namespace(),
464 };
465 config.runner.runtime_root = optional_non_empty_string(raw.runner_runtime_root);
466 config.runner.phase_mount_root = optional_non_empty_string(raw.runner_phase_mount_root);
467 config.runner.writable_slot_classes_mb = string_or_default(
468 "AGENTICS_RUNNER_WRITABLE_SLOT_CLASSES_MB",
469 raw.runner_writable_slot_classes_mb,
470 DEFAULT_RUNNER_WRITABLE_SLOT_CLASSES_MB,
471 )?;
472 config.runner.docker_layer_quota = raw
473 .runner_docker_layer_quota
474 .unwrap_or(DEFAULT_RUNNER_DOCKER_LAYER_QUOTA);
475 config.runner.max_output_files = raw
476 .runner_max_output_files
477 .unwrap_or(DEFAULT_RUNNER_MAX_OUTPUT_FILES);
478 config.runner.max_output_dirs = raw
479 .runner_max_output_dirs
480 .unwrap_or(DEFAULT_RUNNER_MAX_OUTPUT_DIRS);
481 config.runner.max_output_depth = raw
482 .runner_max_output_depth
483 .unwrap_or(DEFAULT_RUNNER_MAX_OUTPUT_DEPTH);
484 config.runner.max_runs = raw.runner_max_runs.unwrap_or(DEFAULT_RUNNER_MAX_RUNS);
485 config.runner.max_result_json_bytes = raw
486 .runner_max_result_json_bytes
487 .unwrap_or(DEFAULT_RUNNER_MAX_RESULT_JSON_BYTES);
488 config.runner.max_public_results = raw
489 .runner_max_public_results
490 .unwrap_or(DEFAULT_RUNNER_MAX_PUBLIC_RESULTS);
491 config.runner.max_result_log_bytes = raw
492 .runner_max_result_log_bytes
493 .unwrap_or(DEFAULT_RUNNER_MAX_RESULT_LOG_BYTES);
494 config.runner.max_interaction_bytes_per_direction = raw
495 .runner_max_interaction_bytes_per_direction
496 .unwrap_or(DEFAULT_RUNNER_MAX_INTERACTION_BYTES_PER_DIRECTION);
497 config.runner.interaction_shutdown_grace_secs = raw
498 .runner_interaction_shutdown_grace_secs
499 .unwrap_or(DEFAULT_RUNNER_INTERACTION_SHUTDOWN_GRACE_SECS);
500 Ok(())
501}
502
503fn string_or_default(
504 field: &'static str,
505 value: Option<String>,
506 default: &str,
507) -> anyhow::Result<String> {
508 match value {
509 Some(value) => required_trimmed_string(field, value),
510 None => Ok(default.to_string()),
511 }
512}
513
514fn optional_non_empty_string(value: Option<String>) -> Option<String> {
515 value.map(trimmed_string).filter(|value| !value.is_empty())
516}
517
518fn trimmed_string(value: String) -> String {
519 value.trim().to_string()
520}
521
522fn required_trimmed_string(field: &'static str, value: String) -> anyhow::Result<String> {
523 let trimmed = trimmed_string(value);
524 if trimmed.is_empty() {
525 anyhow::bail!("{field} must not be empty");
526 }
527 Ok(trimmed)
528}
529
530fn parse_github_user_id_list(
531 field: &'static str,
532 value: &str,
533) -> anyhow::Result<Vec<GithubUserId>> {
534 let trimmed = value.trim();
535 if trimmed.is_empty() {
536 return Ok(Vec::new());
537 }
538 trimmed
539 .split(',')
540 .map(str::trim)
541 .filter(|item| !item.is_empty())
542 .map(|item| {
543 let id = item
544 .parse::<i64>()
545 .map_err(|error| anyhow::anyhow!("invalid {field} entry `{item}`: {error}"))?;
546 GithubUserId::try_new(id)
547 .map_err(|error| anyhow::anyhow!("invalid {field} entry `{item}`: {error}"))
548 })
549 .collect()
550}
551
552fn parse_url_env(field: &'static str, value: String) -> anyhow::Result<url::Url> {
553 let trimmed = required_trimmed_string(field, value)?;
554 trimmed
555 .parse::<url::Url>()
556 .map_err(|error| anyhow::anyhow!("invalid {field} value `{trimmed}`: {error}"))
557}