1use 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];