{"record":{"id":"0cdd2ec166d238a3","repo":"astrid-runtime/astrid","slug":"capsule-name-is-invalid-must-match-a-z-a","errorCode":null,"errorMessage":"capsule name '{}' is invalid (must match ^[a-z][a-z0-9-]*$)","messagePattern":"capsule name '(.+?)' is invalid \\(must match \\^\\[a-z\\]\\[a-z0-9-\\]\\*\\$\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/distro/validate.rs","lineNumber":133,"sourceCode":"                );\n            }\n        }\n    }\n\n    // At least one capsule.\n    if manifest.capsules.is_empty() {\n        anyhow::bail!(\"distro must contain at least one capsule\");\n    }\n\n    // No duplicate capsule names, and each name must be a valid\n    // identifier. Names become path components in the `.shuttle` layout\n    // (`capsules/<name>.capsule`) and on disk under the capsule store;\n    // constraining them to `^[a-z][a-z0-9-]*$` keeps a manifest from\n    // introducing `/`, `..`, or other path-hostile characters there.\n    let mut seen_names = HashSet::new();\n    for cap in &manifest.capsules {\n        if !is_valid_id(&cap.name) {\n            anyhow::bail!(\n                \"capsule name '{}' is invalid (must match ^[a-z][a-z0-9-]*$)\",\n                cap.name,\n            );\n        }\n        if !seen_names.insert(&cap.name) {\n            anyhow::bail!(\"duplicate capsule name '{}'\", cap.name);\n        }\n        // Distros compose *released* capsules. `branch`/`rev` selectors\n        // can only be honored by compiling from a git ref — the exact\n        // toolchain dependency offline/headless distro seeding exists to\n        // remove. Reject them: a distro must pin a released `version` or\n        // `tag`. (Git-ref installs remain available via the standalone\n        // `astrid capsule install` command.)\n        if cap.branch.is_some() || cap.rev.is_some() {\n            anyhow::bail!(\n                \"capsule '{}': branch/rev require building from source and are not allowed in a \\\n                 distro manifest — pin a released `version` or `tag` (git-ref installs are \\\n                 available via `astrid capsule install`).\",","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/distro/validate.rs#L115-L151","documentation":"Capsule names become filesystem path components in the `.shuttle` layout (`capsules/<name>.capsule`) and under the capsule store, so validate_manifest enforces the safe identifier pattern `^[a-z][a-z0-9-]*$`. This blocks `/`, `..`, uppercase, underscores, and other path-hostile or ambiguous characters from reaching the disk layout.","triggerScenarios":"A `[[capsules]]` entry whose `name` fails `is_valid_id`: starts with a digit or `-`, contains uppercase, `_`, `/`, `.`, `..`, spaces, or is empty.","commonSituations":"Naming a capsule after a GitHub repo (`My-Lib_v2`); pasting a path like `tools/http`; typos such as leading whitespace or a leading digit `2fa`.","solutions":["Rename the capsule to lowercase letters/digits/hyphens starting with a letter (e.g. `my-lib`).","Update all references to the old capsule name in requires/selectors.","Re-run distro validation to confirm the new name passes."],"exampleFix":"# before\n[[capsules]]\nname = \"My_Lib/2\"\n\n# after\n[[capsules]]\nname = \"my-lib-2\"","handlingStrategy":"validation","validationCode":"fn is_valid_id(name: &str) -> bool {\n    let mut chars = name.chars();\n    matches!(chars.next(), Some(c) if c.is_ascii_lowercase())\n        && chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')\n}\nfor cap in &manifest.capsules {\n    assert!(is_valid_id(&cap.name), \"invalid capsule name: {}\", cap.name);\n}","typeGuard":"fn is_valid_capsule_name(s: &str) -> bool {\n    !s.is_empty()\n        && s.chars().next().is_some_and(|c| c.is_ascii_lowercase())\n        && s.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')\n}","tryCatchPattern":"if let Err(e) = validate_manifest(&manifest) {\n    if e.to_string().contains(\"is invalid (must match\") {\n        eprintln!(\"rename capsule: {e}\");\n    }\n}","preventionTips":["Stick to lowercase letters, digits, and hyphens; start with a letter","Never embed paths, underscores, or uppercase in capsule names","Validate names at manifest-generation time, not just at build time"],"tags":["cli","manifest","naming","path-safety","validation"],"backgroundTag":"invalid-identifier-format","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}