Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions crates/temper-platform/src/os_apps/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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:?}"
);
Expand Down
90 changes: 90 additions & 0 deletions crates/temper-platform/src/os_apps/mod_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading