From c42560ff6231b3ff2c93a9c168549bbebdef3580 Mon Sep 17 00:00:00 2001 From: Rita Agafonova <36133358+rita-aga@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:12:42 -0400 Subject: [PATCH] fix(os-apps): fail install when a required WASM module artifact is missing (ARN-61/ARN-273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit install_os_app_with_plan previously logged an error when a declared platform-required/app-required WASM module had no artifact in the bundle, but still returned Ok(InstallResult) — the app was bootstrapped and marked installed, then the missing module lazy-flapped 503s at request time ("Configured required WASM module artifact is missing from the app bundle"). This is what took paw-fs/blob_adapter down on prod (Files/$value 503s) after a Genesis version was published without the blob_adapter binary. Refuse activation instead: collect required modules missing an artifact and, after the wasm phase, return Err before the App entity is bootstrapped and before install metadata is recorded. The error surfaces through install-from-genesis (and is swallowed-but-logged at startup bootstrap, so a good durable installed record is preserved across restart rather than clobbered by a reinstall of an incomplete pinned ref). Optional modules still only warn. Tests: app-required artifact missing -> activation errors; optional artifact missing -> install proceeds. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NEWiRhC3UxsAzCMYxqV5FZ --- crates/temper-platform/src/os_apps/mod.rs | 26 ++++++ .../temper-platform/src/os_apps/mod_test.rs | 90 +++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/crates/temper-platform/src/os_apps/mod.rs b/crates/temper-platform/src/os_apps/mod.rs index b1e0b78ad..5c319ff32 100644 --- a/crates/temper-platform/src/os_apps/mod.rs +++ b/crates/temper-platform/src/os_apps/mod.rs @@ -1394,6 +1394,12 @@ pub(super) async fn install_os_app_with_plan( let mut wasm_registered = Vec::new(); let mut wasm_skipped = Vec::new(); let mut wasm_failures = Vec::new(); + // ARN-61/ARN-273: modules declared platform-required/app-required whose + // compiled artifact is absent from the bundle. A non-empty set fails the + // install loudly (see the guard after this block) instead of activating an + // app that will lazy-flap 503s at request time (e.g. paw-fs/blob_adapter + // missing from a published Genesis version → Files/$value 503s). + let mut missing_required_modules = Vec::new(); if plan.wasm { let existing_sources = match state.server.load_wasm_module_sources(tenant).await { Ok(map) => map, @@ -1514,6 +1520,7 @@ pub(super) async fn install_os_app_with_plan( } WasmModuleCriticality::PlatformRequired | WasmModuleCriticality::AppRequired => { wasm_failures.push(module_name.clone()); + missing_required_modules.push(module_name.clone()); tracing::error!( tenant, module = %module_name, @@ -1525,6 +1532,25 @@ pub(super) async fn install_os_app_with_plan( } } + // ARN-61/ARN-273: refuse to activate an app whose required WASM modules have + // no artifact in the bundle. Previously the install logged an error but + // returned Ok(InstallResult), so the app was bootstrapped and marked + // installed while the missing module 503-flapped at request time. Fail here — + // before Step 5 bootstraps the App entity and before install metadata is + // recorded — so a broken required module is a loud, safe activation failure + // (surfaced through install-from-genesis) rather than a silent prod outage. + // Optional modules are intentionally excluded (they only warn above). + if plan.wasm && !missing_required_modules.is_empty() { + missing_required_modules.sort(); + return Err(format!( + "Refusing to activate os-app '{app_name}' for tenant '{tenant}': \ + required WASM module artifact(s) missing from the app bundle: {}. \ + The published bundle must include a compiled binary for every module \ + declared platform-required/app-required in app.toml.", + missing_required_modules.join(", ") + )); + } + tracing::info!( "Installed os-app '{app_name}' for tenant '{tenant}': added={added:?} updated={updated:?} skipped={skipped:?} wasm={wasm_registered:?}" ); diff --git a/crates/temper-platform/src/os_apps/mod_test.rs b/crates/temper-platform/src/os_apps/mod_test.rs index 42f887e25..5d2079e0c 100644 --- a/crates/temper-platform/src/os_apps/mod_test.rs +++ b/crates/temper-platform/src/os_apps/mod_test.rs @@ -1022,6 +1022,96 @@ fn test_find_wasm_modules_discovers_packaged_root_wasm() { ); } +/// Register a throwaway single-app catalog dir declaring one wasm module with +/// the given criticality and NO compiled artifact on disk. Returns the unique +/// app name so `get_os_app` / `install_os_app_with_plan` can resolve it. +fn write_wasm_only_app_missing_artifact(criticality: &str) -> (String, std::path::PathBuf) { + let app_name = format!("arn61-missing-{}", uuid::Uuid::new_v4()); + let apps_dir = + std::env::temp_dir().join(format!("temper-arn61-catalog-{}", uuid::Uuid::new_v4())); + let app_dir = apps_dir.join(&app_name); + fs::create_dir_all(&app_dir).expect("create app dir"); + fs::write( + app_dir.join("app.toml"), + format!( + "name = \"{app_name}\"\n\n[[wasm_modules]]\nname = \"needs_binary\"\ncriticality = \"{criticality}\"\nstartup_loading = \"lazy\"\n" + ), + ) + .expect("write app.toml"); + // The catalog scan requires an APP.md guide alongside app.toml. + fs::write( + app_dir.join("APP.md"), + format!("# {app_name}\n\nTest app.\n"), + ) + .expect("write APP.md"); + add_os_apps_dir(apps_dir.clone()); + (app_name, apps_dir) +} + +/// ARN-61/ARN-273: an app declaring an `app-required` WASM module whose binary +/// is absent from the bundle must FAIL activation loudly instead of installing +/// and lazy-flapping 503s at request time. +#[tokio::test] +async fn test_install_fails_when_app_required_wasm_missing() { + let state = PlatformState::new(None); + let (app_name, apps_dir) = write_wasm_only_app_missing_artifact("app-required"); + + // Sanity: the module is declared but has no artifact in the resolved bundle. + let bundle = get_os_app(&app_name).expect("app should be in catalog"); + assert!(bundle.wasm_module_configs.contains_key("needs_binary")); + assert!(!bundle.wasm_modules.contains_key("needs_binary")); + + let result = install_os_app_with_plan( + &state, + "test-arn61-required", + &app_name, + OsAppInstallPlan { + specs: false, + policies: false, + wasm: true, + content: false, + seed: false, + }, + ) + .await; + + fs::remove_dir_all(&apps_dir).ok(); + + let err = result.expect_err("missing app-required artifact must fail activation"); + assert!( + err.contains("needs_binary") && err.contains("missing from the app bundle"), + "error should name the missing required module: {err}" + ); +} + +/// A missing *optional* WASM artifact must NOT block activation — the install +/// proceeds and simply skips the module. +#[tokio::test] +async fn test_install_proceeds_when_optional_wasm_missing() { + let state = PlatformState::new(None); + let (app_name, apps_dir) = write_wasm_only_app_missing_artifact("optional"); + + let result = install_os_app_with_plan( + &state, + "test-arn61-optional", + &app_name, + OsAppInstallPlan { + specs: false, + policies: false, + wasm: true, + content: false, + seed: false, + }, + ) + .await; + + fs::remove_dir_all(&apps_dir).ok(); + + let result = result.expect("missing optional artifact must not block activation"); + assert!(result.wasm_modules.is_empty()); + assert!(result.wasm_skipped.contains(&"needs_binary".to_string())); +} + #[tokio::test] async fn test_install_os_app_registers_entities() { let state = PlatformState::new(None);