astrid-runtime/astrid · error

distro must have at least one capsule with role = "uplink"…

Error message

distro must have at least one capsule with role = "uplink" (a frontend)

What it means

A distro must include at least one capsule whose role is "uplink", which serves as the frontend entry point. validate_manifest scans all capsule roles and bails if none is an uplink, since a distro without a frontend cannot route traffic.

Solutions

  1. Add `role = "uplink"` to the capsule that should act as the frontend.
  2. If a frontend capsule is missing entirely, add one to the manifest.
  3. Verify the role string is exactly "uplink" (no typos or extra whitespace).

Example fix

// before
capsules.backend.role = "service"

// after
capsules.frontend.role = "uplink"
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_uplink(capsules: &[Capsule]) -> Result<(), String> {
    if capsules.iter().any(|c| c.role.as_deref() == Some("uplink")) {
        Ok(())
    } else {
        Err("distro needs at least one role = \"uplink\" capsule".into())
    }
}

Type guard

fn is_uplink(c: &Capsule) -> bool {
    c.role.as_deref() == Some("uplink")
}

Try / catch

if let Err(e) = ensure_uplink(&manifest.capsules) {
    eprintln!("distro invalid: {e}");
    std::process::exit(1);
}

Prevention

When it happens

Trigger: Building/validating a distro manifest where every capsule's role is missing or set to something other than "uplink" (e.g. all backend/service capsules).

Common situations: Authoring a new distro from scratch with only backend capsules; renaming a frontend capsule's role to a custom value; deleting the frontend capsule without replacing it.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/9a1986ad19bb7ab0. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-cli/src/commands/distro/validate.rs:195

        }
        if matches!(tag, Some("")) {
            anyhow::bail!("capsule '{}': tag must not be empty", cap.name);
        }
        if version.is_empty() && tag.is_none() {
            anyhow::bail!(
                "capsule '{}': distro capsules require a concrete released version or tag",
                cap.name
            );
        }
    }

    // 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,

View on GitHub (pinned to affd8760f4)