From 55c20e0d6e8904d6fcbec1049156838c6b6d60dd Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 9 Jul 2026 14:46:50 -0400 Subject: [PATCH] feat(install): route any manifest-known app through the orchestrator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit package.install routed to the manifest-driven orchestrator only for a hardcoded per-app allowlist; every other app fell through to the legacy flow, which ignores manifests entirely and creates a bare container with no ports or volumes (strfry crash-looped this way on .228, 2026-07-09). New ContainerOrchestrator::knows_app(app_id) (default false; prod checks its loaded-manifest map, disk + signed-catalog overlay). The install gate is now allowlist OR knows_app — no per-app Rust for manifest-driven apps, matching the packaging invariant. Apps without a manifest keep the legacy flow including the container-exists adopt block; the unknown-app_id → legacy fallback inside the orchestrator branch is unchanged. Tests: knows_app_reflects_loaded_manifests + install suite 37/37. Co-Authored-By: Claude Fable 5 --- .../src/api/rpc/package/install.rs | 16 ++++++++++-- .../src/container/prod_orchestrator.rs | 25 +++++++++++++++++++ core/archipelago/src/container/traits.rs | 10 ++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index 90c21901..b1484e20 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -330,8 +330,20 @@ impl RpcHandler { // mode). // The adoption block is being phased out as apps move to the // orchestrator path. Non-orchestrator apps still hit it. - let orchestrator_managed = - should_try_orchestrator_install(package_id, self.orchestrator.is_some()); + let orchestrator_managed = match self.orchestrator.as_ref() { + // Migration allowlist OR any app whose manifest the orchestrator + // knows (disk or signed-catalog overlay). A manifest-driven app + // must never fall through to the legacy flow — it ignores the + // manifest entirely and creates a bare container with no ports or + // volumes (strfry, 2026-07-09). + Some(orch) => { + should_try_orchestrator_install(package_id, true) + || orch + .knows_app(orchestrator_install_app_id(package_id)) + .await + } + None => false, + }; // Check if container already exists (legacy adoption — non-orchestrator // apps only). diff --git a/core/archipelago/src/container/prod_orchestrator.rs b/core/archipelago/src/container/prod_orchestrator.rs index cc49b047..255d3b81 100644 --- a/core/archipelago/src/container/prod_orchestrator.rs +++ b/core/archipelago/src/container/prod_orchestrator.rs @@ -3610,6 +3610,10 @@ enum HookOutcome { #[async_trait] impl ContainerOrchestrator for ProdContainerOrchestrator { + async fn knows_app(&self, app_id: &str) -> bool { + self.state.read().await.manifests.contains_key(app_id) + } + async fn install(&self, app_id: &str) -> Result { { let mut state = self.state.write().await; @@ -4679,6 +4683,27 @@ app: assert!(!calls.iter().any(|c| c.starts_with("build_image:"))); } + #[tokio::test] + async fn knows_app_reflects_loaded_manifests() { + // The RPC install gate routes any app with a known manifest through + // the orchestrator (no per-app allowlist). knows_app must be true for + // a loaded manifest and false otherwise — a false positive would send + // a manifest-less app into `loaded()`'s unknown-app_id error path, + // skipping the legacy adopt block; a false negative sends a + // manifest-driven app to the legacy flow, which creates a bare + // container (strfry, 2026-07-09). + let rt = Arc::new(MockRuntime::default()); + let orch = orch_with(rt).await; + assert!(!orch.knows_app("strfry").await); + orch.insert_manifest_for_test( + pull_manifest("strfry", "docker.io/dockurr/strfry:1.0.4"), + PathBuf::from("/tmp/fixtures/strfry"), + ) + .await; + assert!(orch.knows_app("strfry").await); + assert!(!orch.knows_app("no-such-app").await); + } + #[tokio::test] async fn mempool_core_rpc_host_follows_bitcoin_node() { // B12: mempool's CORE_RPC_HOST must resolve to whichever Bitcoin node diff --git a/core/archipelago/src/container/traits.rs b/core/archipelago/src/container/traits.rs index 7ebfc364..b69ea915 100644 --- a/core/archipelago/src/container/traits.rs +++ b/core/archipelago/src/container/traits.rs @@ -25,6 +25,16 @@ pub trait ContainerOrchestrator: Send + Sync { /// to a manifest the orchestrator already knows about. async fn install(&self, app_id: &str) -> Result; + /// True when this orchestrator holds a manifest for `app_id` (disk or + /// signed-catalog overlay) — i.e. `install(app_id)` would not fail with + /// "unknown app_id". Lets the RPC layer route any manifest-driven app + /// through the orchestrator without a per-app allowlist. Defaults to + /// `false` so orchestrators without a manifest registry keep routing + /// through the legacy install flow. + async fn knows_app(&self, _app_id: &str) -> bool { + false + } + /// Start an already-created container. async fn start(&self, app_id: &str) -> Result<()>;