From 72a264e0d1e09ba61b6be258bac03b9d8272718d Mon Sep 17 00:00:00 2001 From: Maofeng Date: Wed, 29 Jul 2026 19:13:41 +0800 Subject: [PATCH] feat(console): expose component capabilities - project structured capability package, name, version, and interface fields - derive capabilities during registration and artifact restoration - keep Component imports as the source of truth without changing .wasmpkg - verify empty and Clock capability responses through the management API --- crates/wasmeld-console/src/lib.rs | 47 +++++++++++++++++++++++++++-- crates/wasmeld-console/tests/api.rs | 39 ++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/crates/wasmeld-console/src/lib.rs b/crates/wasmeld-console/src/lib.rs index f4233a7..8a7335e 100644 --- a/crates/wasmeld-console/src/lib.rs +++ b/crates/wasmeld-console/src/lib.rs @@ -39,7 +39,8 @@ use wasmeld_package::{ wit_package::{WitPackageError, WitPackageMetadata}, }; use wasmeld_runtime::{ - ResourceLimits, Runtime, RuntimeConfig, RuntimeError, ServiceKey, ServiceManifest, + CapabilityDescriptor, ResourceLimits, Runtime, RuntimeConfig, RuntimeError, ServiceKey, + ServiceManifest, }; use persistence::{Persistence, StoredDeployment, StoredEvent, StoredService}; @@ -113,6 +114,7 @@ struct ConsoleState { #[derive(Clone)] struct ServiceRecord { manifest: ServiceManifest, + capabilities: Vec, status: ServiceStatus, updated_at_ms: u64, calls: u64, @@ -144,6 +146,26 @@ pub struct DeploymentView { pub updated_at_ms: u64, } +/// Exact versioned host interface imported by a registered Component. +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct CapabilityView { + pub interface: String, + pub package: String, + pub name: String, + pub version: String, +} + +impl From for CapabilityView { + fn from(capability: CapabilityDescriptor) -> Self { + Self { + interface: capability.interface().to_owned(), + package: capability.package().to_owned(), + name: capability.name().to_owned(), + version: capability.version().to_owned(), + } + } +} + /// API projection of a registered service and its current metrics. #[derive(Clone, Debug, Serialize)] pub struct ServiceView { @@ -151,6 +173,7 @@ pub struct ServiceView { pub revision: String, pub artifact: String, pub world: String, + pub capabilities: Vec, pub status: ServiceStatus, pub updated_at_ms: u64, pub limits: ResourceLimits, @@ -166,6 +189,7 @@ impl ServiceRecord { revision: self.manifest.revision.clone(), artifact: self.manifest.component.clone(), world: self.manifest.world.clone(), + capabilities: self.capabilities.clone(), status: self.status, updated_at_ms: self.updated_at_ms, limits: self.manifest.limits.clone(), @@ -743,8 +767,9 @@ impl Console { let _ = fs::remove_file(&manifest_path); return Err(error.into()); } + let capabilities = runtime_capabilities(runtime, &key)?; - let view = self.insert_service(manifest, ServiceStatus::Stopped)?; + let view = self.insert_service(manifest, capabilities, ServiceStatus::Stopped)?; self.push_event( EventKind::Registered, Some(&key), @@ -910,10 +935,12 @@ impl Console { ))); } runtime.register_from_file(manifest.clone())?; + let capabilities = runtime_capabilities(runtime, &key)?; self.state()?.services.insert( key, ServiceRecord { manifest, + capabilities, status: ServiceStatus::Stopped, updated_at_ms: stored.updated_at_ms, calls: stored.calls, @@ -956,7 +983,8 @@ impl Console { continue; } runtime.register_from_file(manifest.clone())?; - self.insert_service(manifest, ServiceStatus::Stopped)?; + let capabilities = runtime_capabilities(runtime, &key)?; + self.insert_service(manifest, capabilities, ServiceStatus::Stopped)?; } Ok(()) @@ -1070,12 +1098,14 @@ impl Console { fn insert_service( &self, manifest: ServiceManifest, + capabilities: Vec, status: ServiceStatus, ) -> Result { let key = manifest.key()?; let mut state = self.state()?; let record = ServiceRecord { manifest, + capabilities, status, updated_at_ms: unix_time_ms(), calls: 0, @@ -1140,6 +1170,17 @@ impl Console { } } +fn runtime_capabilities( + runtime: &Runtime, + key: &ServiceKey, +) -> Result, ConsoleError> { + Ok(runtime + .capabilities(key)? + .into_iter() + .map(Into::into) + .collect()) +} + fn deployment_view( state: &ConsoleState, service_id: &str, diff --git a/crates/wasmeld-console/tests/api.rs b/crates/wasmeld-console/tests/api.rs index 2d4f999..c8bdd3b 100644 --- a/crates/wasmeld-console/tests/api.rs +++ b/crates/wasmeld-console/tests/api.rs @@ -40,6 +40,7 @@ async fn manages_a_resident_component_over_http() { assert_eq!(status, StatusCode::CREATED, "{registered}"); assert_eq!(registered["id"], "echo"); assert_eq!(registered["status"], "stopped"); + assert_eq!(registered["capabilities"], json!([])); let response = application .clone() @@ -89,6 +90,41 @@ async fn manages_a_resident_component_over_http() { assert!(events["events"].as_array().unwrap().len() >= 5); } +#[tokio::test] +async fn reports_exact_component_host_capabilities() { + let artifact_dir = TempDir::new().expect("temporary artifact directory"); + let application = test_app(artifact_dir.path()).await; + let component = fs::read(component_artifact("clock_probe_component.wasm")) + .expect("clock probe component should be readable"); + + let response = application + .clone() + .oneshot(package_request("clock-probe", "0.1.0", &component)) + .await + .expect("register request should complete"); + assert_eq!(response.status(), StatusCode::CREATED); + let registered = response_json(response).await; + assert_eq!( + registered["capabilities"], + json!([{ + "interface": "wasmeld:clock/monotonic-clock@0.1.0", + "package": "wasmeld:clock", + "name": "monotonic-clock", + "version": "0.1.0" + }]) + ); + + let response = application + .oneshot(get_request("/api/v1/services")) + .await + .expect("service list request should complete"); + let services = response_json(response).await; + assert_eq!( + services["services"][0]["capabilities"], + registered["capabilities"] + ); +} + #[tokio::test] async fn routes_raw_gateway_calls_through_the_active_deployment() { let artifact_dir = TempDir::new().expect("temporary artifact directory"); @@ -686,6 +722,8 @@ fn package_request(id: &str, revision: &str, component: &[u8]) -> Request let mut package = Cursor::new(Vec::new()); let world = if id == "counter" { "component:counter/counter-component@0.1.0" + } else if id == "clock-probe" { + "component:clock-probe/clock-probe-component@0.1.0" } else { "component:echo/echo-component@0.1.0" }; @@ -787,6 +825,7 @@ fn component_artifact(name: &str) -> PathBuf { for manifest in [ "components/echo/Cargo.toml", "components/counter/Cargo.toml", + "components/clock-probe/Cargo.toml", ] { let status = Command::new("rustup") .args([