#1947·astrid

distro.lock writer emits resolved_ref but kernel reader demands resolved-ref with deny_unknown_fields

Author: jvsteinerCreated Sep 15, 2026Updated Sep 15, 2026

Summary

The CLI writes distro.lock with a snake_case resolved_ref key, while the kernel reads it with rename_all = "kebab-case" and deny_unknown_fields. Every lockfile containing a resolved ref is therefore unreadable by the kernel that has to migrate it, and boot aborts:

Error: Failed to boot Kernel: legacy distro /Users/jamie/.astrid/home/claude-code/.config/distro.lock: decode Distro.lock: TOML parse error at line 13, column 1
   |
13 | resolved_ref = "v0.2.0"
   | ^^^^^^^^^^^^
unknown field `resolved_ref`, expected one of `name`, `version`, `source`, `hash`, `resolved-ref`

The mismatch is internal to one repo — the writer and the reader disagree about their own file format.

Mechanism

Writercrates/astrid-cli/src/commands/distro/lock.rs. DistroLock (line 20) and DistroLockMeta (line 39) both carry #[serde(rename_all = "kebab-case")], but LockedCapsule (line 50) does not:

rust
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct DistroLockMeta { ... pub(crate) resolved_at: String }   // emits resolved-at

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct LockedCapsule {                                          // <- no rename_all
    ...
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) resolved_ref: Option<String>,                               // emits resolved_ref
}

Readercrates/astrid-kernel/src/principal_distro_migration.rs:70, kebab-case and strict:

rust
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
struct ... {
    ...
    resolved_ref: Option<String>,   // expects resolved-ref
}

The result is visible in the emitted file itself, which mixes both conventions:

toml
schema-version = 1

[distro]
id = "claude"
version = "0.2.0"
resolved-at = "2026-07-10T16:59:12.911595+00:00"   # kebab — DistroLockMeta

[[capsule]]
name = "astrid-capsule-cli"
version = "0.2.0"
source = "@unicity-astrid/capsule-cli"
hash = "blake3:f3048613dcf849d1aa0479848712a153353413703b1ae19a4950a41cce45a95e"
resolved_ref = "v0.2.0"                            # snake — LockedCapsule

deny_unknown_fields turns what would otherwise be a silently dropped optional field into a hard boot failure. Both of my principals (default and claude-code) had locks in this shape.

Reproduction Steps

  1. Generate a distro.lock with the CLI where at least one capsule has a resolved ref (any distro install or regenerate_distro_lock, crates/astrid-cli/src/commands/capsule/install_update.rs:204).
  2. Confirm the emitted file contains resolved_ref = "..." under [[capsule]].
  3. Run astrid start on 2026.9.2 so the kernel's legacy distro migration reads it.
  4. Boot aborts with decode Distro.lock: ... unknown field 'resolved_ref', expected ... 'resolved-ref'.

Rewriting the key to resolved-ref by hand lets boot proceed.

Expected Behavior

  • Add #[serde(rename_all = "kebab-case")] to LockedCapsule so the writer is self-consistent with the rest of the lockfile.
  • Accept the already-written form on read: #[serde(alias = "resolved_ref")] on the reader's field, so existing lockfiles in the field are not bricked by the fix.
  • Add a round-trip test that serialises with the CLI type and deserialises with the kernel type. The two structs describe the same file and nothing currently forces them to agree.

Environment

  • OS: macOS 15.6 (Darwin 24.6.0), arm64
  • Astrid: 2026.9.2, installed via astrid update
  • Source: astrid-runtime/astrid @ 73661c9b (Cargo version 0.10.4)
  • Affected files: home/default/.config/distro.lock, home/claude-code/.config/distro.lock (distro claude 0.2.0, resolved 2026-07-10)

Logs / Backtrace

Error: Failed to boot Kernel: legacy distro /Users/jamie/.astrid/home/claude-code/.config/distro.lock: decode Distro.lock: TOML parse error at line 13, column 1
   |
13 | resolved_ref = "v0.2.0"
   | ^^^^^^^^^^^^
unknown field `resolved_ref`, expected one of `name`, `version`, `source`, `hash`, `resolved-ref`