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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ The example module declares no subscriptions, so `just run` needs no `engine.tom
A manifest may pin its artifact with `digest = "sha256:<64 hex chars>"` in `[component]` (one `sha256sum` of the `.wasm`).
A present pin is strictly verified against the loaded bytes before compilation; a mismatch or a malformed pin refuses the boot.
An absent pin loads with a warning that logs the computed digest; set `require_component_digest = true` under `[engine]` in `engine.toml` to make an absent pin a boot error.
The warning is silent when an `engine.toml` `[implements]` row pins the same artifact, because the bytes are verified against that pin instead.
An operator may pin the same artifact independently with `digest` on its `[[modules]]` entry in `engine.toml`; both pins are verified against the loaded bytes, and the warning is silent when the operator pin covers the artifact.
The default sibling `component.toml` lives in the same trust domain as the artifact, so an author-side pin closes accidental drift only.
Against a compromised artifact store, supply an operator-owned manifest from outside the artifact directory via the `manifest` key on `[[modules]]`, combined with `require_component_digest = true`.
Against a compromised artifact store, set `[[modules]].digest`, which lives in trusted config; an operator-owned manifest outside the artifact directory via the `manifest` key on `[[modules]]` combined with `require_component_digest = true` closes the same gap.

## Licence

Expand Down
2 changes: 1 addition & 1 deletion crates/nexum-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ derive_more.workspace = true
# `strum::IntoStaticStr`: the snake_case variant name is the tracing
# `source` field (`LogSource`) and the boot-refusal `error_kind` label.
strum.workspace = true
# Full-semver `provides` versions and their compatibility tracks.
# Full-semver interface ids and their compatibility tracks.
semver.workspace = true
# No `rt`: spawning belongs to `nexum-tasks`. Omitting it enforces nothing,
# since `nexum-tasks` unifies `rt` back in; the `clippy.toml` ban does.
Expand Down
1 change: 1 addition & 0 deletions crates/nexum-runtime/examples/embed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ async fn main() -> anyhow::Result<()> {
id: "example".to_owned(),
path: "target/wasm32-wasip2/release/example.wasm".into(),
manifest: Some("modules/example/component.toml".into()),
digest: None,
}],
..EngineConfig::default()
};
Expand Down
60 changes: 4 additions & 56 deletions crates/nexum-runtime/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,25 +247,15 @@ impl<T: RuntimeTypes> AssembledRuntime<T> {
configured component, so it gets the [policy] defaults"
);
}
if !engine_cfg.implements.is_empty() {
warn!(
"ignoring engine.toml [implements] rows: the override is not a \
configured component, so no binding can authorize it"
);
}
// The override is not any configured component, and its file
// stem is not an operator-written id, so no [policy.component]
// row may bind to it (ADR-0018); the stem is display-only.
// [implements] binds on the same id column, so it is cleared
// for the same reason: a `provides` claimant refuses here.
let policy = PolicySection {
component: Default::default(),
..engine_cfg.policy.clone()
};
let implements = crate::engine_config::ImplementsSection::default();
let env = supervisor::BootEnv {
policy: &policy,
implements: &implements,
..supervisor::BootEnv::from_config(engine_cfg)
};
let id = wasm
Expand All @@ -276,6 +266,9 @@ impl<T: RuntimeTypes> AssembledRuntime<T> {
id,
path: wasm,
manifest,
// No [[modules]] entry describes the override, so no
// operator pin can apply to it.
digest: None,
};
Supervisor::boot_single(
&engine,
Expand Down Expand Up @@ -1137,6 +1130,7 @@ mod tests {
id: stem.clone(),
path: dir.path().join("unrelated.wasm"),
manifest: None,
digest: None,
});
config.policy.component.insert(
stem,
Expand All @@ -1160,52 +1154,6 @@ mod tests {
handle.wait().await.expect("clean shutdown");
}

/// As the policy-row rule above, for `[implements]`: the override's
/// file stem is author-controlled, so a row keyed to it must not
/// authorize the override's `provides` claim. The claimant refuses as
/// unbound rather than silently loading (ADR-0018).
#[tokio::test]
async fn a_module_source_override_never_binds_an_implements_row() {
let dir = tempfile::tempdir().expect("tempdir");
let wasm = dir.path().join("claimant.wasm");
std::fs::write(&wasm, b"never read: the refusal precedes the read").expect("write wasm");
let manifest = TestManifest::new("claimant")
.provides("acme:pool/quoter@2.0.0")
.write_to(dir.path());
let stem = "claimant".to_owned();

let mut config = EngineConfig::default();
config.engine.state_dir = dir.path().join("state");
config.modules.push(ModuleEntry {
id: stem.clone(),
path: dir.path().join("unrelated.wasm"),
manifest: None,
});
config.implements.insert(
crate::interface_id::InterfaceTrack::parse("acme:pool/quoter@2").expect("valid track"),
crate::engine_config::Implementer {
component: stem,
digest: None,
},
);

let err = RuntimeBuilder::new(&config)
.with_types::<CoreRuntime>()
.with_module_source(Some(wasm), Some(manifest))
.with_components(ComponentsBuilder::new(
ProviderPoolBuilder,
LocalStoreBuilder,
))
.launch()
.await
.err()
.expect("an override claimant must refuse as unbound");
Refusal::from(err).variant::<crate::supervisor::LoadRefusal>(|e| {
matches!(e, crate::supervisor::LoadRefusal::ImplementerUnbound { bound, .. }
if bound == "nothing")
});
}

/// Every module failing `init` aborts launch instead of idling.
#[tokio::test]
async fn launch_bails_when_all_modules_fail_init() {
Expand Down
6 changes: 3 additions & 3 deletions crates/nexum-runtime/src/digest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ pub enum DigestParseError {
/// send the operator to edit the wrong one.
#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display)]
pub enum DigestPin {
/// `[implements].<track>.digest`, in the trusted `engine.toml`.
#[display("[implements] digest in engine.toml")]
/// `[[modules]].digest`, in the trusted `engine.toml`.
#[display("[[modules]].digest in engine.toml")]
Operator,
/// `[component].digest`, in the author-supplied manifest.
#[display("[component].digest in the manifest")]
Expand Down Expand Up @@ -228,6 +228,6 @@ mod tests {
// wording must not serve both.
assert!(msg.contains("[component].digest in the manifest"), "{msg}");
let msg = message(DigestPin::Operator);
assert!(msg.contains("[implements] digest in engine.toml"), "{msg}");
assert!(msg.contains("[[modules]].digest in engine.toml"), "{msg}");
}
}
30 changes: 4 additions & 26 deletions crates/nexum-runtime/src/engine_config/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,33 +73,11 @@ pub enum EngineConfigError {
/// The entry as written.
entry: String,
},
/// Refused rather than dropped: an ignored row loses an authorization
/// and its claimant then refuses at load with the wrong message.
#[error(
"engine config: [implements] key {key:?} is not an interface track \
(namespace:package/interface@major, or @0.minor below 1.0)"
)]
InvalidInterfaceTrack {
/// The key as written.
key: String,
},
/// A row binding to nothing is a typo, and an unapplied binding fails
/// closed at load with a message that points away from the typo.
#[error(
"engine config: [implements].{interface:?} names component {id:?}, \
which matches no [[modules]].id"
)]
UnknownImplementsComponent {
/// The row's key as written.
interface: String,
/// The dangling component value.
/// A `[[modules]].digest` that is not a digest.
#[error("engine config: [[modules]] entry {id:?} digest {value:?}: {source}")]
InvalidModuleDigest {
/// The entry's operator-written id.
id: String,
},
/// Refused at load, as the `[component].digest` grammar is.
#[error("engine config: [implements].{interface:?} digest {value:?}: {source}")]
InvalidImplementerDigest {
/// The row's key as written.
interface: String,
/// The digest as written.
value: String,
/// Why the digest refused.
Expand Down
70 changes: 0 additions & 70 deletions crates/nexum-runtime/src/engine_config/implements.rs

This file was deleted.

Loading
Loading