#1948·astrid

Storage-backed capsule upgrade leaves a stale global authority receipt that the legacy barrier rejects

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

Summary

A storage-backed capsule install deliberately skips writing the global etc/capsule-authority/ receipt, publishing the receipt inside the durable package instead. But the legacy migration barrier still reads that global receipt as authoritative. After any storage-backed upgrade, the on-disk receipt names the previous version, and the barrier rejects the capsule:

Error: Failed to boot Kernel: legacy capsule migration failed: verify legacy capsule authority astrid-capsule-agents

8 of my 19 capsules were in this state.

Mechanism

crates/astrid-capsule-install/src/local.rs:733 makes the global receipt conditional:

rust
// Workspace/native installs still need a host receipt while their files
// are authoritative. A storage-backed install publishes the exact receipt
// inside the durable package atomically; its target directory is only a
// disposable cache, so emitting a legacy global receipt would make the
// migration barrier reject an otherwise valid post-cutover restart.
let authority_transaction = options
    .storage
    .is_none()
    .then(|| AuthorityReceiptTransaction::stage(home, &target_dir, &installed_authority))
    .transpose()?;

The reasoning is explicit and internally coherent. The gap is that the capsule's meta.json and Capsule.toml under the native directory are updated (local.rs:847 writes meta unconditionally), so the native tree advances to the new version while the global receipt stays frozen at the old one.

When that install later has to pass through the legacy barrier — crates/astrid-capsule-install/src/storage/migration.rs:145 — it calls verify_installed_authority, which reads the stale global receipt and bails at crates/astrid-capsule-install/src/authority.rs:533:

rust
if authority.capsule_id != manifest.package.name
    || authority.version != manifest.package.version
{
    bail!(
        "installed capsule identity/version differs from its authority receipt (approved {} {}, found {} {})",
        authority.capsule_id, authority.version,
        manifest.package.name, manifest.package.version
    );
}

So the comment's "otherwise valid post-cutover restart" holds only if the barrier never runs again. On an install that still has native capsule directories awaiting migration, it does.

The state I found (manifest version vs. receipt version):

capsule Capsule.toml etc/capsule-authority/ receipt
astrid-capsule-agents 0.2.0 0.1.0
astrid-capsule-context-engine 0.2.0 0.1.0
astrid-capsule-fs 0.2.0 0.1.0
astrid-capsule-memory 0.2.0 0.1.0
astrid-capsule-prompt-builder 0.2.0 0.1.0
astrid-capsule-react 0.2.2 0.2.1
astrid-capsule-router 0.2.0 0.1.0
astrid-capsule-system 0.2.0 0.1.0

The astrid-capsule-agents receipt is dated 2026-07-24; its meta.json records updated_at: 2026-09-05T21:02:17Z. Six weeks of drift.

Reproduction Steps

  1. Install capsules so native directories exist under home/<principal>/.local/capsules/ with receipts in etc/capsule-authority/.
  2. Upgrade one of them through the storage-backed path (options.storage.is_some()), so meta.json and Capsule.toml advance but the global receipt does not.
  3. Run astrid start on a build where the legacy migration barrier still processes native capsules.
  4. Boot aborts on the first capsule whose receipt version trails its manifest.

Deleting the stale receipts lets verify_installed_authority take its None branch (authority.rs:513), which re-snapshots them as LegacyMigration and proceeds — but that silently re-approves whatever is on disk, which is not a remedy anyone should have to reach for.

Expected Behavior

  • Keep the global receipt in step with the native directory whenever that directory is updated, or delete it alongside the content it describes so it cannot go stale.
  • Have the barrier prefer the durable package receipt when one exists, and treat the global receipt as a fallback only for capsules with no durable entry.
  • Add a test covering "storage-backed upgrade, then legacy barrier runs" — the two code paths are individually correct and only fail in combination.

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)
  • 19 native capsules under home/default/.local/capsules/; 29 receipts under etc/capsule-authority/; 8 stale

Logs / Backtrace

Error: Failed to boot Kernel: legacy capsule migration failed: verify legacy capsule authority astrid-capsule-agents

The underlying cause, recovered by hand (see the companion issue on flattened error chains):

etc/capsule-authority/832eed9eddef9775b3116be39d8ce184fa9fe264abb2d26df452121c7ee5f0c5.json
  { "schema_version": 1, "source": "legacy-migration",
    "capsule_id": "astrid-capsule-agents", "version": "0.1.0", ... }

home/default/.local/capsules/astrid-capsule-agents/Capsule.toml
  [package] name = "astrid-capsule-agents"  version = "0.2.0"

home/default/.local/capsules/astrid-capsule-agents/meta.json
  { "version": "0.2.0", "updated_at": "2026-09-05T21:02:17.365614+00:00", ... }