Skip to content
Merged
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
13 changes: 7 additions & 6 deletions crates/nexum-runtime/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use tracing::{error, info, warn};
use wasmtime::Engine;

use crate::addons::{AddOnHandle, AddOns, AddOnsContext, RuntimeAddOn};
use crate::engine_config::EngineConfig;
use crate::engine_config::{EngineConfig, ModuleEntry};
use crate::host::component::{
BuilderContext, ComponentBuilder, Components, ComponentsBuilder, RuntimeTypes,
};
Expand Down Expand Up @@ -183,15 +183,16 @@ impl<T: RuntimeTypes> AssembledRuntime<'_, T> {
"ignoring engine.toml [[modules]] because a module source override was given"
);
}
let entry = ModuleEntry {
path: wasm.to_path_buf(),
manifest: manifest.map(Path::to_path_buf),
};
Supervisor::boot_single(
&engine,
&linker,
wasm,
manifest,
&entry,
&components,
&engine_cfg.limits,
&supervisor::ConfiguredChains::from_config(engine_cfg),
engine_cfg.engine.require_component_digest,
&supervisor::BootEnv::from_config(engine_cfg),
&extensions,
clocks,
)
Expand Down
63 changes: 34 additions & 29 deletions crates/nexum-runtime/src/supervisor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ pub use prepass::ConfiguredChains;
pub use store::{WasiClockOverride, build_linker, build_provider_linker};
pub use subscriptions::ChainLogSub;

use std::path::Path;
use std::sync::Arc;

use anyhow::{Context, Result};
Expand Down Expand Up @@ -46,6 +45,23 @@ pub struct Supervisor<T: RuntimeTypes> {
chain_log_cursors: ChainLogCursors,
}

/// Boot inputs derived from [`EngineConfig`], bundled once at the call site.
pub struct BootEnv<'a> {
pub limits: &'a ModuleLimits,
pub configured_chains: ConfiguredChains,
pub require_component_digest: bool,
}

impl<'a> BootEnv<'a> {
pub fn from_config(cfg: &'a EngineConfig) -> Self {
Self {
limits: &cfg.limits,
configured_chains: ConfiguredChains::from_config(cfg),
require_component_digest: cfg.engine.require_component_digest,
}
}
}

