astrid-runtime/astrid · error

legacy principal has no durable UID

Error message

legacy principal {alias} has no durable UID

What it means

Raised when neither a migration receipt nor a live alias binding provides a durable UID for a legacy principal. Migration needs a stable UID to associate the principal's data; without one it cannot proceed and returns InvalidData.

Solutions

  1. Restore the missing receipt or live alias binding (re-register the principal)
  2. Reconstruct the durable UID from backups of the alias store
  3. Remove the orphaned legacy principal directory if the principal no longer exists
  4. Re-run migration once a durable UID is available

Example fix

// before: orphaned legacy dir aborts migration
migrate_legacy_principal_homes(&home, &store)?;
// after: re-register or remove the orphan first
ensure_principal_registered(&store, &alias)?;
migrate_legacy_principal_homes(&home, &store)?;
Defensive patterns

Strategy: validation

Validate before calling

fn principal_resolvable(store: &Store, alias: &str) -> bool {
    store.receipt_for(alias).is_some() || store.live_binding(alias).is_some()
}

Type guard

fn has_durable_uid(r: Option<&Receipt>, l: Option<&Binding>) -> bool { r.is_some() || l.is_some() }

Try / catch

match migrate_legacy_principal_homes(&home, &store) {
    Err(e) if e.kind() == io::ErrorKind::InvalidData => {
        re_register_orphaned_principals(&store)?;
        migrate_legacy_principal_homes(&home, &store)
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling migrate_legacy_principal_homes when for a given alias both the migration receipt and the live alias binding are absent (the (None, None) match arm), e.g. the receipt file was deleted and the alias store has no record.

Common situations: Partially cleaned state after manual deletion of receipts, a store that was reset while legacy directories remain, or migration re-run against a pruned store.

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/3b6544f7e94d9ffc. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-kernel/src/principal_home_migration/mod.rs:117

            )
        })?;
        // A migration receipt is immutable provenance and takes precedence
        // over the mutable alias directory. If an alias was deleted and then
        // reused, never stream the surviving old source into the replacement
        // UID; fail closed instead.
        let receipt_uid = receipt_uid_for_alias(home, &alias)?;
        let live_uid = principals.uid_for(&alias).ok();
        let uid = match (receipt_uid, live_uid) {
            (Some(receipt), Some(live)) if receipt != live => {
                return Err(conflict_path(
                    &entry.path(),
                    "migration receipt UID differs from the live alias binding",
                ));
            },
            (Some(receipt), _) => receipt,
            (None, Some(live)) => live,
            (None, None) => {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    format!("legacy principal {alias} has no durable UID"),
                ));
            },
        };
        migrate_one_principal(home, store, uid, &alias, &entry.path())?;
    }
    Ok(())
}

/// Snapshot only ordinary entries owned by this migration.
///
/// Dedicated subtrees (capsules, env, audit, logs, and other released
/// operational paths) are excluded because their own migrations bind and
/// retire them. The fields use the same digest/count/byte inventory as the
/// ordinary-home receipt. `present` distinguishes an absent source from an
/// existing source containing only dedicated entries.
#[derive(Clone, Debug, PartialEq, Eq)]

View on GitHub (pinned to affd8760f4)