distro.lock writer emits resolved_ref but kernel reader demands resolved-ref with deny_unknown_fields
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
Writer — crates/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:
#[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
}Reader — crates/astrid-kernel/src/principal_distro_migration.rs:70, kebab-case and strict:
#[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:
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 — LockedCapsuledeny_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
- Generate a
distro.lockwith the CLI where at least one capsule has a resolved ref (any distro install orregenerate_distro_lock,crates/astrid-cli/src/commands/capsule/install_update.rs:204). - Confirm the emitted file contains
resolved_ref = "..."under[[capsule]]. - Run
astrid starton 2026.9.2 so the kernel's legacy distro migration reads it. - 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")]toLockedCapsuleso 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(distroclaude0.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`Source: astrid-runtime/astrid