astrid-runtime/astrid · error
capsule ' ' env. references undefined variable ' }}}}
Error message
capsule '{}' env.{key} references undefined variable '{{{{ {var_ref} }}}}' What it means
Capsule environment values may interpolate variables defined in the manifest's [variables] table using {{{{ name }}}} syntax. validate_manifest extracts every variable reference from each capsule env value and fails if any referenced name is not defined in manifest.variables.
Solutions
- Add the missing variable to the manifest's [variables] table.
- Fix the typo in the env value so it matches an existing variable name.
- Remove the reference if the variable is no longer needed.
Example fix
// before
[variables]
region = "eu-west"
env: API_URL = "{{{{ api_endpoint }}}}" # undefined
// after
[variables]
region = "eu-west"
api_endpoint = "https://api.example.com"
env: API_URL = "{{{{ api_endpoint }}}}" Defensive patterns
Strategy: validation
Validate before calling
fn check_env_vars(capsules: &[Capsule], defined: &HashSet<String>) -> Vec<String> {
capsules.iter()
.flat_map(|c| c.env.iter().map(move |(k, v)| (c.name.as_str(), k, v)))
.flat_map(|(name, k, v)| extract_variable_refs(v)
.filter(|r| !defined.contains(*r)).map(move |r| format!("{name}: {k} -> {r}")))
.collect()
} Type guard
fn var_is_defined(r: &str, defined: &HashSet<&str>) -> bool {
defined.contains(r)
} Try / catch
let problems = check_env_vars(&manifest.capsules, &defined_vars);
if !problems.is_empty() {
eprintln!("undefined variable refs: {problems:?}");
std::process::exit(1);
} Prevention
- Keep [variables] adjacent to env usage and review both together in PRs.
- Rename variables with project-wide search/replace, never by hand.
- Add a pre-commit lint that runs manifest validation.
When it happens
Trigger: A capsule env value like `TOKEN = "{{{{ api_token }}}}"` where `api_token` has no entry in the manifest's variables table. Triggered during any distro manifest validation.
Common situations: Typo in a variable reference; a variable was removed/renamed in [variables] but env values still reference the old name; copying env blocks between distros with different variable sets.
Understand the failure class
Background: "missing required config value" errors: why libraries refuse to start when a configuration key is empty, unset, or blank — this error's family across 48 libraries.
Related errors
- branding.accent-color
- branding.icon is bytes — distros must not embed assets…
- branding.primary-color
- capsule archive contains duplicate entry
- capsule archive contains unsupported entry
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/62b8c26f78d97576.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-cli/src/commands/distro/validate.rs:204
}
}
// At least one uplink.
let has_uplink = manifest
.capsules
.iter()
.any(|c| c.role.as_deref() == Some("uplink"));
if !has_uplink {
anyhow::bail!("distro must have at least one capsule with role = \"uplink\" (a frontend)");
}
// Variable references in capsule env.
let defined_vars: HashSet<&str> = manifest.variables.keys().map(String::as_str).collect();
for cap in &manifest.capsules {
for (key, value) in &cap.env {
for var_ref in extract_variable_refs(value) {
if !defined_vars.contains(var_ref) {
anyhow::bail!(
"capsule '{}' env.{key} references undefined variable '{{{{ {var_ref} }}}}'",
cap.name,
);
}
}
}
}
// Invite policy — additive, so the rule is "if any field is set,
// the shape must be coherent". The kernel still cap-gates issuance
// at runtime; this is fail-fast for typos.
if let Some(invites) = &manifest.invites {
if !invites.issuers.is_empty() && invites.default_group.is_none() {
anyhow::bail!(
"invites.issuers is non-empty but invites.default-group is unset — \
either configure both or remove the [invites] section"
);
}View on GitHub (pinned to affd8760f4)