feat(runtime): modularize host capabilities
- add an exact-version Capability Registry and structured descriptors - move Clock bindings, host state, and Linker installation into its own module - derive required capabilities from Component imports and link only requested interfaces - cover exact-version resolution and minimal per-component capability sets
This commit is contained in:
@@ -5,12 +5,3 @@ wasmtime::component::bindgen!({
|
||||
path: "../../wit/service",
|
||||
world: "service-component",
|
||||
});
|
||||
|
||||
pub(crate) mod clock {
|
||||
// Host capabilities are generated separately so adding a capability does
|
||||
// not expand the baseline service contract for every component.
|
||||
wasmtime::component::bindgen!({
|
||||
path: "../../wit/clock",
|
||||
world: "clock-host",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//! `wasmeld:clock/monotonic-clock@0.1.0` host implementation.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use wasmtime::component::Linker;
|
||||
|
||||
use crate::{RuntimeError, runtime::HostState};
|
||||
|
||||
wasmtime::component::bindgen!({
|
||||
path: "../../wit/clock",
|
||||
world: "clock-host",
|
||||
});
|
||||
|
||||
pub(crate) const INTERFACE: &str = "wasmeld:clock/monotonic-clock@0.1.0";
|
||||
pub(crate) const PACKAGE: &str = "wasmeld:clock";
|
||||
pub(crate) const NAME: &str = "monotonic-clock";
|
||||
pub(crate) const VERSION: &str = "0.1.0";
|
||||
|
||||
pub(crate) struct ClockState {
|
||||
origin: Instant,
|
||||
}
|
||||
|
||||
impl ClockState {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
origin: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn now(&self) -> u64 {
|
||||
u64::try_from(self.origin.elapsed().as_nanos()).unwrap_or(u64::MAX)
|
||||
}
|
||||
}
|
||||
|
||||
impl wasmeld::clock::monotonic_clock::Host for HostState {
|
||||
fn now(&mut self) -> u64 {
|
||||
self.capabilities.monotonic_clock_now()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_to_linker(linker: &mut Linker<HostState>) -> Result<(), RuntimeError> {
|
||||
wasmeld::clock::monotonic_clock::add_to_linker::<HostState, HostState>(linker, |state| state)
|
||||
.map_err(|error| RuntimeError::ActorInitialization(error.to_string()))
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//! Versioned host capabilities available to WebAssembly Components.
|
||||
|
||||
mod clock;
|
||||
mod registry;
|
||||
|
||||
pub use registry::CapabilityDescriptor;
|
||||
pub(crate) use registry::{Capability, CapabilityRegistry};
|
||||
|
||||
/// Per-Actor state owned by enabled host capabilities.
|
||||
pub(crate) struct HostCapabilities {
|
||||
clock: Option<clock::ClockState>,
|
||||
}
|
||||
|
||||
impl HostCapabilities {
|
||||
pub(crate) fn new(capabilities: &[Capability]) -> Self {
|
||||
Self {
|
||||
clock: capabilities
|
||||
.contains(&Capability::MonotonicClock)
|
||||
.then(clock::ClockState::new),
|
||||
}
|
||||
}
|
||||
|
||||
fn monotonic_clock_now(&self) -> u64 {
|
||||
self.clock
|
||||
.as_ref()
|
||||
.expect("the Clock interface is linked only when its state is enabled")
|
||||
.now()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//! Exact interface lookup and Linker installation for host capabilities.
|
||||
|
||||
use wasmtime::component::Linker;
|
||||
|
||||
use super::clock;
|
||||
use crate::{RuntimeError, runtime::HostState};
|
||||
|
||||
/// Stable description of one versioned WIT interface used by a Component.
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub struct CapabilityDescriptor {
|
||||
interface: &'static str,
|
||||
package: &'static str,
|
||||
name: &'static str,
|
||||
version: &'static str,
|
||||
}
|
||||
|
||||
impl CapabilityDescriptor {
|
||||
const fn new(
|
||||
interface: &'static str,
|
||||
package: &'static str,
|
||||
name: &'static str,
|
||||
version: &'static str,
|
||||
) -> Self {
|
||||
Self {
|
||||
interface,
|
||||
package,
|
||||
name,
|
||||
version,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the fully qualified and versioned WIT interface identity.
|
||||
pub const fn interface(&self) -> &'static str {
|
||||
self.interface
|
||||
}
|
||||
|
||||
/// Returns the versioned WIT package identity without the version suffix.
|
||||
pub const fn package(&self) -> &'static str {
|
||||
self.package
|
||||
}
|
||||
|
||||
/// Returns the WIT interface name inside the package.
|
||||
pub const fn name(&self) -> &'static str {
|
||||
self.name
|
||||
}
|
||||
|
||||
/// Returns the exact semantic version implemented by the Runtime.
|
||||
pub const fn version(&self) -> &'static str {
|
||||
self.version
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub(crate) enum Capability {
|
||||
MonotonicClock,
|
||||
}
|
||||
|
||||
impl Capability {
|
||||
pub(crate) const fn descriptor(self) -> CapabilityDescriptor {
|
||||
match self {
|
||||
Self::MonotonicClock => CapabilityDescriptor::new(
|
||||
clock::INTERFACE,
|
||||
clock::PACKAGE,
|
||||
clock::NAME,
|
||||
clock::VERSION,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_to_linker(self, linker: &mut Linker<HostState>) -> Result<(), RuntimeError> {
|
||||
match self {
|
||||
Self::MonotonicClock => clock::add_to_linker(linker),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Registry of the exact WIT interface versions implemented by this Runtime.
|
||||
pub(crate) struct CapabilityRegistry;
|
||||
|
||||
impl CapabilityRegistry {
|
||||
/// Resolves an exact Component import to a supported host capability.
|
||||
pub(crate) fn resolve(interface: &str) -> Option<Capability> {
|
||||
match interface {
|
||||
clock::INTERFACE => Some(Capability::MonotonicClock),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs only the capabilities requested by one Component.
|
||||
pub(crate) fn add_to_linker(
|
||||
linker: &mut Linker<HostState>,
|
||||
capabilities: &[Capability],
|
||||
) -> Result<(), RuntimeError> {
|
||||
for capability in capabilities {
|
||||
capability.add_to_linker(linker)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Capability, CapabilityRegistry};
|
||||
|
||||
#[test]
|
||||
fn resolves_only_the_exact_supported_interface_version() {
|
||||
assert_eq!(
|
||||
CapabilityRegistry::resolve("wasmeld:clock/monotonic-clock@0.1.0"),
|
||||
Some(Capability::MonotonicClock)
|
||||
);
|
||||
assert_eq!(
|
||||
CapabilityRegistry::resolve("wasmeld:clock/monotonic-clock@0.2.0"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
CapabilityRegistry::resolve("wasmeld:clock/other@0.1.0"),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,12 @@
|
||||
//! service exports, and invokes it through one serial Actor.
|
||||
|
||||
mod bindings;
|
||||
mod capability;
|
||||
mod error;
|
||||
mod manifest;
|
||||
mod runtime;
|
||||
|
||||
pub use capability::CapabilityDescriptor;
|
||||
pub use error::RuntimeError;
|
||||
pub use manifest::{ResourceLimits, ServiceKey, ServiceManifest};
|
||||
pub use runtime::{ActorHandle, Runtime, RuntimeConfig, RuntimeStats};
|
||||
|
||||
@@ -30,8 +30,9 @@ use wasmtime_wasi::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
RuntimeError, ServiceKey, ServiceManifest,
|
||||
bindings::{ServiceComponent, ServiceError, clock as clock_bindings},
|
||||
CapabilityDescriptor, RuntimeError, ServiceKey, ServiceManifest,
|
||||
bindings::{ServiceComponent, ServiceError},
|
||||
capability::{Capability, CapabilityRegistry, HostCapabilities},
|
||||
manifest::ResourceLimits,
|
||||
};
|
||||
|
||||
@@ -40,7 +41,6 @@ const MAX_EPOCH_TICK: Duration = Duration::from_millis(10);
|
||||
const DEFAULT_MAX_WASM_STACK: usize = 512 * 1024;
|
||||
const ACTOR_RESOURCE_COUNT_LIMIT: usize = 16;
|
||||
const ACTOR_TABLE_ELEMENT_LIMIT: usize = 16 * 1024;
|
||||
const MONOTONIC_CLOCK_IMPORT: &str = "wasmeld:clock/monotonic-clock@0.1.0";
|
||||
const ALLOWED_WASI_IMPORTS: &[&str] = &[
|
||||
"wasi:cli/environment@",
|
||||
"wasi:cli/exit@",
|
||||
@@ -162,12 +162,7 @@ impl Drop for EpochTicker {
|
||||
struct RegisteredService {
|
||||
manifest: ServiceManifest,
|
||||
component: Arc<Component>,
|
||||
capabilities: Vec<HostCapability>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum HostCapability {
|
||||
MonotonicClock,
|
||||
capabilities: Vec<Capability>,
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
@@ -327,6 +322,26 @@ impl Runtime {
|
||||
actor.invoke(input)
|
||||
}
|
||||
|
||||
/// Returns the exact versioned host capabilities imported by a service.
|
||||
pub fn capabilities(
|
||||
&self,
|
||||
key: &ServiceKey,
|
||||
) -> Result<Vec<CapabilityDescriptor>, RuntimeError> {
|
||||
let services = self
|
||||
.inner
|
||||
.services
|
||||
.lock()
|
||||
.map_err(|_| RuntimeError::LockPoisoned)?;
|
||||
let service = services
|
||||
.get(key)
|
||||
.ok_or_else(|| RuntimeError::ServiceNotRegistered(key.clone()))?;
|
||||
Ok(service
|
||||
.capabilities
|
||||
.iter()
|
||||
.map(|capability| capability.descriptor())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Stops one Actor and releases its Store and Component Instance.
|
||||
pub fn stop(&self, key: &ServiceKey) -> Result<(), RuntimeError> {
|
||||
let _lifecycle = self
|
||||
@@ -557,15 +572,15 @@ impl ActorStatus {
|
||||
}
|
||||
}
|
||||
|
||||
struct HostState {
|
||||
pub(crate) struct HostState {
|
||||
store_limits: StoreLimits,
|
||||
wasi: WasiCtx,
|
||||
wasi_table: ResourceTable,
|
||||
clock_origin: Instant,
|
||||
pub(crate) capabilities: HostCapabilities,
|
||||
}
|
||||
|
||||
impl HostState {
|
||||
fn new(limits: &ResourceLimits) -> Self {
|
||||
fn new(limits: &ResourceLimits, capabilities: &[Capability]) -> Self {
|
||||
let store_limits = StoreLimitsBuilder::new()
|
||||
.memory_size(limits.memory_bytes)
|
||||
.table_elements(ACTOR_TABLE_ELEMENT_LIMIT)
|
||||
@@ -585,7 +600,7 @@ impl HostState {
|
||||
store_limits,
|
||||
wasi: wasi.build(),
|
||||
wasi_table: ResourceTable::new(),
|
||||
clock_origin: Instant::now(),
|
||||
capabilities: HostCapabilities::new(capabilities),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -594,12 +609,6 @@ impl HasData for HostState {
|
||||
type Data<'a> = &'a mut HostState;
|
||||
}
|
||||
|
||||
impl clock_bindings::wasmeld::clock::monotonic_clock::Host for HostState {
|
||||
fn now(&mut self) -> u64 {
|
||||
u64::try_from(self.clock_origin.elapsed().as_nanos()).unwrap_or(u64::MAX)
|
||||
}
|
||||
}
|
||||
|
||||
impl WasiView for HostState {
|
||||
fn ctx(&mut self) -> WasiCtxView<'_> {
|
||||
WasiCtxView {
|
||||
@@ -639,12 +648,12 @@ impl ActorWorker {
|
||||
});
|
||||
}
|
||||
|
||||
let mut store = Store::new(&engine, HostState::new(&limits));
|
||||
let mut store = Store::new(&engine, HostState::new(&limits, &service.capabilities));
|
||||
store.limiter(|state| &mut state.store_limits);
|
||||
|
||||
let mut linker = Linker::new(&engine);
|
||||
add_restricted_wasi(&mut linker)?;
|
||||
add_host_capabilities(&mut linker, &service.capabilities)?;
|
||||
CapabilityRegistry::add_to_linker(&mut linker, &service.capabilities)?;
|
||||
|
||||
let epoch_ticks = ticks_for(limits.deadline(), epoch_tick);
|
||||
configure_call_budget(&mut store, &limits, epoch_ticks)?;
|
||||
@@ -758,33 +767,16 @@ fn link_wasi(result: wasmtime::Result<()>) -> Result<(), RuntimeError> {
|
||||
result.map_err(|error| RuntimeError::ActorInitialization(error.to_string()))
|
||||
}
|
||||
|
||||
fn add_host_capabilities(
|
||||
linker: &mut Linker<HostState>,
|
||||
capabilities: &[HostCapability],
|
||||
) -> Result<(), RuntimeError> {
|
||||
for capability in capabilities {
|
||||
match capability {
|
||||
HostCapability::MonotonicClock => link_wasi(
|
||||
clock_bindings::wasmeld::clock::monotonic_clock::add_to_linker::<
|
||||
HostState,
|
||||
HostState,
|
||||
>(linker, |state| state),
|
||||
)?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_component_imports(
|
||||
component: &Component,
|
||||
engine: &Engine,
|
||||
) -> Result<Vec<HostCapability>, RuntimeError> {
|
||||
) -> Result<Vec<Capability>, RuntimeError> {
|
||||
// Import validation is deny-by-default. A host function is linked only
|
||||
// after its exact WIT identity or WASI family has passed this allowlist.
|
||||
let mut capabilities = Vec::new();
|
||||
for (name, _) in component.component_type().imports(engine) {
|
||||
if name == MONOTONIC_CLOCK_IMPORT {
|
||||
capabilities.push(HostCapability::MonotonicClock);
|
||||
if let Some(capability) = CapabilityRegistry::resolve(name) {
|
||||
capabilities.push(capability);
|
||||
} else if !ALLOWED_WASI_IMPORTS
|
||||
.iter()
|
||||
.any(|allowed| name.starts_with(allowed))
|
||||
@@ -793,6 +785,8 @@ fn validate_component_imports(
|
||||
}
|
||||
}
|
||||
|
||||
capabilities.sort_unstable();
|
||||
capabilities.dedup();
|
||||
Ok(capabilities)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ fn echo_component_round_trips_bytes() {
|
||||
let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start");
|
||||
let (key, actor) = start_component(&runtime, "echo", "echo_component.wasm");
|
||||
|
||||
assert!(runtime.capabilities(&key).unwrap().is_empty());
|
||||
assert_eq!(actor.invoke(b"hello wasm".to_vec()).unwrap(), b"hello wasm");
|
||||
|
||||
runtime.stop(&key).unwrap();
|
||||
@@ -171,6 +172,13 @@ fn explicit_clock_capability_is_linked() {
|
||||
let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start");
|
||||
let (key, actor) = start_component(&runtime, "clock-probe", "clock_probe_component.wasm");
|
||||
|
||||
let capabilities = runtime.capabilities(&key).unwrap();
|
||||
assert_eq!(capabilities.len(), 1);
|
||||
assert_eq!(
|
||||
capabilities[0].interface(),
|
||||
"wasmeld:clock/monotonic-clock@0.1.0"
|
||||
);
|
||||
|
||||
let first = actor.invoke(b"first".to_vec()).unwrap();
|
||||
let second = actor.invoke(b"second".to_vec()).unwrap();
|
||||
assert_eq!(&first[8..], b"first");
|
||||
|
||||
Reference in New Issue
Block a user