Migration barrier asserts over on-disk env/secret scopes it never iterates, blocking boot
Summary
The legacy env/secret migration barrier imports a principal's scopes by iterating that principal's capsules, then asserts that no native env/secret sources remain for that principal. When a principal owns env/secret scopes but no capsules — or holds scopes for a capsule that has since been uninstalled — nothing is imported, the sources remain, and the assertion fails. Boot aborts with:
Error: Failed to boot Kernel: legacy env/secret sources remain for claude-code (uid 43f45f18…); migration API did not retire every scopeThere is no way for an operator to satisfy this: the loop that would retire the scope is keyed on a capsule list that does not contain it.
Mechanism
In import_env_and_secrets (crates/astrid-kernel/src/legacy_migration_barrier/env_import.rs:18) the import is driven by the capsules owned by that principal:
let owner = astrid_storage::StateOwner::Principal(*uid);
let summaries = store.capsules().list(&owner).map_err(storage_io)?;
...
for summary in summaries {
let capsule = summary.id();
let env = env_root.join(format!("{capsule}.env.json"));
let secret = secret_root.join(capsule);
...
}The post-condition (env_import.rs:80-92) is over the filesystem, not over that capsule list:
if let Some(status) = statuses.into_iter().find(|status| {
status.native_env_present
|| status.native_secret_present
|| !status.unreceipted_capsules.is_empty()
}) {
return Err(io::Error::other(format!(
"legacy env/secret sources remain for {} (uid {}); migration API did not retire every scope",
status.alias, status.uid
)));
}Two distinct populations fall through the gap:
Cross-principal ownership. Native capsules live under
home/default/.local/capsules/and migrate toStateOwner::Principal(default_uid). Myclaude-codeprincipal owned env scopes forastrid-capsule-agents,astrid-capsule-memory,astrid-capsule-openai-compatandclaude-runner, plus a secret scope forastrid-capsule-openai-compat— but zero capsules under its own owner. The loop iterated an empty list, imported nothing, and the assertion then failed on the four files it had just declined to look at.Uninstalled capsules.
astrid-capsule-srouterwas removed long ago but leftsecrets/<principal>/astrid-capsule-srouter/and<principal>.config/env/astrid-capsule-srouter.env.jsonbehind, under both principals. No capsule summary exists, so the scope is never imported and never retired — permanent boot failure.
The error string is itself an acknowledgement that the invariant is not established by the code above it.
Reproduction Steps
- Install capsules so they live under
home/default/.local/capsules/(they migrate to thedefaultowner). - Create env/secret scopes for a different principal, e.g.
home/claude-code/.config/env/astrid-capsule-memory.env.jsonandsecrets/claude-code/astrid-capsule-openai-compat/. - Separately, uninstall a capsule but leave its env/secret scope on disk (
astrid-capsule-srouterin my case). - Run
astrid starton 2026.9.2. - Boot aborts with
legacy env/secret sources remain for claude-code (uid …); migration API did not retire every scope.
Removing the orphaned astrid-capsule-srouter scopes by hand clears that instance but exposes the next one, and the cross-principal scopes cannot be cleared at all without discarding the secrets.
Expected Behavior
The set the barrier retires and the set it asserts over must be the same set. Concretely:
- Drive the import from what is on disk for that principal (enumerate
env_dir()andsecrets_dir()/<alias>), not from the capsule list, so every present scope is considered. - Import a scope whose capsule is owned by another principal, or resolve the owner from the capsule registry rather than assuming
Principal(uid)of the same alias. - Handle scopes for uninstalled capsules explicitly — import, archive, or retire them — rather than leaving them to fail the post-condition.
- If a scope genuinely cannot be assigned, fail with the offending paths named and an operator-actionable remedy, not a generic "did not retire every scope".
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) ~/.astridfirst created 2026-05-24; last healthy boot on 0.10.4- Principals:
default(19 native capsules),claude-code(0 capsules, 4 env + 1 secret scope)
Logs / Backtrace
i Starting Astrid daemon (persistent mode)...
2026-09-15T06:35:48.301579Z INFO astrid_config::loader: loaded user config path=/Users/jamie/.astrid/config.toml
✗ error: Daemon exited prematurely (exit status: 1). Check logs: /Users/jamie/.astrid/log
# ~/.astrid/log/daemon-boot.log
Error: Failed to boot Kernel: legacy env/secret sources remain for claude-code (uid 43f45f180cd89e5a41fa3abb2ab4b555f9ac9cf60f748012351fe84581b01656); migration API did not retire every scopeOn-disk state at the time of failure:
home/default/.local/capsules/ -> 19 capsules (migrate to owner `default`)
home/claude-code/.local/capsules/ -> absent
home/claude-code/.config/env/ -> astrid-capsule-agents.env.json
astrid-capsule-memory.env.json
astrid-capsule-openai-compat.env.json
astrid-capsule-srouter.env.json
claude-runner.env.json
secrets/claude-code/ -> astrid-capsule-openai-compat astrid-capsule-srouter
secrets/default/ -> astrid-capsule-srouterSource: astrid-runtime/astrid