Skip to main content

agentics_config/
env_policy.rs

1//! Stage-aware environment policy checks.
2//!
3//! `Config::from_env` keeps the typed runtime defaults, while this module owns
4//! launch-time policy: every stage env var must either be required, optional
5//! with a documented default, deprecated, ignored, or explicitly external.
6
7use std::collections::{BTreeSet, HashMap};
8use std::fmt;
9use std::str::FromStr;
10
11pub const ENV_AGENTICS_DEPLOYMENT_STAGE: &str = "AGENTICS_DEPLOYMENT_STAGE";
12pub const ENV_AGENTICS_REHEARSAL_ENVIRONMENT: &str = "AGENTICS_REHEARSAL_ENVIRONMENT";
13pub const ENV_STALE_REVIEW_RECORD_LIMIT: &str =
14    "AGENTICS_MAX_ACTIVE_CHALLENGE_REVIEW_RECORDS_PER_AGENT";
15pub const ENV_REVIEW_RECORD_LIMIT: &str = "AGENTICS_MAX_ACTIVE_CHALLENGE_REVIEW_RECORDS_PER_HUMAN";
16pub const ENV_AGENTICS_WEB_HOST: &str = "AGENTICS_WEB_HOST";
17pub const ENV_RUST_LOG: &str = "RUST_LOG";
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum DeploymentStage {
21    Dev,
22    Test,
23    Rehearsal,
24    Production,
25}
26
27impl DeploymentStage {
28    pub fn as_str(self) -> &'static str {
29        match self {
30            Self::Dev => "dev",
31            Self::Test => "test",
32            Self::Rehearsal => "rehearsal",
33            Self::Production => "production",
34        }
35    }
36
37    fn rejects_placeholders(self) -> bool {
38        matches!(self, Self::Rehearsal | Self::Production)
39    }
40}
41
42impl fmt::Display for DeploymentStage {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        f.write_str(self.as_str())
45    }
46}
47
48impl FromStr for DeploymentStage {
49    type Err = anyhow::Error;
50
51    fn from_str(value: &str) -> Result<Self, Self::Err> {
52        match value.trim() {
53            "dev" => Ok(Self::Dev),
54            "test" => Ok(Self::Test),
55            "rehearsal" => Ok(Self::Rehearsal),
56            "production" => Ok(Self::Production),
57            other => anyhow::bail!(
58                "{ENV_AGENTICS_DEPLOYMENT_STAGE} must be one of dev, test, rehearsal, or production; got `{other}`"
59            ),
60        }
61    }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum EnvServiceRole {
66    Compose,
67    Api,
68    Worker,
69    Migrate,
70    Web,
71    LocalDev,
72    TestHarness,
73}
74
75impl EnvServiceRole {
76    pub fn as_str(self) -> &'static str {
77        match self {
78            Self::Compose => "compose",
79            Self::Api => "api",
80            Self::Worker => "worker",
81            Self::Migrate => "migrate",
82            Self::Web => "web",
83            Self::LocalDev => "local-dev",
84            Self::TestHarness => "test-harness",
85        }
86    }
87}
88
89impl fmt::Display for EnvServiceRole {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        f.write_str(self.as_str())
92    }
93}
94
95impl FromStr for EnvServiceRole {
96    type Err = anyhow::Error;
97
98    fn from_str(value: &str) -> Result<Self, Self::Err> {
99        match value.trim() {
100            "compose" => Ok(Self::Compose),
101            "api" => Ok(Self::Api),
102            "worker" => Ok(Self::Worker),
103            "migrate" => Ok(Self::Migrate),
104            "web" => Ok(Self::Web),
105            "local-dev" => Ok(Self::LocalDev),
106            "test-harness" => Ok(Self::TestHarness),
107            other => anyhow::bail!(
108                "env policy role must be one of compose, api, worker, migrate, web, local-dev, or test-harness; got `{other}`"
109            ),
110        }
111    }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct EnvPolicyWarning {
116    pub name: &'static str,
117    pub message: String,
118}
119
120impl fmt::Display for EnvPolicyWarning {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        write!(f, "{}: {}", self.name, self.message)
123    }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct EnvPolicyReport {
128    pub stage: DeploymentStage,
129    pub role: EnvServiceRole,
130    pub warnings: Vec<EnvPolicyWarning>,
131}
132
133#[derive(Debug, Clone, Copy)]
134struct OptionalEnv {
135    name: &'static str,
136    default: &'static str,
137}
138
139impl OptionalEnv {
140    const fn new(name: &'static str, default: &'static str) -> Self {
141        Self { name, default }
142    }
143}
144
145pub fn process_env_map() -> HashMap<String, String> {
146    std::env::vars().collect()
147}
148
149pub fn deployment_stage_from_env_map(
150    env: &HashMap<String, String>,
151) -> anyhow::Result<DeploymentStage> {
152    let value = required_env_value(env, ENV_AGENTICS_DEPLOYMENT_STAGE)?;
153    value.parse()
154}
155
156pub fn validate_current_env_policy(role: EnvServiceRole) -> anyhow::Result<EnvPolicyReport> {
157    let env = process_env_map();
158    validate_env_policy(&env, role)
159}
160
161pub fn validate_env_policy(
162    env: &HashMap<String, String>,
163    role: EnvServiceRole,
164) -> anyhow::Result<EnvPolicyReport> {
165    let stage = deployment_stage_from_env_map(env)?;
166    validate_stage_role(stage, role)?;
167
168    let mut errors = Vec::new();
169    let mut warnings = Vec::new();
170
171    collect_deprecated_errors(env, &mut errors);
172    collect_ignored_warnings(env, &mut warnings);
173    collect_required_value_errors(stage, env, &mut errors);
174
175    for name in required_envs(stage, role) {
176        match env_value(env, name) {
177            Some(value) => {
178                if stage.rejects_placeholders() && is_placeholder(value) {
179                    errors.push(format!("{name} still uses a replace-with-* placeholder"));
180                }
181            }
182            None => errors.push(format!("{name} must be set for {stage} {role} startup")),
183        }
184    }
185
186    for optional in optional_envs(stage, role) {
187        if env_value(env, optional.name).is_none() {
188            warnings.push(EnvPolicyWarning {
189                name: optional.name,
190                message: format!("unset; default: {}", optional.default),
191            });
192        }
193    }
194
195    if !errors.is_empty() {
196        anyhow::bail!(errors.join("; "));
197    }
198
199    Ok(EnvPolicyReport {
200        stage,
201        role,
202        warnings,
203    })
204}
205
206pub fn known_stage_env_names() -> BTreeSet<&'static str> {
207    let mut names = BTreeSet::new();
208    for name in [
209        ENV_AGENTICS_DEPLOYMENT_STAGE,
210        ENV_AGENTICS_REHEARSAL_ENVIRONMENT,
211        ENV_STALE_REVIEW_RECORD_LIMIT,
212        ENV_REVIEW_RECORD_LIMIT,
213        ENV_AGENTICS_WEB_HOST,
214        ENV_RUST_LOG,
215    ] {
216        names.insert(name);
217    }
218    for name in DEV_REQUIRED
219        .iter()
220        .chain(TEST_REQUIRED)
221        .chain(REHEARSAL_REQUIRED)
222        .chain(PRODUCTION_REQUIRED)
223        .chain(API_COMMON_REQUIRED)
224        .chain(API_HOSTED_REQUIRED)
225        .chain(WORKER_COMMON_REQUIRED)
226        .chain(WORKER_HOSTED_REQUIRED)
227        .chain(MIGRATE_REQUIRED)
228        .chain(WEB_REQUIRED)
229    {
230        names.insert(*name);
231    }
232    for optional in DEV_OPTIONAL
233        .iter()
234        .chain(TEST_OPTIONAL)
235        .chain(REHEARSAL_OPTIONAL)
236        .chain(PRODUCTION_OPTIONAL)
237        .chain(API_OPTIONAL)
238        .chain(WORKER_OPTIONAL)
239        .chain(MIGRATE_OPTIONAL)
240        .chain(WEB_OPTIONAL)
241    {
242        names.insert(optional.name);
243    }
244    names.insert("COMPOSE_PROFILES");
245    names.insert("DATABASE_URL");
246    names
247}
248
249fn validate_stage_role(stage: DeploymentStage, role: EnvServiceRole) -> anyhow::Result<()> {
250    match (stage, role) {
251        (DeploymentStage::Dev, EnvServiceRole::LocalDev)
252        | (DeploymentStage::Test, EnvServiceRole::TestHarness)
253        | (DeploymentStage::Rehearsal | DeploymentStage::Production, EnvServiceRole::Compose)
254        | (_, EnvServiceRole::Api | EnvServiceRole::Worker | EnvServiceRole::Migrate)
255        | (_, EnvServiceRole::Web) => Ok(()),
256        (DeploymentStage::Dev | DeploymentStage::Test, EnvServiceRole::Compose) => Ok(()),
257        (other_stage, other_role) => anyhow::bail!(
258            "{ENV_AGENTICS_DEPLOYMENT_STAGE}={other_stage} cannot be validated with {other_role} env policy"
259        ),
260    }
261}
262
263fn collect_deprecated_errors(env: &HashMap<String, String>, errors: &mut Vec<String>) {
264    if env_value(env, ENV_STALE_REVIEW_RECORD_LIMIT).is_some() {
265        errors.push(format!(
266            "{ENV_STALE_REVIEW_RECORD_LIMIT} has been removed; use {ENV_REVIEW_RECORD_LIMIT}"
267        ));
268    }
269    if env_value(env, ENV_AGENTICS_REHEARSAL_ENVIRONMENT).is_some() {
270        errors.push(format!(
271            "{ENV_AGENTICS_REHEARSAL_ENVIRONMENT} has been removed; use {ENV_AGENTICS_DEPLOYMENT_STAGE}=rehearsal"
272        ));
273    }
274}
275
276fn collect_ignored_warnings(env: &HashMap<String, String>, warnings: &mut Vec<EnvPolicyWarning>) {
277    if env_value(env, ENV_AGENTICS_WEB_HOST).is_some() {
278        warnings.push(EnvPolicyWarning {
279            name: ENV_AGENTICS_WEB_HOST,
280            message: "ignored; web bind host is owned by the Compose command".to_string(),
281        });
282    }
283    if env_value(env, ENV_RUST_LOG).is_some() {
284        warnings.push(EnvPolicyWarning {
285            name: ENV_RUST_LOG,
286            message: "ignored; use AGENTICS_LOG_LEVEL for Agentics service logging".to_string(),
287        });
288    }
289}
290
291fn collect_required_value_errors(
292    stage: DeploymentStage,
293    env: &HashMap<String, String>,
294    errors: &mut Vec<String>,
295) {
296    if stage == DeploymentStage::Production {
297        require_exact_value(env, "AGENTICS_WEB_SESSION_COOKIE_SECURE", "true", errors);
298    }
299    if matches!(
300        stage,
301        DeploymentStage::Rehearsal | DeploymentStage::Production
302    ) {
303        require_exact_value(
304            env,
305            "AGENTICS_RUNNER_SECURITY_PROFILE",
306            "production",
307            errors,
308        );
309        require_exact_value(env, "AGENTICS_HOST_PROBE_MODE", "require", errors);
310        require_exact_value(env, "AGENTICS_REQUIRE_DIGEST_PINNED_IMAGES", "true", errors);
311        require_exact_value(
312            env,
313            "AGENTICS_RUNNER_WRITABLE_STORAGE_MODE",
314            "xfs-project-quota-slots",
315            errors,
316        );
317        require_exact_value(env, "AGENTICS_RUNNER_DOCKER_LAYER_QUOTA", "true", errors);
318    }
319}
320
321fn require_exact_value(
322    env: &HashMap<String, String>,
323    name: &'static str,
324    expected: &'static str,
325    errors: &mut Vec<String>,
326) {
327    if let Some(value) = env_value(env, name)
328        && value != expected
329    {
330        errors.push(format!("{name} must be `{expected}`"));
331    }
332}
333
334fn required_envs(stage: DeploymentStage, role: EnvServiceRole) -> &'static [&'static str] {
335    match role {
336        EnvServiceRole::Compose => match stage {
337            DeploymentStage::Dev => DEV_REQUIRED,
338            DeploymentStage::Test => TEST_REQUIRED,
339            DeploymentStage::Rehearsal => REHEARSAL_REQUIRED,
340            DeploymentStage::Production => PRODUCTION_REQUIRED,
341        },
342        EnvServiceRole::LocalDev => DEV_REQUIRED,
343        EnvServiceRole::TestHarness => TEST_REQUIRED,
344        EnvServiceRole::Api => match stage {
345            DeploymentStage::Rehearsal | DeploymentStage::Production => API_HOSTED_REQUIRED,
346            DeploymentStage::Dev | DeploymentStage::Test => API_COMMON_REQUIRED,
347        },
348        EnvServiceRole::Worker => match stage {
349            DeploymentStage::Rehearsal | DeploymentStage::Production => WORKER_HOSTED_REQUIRED,
350            DeploymentStage::Dev | DeploymentStage::Test => WORKER_COMMON_REQUIRED,
351        },
352        EnvServiceRole::Migrate => MIGRATE_REQUIRED,
353        EnvServiceRole::Web => WEB_REQUIRED,
354    }
355}
356
357fn optional_envs(stage: DeploymentStage, role: EnvServiceRole) -> &'static [OptionalEnv] {
358    match role {
359        EnvServiceRole::Compose => match stage {
360            DeploymentStage::Dev => DEV_OPTIONAL,
361            DeploymentStage::Test => TEST_OPTIONAL,
362            DeploymentStage::Rehearsal => REHEARSAL_OPTIONAL,
363            DeploymentStage::Production => PRODUCTION_OPTIONAL,
364        },
365        EnvServiceRole::LocalDev => DEV_OPTIONAL,
366        EnvServiceRole::TestHarness => TEST_OPTIONAL,
367        EnvServiceRole::Api => API_OPTIONAL,
368        EnvServiceRole::Worker => WORKER_OPTIONAL,
369        EnvServiceRole::Migrate => MIGRATE_OPTIONAL,
370        EnvServiceRole::Web => WEB_OPTIONAL,
371    }
372}
373
374fn required_env_value<'a>(
375    env: &'a HashMap<String, String>,
376    name: &'static str,
377) -> anyhow::Result<&'a str> {
378    env_value(env, name).ok_or_else(|| anyhow::anyhow!("{name} must be set"))
379}
380
381fn env_value<'a>(env: &'a HashMap<String, String>, name: &str) -> Option<&'a str> {
382    env.get(name)
383        .map(String::as_str)
384        .map(str::trim)
385        .filter(|value| !value.is_empty())
386}
387
388fn is_placeholder(value: &str) -> bool {
389    value
390        .split(['/', ':', '@', ','])
391        .any(|part| part.trim().starts_with("replace-with-"))
392        || value.trim().starts_with("replace-with-")
393}
394
395const DEV_REQUIRED: &[&str] = &[
396    ENV_AGENTICS_DEPLOYMENT_STAGE,
397    "AGENTICS_DATABASE_URL",
398    "AGENTICS_LOCAL_DEV_DATABASE_NAME",
399    "AGENTICS_LOCAL_DEV_DATABASE_URL",
400    "AGENTICS_LOCAL_DEV_DATABASE_URL_CONFIRM",
401    "AGENTICS_LOCAL_DEV_CHALLENGE_SOURCE_ROOT",
402    "AGENTICS_LOCAL_DEV_TEST_SOLUTIONS_ROOT",
403    "AGENTICS_STORAGE_BACKEND",
404    "AGENTICS_S3_BUCKET",
405    "AGENTICS_S3_REGION",
406    "AGENTICS_S3_ENDPOINT_URL",
407    "AGENTICS_S3_FORCE_PATH_STYLE",
408    "AGENTICS_API_BASE_URL",
409];
410
411const TEST_REQUIRED: &[&str] = &[
412    ENV_AGENTICS_DEPLOYMENT_STAGE,
413    "DATABASE_URL",
414    "AGENTICS_DATABASE_URL",
415    "AGENTICS_POSTGRES_USER",
416    "AGENTICS_POSTGRES_PASSWORD",
417    "AGENTICS_POSTGRES_DB",
418    "AGENTICS_RUSTFS_ACCESS_KEY",
419    "AGENTICS_RUSTFS_SECRET_KEY",
420    "AGENTICS_STORAGE_BACKEND",
421    "AGENTICS_S3_BUCKET",
422    "AGENTICS_S3_PREFIX",
423    "AGENTICS_S3_REGION",
424    "AGENTICS_S3_ENDPOINT_URL",
425    "AGENTICS_S3_FORCE_PATH_STYLE",
426    "AGENTICS_API_HOST",
427    "AGENTICS_API_PORT",
428    "AGENTICS_WEB_PORT",
429    "AGENTICS_CORS_ALLOWED_ORIGINS",
430    "AGENTICS_BOOTSTRAP_ADMIN_GITHUB_USER_IDS",
431    "AGENTICS_AGENT_REGISTRATION_MODE",
432    "AGENTICS_WEB_SESSION_COOKIE_SECURE",
433    "AGENTICS_TEST_DOCKER_HOST",
434    "AGENTICS_TEST_DOCKER_SOCKET_PATH",
435];
436
437const REHEARSAL_REQUIRED: &[&str] = &[
438    ENV_AGENTICS_DEPLOYMENT_STAGE,
439    "AGENTICS_POSTGRES_USER",
440    "AGENTICS_POSTGRES_PASSWORD",
441    "AGENTICS_POSTGRES_DB",
442    "AGENTICS_POSTGRES_PORT",
443    "AGENTICS_DATABASE_URL",
444    "AGENTICS_RUSTFS_ACCESS_KEY",
445    "AGENTICS_RUSTFS_SECRET_KEY",
446    "AGENTICS_RUSTFS_PORT",
447    "AGENTICS_RUSTFS_CONSOLE_PORT",
448    "AGENTICS_STORAGE_BACKEND",
449    "AGENTICS_S3_BUCKET",
450    "AGENTICS_S3_PREFIX",
451    "AGENTICS_S3_REGION",
452    "AGENTICS_S3_ENDPOINT_URL",
453    "AGENTICS_REHEARSAL_HOST_S3_ENDPOINT_URL",
454    "AGENTICS_S3_FORCE_PATH_STYLE",
455    "AGENTICS_STORAGE_WORK_ROOT",
456    "AGENTICS_API_HOST_PORT",
457    "AGENTICS_WEB_HOST_PORT",
458    "AGENTICS_API_BASE_URL",
459    "AGENTICS_WEB_BASE_URL",
460    "AGENTICS_CORS_ALLOWED_ORIGINS",
461    "AGENTICS_BOOTSTRAP_ADMIN_GITHUB_USER_IDS",
462    "AGENTICS_WEB_SESSION_COOKIE_SECURE",
463    "AGENTICS_AGENT_REGISTRATION_MODE",
464    "AGENTICS_GITHUB_APP_CLIENT_ID",
465    "AGENTICS_GITHUB_APP_CLIENT_SECRET",
466    "AGENTICS_GITHUB_APP_REDIRECT_URL",
467    "AGENTICS_CHALLENGE_REVIEW_REPOSITORY_HOST_ROOT",
468    "AGENTICS_CHALLENGE_REVIEW_REPOSITORY_CONTAINER_ROOT",
469    "AGENTICS_DGX_STATE_ROOT",
470    "AGENTICS_DOCKER_SOCKET_PATH",
471    "AGENTICS_DOCKER_HOST",
472    "AGENTICS_RUNNER_NAMESPACE",
473    "AGENTICS_RUNTIME_UID",
474    "AGENTICS_RUNTIME_GID",
475    "AGENTICS_DOCKER_SOCKET_GID",
476    "AGENTICS_RUNNER_SECURITY_PROFILE",
477    "AGENTICS_HOST_PROBE_MODE",
478    "AGENTICS_HOST_PROBE_COMMAND",
479    "AGENTICS_REQUIRE_DIGEST_PINNED_IMAGES",
480    "AGENTICS_RUNNER_WRITABLE_STORAGE_MODE",
481    "AGENTICS_RUNNER_RUNTIME_ROOT",
482    "AGENTICS_RUNNER_PHASE_MOUNT_ROOT",
483    "AGENTICS_DGX_PHASE_MOUNT_ROOT",
484    "AGENTICS_RUNNER_WRITABLE_SLOT_CLASSES_MB",
485    "AGENTICS_DGX_PHASE_SLOT_CLASSES_MB",
486    "AGENTICS_DGX_PHASE_SLOTS_PER_CLASS",
487    "AGENTICS_DGX_PHASE_SLOT_INODES_PER_MB",
488    "AGENTICS_RUNNER_DOCKER_LAYER_QUOTA",
489];
490
491const PRODUCTION_REQUIRED: &[&str] = &[
492    ENV_AGENTICS_DEPLOYMENT_STAGE,
493    "AGENTICS_POSTGRES_USER",
494    "AGENTICS_POSTGRES_PASSWORD",
495    "AGENTICS_POSTGRES_DB",
496    "AGENTICS_RUSTFS_ACCESS_KEY",
497    "AGENTICS_RUSTFS_SECRET_KEY",
498    "AGENTICS_STORAGE_BACKEND",
499    "AGENTICS_S3_BUCKET",
500    "AGENTICS_S3_PREFIX",
501    "AGENTICS_S3_REGION",
502    "AGENTICS_S3_ENDPOINT_URL",
503    "AGENTICS_S3_FORCE_PATH_STYLE",
504    "AGENTICS_STORAGE_WORK_ROOT",
505    "AGENTICS_API_BASE_URL",
506    "AGENTICS_WEB_BASE_URL",
507    "AGENTICS_CORS_ALLOWED_ORIGINS",
508    "AGENTICS_BOOTSTRAP_ADMIN_GITHUB_USER_IDS",
509    "AGENTICS_WEB_SESSION_COOKIE_SECURE",
510    "AGENTICS_GITHUB_APP_CLIENT_ID",
511    "AGENTICS_GITHUB_APP_CLIENT_SECRET",
512    "AGENTICS_GITHUB_APP_REDIRECT_URL",
513    "AGENTICS_CHALLENGE_REVIEW_REPOSITORY_HOST_ROOT",
514    "AGENTICS_CHALLENGE_REVIEW_REPOSITORY_CONTAINER_ROOT",
515    "AGENTICS_DOCKER_SOCKET_PATH",
516    "AGENTICS_DOCKER_HOST",
517    "AGENTICS_RUNNER_NAMESPACE",
518    "AGENTICS_RUNTIME_UID",
519    "AGENTICS_RUNTIME_GID",
520    "AGENTICS_DOCKER_SOCKET_GID",
521    "AGENTICS_RUNNER_SECURITY_PROFILE",
522    "AGENTICS_HOST_PROBE_MODE",
523    "AGENTICS_HOST_PROBE_COMMAND",
524    "AGENTICS_REQUIRE_DIGEST_PINNED_IMAGES",
525    "AGENTICS_RUNNER_WRITABLE_STORAGE_MODE",
526    "AGENTICS_RUNNER_RUNTIME_ROOT",
527    "AGENTICS_RUNNER_PHASE_MOUNT_ROOT",
528    "AGENTICS_RUNNER_WRITABLE_SLOT_CLASSES_MB",
529    "AGENTICS_RUNNER_DOCKER_LAYER_QUOTA",
530];
531
532const API_COMMON_REQUIRED: &[&str] = &[
533    ENV_AGENTICS_DEPLOYMENT_STAGE,
534    "AGENTICS_DATABASE_URL",
535    "AGENTICS_STORAGE_BACKEND",
536    "AGENTICS_S3_BUCKET",
537    "AGENTICS_S3_REGION",
538    "AGENTICS_S3_ENDPOINT_URL",
539    "AGENTICS_S3_FORCE_PATH_STYLE",
540    "AGENTICS_API_HOST",
541    "AGENTICS_API_PORT",
542    "AGENTICS_CORS_ALLOWED_ORIGINS",
543];
544
545const API_HOSTED_REQUIRED: &[&str] = &[
546    ENV_AGENTICS_DEPLOYMENT_STAGE,
547    "AGENTICS_DATABASE_URL",
548    "AGENTICS_STORAGE_BACKEND",
549    "AGENTICS_S3_BUCKET",
550    "AGENTICS_S3_REGION",
551    "AGENTICS_S3_ENDPOINT_URL",
552    "AGENTICS_S3_FORCE_PATH_STYLE",
553    "AGENTICS_STORAGE_WORK_ROOT",
554    "AGENTICS_API_HOST",
555    "AGENTICS_API_PORT",
556    "AGENTICS_CORS_ALLOWED_ORIGINS",
557    "AGENTICS_BOOTSTRAP_ADMIN_GITHUB_USER_IDS",
558    "AGENTICS_WEB_SESSION_COOKIE_SECURE",
559    "AGENTICS_GITHUB_APP_CLIENT_ID",
560    "AGENTICS_GITHUB_APP_CLIENT_SECRET",
561    "AGENTICS_GITHUB_APP_REDIRECT_URL",
562];
563
564const WORKER_COMMON_REQUIRED: &[&str] = &[
565    ENV_AGENTICS_DEPLOYMENT_STAGE,
566    "AGENTICS_DATABASE_URL",
567    "AGENTICS_STORAGE_BACKEND",
568    "AGENTICS_S3_BUCKET",
569    "AGENTICS_S3_REGION",
570    "AGENTICS_S3_ENDPOINT_URL",
571    "AGENTICS_S3_FORCE_PATH_STYLE",
572    "AGENTICS_RUNNER_NAMESPACE",
573    "AGENTICS_RUNNER_RUNTIME_ROOT",
574];
575
576const WORKER_HOSTED_REQUIRED: &[&str] = &[
577    ENV_AGENTICS_DEPLOYMENT_STAGE,
578    "AGENTICS_DATABASE_URL",
579    "AGENTICS_STORAGE_BACKEND",
580    "AGENTICS_S3_BUCKET",
581    "AGENTICS_S3_REGION",
582    "AGENTICS_S3_ENDPOINT_URL",
583    "AGENTICS_S3_FORCE_PATH_STYLE",
584    "AGENTICS_STORAGE_WORK_ROOT",
585    "AGENTICS_DOCKER_HOST",
586    "AGENTICS_RUNNER_NAMESPACE",
587    "AGENTICS_RUNNER_SECURITY_PROFILE",
588    "AGENTICS_HOST_PROBE_MODE",
589    "AGENTICS_HOST_PROBE_COMMAND",
590    "AGENTICS_REQUIRE_DIGEST_PINNED_IMAGES",
591    "AGENTICS_RUNNER_WRITABLE_STORAGE_MODE",
592    "AGENTICS_RUNNER_RUNTIME_ROOT",
593    "AGENTICS_RUNNER_PHASE_MOUNT_ROOT",
594    "AGENTICS_RUNNER_WRITABLE_SLOT_CLASSES_MB",
595    "AGENTICS_RUNNER_DOCKER_LAYER_QUOTA",
596];
597
598const MIGRATE_REQUIRED: &[&str] = &[ENV_AGENTICS_DEPLOYMENT_STAGE, "AGENTICS_DATABASE_URL"];
599
600const WEB_REQUIRED: &[&str] = &[
601    ENV_AGENTICS_DEPLOYMENT_STAGE,
602    "AGENTICS_API_BASE_URL",
603    "AGENTICS_WEB_PORT",
604];
605
606const DEV_OPTIONAL: &[OptionalEnv] = &[
607    OptionalEnv::new(
608        "AGENTICS_RUST_TOOLCHAIN_IMAGE",
609        "local rust-toolchain image",
610    ),
611    OptionalEnv::new("AGENTICS_POSTGRES_USER", "agentics"),
612    OptionalEnv::new("AGENTICS_POSTGRES_PASSWORD", "agentics"),
613    OptionalEnv::new("AGENTICS_POSTGRES_DB", "agentics_dev"),
614    OptionalEnv::new("AGENTICS_POSTGRES_PORT", "5432-derived local database port"),
615    OptionalEnv::new(
616        "AGENTICS_CHALLENGES_ROOT",
617        "prepared local dev challenge root",
618    ),
619    OptionalEnv::new("AGENTICS_RUSTFS_ACCESS_KEY", "agenticsrustfs"),
620    OptionalEnv::new("AGENTICS_RUSTFS_SECRET_KEY", "agenticsrustfssecret"),
621    OptionalEnv::new("AGENTICS_RUSTFS_PORT", "9000"),
622    OptionalEnv::new("AGENTICS_RUSTFS_CONSOLE_PORT", "9001"),
623    OptionalEnv::new("AGENTICS_S3_PREFIX", "no prefix"),
624    OptionalEnv::new("AGENTICS_STORAGE_MAX_BUNDLE_ARCHIVE_BYTES", "1073741824"),
625    OptionalEnv::new("AGENTICS_STORAGE_MAX_STATEMENT_BYTES", "1048576"),
626    OptionalEnv::new("AGENTICS_STORAGE_MAX_JSON_ARTIFACT_BYTES", "1048576"),
627    OptionalEnv::new("AGENTICS_STORAGE_TMP_OBJECT_GRACE_HOURS", "24"),
628    OptionalEnv::new("AGENTICS_API_HOST", "127.0.0.1"),
629    OptionalEnv::new("AGENTICS_API_PORT", "3100"),
630    OptionalEnv::new("AGENTICS_API_HOST_PORT", "3110"),
631    OptionalEnv::new("AGENTICS_WEB_PORT", "3001"),
632    OptionalEnv::new("AGENTICS_WEB_HOST_PORT", "3010"),
633    OptionalEnv::new("AGENTICS_WEB_BASE_URL", "http://localhost:3010"),
634    OptionalEnv::new("AGENTICS_CORS_ALLOWED_ORIGINS", "localhost web origins"),
635    OptionalEnv::new(
636        "AGENTICS_WEB_ALLOWED_DEV_ORIGINS",
637        "127.0.0.1 and localhost",
638    ),
639    OptionalEnv::new(
640        "NEXT_PUBLIC_AGENTICS_API_BASE_URL",
641        "same-origin Next proxy",
642    ),
643    OptionalEnv::new(
644        "NEXT_PUBLIC_AGENTICS_GA_MEASUREMENT_ID",
645        "analytics disabled",
646    ),
647    OptionalEnv::new("AGENTICS_MOLTBOOK_SUBMOLT_NAME", "agentics-platform"),
648    OptionalEnv::new(
649        "AGENTICS_MOLTBOOK_SUBMOLT_URL",
650        "https://www.moltbook.com/m/agentics-platform",
651    ),
652    OptionalEnv::new(
653        "AGENTICS_BOOTSTRAP_ADMIN_GITHUB_USER_IDS",
654        "no bootstrap admins",
655    ),
656    OptionalEnv::new(
657        "AGENTICS_GITHUB_APP_CLIENT_ID",
658        "GitHub sign-in unavailable",
659    ),
660    OptionalEnv::new(
661        "AGENTICS_GITHUB_APP_CLIENT_SECRET",
662        "GitHub sign-in unavailable",
663    ),
664    OptionalEnv::new(
665        "AGENTICS_GITHUB_APP_REDIRECT_URL",
666        "GitHub sign-in unavailable",
667    ),
668    OptionalEnv::new("AGENTICS_WEB_SESSION_COOKIE_SECURE", "false for loopback"),
669    OptionalEnv::new("AGENTICS_AGENT_REGISTRATION_MODE", "pioneer_code"),
670    OptionalEnv::new("AGENTICS_OFFICIAL_LOG_REDACTION", "contract_based"),
671    OptionalEnv::new("AGENTICS_LOG_LEVEL", "info"),
672    OptionalEnv::new("AGENTICS_MAX_ACTIVE_AGENTS", "1000"),
673    OptionalEnv::new("AGENTICS_VALIDATION_RUNS_PER_AGENT_CHALLENGE_DAY", "20"),
674    OptionalEnv::new("AGENTICS_OFFICIAL_RUNS_PER_AGENT_CHALLENGE_DAY", "5"),
675    OptionalEnv::new("AGENTICS_MAX_ACTIVE_OFFICIAL_JOBS", "20"),
676];
677
678const TEST_OPTIONAL: &[OptionalEnv] = &[
679    OptionalEnv::new(
680        "AGENTICS_RUST_TOOLCHAIN_IMAGE",
681        "local rust-toolchain image",
682    ),
683    OptionalEnv::new("AGENTICS_TEST_DISABLE_CARGO_CACHE", "false"),
684    OptionalEnv::new(
685        "AGENTICS_TEST_CARGO_REGISTRY_VOLUME",
686        "agentics-test-cargo-registry",
687    ),
688    OptionalEnv::new("AGENTICS_TEST_CARGO_GIT_VOLUME", "agentics-test-cargo-git"),
689    OptionalEnv::new(
690        "AGENTICS_TEST_CARGO_TARGET_VOLUME",
691        "agentics-test-cargo-target",
692    ),
693    OptionalEnv::new("AGENTICS_STORAGE_MAX_BUNDLE_ARCHIVE_BYTES", "1073741824"),
694    OptionalEnv::new("AGENTICS_STORAGE_MAX_STATEMENT_BYTES", "1048576"),
695    OptionalEnv::new("AGENTICS_STORAGE_MAX_JSON_ARTIFACT_BYTES", "1048576"),
696    OptionalEnv::new("AGENTICS_STORAGE_TMP_OBJECT_GRACE_HOURS", "24"),
697    OptionalEnv::new(
698        "NEXT_PUBLIC_AGENTICS_GA_MEASUREMENT_ID",
699        "analytics disabled",
700    ),
701    OptionalEnv::new(
702        "AGENTICS_TEST_RUNNER_WRITABLE_STORAGE_MODE",
703        "xfs-project-quota-slots",
704    ),
705    OptionalEnv::new(
706        "AGENTICS_TEST_RUNNER_WRITABLE_SLOT_CLASSES_MB",
707        "64,256,1024,4096",
708    ),
709    OptionalEnv::new("AGENTICS_TEST_RUNNER_DOCKER_LAYER_QUOTA", "true"),
710    OptionalEnv::new("AGENTICS_LOG_LEVEL", "info"),
711];
712
713const REHEARSAL_OPTIONAL: &[OptionalEnv] = &[
714    OptionalEnv::new("COMPOSE_PROFILES", "no optional Compose profile"),
715    OptionalEnv::new("AGENTICS_COMPOSE_PROD_PROJECT", "agentics-prod"),
716    OptionalEnv::new(
717        "AGENTICS_COMPOSE_PROD_SERVICE_ENV_FILE",
718        "./env/prod.env.example",
719    ),
720    OptionalEnv::new("AGENTICS_COMPOSE_BIND_IP", "127.0.0.1"),
721    OptionalEnv::new("AGENTICS_CHALLENGES_ROOT", "/app/challenges"),
722    OptionalEnv::new(
723        "NEXT_PUBLIC_AGENTICS_API_BASE_URL",
724        "same-origin Next proxy",
725    ),
726    OptionalEnv::new(
727        "NEXT_PUBLIC_AGENTICS_GA_MEASUREMENT_ID",
728        "analytics disabled",
729    ),
730    OptionalEnv::new("AGENTICS_MOLTBOOK_SUBMOLT_NAME", "agentics-platform"),
731    OptionalEnv::new(
732        "AGENTICS_MOLTBOOK_SUBMOLT_URL",
733        "https://www.moltbook.com/m/agentics-platform",
734    ),
735    OptionalEnv::new("AGENTICS_WORKER_ACCELERATORS", "none"),
736    OptionalEnv::new(
737        "AGENTICS_WORKER_GPU_PROBE_IMAGE",
738        "required only for gpu workers",
739    ),
740    OptionalEnv::new(
741        "AGENTICS_DGX_DOCKER_DATA_ROOT",
742        "/srv/agentics/docker-data-root",
743    ),
744    OptionalEnv::new(
745        "AGENTICS_DGX_RUNNER_DOCKER_EXEC_ROOT",
746        "/srv/agentics/docker-exec",
747    ),
748    OptionalEnv::new(
749        "AGENTICS_DGX_RUNNER_DOCKER_PIDFILE",
750        "/srv/agentics/docker.pid",
751    ),
752    OptionalEnv::new(
753        "AGENTICS_DGX_RUNNER_DOCKER_LOG",
754        "/srv/agentics/dockerd.log",
755    ),
756    OptionalEnv::new("AGENTICS_DGX_RUNNER_DOCKER_BRIDGE", "agentics0"),
757    OptionalEnv::new("AGENTICS_DGX_RUNNER_DOCKER_BRIDGE_CIDR", "172.30.0.1/16"),
758    OptionalEnv::new("AGENTICS_DGX_DOCKER_PULL_POLICY", "if-not-present"),
759    OptionalEnv::new("AGENTICS_DGX_PERSIST_FSTAB", "false"),
760    OptionalEnv::new("AGENTICS_DGX_RUN_MUTATING_PROBES", "false"),
761    OptionalEnv::new("AGENTICS_REHEARSAL_CPU_IMAGE_SOURCE", "registry"),
762    OptionalEnv::new(
763        "AGENTICS_REHEARSAL_CPU_IMAGE_REFERENCE",
764        "built-in CPU fixture image",
765    ),
766    OptionalEnv::new("AGENTICS_LOG_LEVEL", "info"),
767    OptionalEnv::new("AGENTICS_RUNNER_MAX_OUTPUT_FILES", "8192"),
768    OptionalEnv::new("AGENTICS_RUNNER_MAX_OUTPUT_DIRS", "1024"),
769    OptionalEnv::new("AGENTICS_RUNNER_MAX_OUTPUT_DEPTH", "32"),
770    OptionalEnv::new("AGENTICS_RUNNER_MAX_RUNS", "challenge bundle maximum"),
771    OptionalEnv::new("AGENTICS_RUNNER_MAX_RESULT_JSON_BYTES", "4194304"),
772    OptionalEnv::new("AGENTICS_RUNNER_MAX_PUBLIC_RESULTS", "1024"),
773    OptionalEnv::new("AGENTICS_RUNNER_MAX_RESULT_LOG_BYTES", "262144"),
774    OptionalEnv::new(
775        "AGENTICS_RUNNER_MAX_INTERACTION_BYTES_PER_DIRECTION",
776        "268435456",
777    ),
778    OptionalEnv::new("AGENTICS_RUNNER_INTERACTION_SHUTDOWN_GRACE_SECS", "2"),
779    OptionalEnv::new("AGENTICS_MAX_ACTIVE_AGENTS", "1000"),
780    OptionalEnv::new("AGENTICS_VALIDATION_RUNS_PER_AGENT_CHALLENGE_DAY", "20"),
781    OptionalEnv::new("AGENTICS_OFFICIAL_RUNS_PER_AGENT_CHALLENGE_DAY", "5"),
782    OptionalEnv::new("AGENTICS_MAX_ACTIVE_OFFICIAL_JOBS", "20"),
783    OptionalEnv::new(ENV_REVIEW_RECORD_LIMIT, "10"),
784    OptionalEnv::new(
785        "AGENTICS_CHALLENGE_PRIVATE_ASSET_BYTES_PER_REVIEW_RECORD",
786        "1073741824",
787    ),
788    OptionalEnv::new("AGENTICS_CHALLENGE_REVIEW_RECORD_VALIDATIONS_PER_DAY", "10"),
789    OptionalEnv::new("AGENTICS_CHALLENGE_REVIEW_RECORD_TTL_DAYS", "14"),
790    OptionalEnv::new("AGENTICS_UNPUBLISHED_CHALLENGE_ASSET_GRACE_DAYS", "7"),
791];
792
793const PRODUCTION_OPTIONAL: &[OptionalEnv] = &[
794    OptionalEnv::new("AGENTICS_COMPOSE_PROD_PROJECT", "agentics-prod"),
795    OptionalEnv::new(
796        "AGENTICS_COMPOSE_PROD_SERVICE_ENV_FILE",
797        "./env/prod.env.example",
798    ),
799    OptionalEnv::new("AGENTICS_COMPOSE_BIND_IP", "127.0.0.1"),
800    OptionalEnv::new("AGENTICS_POSTGRES_PORT", "5432"),
801    OptionalEnv::new("AGENTICS_CHALLENGES_ROOT", "/app/challenges"),
802    OptionalEnv::new("AGENTICS_API_HOST", "0.0.0.0 in Compose API service"),
803    OptionalEnv::new("AGENTICS_API_HOST_PORT", "3100"),
804    OptionalEnv::new("AGENTICS_API_PORT", "3100"),
805    OptionalEnv::new("AGENTICS_WEB_HOST_PORT", "3001"),
806    OptionalEnv::new("AGENTICS_WEB_PORT", "3001"),
807    OptionalEnv::new(
808        "NEXT_PUBLIC_AGENTICS_API_BASE_URL",
809        "same-origin Next proxy",
810    ),
811    OptionalEnv::new(
812        "NEXT_PUBLIC_AGENTICS_GA_MEASUREMENT_ID",
813        "analytics disabled",
814    ),
815    OptionalEnv::new("AGENTICS_MOLTBOOK_SUBMOLT_NAME", "agentics-platform"),
816    OptionalEnv::new(
817        "AGENTICS_MOLTBOOK_SUBMOLT_URL",
818        "https://www.moltbook.com/m/agentics-platform",
819    ),
820    OptionalEnv::new("AGENTICS_AGENT_REGISTRATION_MODE", "pioneer_code"),
821    OptionalEnv::new("AGENTICS_WORKER_ACCELERATORS", "none"),
822    OptionalEnv::new(
823        "AGENTICS_WORKER_GPU_PROBE_IMAGE",
824        "required only for gpu workers",
825    ),
826    OptionalEnv::new(
827        "AGENTICS_DGX_DOCKER_DATA_ROOT",
828        "/srv/agentics/docker-data-root",
829    ),
830    OptionalEnv::new(
831        "AGENTICS_DGX_RUNNER_DOCKER_EXEC_ROOT",
832        "/srv/agentics/docker-exec",
833    ),
834    OptionalEnv::new(
835        "AGENTICS_DGX_RUNNER_DOCKER_PIDFILE",
836        "/srv/agentics/docker.pid",
837    ),
838    OptionalEnv::new(
839        "AGENTICS_DGX_RUNNER_DOCKER_LOG",
840        "/srv/agentics/dockerd.log",
841    ),
842    OptionalEnv::new("AGENTICS_DGX_RUNNER_DOCKER_BRIDGE", "agentics0"),
843    OptionalEnv::new("AGENTICS_DGX_RUNNER_DOCKER_BRIDGE_CIDR", "172.30.0.1/16"),
844    OptionalEnv::new("AGENTICS_DGX_DOCKER_PULL_POLICY", "if-not-present"),
845    OptionalEnv::new("AGENTICS_DGX_RUN_MUTATING_PROBES", "false"),
846    OptionalEnv::new("AGENTICS_LOG_LEVEL", "info"),
847    OptionalEnv::new("AGENTICS_RUNNER_MAX_OUTPUT_FILES", "8192"),
848    OptionalEnv::new("AGENTICS_RUNNER_MAX_OUTPUT_DIRS", "1024"),
849    OptionalEnv::new("AGENTICS_RUNNER_MAX_OUTPUT_DEPTH", "32"),
850    OptionalEnv::new("AGENTICS_RUNNER_MAX_RUNS", "challenge bundle maximum"),
851    OptionalEnv::new("AGENTICS_RUNNER_MAX_RESULT_JSON_BYTES", "4194304"),
852    OptionalEnv::new("AGENTICS_RUNNER_MAX_PUBLIC_RESULTS", "1024"),
853    OptionalEnv::new("AGENTICS_RUNNER_MAX_RESULT_LOG_BYTES", "262144"),
854    OptionalEnv::new(
855        "AGENTICS_RUNNER_MAX_INTERACTION_BYTES_PER_DIRECTION",
856        "268435456",
857    ),
858    OptionalEnv::new("AGENTICS_RUNNER_INTERACTION_SHUTDOWN_GRACE_SECS", "2"),
859    OptionalEnv::new("AGENTICS_MAX_ACTIVE_AGENTS", "1000"),
860    OptionalEnv::new("AGENTICS_VALIDATION_RUNS_PER_AGENT_CHALLENGE_DAY", "20"),
861    OptionalEnv::new("AGENTICS_OFFICIAL_RUNS_PER_AGENT_CHALLENGE_DAY", "5"),
862    OptionalEnv::new("AGENTICS_MAX_ACTIVE_OFFICIAL_JOBS", "20"),
863    OptionalEnv::new(ENV_REVIEW_RECORD_LIMIT, "10"),
864    OptionalEnv::new(
865        "AGENTICS_CHALLENGE_PRIVATE_ASSET_BYTES_PER_REVIEW_RECORD",
866        "1073741824",
867    ),
868    OptionalEnv::new("AGENTICS_CHALLENGE_REVIEW_RECORD_VALIDATIONS_PER_DAY", "10"),
869    OptionalEnv::new("AGENTICS_CHALLENGE_REVIEW_RECORD_TTL_DAYS", "14"),
870    OptionalEnv::new("AGENTICS_UNPUBLISHED_CHALLENGE_ASSET_GRACE_DAYS", "7"),
871];
872
873const API_OPTIONAL: &[OptionalEnv] = &[
874    OptionalEnv::new("AGENTICS_LOG_LEVEL", "info"),
875    OptionalEnv::new("AGENTICS_WEB_SESSION_COOKIE_NAME", "agentics_session"),
876    OptionalEnv::new("AGENTICS_WEB_CSRF_COOKIE_NAME", "agentics_csrf"),
877    OptionalEnv::new("AGENTICS_WEB_SESSION_TTL_HOURS", "24"),
878];
879
880const WORKER_OPTIONAL: &[OptionalEnv] = &[
881    OptionalEnv::new("AGENTICS_LOG_LEVEL", "info"),
882    OptionalEnv::new("AGENTICS_WORKER_POLL_INTERVAL_MS", "3000"),
883    OptionalEnv::new("AGENTICS_WORKER_STALE_JOB_MINUTES", "1"),
884    OptionalEnv::new("AGENTICS_WORKER_ACCELERATORS", "none"),
885    OptionalEnv::new(
886        "AGENTICS_WORKER_GPU_PROBE_IMAGE",
887        "required only for gpu workers",
888    ),
889];
890
891const MIGRATE_OPTIONAL: &[OptionalEnv] = &[OptionalEnv::new("AGENTICS_LOG_LEVEL", "info")];
892
893const WEB_OPTIONAL: &[OptionalEnv] = &[
894    OptionalEnv::new(
895        "AGENTICS_WEB_ALLOWED_DEV_ORIGINS",
896        "127.0.0.1 and localhost",
897    ),
898    OptionalEnv::new(
899        "NEXT_PUBLIC_AGENTICS_API_BASE_URL",
900        "same-origin Next proxy",
901    ),
902    OptionalEnv::new(
903        "NEXT_PUBLIC_AGENTICS_GA_MEASUREMENT_ID",
904        "analytics disabled",
905    ),
906];