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

  1. Add the missing variable to the manifest's [variables] table.
  2. Fix the typo in the env value so it matches an existing variable name.
  3. 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

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


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)