astrid-runtime/astrid · error

{e}

Error message

{e}

What it means

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.

Source

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

                        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"
            );
        }
        if let Some(exp) = &invites.default_expires {
            parse_invite_duration(exp).map_err(|e| anyhow::anyhow!(e))?;
        }
        if let Some(cap) = &invites.max_principals
            && cap != "unlimited"
            && cap.parse::<u32>().is_err()
        {
            anyhow::bail!(
                "invites.max-principals must be \"unlimited\" or a non-negative integer (got {cap:?})",
            );
        }
    }

    // Branding — only structural rails. The dashboard interprets the
    // values; the parser just refuses obvious garbage.
    if let Some(branding) = &manifest.branding {
        if let Some(icon) = &branding.icon
            && icon.len() > 64 * 1024
        {
            anyhow::bail!(

View on GitHub (pinned to affd8760f4)

Solutions

  1. Correct invites.default-expires to a supported duration string with its unit (e.g. '72h', '30d').
  2. Read the wrapped parse message to see which part of the string was rejected.
  3. Remove the default-expires key if no default expiry is needed.

Example fix

# before (Distro.toml)
[invites]
default-expires = "30 days"
# after
[invites]
default-expires = "30d"
Defensive patterns

Strategy: validation

Validate before calling

// Validate the duration string before writing it into Distro.toml
fn plausible_duration(s: &str) -> bool {
    let t = s.trim();
    !t.is_empty()
        && t[..t.len()-1].parse::<u64>().is_ok()
        && matches!(t.chars().last(), Some('s') | Some('m') | Some('h') | Some('d') | Some('w'))
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "invalid duration" / "failed to parse duration": why your timeout, interval, or TTL string is rejected and which formats each library accepts — this error's family across 32 libraries.

Related errors


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