astrid-runtime/astrid · error
lifecycle manifest declares environment state but no durable
Error message
lifecycle manifest declares environment state but no durable principal UID binding was supplied
What it means
run_lifecycle_in_scope executes a capsule lifecycle phase (install/upgrade/etc.) with a scoped runtime. If the manifest declares env state (manifest.env non-empty), a durable principal UID binding (RuntimePrincipalStore) is mandatory, because environment state must be persisted under a durable principal's storage — a plain in-memory KV would silently lose it. The function bails when env state is declared but principal_storage is None.
Source
Thrown at crates/astrid-capsule-install/src/lifecycle.rs:186
external_bus,
)
}
#[allow(clippy::too_many_arguments)]
fn run_lifecycle_in_scope(
target_dir: &Path,
wasm_bytes: Vec<u8>,
manifest: &CapsuleManifest,
_home: Option<&AstridHome>,
target_principal: &PrincipalId,
principal_uid: Option<astrid_core::identity::PrincipalUid>,
principal_storage: Option<RuntimePrincipalStore>,
phase: LifecyclePhase,
previous_version: Option<&str>,
external_bus: Option<EventBus>,
) -> anyhow::Result<()> {
if principal_storage.is_none() && !manifest.env.is_empty() {
anyhow::bail!(
"lifecycle manifest declares environment state but no durable principal UID binding was supplied"
);
}
let kv_store: Arc<dyn astrid_storage::KvStore> = principal_storage.as_ref().map_or_else(
|| Arc::new(astrid_storage::MemoryKvStore::new()) as Arc<dyn astrid_storage::KvStore>,
|store| store.kv(),
);
let capsule_id = manifest.package.name.clone();
let kv = astrid_storage::ScopedKvStore::new(
Arc::clone(&kv_store),
lifecycle_kv_namespace(target_principal, &capsule_id),
)
.context("failed to create scoped KV store")?;
let event_bus = external_bus.unwrap_or_else(|| EventBus::with_capacity(128));
// Reuse the current tokio runtime when there is one (CLI's
// `#[tokio::main]`, kernel handler thread). Only build a new one
// for standalone/test contexts.View on GitHub (pinned to affd8760f4)
Solutions
- Open/attach a RuntimePrincipalStore (durable principal UID binding) and pass it so the lifecycle can persist env state.
- If the capsule genuinely needs no persistent env state, remove the `env` section from the lifecycle manifest.
- Use run_lifecycle_for_principal (or _with_storage) instead of the storage-less run_lifecycle path for capsules with env declarations.
Example fix
// before run_lifecycle(&capsule, &manifest, LifecyclePhase::Install, None)?; // env declared, no store // after let store = RuntimePrincipalStore::open(&home, &principal_uid)?; run_lifecycle_for_principal(&capsule, &manifest, LifecyclePhase::Install, Some(store), None)?;
Defensive patterns
Strategy: validation
Validate before calling
// caller-side pre-check:
if !manifest.env.is_empty() && principal_storage.is_none() {
return Err(anyhow::anyhow!(
"manifest declares env state; open a RuntimePrincipalStore first"));
} Try / catch
match run_lifecycle(...) {
Err(e) if e.to_string().contains("no durable principal UID binding") => {
eprintln!("capsule declares env state; use run_lifecycle_for_principal with an open store");
}
other => other?,
} Prevention
- Whenever a manifest has an `env` section, always use the *_for_principal / *_with_storage entry points.
- Keep manifest env declarations and runner storage wiring in sync during capsule upgrades.
- In test harnesses, wire a real RuntimePrincipalStore rather than None when testing stateful capsules.
When it happens
Trigger: Calling run_lifecycle / run_lifecycle_for_principal paths that construct run_lifecycle_in_scope with principal_storage = None while the capsule's lifecycle manifest has a non-empty `env` section — e.g. an install flow that forgot to open/attach the principal store, or running a manifest that newly added env declarations without migrating the runner.
Common situations: Adding `env` to a capsule's lifecycle manifest while the host integration still invokes the lifecycle without a principal store; a CLI/daemon code path that lazily skips opening RuntimePrincipalStore for capsules believed to be stateless; test harnesses running lifecycle scripts without storage wiring.
Understand the failure class
Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.
Related errors
- no corpus selected; omit --no-synthetic or pass a corpus or
- duplicate corpus label {:?}
- --git-history requires non-empty NAME=REPO::RELATIVE_PATH
- delete of '{derived}' left unreclaimed state: {details}
- gateway is not wired to a live event bus; agent approval una
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/49fc0e020788ac8e.
Report an issue: GitHub.