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:
Maofeng
2026-07-29 19:13:35 +08:00
parent 3f5bb344f7
commit 5b32886efe
7 changed files with 238 additions and 50 deletions
+35 -41
View File
@@ -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)
}