{"record":{"id":"9a1986ad19bb7ab0","repo":"astrid-runtime/astrid","slug":"distro-must-have-at-least-one-capsule-with-role","errorCode":null,"errorMessage":"distro must have at least one capsule with role = \"uplink\" (a frontend)","messagePattern":"distro must have at least one capsule with role = \"uplink\" \\(a frontend\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/distro/validate.rs","lineNumber":195,"sourceCode":"        }\n        if matches!(tag, Some(\"\")) {\n            anyhow::bail!(\"capsule '{}': tag must not be empty\", cap.name);\n        }\n        if version.is_empty() && tag.is_none() {\n            anyhow::bail!(\n                \"capsule '{}': distro capsules require a concrete released version or tag\",\n                cap.name\n            );\n        }\n    }\n\n    // At least one uplink.\n    let has_uplink = manifest\n        .capsules\n        .iter()\n        .any(|c| c.role.as_deref() == Some(\"uplink\"));\n    if !has_uplink {\n        anyhow::bail!(\"distro must have at least one capsule with role = \\\"uplink\\\" (a frontend)\");\n    }\n\n    // Variable references in capsule env.\n    let defined_vars: HashSet<&str> = manifest.variables.keys().map(String::as_str).collect();\n    for cap in &manifest.capsules {\n        for (key, value) in &cap.env {\n            for var_ref in extract_variable_refs(value) {\n                if !defined_vars.contains(var_ref) {\n                    anyhow::bail!(\n                        \"capsule '{}' env.{key} references undefined variable '{{{{ {var_ref} }}}}'\",\n                        cap.name,\n                    );\n                }\n            }\n        }\n    }\n\n    // Invite policy — additive, so the rule is \"if any field is set,","sourceCodeStart":177,"sourceCodeEnd":213,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/distro/validate.rs#L177-L213","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Add `role = \"uplink\"` to the capsule that should act as the frontend.","If a frontend capsule is missing entirely, add one to the manifest.","Verify the role string is exactly \"uplink\" (no typos or extra whitespace)."],"exampleFix":"// before\ncapsules.backend.role = \"service\"\n\n// after\ncapsules.frontend.role = \"uplink\"","handlingStrategy":"validation","validationCode":"fn ensure_uplink(capsules: &[Capsule]) -> Result<(), String> {\n    if capsules.iter().any(|c| c.role.as_deref() == Some(\"uplink\")) {\n        Ok(())\n    } else {\n        Err(\"distro needs at least one role = \\\"uplink\\\" capsule\".into())\n    }\n}","typeGuard":"fn is_uplink(c: &Capsule) -> bool {\n    c.role.as_deref() == Some(\"uplink\")\n}","tryCatchPattern":"if let Err(e) = ensure_uplink(&manifest.capsules) {\n    eprintln!(\"distro invalid: {e}\");\n    std::process::exit(1);\n}","preventionTips":["Keep a frontend/uplink capsule in every distro template.","Lint for the exact role string \"uplink\" (avoid typos like \"up-link\").","When removing capsules, check no distro loses its only uplink."],"tags":["manifest","validation","configuration"],"backgroundTag":"missing-required-config-field","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}