{"record":{"id":"f5fc380863263444","repo":"astrid-runtime/astrid","slug":"e","errorCode":null,"errorMessage":"{e}","messagePattern":"\\{e\\}","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/distro/validate.rs","lineNumber":224,"sourceCode":"                        cap.name,\n                    );\n                }\n            }\n        }\n    }\n\n    // Invite policy — additive, so the rule is \"if any field is set,\n    // the shape must be coherent\". The kernel still cap-gates issuance\n    // at runtime; this is fail-fast for typos.\n    if let Some(invites) = &manifest.invites {\n        if !invites.issuers.is_empty() && invites.default_group.is_none() {\n            anyhow::bail!(\n                \"invites.issuers is non-empty but invites.default-group is unset — \\\n                 either configure both or remove the [invites] section\"\n            );\n        }\n        if let Some(exp) = &invites.default_expires {\n            parse_invite_duration(exp).map_err(|e| anyhow::anyhow!(e))?;\n        }\n        if let Some(cap) = &invites.max_principals\n            && cap != \"unlimited\"\n            && cap.parse::<u32>().is_err()\n        {\n            anyhow::bail!(\n                \"invites.max-principals must be \\\"unlimited\\\" or a non-negative integer (got {cap:?})\",\n            );\n        }\n    }\n\n    // Branding — only structural rails. The dashboard interprets the\n    // values; the parser just refuses obvious garbage.\n    if let Some(branding) = &manifest.branding {\n        if let Some(icon) = &branding.icon\n            && icon.len() > 64 * 1024\n        {\n            anyhow::bail!(","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/distro/validate.rs#L206-L242","documentation":"During manifest validation, if the [invites] block sets a default-expires value, it is run through parse_invite_duration and any error is re-raised verbatim via anyhow::anyhow!(e). So the surfaced message is whatever the duration parser produced — a malformed human duration string (e.g. '30d', '2w', '72h') in invites.default-expires.","triggerScenarios":"validate_manifest on a Distro.toml whose invites.default-expires is set to a string parse_invite_duration cannot parse: missing unit ('30'), unsupported unit, negative duration, empty string, or stray whitespace/characters.","commonSituations":"Distro authors writing '1 month' or '30 days' instead of the accepted compact unit forms; copy-pasting cron-style values; typos like '30dd'; localized unit names.","solutions":["Correct invites.default-expires to a supported duration string with its unit (e.g. '72h', '30d').","Read the wrapped parse message to see which part of the string was rejected.","Remove the default-expires key if no default expiry is needed."],"exampleFix":"# before (Distro.toml)\n[invites]\ndefault-expires = \"30 days\"\n# after\n[invites]\ndefault-expires = \"30d\"","handlingStrategy":"validation","validationCode":"// Validate the duration string before writing it into Distro.toml\nfn plausible_duration(s: &str) -> bool {\n    let t = s.trim();\n    !t.is_empty()\n        && t[..t.len()-1].parse::<u64>().is_ok()\n        && matches!(t.chars().last(), Some('s') | Some('m') | Some('h') | Some('d') | Some('w'))\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Use compact unit forms (72h, 30d) with no spaces.","Lint Distro.toml with `astrid distro validate` before committing.","Document accepted duration units next to the [invites] template."],"tags":["config","validation","duration","manifest"],"backgroundTag":"invalid-duration-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"}