/// Cached at boot so restarts rebuild an identical store and linker.
pub(super) struct Shared<T: RuntimeTypes> {
pub(super) engine: Engine,
Expand All @@ -68,7 +84,7 @@ impl<T: RuntimeTypes> Supervisor<T> {
extensions: &[Arc<dyn Extension<T>>],
clocks: Option<WasiClockOverride>,
) -> Result<Self> {
let shared = wire_extensions(engine, components, extensions, clocks)?;
let shared = wire_extensions(engine, components, extensions, clocks, true)?;
let registry = capability_registry(&shared.extensions);
let prepass = prepass::run(engine_cfg, &registry)?;
// Providers boot first, so every module store built after already
Expand All @@ -92,57 +108,40 @@ impl<T: RuntimeTypes> Supervisor<T> {
}

/// Single-component boot for `just run` without an `engine.toml`.
#[allow(clippy::too_many_arguments)]
pub async fn boot_single(
engine: &Engine,
linker: &Linker<HostState<T>>,
wasm: &Path,
manifest: Option<&Path>,
entry: &ModuleEntry,
components: &Components<T>,
limits: &ModuleLimits,
configured_chains: &ConfiguredChains,
require_component_digest: bool,
env: &BootEnv<'_>,
extensions: &[Arc<dyn Extension<T>>],
clocks: Option<WasiClockOverride>,
) -> Result<Self> {
enforce_extension_uniqueness(extensions)?;
let services = HostServices::from_extensions(extensions)?;
// Providers come only from `engine.toml`, so no kinds register here.
let shared = Shared {
engine: engine.clone(),
components: components.clone(),
extensions: extensions.to_vec(),
services,
kinds: ProviderKinds::new(),
clocks,
};
// Provider kinds come only from `engine.toml`, so none register here.
let shared = wire_extensions(engine, components, extensions, clocks, false)?;
let registry = capability_registry(&shared.extensions);
let entry = ModuleEntry {
path: wasm.to_path_buf(),
manifest: manifest.map(Path::to_path_buf),
};
let loaded_manifest =
load_required_manifest(&entry.path, entry.manifest.as_deref(), &registry, "module")?;
enforce_configured_chains(
&manifest_namespace(&loaded_manifest, MODULE_FALLBACK_NAME),
&loaded_manifest,
configured_chains,
&env.configured_chains,
)?;
let loaded = load::module(
&shared,
linker,
&entry,
entry,
loaded_manifest,
limits,
require_component_digest,
env.limits,
env.require_component_digest,
&[],
)
.await?;
Ok(Self {
shared,
modules: vec![loaded],
providers: Vec::new(),
policy: limits.poison(),
policy: env.limits.poison(),
chain_log_cursors: ChainLogCursors::default(),
})
}
Expand Down Expand Up @@ -186,15 +185,21 @@ impl<T: RuntimeTypes> Supervisor<T> {
}

/// The resulting [`Shared`] is the one wiring every later phase reads.
/// `with_provider_kinds: false` skips [`provider_kinds`], which refuses a serviceless kind.
fn wire_extensions<T: RuntimeTypes>(
engine: &Engine,
components: &Components<T>,
extensions: &[Arc<dyn Extension<T>>],
clocks: Option<WasiClockOverride>,
with_provider_kinds: bool,
) -> Result<Shared<T>> {
enforce_extension_uniqueness(extensions)?;
let services = HostServices::from_extensions(extensions)?;
let kinds = provider_kinds(extensions, &services)?;
let kinds = if with_provider_kinds {
provider_kinds(extensions, &services)?
} else {
ProviderKinds::new()
};
Ok(Shared {
engine: engine.clone(),
components: components.clone(),
Expand Down
46 changes: 46 additions & 0 deletions crates/nexum-runtime/src/supervisor/tests/boot_refusals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,52 @@ async fn boot_admits_a_registered_provider_kind_past_the_kind_gate() {
.lacks("requires a module.toml");
}

/// The multi-entry path wires provider kinds, so a serviceless kind refuses
/// the boot before any entry loads.
#[tokio::test]
async fn boot_refuses_a_provider_kind_without_a_host_service() {
BootScenario::over(mock_components())
.extensions(serviceless_acme_extensions())
.expect_refusal()
.await
.names("extension acme registers provider kind acme-adapter without a host service");
}

/// Provider kinds come only from `engine.toml`, so single boot skips the
/// service gate and the first refusal is the missing manifest.
#[tokio::test]
async fn boot_single_skips_the_provider_kind_service_gate() {
let extensions = serviceless_acme_extensions();
let engine = test_wasmtime_engine();
let linker = crate::supervisor::build_linker(&engine, &extensions).expect("build_linker");
let dir = tempfile::tempdir().expect("tempdir");
let entry = ModuleEntry {
path: dir.path().join("missing.wasm"),
manifest: None,
};
let limits = ModuleLimits::default();
let env = BootEnv {
limits: &limits,
configured_chains: test_chains(),
require_component_digest: false,
};
let err = Supervisor::boot_single(
&engine,
&linker,
&entry,
&mock_components(),
&env,
&extensions,
None,
)
.await
.err()
.expect("a missing manifest must refuse the boot");
Refusal::from(err)
.names("no module.toml")
.lacks("without a host service");
}

/// `[capabilities]` is declared so the failing gate is the subscription kind.
#[tokio::test]
async fn boot_refuses_an_undeclared_extension_subscription_kind() {
Expand Down
21 changes: 21 additions & 0 deletions crates/nexum-runtime/src/supervisor/tests/chain_gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,27 @@ async fn boot_refuses_a_subscription_on_an_unconfigured_chain() {
}
}

/// The single-boot path reads the same configured-chains gate.
#[tokio::test]
async fn boot_single_refuses_a_subscription_on_an_unconfigured_chain() {
let dir = tempfile::tempdir().expect("tempdir");
let manifest = TestManifest::new("gated")
.cap("logging")
.block_sub(424_242)
.write_to(dir.path());
let wasm = dir.path().join("missing.wasm");

let (_store, result) = try_boot_single(&wasm, Some(&manifest), false, None).await;
Refusal::from(
result
.err()
.expect("an unconfigured chain must refuse the boot"),
)
.names("module gated subscribes to chain 424242")
.names("configured chains: 1, 100, 11155111")
.lacks("compile");
}

#[tokio::test]
async fn boot_admits_a_block_subscription_on_a_configured_chain_past_the_chain_gate() {
BootScenario::new()
Expand Down
48 changes: 43 additions & 5 deletions crates/nexum-runtime/src/supervisor/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,22 @@ async fn try_boot_single(
let engine = test_wasmtime_engine();
let linker = make_linker(&engine);
let (dir, store) = temp_local_store();
let entry = ModuleEntry {
path: wasm.to_path_buf(),
manifest: manifest.map(Path::to_path_buf),
};
let limits = ModuleLimits::default();
let env = BootEnv {
limits: &limits,
configured_chains: test_chains(),
require_component_digest: require_digest,
};
let result = Supervisor::boot_single(
&engine,
&linker,
wasm,
manifest,
&entry,
&test_components(store),
&ModuleLimits::default(),
&test_chains(),
require_digest,
&env,
&core_extensions(),
clocks,
)
Expand Down Expand Up @@ -157,3 +164,34 @@ impl Extension<crate::test_utils::MockTypes> for AcmeExtension {
fn acme_extensions() -> Vec<Arc<dyn Extension<crate::test_utils::MockTypes>>> {
vec![Arc::new(AcmeExtension)]
}

/// [`AcmeExtension`] minus its service, for the serviceless kind gate tests.
struct ServicelessAcmeExtension;

impl Extension<crate::test_utils::MockTypes> for ServicelessAcmeExtension {
fn namespace(&self) -> &'static str {
"acme"
}

fn capabilities(&self) -> manifest::NamespaceCaps {
manifest::NamespaceCaps {
prefix: "test:acme/",
ifaces: &[],
}
}

fn link(
&self,
_linker: &mut Linker<HostState<crate::test_utils::MockTypes>>,
) -> anyhow::Result<()> {
Ok(())
}

fn provider(&self) -> Option<Box<dyn ProviderKind<crate::test_utils::MockTypes>>> {
Some(Box::new(AcmeKind))
}
}

fn serviceless_acme_extensions() -> Vec<Arc<dyn Extension<crate::test_utils::MockTypes>>> {
vec![Arc::new(ServicelessAcmeExtension)]
}
Loading