diff --git a/Cargo.toml b/Cargo.toml index a86d086c..bf774b5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ url = { workspace = true } wasm-metadata = { workspace = true } wasm-pkg-client = { workspace = true } wasm-pkg-core = { workspace = true } -wash-runtime = { workspace = true, features = ["washlet", "oci", "wasi-http", "wasi-config", "wasi-logging"] } +wash-runtime = { workspace = true, features = ["washlet", "oci", "wasi-http", "wasi-config", "wasi-logging", "wasi-keyvalue", "wasi-blobstore"] } wasmtime = { workspace = true } wasmtime-wasi = { workspace = true } which = { workspace = true } diff --git a/crates/wash-runtime/src/engine/mod.rs b/crates/wash-runtime/src/engine/mod.rs index db300ebd..6caf6dbd 100644 --- a/crates/wash-runtime/src/engine/mod.rs +++ b/crates/wash-runtime/src/engine/mod.rs @@ -135,8 +135,11 @@ impl Engine { // Create a temporary directory for the empty dir volume let temp_dir = tempfile::tempdir() .context("failed to create temp dir for empty dir volume")?; - tracing::debug!(path = ?temp_dir.path(), "created temp dir for empty dir volume"); - temp_dir.keep() + // Persist the temp dir and use the returned host path. Keep returns the + // host path to the directory so we can log it for debugging purposes. + let kept_path = temp_dir.keep(); + tracing::info!(host_path = %kept_path.display(), "created and persisted temp dir for empty dir volume"); + kept_path } }; diff --git a/crates/wash-runtime/src/engine/workload.rs b/crates/wash-runtime/src/engine/workload.rs index 79531be7..e2eb7319 100644 --- a/crates/wash-runtime/src/engine/workload.rs +++ b/crates/wash-runtime/src/engine/workload.rs @@ -233,6 +233,14 @@ impl WorkloadService { Ok(command) } + /// Pre-instantiate the component for use as an export provider (returns raw InstancePre). + /// This is used for component-to-component linking where we need access to the raw Instance exports. + pub fn pre_instantiate_for_linking(&mut self) -> anyhow::Result> { + let component = self.metadata.component.clone(); + let pre = self.metadata.linker.instantiate_pre(&component)?; + Ok(pre) + } + /// Whether or not the service is currently running. pub fn is_running(&self) -> bool { self.handle.is_some() @@ -465,6 +473,12 @@ impl ResolvedWorkload { } } + debug!( + interface_count = interface_map.len(), + interfaces = ?interface_map.keys().collect::>(), + "component interface map built for linking" + ); + self.resolve_workload_imports(&interface_map).await?; Ok(()) @@ -478,8 +492,6 @@ impl ResolvedWorkload { ) -> anyhow::Result<()> { let component_ids: Vec> = self.components.read().await.keys().cloned().collect(); for component_id in component_ids { - // In order to have mutable access to both the workload component and components that need - // to be instantiated as "plugins" during linking, we remove and re-add the component to the list. let mut workload_component = { self.components .write() @@ -527,28 +539,59 @@ impl ResolvedWorkload { let ty = component.component_type(); let imports: Vec<_> = ty.imports(component.engine()).collect(); - // TODO: some kind of shared import_name -> component registry. need to remove when new store - // store id, instance, import_name. That will keep the instance properly unique - let instance: Arc>> = Arc::default(); + // Map from package name (e.g., "wasi:blobstore@0.2.0-draft") to instance Arc. + // All interfaces in the same package (e.g., wasi:blobstore/container, wasi:blobstore/blobstore) + // must share the same instance so that resources can be passed between them. + let mut package_instances: HashMap>>> = + HashMap::new(); + for (import_name, import_item) in imports.into_iter() { match import_item { ComponentItem::ComponentInstance(import_instance_ty) => { trace!(name = import_name, "processing component instance import"); - let mut all_components = self.components.write().await; - let (plugin_component, instance_idx) = { - let Some(exporter_component) = interface_map.get(import_name) else { + + // Extract package name from import_name (e.g., "wasi:blobstore/container@0.2.0-draft" -> "wasi:blobstore@0.2.0-draft") + let package_name = if let Some(slash_pos) = import_name.rfind('/') { + // Everything before the last '/' plus everything after '@' + let before_slash = &import_name[..slash_pos]; + if let Some(at_pos) = import_name.rfind('@') { + format!("{}@{}", before_slash, &import_name[at_pos + 1..]) + } else { + before_slash.to_string() + } + } else { + // Fallback: use full import_name if it doesn't have expected format + import_name.to_string() + }; + + // Get or create instance Arc for this package + let instance = package_instances + .entry(package_name.clone()) + .or_insert_with(|| Arc::default()) + .clone(); + debug!( + import_name = import_name, + available_in_map = interface_map.contains_key(import_name), + map_keys = ?interface_map.keys().collect::>(), + "checking if import can be satisfied by component export" + ); + let all_components = self.components.read().await; + let (exporter_component_id, instance_idx, plugin_component_ref) = { + let Some(exporter_component_id) = interface_map.get(import_name) else { // TODO: error because unsatisfied import, if there's no available // export then it's an unresolvable workload - trace!( + debug!( name = import_name, + available_exports = ?interface_map.keys().collect::>(), "import not found in component exports, skipping" ); continue; }; - let Some(plugin_component) = all_components.get_mut(exporter_component) + let Some(plugin_component) = all_components.get(exporter_component_id) else { trace!( name = import_name, + exporter_id = %exporter_component_id, "exporting component not found in all components, skipping" ); continue; @@ -561,14 +604,31 @@ impl ResolvedWorkload { trace!(name = import_name, "skipping non-instance import"); continue; }; - (plugin_component, idx) + ( + exporter_component_id.clone(), + idx, + plugin_component.metadata.component.clone(), + ) }; trace!(name = import_name, index = ?instance_idx, "found import at index"); - // Preinstantiate the plugin instance so we can use it later - let pre = plugin_component - .pre_instantiate() - .context("failed to pre-instantiate during component linking")?; + // Release the read lock before acquiring write lock for pre-instantiation + drop(all_components); + + // Pre-instantiate the exporting component for linking (returns raw InstancePre) + let pre = { + let mut components = self.components.write().await; + let Some(exporter) = components.get_mut(&exporter_component_id) else { + trace!( + name = import_name, + exporter_id = %exporter_component_id, + "exporting component not found for pre-instantiation, skipping" + ); + continue; + }; + exporter.pre_instantiate() + .context("failed to pre-instantiate exporting component during component linking")? + }; let mut linker_instance = match linker.instance(import_name) { Ok(i) => i, @@ -579,13 +639,11 @@ impl ResolvedWorkload { }; for (export_name, export_ty) in - import_instance_ty.exports(plugin_component.metadata.component.engine()) + import_instance_ty.exports(plugin_component_ref.engine()) { match export_ty { ComponentItem::ComponentFunc(_func_ty) => { - let (item, func_idx) = match plugin_component - .metadata - .component + let (item, _func_idx) = match plugin_component_ref .export_index(Some(&instance_idx), export_name) { Some(res) => res, @@ -607,8 +665,25 @@ impl ResolvedWorkload { fn_name = export_name, "linking function import" ); + // Debug: List all available exports before creating closure + debug!( + name = import_name, + "listing all available exports from component type" + ); + let available_exports: Vec = plugin_component_ref + .component_type() + .exports(plugin_component_ref.engine()) + .map(|(name, _)| name.to_string()) + .collect(); + debug!( + name = import_name, + ?available_exports, + "available exports from exporting component" + ); + let import_name: Arc = import_name.into(); let export_name: Arc = export_name.into(); + let instance_idx_clone = instance_idx.clone(); let pre = pre.clone(); let instance = instance.clone(); linker_instance @@ -619,6 +694,7 @@ impl ResolvedWorkload { // to detect a diff store to drop the old one let import_name = import_name.clone(); let export_name = export_name.clone(); + let instance_idx = instance_idx_clone.clone(); let pre = pre.clone(); let instance = instance.clone(); Box::new(async move { @@ -632,17 +708,45 @@ impl ResolvedWorkload { instance } else { // Likely unnecessary, but explicit drop of the read lock + debug!( + name = %import_name, + fn_name = %export_name, + store_id = %store_id, + "creating new instance via pre.instantiate_async" + ); let new_instance = pre.instantiate_async(&mut store).await?; + debug!( + name = %import_name, + fn_name = %export_name, + store_id = %store_id, + "successfully created new instance" + ); drop(existing_instance); *instance.write().await = Some((store_id, new_instance)); new_instance }; + debug!( + name = %import_name, + fn_name = %export_name, + "looking up function from runtime instance by name" + ); + + // Look up the function by name from the runtime instance. + // We do a two-step lookup: + // 1. Look up the interface instance by name (e.g., "wasi:keyvalue/store@0.2.0-draft") + // 2. Use that runtime instance index to look up the function within it + let interface_idx = instance.get_export(&mut store, None, &import_name) + .ok_or_else(|| anyhow::anyhow!("interface '{}' not found in runtime instance", import_name))?; + + let func_idx = instance.get_export(&mut store, Some(&interface_idx), &export_name) + .ok_or_else(|| anyhow::anyhow!("function '{}' not found in interface '{}'", export_name, import_name))?; + let func = instance .get_func(&mut store, func_idx) - .context("function not found")?; + .with_context(|| format!("'{}' in interface '{}' is not a function", export_name, import_name))?; trace!( name = %import_name, fn_name = %export_name, @@ -703,9 +807,7 @@ impl ResolvedWorkload { .expect("failed to create async func"); } ComponentItem::Resource(resource_ty) => { - let (item, _idx) = match plugin_component - .metadata - .component + let (item, _idx) = match plugin_component_ref .export_index(Some(&instance_idx), export_name) { Some(res) => res, @@ -1008,8 +1110,55 @@ impl UnresolvedWorkload { ) -> anyhow::Result, Vec)>> { let mut bound_plugins: Vec<(Arc, Vec)> = Vec::new(); + // Build a list of interfaces provided by component exports + let mut component_exports: HashSet = HashSet::new(); + for component in self.components.values() { + let exported_instances = component.component_exports()?; + for (name, item) in exported_instances { + if matches!(item, ComponentItem::ComponentInstance(_)) { + component_exports.insert(name.to_string()); + } + } + } + + // Build a list of interfaces required by component imports + let mut component_imports: HashSet = HashSet::new(); + for component in self.components.values() { + let world = component.world(); + for import in &world.imports { + // Construct the full interface name for each interface in the WitInterface + for iface_name in &import.interfaces { + let full_interface = + format!("{}:{}/{}", import.namespace, import.package, iface_name); + component_imports.insert(full_interface); + } + } + } + + // Interfaces that can be satisfied by component-to-component linking + // are those that are BOTH imported by one component AND exported by another + let component_to_component_interfaces: HashSet = component_imports + .iter() + .filter(|import_iface| { + component_exports.iter().any(|export| { + // Match if the export starts with the import interface name + // (to handle version differences like @0.2.0) + export.starts_with(import_iface.as_str()) + }) + }) + .cloned() + .collect(); + + trace!( + component_exports = ?component_exports, + component_imports = ?component_imports, + component_to_component_interfaces = ?component_to_component_interfaces, + "identified interfaces for component-to-component linking" + ); + // Collect all component's required (unmatched) host interfaces // This tracks which interfaces each component still needs to be bound + // Filter out interfaces that can be satisfied by component exports let mut unmatched_interfaces: HashMap, HashSet> = HashMap::new(); trace!(host_interfaces = ?self.host_interfaces, "determining missing guest interfaces"); @@ -1019,7 +1168,23 @@ impl UnresolvedWorkload { let required_interfaces: HashSet = self .host_interfaces .iter() - .filter(|wit_interface| world.includes_bidirectional(wit_interface)) + .filter(|wit_interface| { + // Check both imports and exports (bidirectional) + if !world.includes_bidirectional(wit_interface) { + return false; + } + // Filter out interfaces that can be satisfied by component-to-component linking + // Only filter if ALL interfaces in this WitInterface are component-to-component + let all_provided_by_components = + wit_interface.interfaces.iter().all(|iface_name| { + let full_interface = format!( + "{}:{}/{}", + wit_interface.namespace, wit_interface.package, iface_name + ); + component_to_component_interfaces.contains(&full_interface) + }); + !all_provided_by_components + }) .cloned() .collect(); @@ -1034,7 +1199,23 @@ impl UnresolvedWorkload { let required_interfaces: HashSet = self .host_interfaces .iter() - .filter(|wit_interface| world.includes_bidirectional(wit_interface)) + .filter(|wit_interface| { + // Check both imports and exports (bidirectional) + if !world.includes_bidirectional(wit_interface) { + return false; + } + // Filter out interfaces that can be satisfied by component-to-component linking + // Only filter if ALL interfaces in this WitInterface are component-to-component + let all_provided_by_components = + wit_interface.interfaces.iter().all(|iface_name| { + let full_interface = format!( + "{}:{}/{}", + wit_interface.namespace, wit_interface.package, iface_name + ); + component_to_component_interfaces.contains(&full_interface) + }); + !all_provided_by_components + }) .cloned() .collect(); @@ -1043,7 +1224,10 @@ impl UnresolvedWorkload { } } - trace!(?unmatched_interfaces, "resolving unmatched interfaces"); + trace!( + ?unmatched_interfaces, + "resolving unmatched interfaces after filtering component-provided interfaces" + ); // Iterate through each plugin first, then check every component for matching worlds for (plugin_id, p) in plugins.iter() { @@ -1144,17 +1328,42 @@ impl UnresolvedWorkload { } } - // Check if all required interfaces were matched + // Check if all required IMPORTED interfaces were matched by HostPlugins + // (component-to-component interfaces were already filtered out earlier) + // Note: Unmatched exports are OK - they're optional for host plugins to consume for (component_id, unmatched) in unmatched_interfaces.iter() { - if !unmatched.is_empty() { - tracing::error!( - component_id = component_id.as_ref(), - interfaces = ?unmatched, - "no plugins found for requested interfaces" - ); - bail!( - "workload component {component_id} requested interfaces that are not available on this host: {unmatched:?}", - ) + // Find the component to check which unmatched interfaces are imports vs exports + let component_world = if let Some(service) = self.service.as_ref() { + if service.id() == component_id.as_ref() { + Some(service.world()) + } else { + None + } + } else { + None + } + .or_else(|| self.components.get(component_id).map(|c| c.world())); + + if let Some(world) = component_world { + // Filter to only unmatched IMPORTS (not exports) + let unmatched_imports: Vec<&WitInterface> = unmatched + .iter() + .filter(|iface| { + // Check if this is an import (not just an export) + world.imports.iter().any(|import| import.contains(iface)) + }) + .collect(); + + if !unmatched_imports.is_empty() { + tracing::error!( + component_id = component_id.as_ref(), + interfaces = ?unmatched_imports, + "no plugins found for required imports" + ); + bail!( + "workload component {component_id} has unmatched import interfaces: {unmatched_imports:?}", + ) + } } } @@ -1205,14 +1414,12 @@ impl UnresolvedWorkload { service: self.service, }; - // Link components before plugin resolution + // Link components to each other so component exports satisfy component imports if let Err(e) = resolved_workload.link_components().await { - // If linking fails, unbind all plugins before returning the error warn!( error = ?e, - "failed to link components, unbinding all plugins" + "failed to link components in resolved workload" ); - let _ = resolved_workload.unbind_all_plugins().await; bail!(e); } diff --git a/crates/wash/src/cli/dev.rs b/crates/wash/src/cli/dev.rs index 5791016f..c4de92a4 100644 --- a/crates/wash/src/cli/dev.rs +++ b/crates/wash/src/cli/dev.rs @@ -21,8 +21,8 @@ use wash_runtime::{ host::{Host, HostApi}, plugin::{wasi_config::WasiConfig, wasi_http::HttpServer, wasi_logging::WasiLogging}, types::{ - Component, HostPathVolume, LocalResources, Volume, VolumeMount, VolumeType, Workload, - WorkloadStartRequest, WorkloadState, WorkloadStopRequest, + Component, EmptyDirVolume, HostPathVolume, LocalResources, Volume, VolumeMount, VolumeType, + Workload, WorkloadStartRequest, WorkloadState, WorkloadStopRequest, }, wit::WitInterface, }; @@ -165,28 +165,57 @@ impl CliCommand for DevCommand { // Empty context for pre-hooks, consider adding more ctx.call_hooks(HookType::BeforeDev, Arc::default()).await; let dev_register_plugins = ctx.plugin_manager().get_hooks(HookType::DevRegister).await; + debug!( + count = dev_register_plugins.len(), + plugins = ?dev_register_plugins.iter().map(|p| &p.metadata.name).collect::>(), + "Found DevRegister plugins" + ); let mut dev_register_components = Vec::with_capacity(dev_register_plugins.len()); for plugin in dev_register_plugins { dev_register_components.push(plugin.get_original_component(ctx).await?) } + debug!( + count = dev_register_components.len(), + "Converted DevRegister plugins to components" + ); + + // Note: installed plugins will be loaded into the dev host so they can + // register their provided interfaces as HostPlugins. Do NOT include + // installed plugin component bytes directly into the workload here. let mut host_builder = Host::builder(); // Enable wasi config host_builder = host_builder.with_plugin(Arc::new(WasiConfig::default()))?; - let volume_root = self - .blobstore_root - .clone() - .unwrap_or_else(|| ctx.data_dir().join("dev_blobstore")); - // Ensure the blobstore root directory exists - if !volume_root.exists() { - tokio::fs::create_dir_all(&volume_root) - .await - .context("failed to create blobstore root directory")?; - } - debug!(path = ?volume_root.display(), "using blobstore root directory"); + // Add the plugin manager from CliContext to enable DevRegister plugins + host_builder = host_builder.with_plugin(Arc::clone(ctx.plugin_manager_arc()))?; + + // Decide on volumes for the dev workload. If the user provided a + // `--blobstore-root` use that host path; otherwise request an + // EmptyDir so the engine will create a unique temp dir each run. + let volumes = if let Some(root) = &self.blobstore_root { + // Ensure the provided blobstore root directory exists + if !root.exists() { + tokio::fs::create_dir_all(root) + .await + .context("failed to create blobstore root directory")?; + } + debug!(path = ?root.display(), "using provided blobstore root directory"); + vec![Volume { + name: "dev".to_string(), + volume_type: VolumeType::HostPath(HostPathVolume { + local_path: root.to_string_lossy().to_string(), + }), + }] + } else { + debug!(path = ?ctx.data_dir().join("dev_blobstore").display(), "using ephemeral EmptyDir for dev blobstore"); + vec![Volume { + name: "dev".to_string(), + volume_type: VolumeType::EmptyDir(EmptyDirVolume {}), + }] + }; // TODO(#19): Only spawn the server if the component exports wasi:http // Configure HTTP server with optional TLS, enable HTTP Server @@ -233,9 +262,21 @@ impl CliCommand for DevCommand { host_builder = host_builder.with_plugin(Arc::new(WasiLogging))?; debug!("Logging plugin registered"); + // Note: filesystem-backed keyvalue plugins are loaded into the dev host + // via the PluginManager (see `PluginManager::load_plugins_into_host`). + // Build and start the host let host = host_builder.build()?.start().await?; + // Load installed plugins into the dev host so they start as separate + // plugin workloads and can register their HostPlugin interfaces. + // This ensures provider interfaces (e.g., wasi:keyvalue) are available + // to the dev workload via the host plugin system. + ctx.plugin_manager() + .load_plugins_into_host(host.clone(), ctx.data_dir()) + .await + .context("failed to load installed plugins into dev host")?; + // Collect wasi configuration for the component let wasi_config = self .wasi_config @@ -254,7 +295,7 @@ impl CliCommand for DevCommand { let mut workload = create_workload( wasm_bytes.into(), wasi_config, - volume_root, + volumes, dev_register_components, ); // Running workload ID for reloads @@ -565,18 +606,28 @@ fn extract_component_interfaces(component_bytes: &[u8]) -> anyhow::Result, - volume_root: PathBuf, + volumes: Vec, dev_register_components: Vec, ) -> Workload { // Extract both imports and exports from the component // This populates host_interfaces which is checked bidirectionally during plugin binding - let mut host_interfaces = extract_component_interfaces(&bytes) - .unwrap_or_else(|e| { - warn!(error = ?e, "failed to extract component interfaces, using empty interface list"); - HashSet::new() - }) - .into_iter() - .collect::>(); + let mut host_interfaces = extract_component_interfaces(&bytes).unwrap_or_else(|e| { + warn!(error = ?e, "failed to extract component interfaces, using empty interface list"); + HashSet::new() + }); + + // Also extract interfaces from DevRegister plugin components + // This ensures that plugin imports (like wasmcloud:wash/types) are registered + for dev_component_bytes in &dev_register_components { + let dev_interfaces = + extract_component_interfaces(dev_component_bytes).unwrap_or_else(|e| { + warn!(error = ?e, "failed to extract DevRegister component interfaces"); + HashSet::new() + }); + host_interfaces.extend(dev_interfaces); + } + + let mut host_interfaces = host_interfaces.into_iter().collect::>(); // Apply configuration to specific interfaces for interface in &mut host_interfaces { @@ -609,15 +660,14 @@ fn create_workload( }); components.extend(dev_register_components.into_iter().map(|bytes| Component { bytes, - // TODO: Must have the root, but can't isolate rn - // local_resources: LocalResources { - // volume_mounts: vec![VolumeMount { - // name: "plugin-scratch-dir".to_string(), - // // mount_path: "foo", - // read_only: false, - // }], - // ..Default::default() - // }, + local_resources: LocalResources { + volume_mounts: vec![VolumeMount { + name: "dev".to_string(), + mount_path: "/tmp".to_string(), + read_only: false, + }], + ..Default::default() + }, ..Default::default() })); Workload { @@ -627,12 +677,7 @@ fn create_workload( host_interfaces, annotations: HashMap::default(), service: None, - volumes: vec![Volume { - name: "dev".to_string(), - volume_type: VolumeType::HostPath(HostPathVolume { - local_path: volume_root.to_string_lossy().to_string(), - }), - }], + volumes, } } @@ -856,13 +901,19 @@ mod tests { let temp_dir = TempDir::new().expect("failed to create temp dir"); let volume_root = temp_dir.path().to_path_buf(); + let volumes = vec![Volume { + name: "dev".to_string(), + volume_type: VolumeType::HostPath(HostPathVolume { + local_path: volume_root.to_string_lossy().to_string(), + }), + }]; let wasi_config = HashMap::new(); let dev_register_components = vec![]; let workload = create_workload( component_bytes, wasi_config, - volume_root, + volumes, dev_register_components, ); @@ -897,6 +948,12 @@ mod tests { let temp_dir = TempDir::new().expect("failed to create temp dir"); let volume_root = temp_dir.path().to_path_buf(); + let volumes = vec![Volume { + name: "dev".to_string(), + volume_type: VolumeType::HostPath(HostPathVolume { + local_path: volume_root.to_string_lossy().to_string(), + }), + }]; let mut wasi_config = HashMap::new(); wasi_config.insert( "database_url".to_string(), @@ -908,7 +965,7 @@ mod tests { let workload = create_workload( component_bytes, wasi_config.clone(), - volume_root, + volumes, dev_register_components, ); @@ -936,13 +993,19 @@ mod tests { let temp_dir = TempDir::new().expect("failed to create temp dir"); let volume_root = temp_dir.path().to_path_buf(); + let volumes = vec![Volume { + name: "dev".to_string(), + volume_type: VolumeType::HostPath(HostPathVolume { + local_path: volume_root.to_string_lossy().to_string(), + }), + }]; let wasi_config = HashMap::new(); let dev_register_components = vec![]; let workload = create_workload( component_bytes, wasi_config, - volume_root, + volumes, dev_register_components, ); @@ -960,15 +1023,17 @@ mod tests { let temp_dir = TempDir::new().expect("failed to create temp dir"); let volume_root = temp_dir.path().to_path_buf(); + let volumes = vec![Volume { + name: "dev".to_string(), + volume_type: VolumeType::HostPath(HostPathVolume { + local_path: volume_root.to_string_lossy().to_string(), + }), + }]; let wasi_config = HashMap::new(); let dev_register_components = vec![]; - let workload = create_workload( - invalid_bytes, - wasi_config, - volume_root, - dev_register_components, - ); + let workload = + create_workload(invalid_bytes, wasi_config, volumes, dev_register_components); // Should gracefully fall back to empty interfaces assert_eq!( @@ -990,13 +1055,19 @@ mod tests { let temp_dir = TempDir::new().expect("failed to create temp dir"); let volume_root = temp_dir.path().to_path_buf(); + let volumes = vec![Volume { + name: "dev".to_string(), + volume_type: VolumeType::HostPath(HostPathVolume { + local_path: volume_root.to_string_lossy().to_string(), + }), + }]; let wasi_config = HashMap::new(); let dev_register_components = vec![]; let workload = create_workload( component_bytes.clone(), wasi_config, - volume_root.clone(), + volumes, dev_register_components, ); diff --git a/crates/wash/src/cli/mod.rs b/crates/wash/src/cli/mod.rs index 76fd1408..4bc4d852 100644 --- a/crates/wash/src/cli/mod.rs +++ b/crates/wash/src/cli/mod.rs @@ -529,6 +529,14 @@ impl CliContext { &self.plugin_manager } + /// Returns a reference to the Arc for sharing across hosts + /// + /// This is useful when you need to add the same PluginManager to multiple hosts, + /// ensuring plugins are shared across different host instances. + pub fn plugin_manager_arc(&self) -> &Arc { + &self.plugin_manager + } + pub fn host(&self) -> &Arc { &self.host } diff --git a/crates/wash/src/plugin/mod.rs b/crates/wash/src/plugin/mod.rs index 044c20dc..e94e26d4 100644 --- a/crates/wash/src/plugin/mod.rs +++ b/crates/wash/src/plugin/mod.rs @@ -27,7 +27,10 @@ use wash_runtime::{ host::HostApi, oci::{OciConfig, pull_component}, plugin::HostPlugin, - types::{Component, LocalResources, Workload, WorkloadStartRequest, WorkloadState}, + types::{ + Component, EmptyDirVolume, LocalResources, Volume, VolumeMount, VolumeType, Workload, + WorkloadStartRequest, WorkloadState, + }, wit::{WitInterface, WitWorld}, }; @@ -67,6 +70,20 @@ impl PluginManager { &self, ctx: &CliContext, data_dir: impl AsRef, + ) -> anyhow::Result<()> { + // Load plugins into the CLI context's host by default + let host = ctx.host().clone(); + self.load_plugins_into_host(host, data_dir).await + } + + /// Load existing plugins from the plugins directory and start their workloads on the + /// provided `host` instance. This is useful when you need to load plugins into a + /// host other than the CLI's default host (for example, the dev host created by + /// `wash dev`). + pub async fn load_plugins_into_host( + &self, + host: Arc, + data_dir: impl AsRef, ) -> anyhow::Result<()> { let plugins_dir = data_dir.as_ref().join(PLUGINS_DIR); @@ -95,11 +112,30 @@ impl PluginManager { .and_then(|name| name.to_str()) .unwrap_or("unknown"); + debug!(plugin = %plugin_name, "Loading plugin from filesystem"); + // Load plugin as Vec let plugin = tokio::fs::read(&path) .await .with_context(|| format!("failed to read plugin file: {}", path.display()))?; + // Create an empty-dir volume for this plugin so the engine will create + // a unique temporary directory for the plugin filesystem on each run. + // The component will mount this volume at `/tmp` inside the component. + let plugin_volume = Volume { + name: "plugin_fs".to_string(), + volume_type: VolumeType::EmptyDir(EmptyDirVolume {}), + }; + + let component_local_resources = LocalResources { + volume_mounts: vec![VolumeMount { + name: "plugin_fs".to_string(), + mount_path: "/tmp".to_string(), + read_only: false, + }], + ..Default::default() + }; + let workload = Workload { namespace: "plugins".to_string(), name: plugin_name.to_string(), @@ -107,7 +143,7 @@ impl PluginManager { service: None, components: vec![Component { bytes: plugin.into(), - local_resources: LocalResources::default(), + local_resources: component_local_resources, pool_size: 1, max_invocations: 1, }], @@ -115,11 +151,10 @@ impl PluginManager { WitInterface::from("wasmcloud:wash/types@0.0.2"), WitInterface::from("wasi:config/store@0.2.0-rc.1"), ], - volumes: vec![], + volumes: vec![plugin_volume], }; - let res = ctx - .host() + let res = host .workload_start(WorkloadStartRequest { workload }) .await?; if res.workload_status.workload_state != WorkloadState::Running { @@ -213,6 +248,46 @@ impl PluginManager { } } +impl PluginManager { + /// Read installed plugin components from the CLI data directory and return their + /// raw component bytes. This does not instantiate or start the plugins; it simply + /// returns the bytes so they can be included in a workload (for example, the + /// dev workload) and linked together with the main component. + pub async fn installed_components_bytes( + &self, + ctx: &crate::cli::CliContext, + ) -> anyhow::Result> { + let plugins_dir = ctx.data_dir().join(PLUGINS_DIR); + + let mut components = Vec::new(); + + if !plugins_dir.exists() { + return Ok(components); + } + + let mut entries = tokio::fs::read_dir(&plugins_dir) + .await + .context("failed to read plugins directory")?; + + while let Some(entry) = entries.next_entry().await? { + let path = entry.path(); + + // Only process .wasm files + if path.extension().and_then(|ext| ext.to_str()) != Some("wasm") { + continue; + } + + let plugin = tokio::fs::read(&path) + .await + .with_context(|| format!("failed to read plugin file: {}", path.display()))?; + + components.push(Bytes::from(plugin)); + } + + Ok(components) + } +} + #[async_trait::async_trait] impl HostPlugin for PluginManager { fn id(&self) -> &'static str { diff --git a/examples/http-blobstore/Cargo.lock b/examples/http-blobstore/Cargo.lock index 132b12bb..23792619 100644 --- a/examples/http-blobstore/Cargo.lock +++ b/examples/http-blobstore/Cargo.lock @@ -2,21 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "ahash" version = "0.8.12" @@ -31,36 +16,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" - -[[package]] -name = "autocfg" -version = "1.4.0" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bytes" @@ -70,9 +34,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "equivalent" @@ -151,12 +115,6 @@ dependencies = [ "slab", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "hashbrown" version = "0.14.5" @@ -168,9 +126,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "heck" @@ -204,31 +162,21 @@ checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" [[package]] name = "indexmap" -version = "2.7.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.16.0", "serde", -] - -[[package]] -name = "io-uring" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" -dependencies = [ - "bitflags", - "cfg-if", - "libc", + "serde_core", ] [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "leb128" @@ -236,58 +184,23 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" -[[package]] -name = "libc" -version = "0.2.174" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" - [[package]] name = "log" -version = "0.4.25" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys", -] - -[[package]] -name = "object" -version = "0.36.7" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "pin-project-lite" @@ -303,9 +216,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "prettyplease" -version = "0.2.29" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", @@ -313,18 +226,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -344,38 +257,41 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -[[package]] -name = "rustc-demangle" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" - [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "semver" -version = "1.0.25" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -384,24 +300,22 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -411,18 +325,18 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "spdx" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b69356da67e2fc1f542c71ea7e654a361a79c938e4424392ecf4fa065d2193" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" dependencies = [ "smallvec", ] [[package]] name = "syn" -version = "2.0.98" +version = "2.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" dependencies = [ "proc-macro2", "quote", @@ -431,23 +345,18 @@ dependencies = [ [[package]] name = "tokio" -version = "1.46.0" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", - "io-uring", - "libc", - "mio", "pin-project-lite", - "slab", ] [[package]] name = "unicode-ident" -version = "1.0.16" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-xid" @@ -457,9 +366,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" [[package]] name = "version_check" @@ -467,12 +376,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - [[package]] name = "wasi" version = "0.13.3+wasi-0.2.2" @@ -520,7 +423,7 @@ dependencies = [ "rand", "tokio", "uuid", - "wasi 0.13.3+wasi-0.2.2", + "wasi", "wit-bindgen", ] @@ -537,79 +440,6 @@ dependencies = [ "semver", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "wit-bindgen" version = "0.32.0" @@ -719,18 +549,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", diff --git a/examples/http-counter/src/lib.rs b/examples/http-counter/src/lib.rs index 249a687c..36a3999e 100644 --- a/examples/http-counter/src/lib.rs +++ b/examples/http-counter/src/lib.rs @@ -201,7 +201,7 @@ fn store_response_in_blobstore(response_body: &str) -> Result<()> { fn increment_counter() -> Result { log(Level::Info, "", "Incrementing request counter"); - let bucket = open("")?; + let bucket = open("default")?; let new_count = increment(&bucket, COUNTER_KEY, 1).context("Failed to increment counter")?; diff --git a/plugins/blobstore-filesystem/Cargo.lock b/plugins/blobstore-filesystem/Cargo.lock index eec460cb..5daef663 100644 --- a/plugins/blobstore-filesystem/Cargo.lock +++ b/plugins/blobstore-filesystem/Cargo.lock @@ -26,7 +26,7 @@ version = "0.1.0" dependencies = [ "anyhow", "wasi", - "wit-bindgen", + "wit-bindgen 0.43.0", ] [[package]] @@ -310,11 +310,20 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ - "wit-bindgen-rt 0.33.0", + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen 0.46.0", ] [[package]] @@ -357,10 +366,19 @@ version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a18712ff1ec5bd09da500fe1e91dec11256b310da0ff33f8b4ec92b927cf0c6" dependencies = [ - "wit-bindgen-rt 0.43.0", + "wit-bindgen-rt", "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", +] + [[package]] name = "wit-bindgen-core" version = "0.43.0" @@ -372,15 +390,6 @@ dependencies = [ "wit-parser", ] -[[package]] -name = "wit-bindgen-rt" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" -dependencies = [ - "bitflags", -] - [[package]] name = "wit-bindgen-rt" version = "0.43.0" diff --git a/plugins/blobstore-filesystem/Cargo.toml b/plugins/blobstore-filesystem/Cargo.toml index bbe70fb9..ea2ddb0a 100644 --- a/plugins/blobstore-filesystem/Cargo.toml +++ b/plugins/blobstore-filesystem/Cargo.toml @@ -12,4 +12,4 @@ crate-type = ["cdylib"] [dependencies] anyhow = "1.0.98" wit-bindgen = "0.43.0" -wasi = "=0.13.3+wasi0.2.2" +wasi = "0.14.7+wasi-0.2.4" diff --git a/plugins/inspect/src/bindings.rs b/plugins/inspect/src/bindings.rs index 3ab28a9d..c48a9ab2 100644 --- a/plugins/inspect/src/bindings.rs +++ b/plugins/inspect/src/bindings.rs @@ -3,7 +3,7 @@ use crate::Component; wit_bindgen::generate!({ path: "../../wit", - world: "plugin-guest", + world: "wash-plugin", generate_all, }); diff --git a/plugins/keyvalue-filesystem/Cargo.lock b/plugins/keyvalue-filesystem/Cargo.lock new file mode 100644 index 00000000..b212c6aa --- /dev/null +++ b/plugins/keyvalue-filesystem/Cargo.lock @@ -0,0 +1,478 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "keyvalue-filesystem" +version = "0.1.0" +dependencies = [ + "anyhow", + "wasi", + "wit-bindgen 0.43.0", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "syn" +version = "2.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bc393c395cb621367ff02d854179882b9a351b4e0c93d1397e6090b53a5c2a" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b055604ba04189d54b8c0ab2c2fc98848f208e103882d5c0b984f045d5ea4d20" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161296c618fa2d63f6ed5fffd1112937e803cb9ec71b32b01a76321555660917" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a18712ff1ec5bd09da500fe1e91dec11256b310da0ff33f8b4ec92b927cf0c6" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c53468e077362201de11999c85c07c36e12048a990a3e0d69da2bd61da355d0" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fd734226eac1fd7c450956964e3a9094c9cee65e9dafdf126feef8c0096db65" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531ebfcec48e56473805285febdb450e270fa75b2dacb92816861d0473b4c15f" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7852bf8a9d1ea80884d26b864ddebd7b0c7636697c6ca10f4c6c93945e023966" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a57a11109cc553396f89f3a38a158a97d0b1adaec113bd73e0f64d30fb601f" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1f95a87d03a33e259af286b857a95911eb46236a0f726cbaec1227b3dfc67a" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] diff --git a/plugins/keyvalue-filesystem/Cargo.toml b/plugins/keyvalue-filesystem/Cargo.toml new file mode 100644 index 00000000..3bebece6 --- /dev/null +++ b/plugins/keyvalue-filesystem/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "keyvalue-filesystem" +description = "A wash dev plugin that implements `wasi:keyvalue` using `wasi:filesystem`" +edition = "2021" +version = "0.1.0" + +[workspace] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1.0.98" +wit-bindgen = "0.43.0" +wasi = "0.14" diff --git a/plugins/keyvalue-filesystem/README.md b/plugins/keyvalue-filesystem/README.md new file mode 100644 index 00000000..570d59b1 --- /dev/null +++ b/plugins/keyvalue-filesystem/README.md @@ -0,0 +1,231 @@ +# Keyvalue Filesystem + +This component is a wash plugin that exports `wasi:keyvalue/store@0.2.0-draft`, `wasi:keyvalue/atomics@0.2.0-draft`, and `wasi:keyvalue/batch@0.2.0-draft` and implements the keyvalue functions in terms of `wasi:filesystem` APIs. This is a reference implementation for how you can use a component plugin to extend the functionality of `wash dev`. + +## Features + +- **Store Operations**: Basic key-value operations (get, set, delete, exists, list-keys) +- **Batch Operations**: Efficient multi-key operations (get-many, set-many, delete-many) +- **Atomic Operations**: Atomic increment operations +- **Editable Storage**: Values are stored as files on the filesystem, allowing direct editing + +## Requirements + +This component must be passed a directory as a `wasi:filesystem/preopen` with read and write permissions. This directory will be used as the root for all keyvalue operations. + +## Storage Layout + +The plugin organizes data as follows: + +``` +root_directory/ +├── bucket1/ +│ ├── key1 (file containing value bytes) +│ ├── key2 +│ └── key3 +├── bucket2/ +│ ├── key_a +│ └── key_b +``` + +- Each bucket is a subdirectory under the preopened root directory +- Each key-value pair is stored as a file where: + - The filename is the key + - The file contents are the value bytes +- You can directly edit these files to modify values + +## Building + +To build the plugin: + +```bash +cargo build --target wasm32-wasip2 --release +``` + +The compiled plugin will be at: +``` +target/wasm32-wasip2/release/keyvalue_filesystem.wasm +``` + +## Usage with wash dev + +### Automatic Registration + +The plugin automatically registers with `wash dev` via the `DevRegister` hook. When a component imports `wasi:keyvalue/*`, wash will automatically link it to this plugin if available. + +### Installing the Plugin + +To make the plugin available to wash: + +1. Build the plugin (see above) +2. Install it with wash: + ```bash + wash plugin install target/wasm32-wasip2/release/keyvalue_filesystem.wasm + ``` + +### Using with Components + +When running `wash dev`, components that import `wasi:keyvalue` interfaces will automatically be linked to this plugin: + +```wit +// In your component's WIT file +package my:component; + +world my-component { + import wasi:keyvalue/store@0.2.0-draft2; + import wasi:keyvalue/atomics@0.2.0-draft2; + import wasi:keyvalue/batch@0.2.0-draft2; + // ... rest of your component +} +``` + +### Configuring Storage Location + +The plugin requires a preopened directory. With `wash dev`, you can configure this in your `wasmcloud.toml`: + +```toml +[[component]] +name = "my-component" +path = "./path/to/component.wasm" + +[component.config] +# Configure volume mounts for the keyvalue plugin +volumes = [ + { name = "keyvalue-data", path = "/data", read_only = false } +] +``` + +### Creating Buckets + +Before using the keyvalue operations, you need to create bucket directories: + +```bash +# Create a bucket directory +mkdir -p /data/my-bucket +``` + +Then in your component code: + +```rust +use wasi::keyvalue::store; + +// Open the bucket +let bucket = store::open("my-bucket")?; + +// Set a value +bucket.set("my-key", b"my-value".to_vec())?; + +// Get a value +let value = bucket.get("my-key")?; +``` + +### Editing Values Directly + +Since values are stored as files, you can edit them directly: + +```bash +# View a value +cat /data/my-bucket/my-key + +# Edit a value +echo "new-value" > /data/my-bucket/my-key + +# List all keys in a bucket +ls /data/my-bucket/ +``` + +This is especially useful for: +- Debugging and development +- Manual data inspection +- Quick prototyping +- Testing different scenarios + +## Examples + +### Basic Store Operations + +```rust +use wasi::keyvalue::store; + +// Open a bucket +let bucket = store::open("my-bucket")?; + +// Set a value +bucket.set("user:123", b"John Doe".to_vec())?; + +// Get a value +if let Some(value) = bucket.get("user:123")? { + let name = String::from_utf8(value)?; + println!("User: {}", name); +} + +// Check if key exists +if bucket.exists("user:123")? { + println!("User exists!"); +} + +// Delete a key +bucket.delete("user:123")?; + +// List all keys +let response = bucket.list_keys(None)?; +for key in response.keys { + println!("Key: {}", key); +} +``` + +### Batch Operations + +```rust +use wasi::keyvalue::batch; + +let bucket = store::open("my-bucket")?; + +// Set multiple values at once +batch::set_many(&bucket, vec![ + ("user:1".to_string(), b"Alice".to_vec()), + ("user:2".to_string(), b"Bob".to_vec()), + ("user:3".to_string(), b"Charlie".to_vec()), +])?; + +// Get multiple values +let results = batch::get_many(&bucket, vec![ + "user:1".to_string(), + "user:2".to_string(), + "user:999".to_string(), // Non-existent key +])?; + +for result in results { + if let Some((key, value)) = result { + println!("{}: {:?}", key, value); + } +} + +// Delete multiple keys +batch::delete_many(&bucket, vec![ + "user:1".to_string(), + "user:2".to_string(), +])?; +``` + +### Atomic Operations + +```rust +use wasi::keyvalue::atomics; + +let bucket = store::open("counters")?; + +// Atomic increment (perfect for counters) +let new_count = atomics::increment(&bucket, "page_views".to_string(), 1)?; +println!("Page views: {}", new_count); + +// Increment by larger delta +let total = atomics::increment(&bucket, "total_requests".to_string(), 100)?; +println!("Total requests: {}", total); +``` + +## Implementation Notes + +- **Atomic increment**: Stores u64 values as 8-byte little-endian encoded files +- **Pagination**: list-keys returns up to 1000 keys per page with u64 cursor support +- **Error handling**: Follows WASI keyvalue error semantics (NoSuchStore, AccessDenied, Other) diff --git a/plugins/keyvalue-filesystem/src/bindings.rs b/plugins/keyvalue-filesystem/src/bindings.rs new file mode 100644 index 00000000..af519fc4 --- /dev/null +++ b/plugins/keyvalue-filesystem/src/bindings.rs @@ -0,0 +1,12 @@ +//! Generated WIT bindings for a keyvalue host component + +wit_bindgen::generate!({ + world: "keyvalue-filesystem", + with: { + "wasi:keyvalue/store@0.2.0-draft": generate, + "wasi:keyvalue/atomics@0.2.0-draft": generate, + "wasi:keyvalue/batch@0.2.0-draft": generate, + "wasmcloud:wash/types@0.0.2": generate, + "wasmcloud:wash/plugin@0.0.2": generate, + }, +}); diff --git a/plugins/keyvalue-filesystem/src/lib.rs b/plugins/keyvalue-filesystem/src/lib.rs new file mode 100644 index 00000000..574e62ae --- /dev/null +++ b/plugins/keyvalue-filesystem/src/lib.rs @@ -0,0 +1,361 @@ +//! This component implements `wasi:keyvalue` in terms of `wasi:filesystem`, allowing +//! components to use the keyvalue API with a backing filesystem for development and testing. +//! +//! Storage layout: +//! - Each bucket is a directory under the root preopened directory +//! - Each key-value pair is stored as a file within the bucket directory +//! - The filename is the key, and the file contents are the value bytes + +use wasi::filesystem::types::{Descriptor, DescriptorFlags, DescriptorType, OpenFlags, PathFlags}; + +use crate::bindings::exports::wasi::keyvalue::store::{self, Error, KeyResponse}; +use crate::bindings::exports::wasi::keyvalue::{atomics, batch}; + +/// Generated WIT bindings +pub mod bindings; +/// `wasmcloud:wash/plugin` implementation +pub mod plugin; + +/// A bucket backed by a filesystem directory +pub type BucketImpl = String; + +struct Component; + +impl store::Guest for Component { + type Bucket = BucketImpl; + + fn open(identifier: String) -> Result { + let (root_dir, _) = get_root_dir().map_err(Error::Other)?; + + // Try to open the bucket directory, create it if it doesn't exist + match root_dir.open_at( + PathFlags::empty(), + &identifier, + OpenFlags::CREATE | OpenFlags::DIRECTORY, + DescriptorFlags::READ | DescriptorFlags::MUTATE_DIRECTORY, + ) { + Ok(_) => (), + Err(wasi::filesystem::types::ErrorCode::NoEntry) => { + // Directory doesn't exist, create it + root_dir + .create_directory_at(&identifier) + .map_err(fs_error_to_kv_error)?; + } + Err(e) => return Err(fs_error_to_kv_error(e)), + } + + Ok(store::Bucket::new(identifier)) + } +} + +impl atomics::Guest for Component { + fn increment(bucket: store::BucketBorrow<'_>, key: String, delta: u64) -> Result { + let bucket_impl = bucket.get::(); + + // Read current value + let current_bytes = store::GuestBucket::get(bucket_impl, key.clone())?; + + // Parse as u64 (default to 0 if not present or invalid) + let current_value = current_bytes + .and_then(|bytes| { + if bytes.len() == 8 { + Some(u64::from_le_bytes(bytes.try_into().ok()?)) + } else { + None + } + }) + .unwrap_or(0); + + // Calculate new value + let new_value = current_value.wrapping_add(delta); + + // Write back + store::GuestBucket::set(bucket_impl, key, new_value.to_le_bytes().to_vec())?; + + Ok(new_value) + } +} + +/// Get the root directory of the filesystem. The implementation of `wasi:keyvalue` for wash +/// assumes that the root directory is the last preopened directory provided by the WASI runtime. +/// +/// This is the only function that interacts with wasi:filesystem directly as it's required +/// to get the root directory for all operations. +fn get_root_dir() -> Result<(Descriptor, String), String> { + let mut dirs = ::wasi::filesystem::preopens::get_directories(); + if dirs.len() > 1 { + eprintln!("multiple preopened directories found, using the last one provided"); + } + + if let Some(dir) = dirs.pop() { + Ok((dir.0, dir.1)) + } else { + Err("No preopened directories found".to_string()) + } +} + +/// Convert a filesystem error to a keyvalue error +fn fs_error_to_kv_error(e: wasi::filesystem::types::ErrorCode) -> Error { + match e { + wasi::filesystem::types::ErrorCode::Access => Error::AccessDenied, + wasi::filesystem::types::ErrorCode::NotDirectory + | wasi::filesystem::types::ErrorCode::NoEntry => Error::NoSuchStore, + _ => Error::Other(e.to_string()), + } +} + +// ============================================================================ +// Store Bucket Resource Implementation +// ============================================================================ + +impl store::GuestBucket for BucketImpl { + fn get(&self, key: String) -> Result>, Error> { + let (root_dir, _) = get_root_dir().map_err(Error::Other)?; + + // Open the bucket directory + let bucket_dir = root_dir + .open_at( + PathFlags::empty(), + self, + OpenFlags::DIRECTORY, + DescriptorFlags::READ | DescriptorFlags::MUTATE_DIRECTORY, + ) + .map_err(fs_error_to_kv_error)?; + + // Try to open the key file + let file = match bucket_dir.open_at( + PathFlags::empty(), + &key, + OpenFlags::empty(), + DescriptorFlags::READ, + ) { + Ok(f) => f, + Err(wasi::filesystem::types::ErrorCode::NoEntry) => return Ok(None), + Err(e) => return Err(fs_error_to_kv_error(e)), + }; + + // Check if it's a regular file + let stat = file.stat().map_err(fs_error_to_kv_error)?; + if stat.type_ != DescriptorType::RegularFile { + return Ok(None); + } + + // Read the file contents + let stream = file + .read_via_stream(0) + .map_err(|e| Error::Other(e.to_string()))?; + + let mut buffer = Vec::new(); + let chunk_size = 4096; + + loop { + match stream.read(chunk_size) { + Ok(data) if data.is_empty() => break, + Ok(data) => buffer.extend_from_slice(&data), + Err(wasi::io::streams::StreamError::Closed) => break, + Err(e) => return Err(Error::Other(format!("read error: {:?}", e))), + } + } + + Ok(Some(buffer)) + } + + fn set(&self, key: String, value: Vec) -> Result<(), Error> { + let (root_dir, _) = get_root_dir().map_err(Error::Other)?; + + // Open the bucket directory + let bucket_dir = root_dir + .open_at( + PathFlags::empty(), + self, + OpenFlags::DIRECTORY, + DescriptorFlags::READ | DescriptorFlags::MUTATE_DIRECTORY, + ) + .map_err(fs_error_to_kv_error)?; + + // Create or truncate the key file + let file = bucket_dir + .open_at( + PathFlags::empty(), + &key, + OpenFlags::CREATE | OpenFlags::TRUNCATE, + DescriptorFlags::WRITE, + ) + .map_err(fs_error_to_kv_error)?; + + // Write the value + let stream = file + .write_via_stream(0) + .map_err(|e| Error::Other(e.to_string()))?; + + let mut offset = 0; + let chunk_size = 4096; + while offset < value.len() { + let end = std::cmp::min(offset + chunk_size, value.len()); + let chunk = &value[offset..end]; + + match stream.write(chunk) { + Ok(_) => offset += chunk.len(), + Err(e) => return Err(Error::Other(format!("write error: {:?}", e))), + } + } + + // Flush the stream + stream.flush().map_err(|e| Error::Other(e.to_string()))?; + + Ok(()) + } + + fn delete(&self, key: String) -> Result<(), Error> { + let (root_dir, _) = get_root_dir().map_err(Error::Other)?; + + // Open the bucket directory + let bucket_dir = root_dir + .open_at( + PathFlags::empty(), + self, + OpenFlags::DIRECTORY, + DescriptorFlags::READ | DescriptorFlags::MUTATE_DIRECTORY, + ) + .map_err(fs_error_to_kv_error)?; + + // Try to delete the file (ignore if it doesn't exist) + match bucket_dir.unlink_file_at(&key) { + Ok(_) => Ok(()), + Err(wasi::filesystem::types::ErrorCode::NoEntry) => Ok(()), // Succeed if already deleted + Err(e) => Err(fs_error_to_kv_error(e)), + } + } + + fn exists(&self, key: String) -> Result { + let (root_dir, _) = get_root_dir().map_err(Error::Other)?; + + // Open the bucket directory + let bucket_dir = root_dir + .open_at( + PathFlags::empty(), + self, + OpenFlags::DIRECTORY, + DescriptorFlags::READ, + ) + .map_err(fs_error_to_kv_error)?; + + // Try to stat the file + match bucket_dir.stat_at(PathFlags::empty(), &key) { + Ok(stat) => Ok(stat.type_ == DescriptorType::RegularFile), + Err(wasi::filesystem::types::ErrorCode::NoEntry) => Ok(false), + Err(e) => Err(fs_error_to_kv_error(e)), + } + } + + fn list_keys(&self, cursor: Option) -> Result { + let (root_dir, _) = get_root_dir().map_err(Error::Other)?; + + // Open the bucket directory + let bucket_dir = root_dir + .open_at( + PathFlags::empty(), + self, + OpenFlags::DIRECTORY, + DescriptorFlags::READ, + ) + .map_err(fs_error_to_kv_error)?; + + // Read directory entries + let entries = bucket_dir + .read_directory() + .map_err(|e| Error::Other(e.to_string()))?; + + let mut keys = Vec::new(); + let mut entry_count: u64 = 0; + let start_from = cursor.unwrap_or(0); + + loop { + match entries.read_directory_entry() { + Ok(Some(entry)) => { + // Skip special directories and metadata directories + if entry.name == "." || entry.name == ".." || entry.name.starts_with('.') { + continue; + } + + // Only include regular files + if entry.type_ == DescriptorType::RegularFile { + // Handle cursor-based pagination (skip entries before cursor) + if entry_count < start_from { + entry_count += 1; + continue; + } + + keys.push(entry.name); + entry_count += 1; + + // Limit to 1000 keys per page for performance + if keys.len() >= 1000 { + break; + } + } + } + Ok(None) => break, + Err(e) => return Err(Error::Other(e.to_string())), + } + } + + // Determine next cursor (position of next entry to read) + let next_cursor = if keys.len() >= 1000 { + Some(entry_count) + } else { + None + }; + + Ok(KeyResponse { + keys, + cursor: next_cursor, + }) + } +} + +// ============================================================================ +// Batch Interface Implementation +// ============================================================================ + +impl batch::Guest for Component { + fn get_many( + bucket: store::BucketBorrow<'_>, + keys: Vec, + ) -> Result)>>, Error> { + let bucket_impl = bucket.get::(); + let mut results = Vec::with_capacity(keys.len()); + + for key in keys { + match store::GuestBucket::get(bucket_impl, key.clone()) { + Ok(Some(value)) => results.push(Some((key, value))), + Ok(None) => results.push(None), + Err(e) => return Err(e), + } + } + + Ok(results) + } + + fn set_many( + bucket: store::BucketBorrow<'_>, + key_values: Vec<(String, Vec)>, + ) -> Result<(), Error> { + let bucket_impl = bucket.get::(); + for (key, value) in key_values { + store::GuestBucket::set(bucket_impl, key, value)?; + } + Ok(()) + } + + fn delete_many(bucket: store::BucketBorrow<'_>, keys: Vec) -> Result<(), Error> { + let bucket_impl = bucket.get::(); + for key in keys { + store::GuestBucket::delete(bucket_impl, key)?; + } + Ok(()) + } +} + +// Export the component implementation via the generated bindings +bindings::export!(Component with_types_in bindings); diff --git a/plugins/keyvalue-filesystem/src/plugin.rs b/plugins/keyvalue-filesystem/src/plugin.rs new file mode 100644 index 00000000..234fbefb --- /dev/null +++ b/plugins/keyvalue-filesystem/src/plugin.rs @@ -0,0 +1,31 @@ +//! Implementation of `wasmcloud:wash/plugin` for this [`crate::Component`] +use crate::bindings::wasmcloud::wash::types::{Command, HookType, Metadata, Runner}; + +impl crate::bindings::exports::wasmcloud::wash::plugin::Guest for crate::Component { + /// Called by wash to retrieve the plugin metadata + fn info() -> Metadata { + Metadata { + id: "dev.wasmcloud.keyvalue-filesystem".to_string(), + name: "keyvalue-filesystem".to_string(), + description: "Implements the wasi:keyvalue API using the wasi:filesystem API" + .to_string(), + contact: "wasmCloud Team".to_string(), + url: "https://github.com/wasmcloud/wash".to_string(), + license: "Apache-2.0".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + command: None, + sub_commands: vec![], + hooks: vec![HookType::DevRegister], + } + } + fn initialize(_: Runner) -> anyhow::Result { + Ok(String::with_capacity(0)) + } + // All of these functions aren't valid for this type of plugin + fn run(_: Runner, _: Command) -> anyhow::Result { + Err("no command registered".to_string()) + } + fn hook(_: Runner, _: HookType) -> anyhow::Result { + Err("invalid hook usage".to_string()) + } +} diff --git a/plugins/keyvalue-filesystem/wit/deps/wasi-blobstore-0.2.0-draft/package.wit b/plugins/keyvalue-filesystem/wit/deps/wasi-blobstore-0.2.0-draft/package.wit new file mode 100644 index 00000000..6b0eac30 --- /dev/null +++ b/plugins/keyvalue-filesystem/wit/deps/wasi-blobstore-0.2.0-draft/package.wit @@ -0,0 +1,175 @@ +package wasi:blobstore@0.2.0-draft; + +/// Types used by blobstore +interface types { + use wasi:io/streams@0.2.1.{input-stream, output-stream}; + + /// name of a container, a collection of objects. + /// The container name may be any valid UTF-8 string. + type container-name = string; + + /// name of an object within a container + /// The object name may be any valid UTF-8 string. + type object-name = string; + + /// TODO: define timestamp to include seconds since + /// Unix epoch and nanoseconds + /// https://github.com/WebAssembly/wasi-blob-store/issues/7 + type timestamp = u64; + + /// size of an object, in bytes + type object-size = u64; + + type error = string; + + /// information about a container + record container-metadata { + /// the container's name + name: container-name, + /// date and time container was created + created-at: timestamp, + } + + /// information about an object + record object-metadata { + /// the object's name + name: object-name, + /// the object's parent container + container: container-name, + /// date and time the object was created + created-at: timestamp, + /// size of the object, in bytes + size: object-size, + } + + /// identifier for an object that includes its container name + record object-id { + container: container-name, + object: object-name, + } + + /// A data is the data stored in a data blob. The value can be of any type + /// that can be represented in a byte array. It provides a way to write the value + /// to the output-stream defined in the `wasi-io` interface. + /// Soon: switch to `resource value { ... }` + resource outgoing-value { + new-outgoing-value: static func() -> outgoing-value; + /// Returns a stream for writing the value contents. + /// + /// The returned `output-stream` is a child resource: it must be dropped + /// before the parent `outgoing-value` resource is dropped (or finished), + /// otherwise the `outgoing-value` drop or `finish` will trap. + /// + /// Returns success on the first call: the `output-stream` resource for + /// this `outgoing-value` may be retrieved at most once. Subsequent calls + /// will return error. + outgoing-value-write-body: func() -> result; + /// Finalize an outgoing value. This must be + /// called to signal that the outgoing value is complete. If the `outgoing-value` + /// is dropped without calling `outgoing-value.finalize`, the implementation + /// should treat the value as corrupted. + finish: static func(this: outgoing-value) -> result<_, error>; + } + + /// A incoming-value is a wrapper around a value. It provides a way to read the value + /// from the input-stream defined in the `wasi-io` interface. + /// + /// The incoming-value provides two ways to consume the value: + /// 1. `incoming-value-consume-sync` consumes the value synchronously and returns the + /// value as a list of bytes. + /// 2. `incoming-value-consume-async` consumes the value asynchronously and returns the + /// value as an input-stream. + /// Soon: switch to `resource incoming-value { ... }` + resource incoming-value { + incoming-value-consume-sync: static func(this: incoming-value) -> result; + incoming-value-consume-async: static func(this: incoming-value) -> result; + size: func() -> u64; + } + + type incoming-value-async-body = input-stream; + + type incoming-value-sync-body = list; +} + +/// a Container is a collection of objects +interface container { + use wasi:io/streams@0.2.1.{input-stream, output-stream}; + use types.{container-metadata, error, incoming-value, object-metadata, object-name, outgoing-value}; + + /// this defines the `container` resource + resource container { + /// returns container name + name: func() -> result; + /// returns container metadata + info: func() -> result; + /// retrieves an object or portion of an object, as a resource. + /// Start and end offsets are inclusive. + /// Once a data-blob resource has been created, the underlying bytes are held by the blobstore service for the lifetime + /// of the data-blob resource, even if the object they came from is later deleted. + get-data: func(name: object-name, start: u64, end: u64) -> result; + /// creates or replaces an object with the data blob. + write-data: func(name: object-name, data: borrow) -> result<_, error>; + /// returns list of objects in the container. Order is undefined. + list-objects: func() -> result; + /// deletes object. + /// does not return error if object did not exist. + delete-object: func(name: object-name) -> result<_, error>; + /// deletes multiple objects in the container + delete-objects: func(names: list) -> result<_, error>; + /// returns true if the object exists in this container + has-object: func(name: object-name) -> result; + /// returns metadata for the object + object-info: func(name: object-name) -> result; + /// removes all objects within the container, leaving the container empty. + clear: func() -> result<_, error>; + } + + /// this defines the `stream-object-names` resource which is a representation of stream + resource stream-object-names { + /// reads the next number of objects from the stream + /// + /// This function returns the list of objects read, and a boolean indicating if the end of the stream was reached. + read-stream-object-names: func(len: u64) -> result, bool>, error>; + /// skip the next number of objects in the stream + /// + /// This function returns the number of objects skipped, and a boolean indicating if the end of the stream was reached. + skip-stream-object-names: func(num: u64) -> result, error>; + } +} + +/// wasi-cloud Blobstore service definition +interface blobstore { + use container.{container}; + use types.{error, container-name, object-id}; + + /// creates a new empty container + create-container: func(name: container-name) -> result; + + /// retrieves a container by name + get-container: func(name: container-name) -> result; + + /// deletes a container and all objects within it + delete-container: func(name: container-name) -> result<_, error>; + + /// returns true if the container exists + container-exists: func(name: container-name) -> result; + + /// copies (duplicates) an object, to the same or a different container. + /// returns an error if the target container does not exist. + /// overwrites destination object if it already existed. + copy-object: func(src: object-id, dest: object-id) -> result<_, error>; + + /// moves or renames an object, to the same or a different container + /// returns an error if the destination container does not exist. + /// overwrites destination object if it already existed. + move-object: func(src: object-id, dest: object-id) -> result<_, error>; +} + +world imports { + import wasi:io/error@0.2.1; + import wasi:io/poll@0.2.1; + import wasi:io/streams@0.2.1; + import types; + import container; + import blobstore; +} diff --git a/plugins/keyvalue-filesystem/wit/deps/wasi-cli-0.2.0/package.wit b/plugins/keyvalue-filesystem/wit/deps/wasi-cli-0.2.0/package.wit new file mode 100644 index 00000000..0a2737b7 --- /dev/null +++ b/plugins/keyvalue-filesystem/wit/deps/wasi-cli-0.2.0/package.wit @@ -0,0 +1,159 @@ +package wasi:cli@0.2.0; + +interface environment { + /// Get the POSIX-style environment variables. + /// + /// Each environment variable is provided as a pair of string variable names + /// and string value. + /// + /// Morally, these are a value import, but until value imports are available + /// in the component model, this import function should return the same + /// values each time it is called. + get-environment: func() -> list>; + + /// Get the POSIX-style arguments to the program. + get-arguments: func() -> list; + + /// Return a path that programs should use as their initial current working + /// directory, interpreting `.` as shorthand for this. + initial-cwd: func() -> option; +} + +interface exit { + /// Exit the current instance and any linked instances. + exit: func(status: result); +} + +interface run { + /// Run the program. + run: func() -> result; +} + +interface stdin { + use wasi:io/streams@0.2.0.{input-stream}; + + get-stdin: func() -> input-stream; +} + +interface stdout { + use wasi:io/streams@0.2.0.{output-stream}; + + get-stdout: func() -> output-stream; +} + +interface stderr { + use wasi:io/streams@0.2.0.{output-stream}; + + get-stderr: func() -> output-stream; +} + +/// Terminal input. +/// +/// In the future, this may include functions for disabling echoing, +/// disabling input buffering so that keyboard events are sent through +/// immediately, querying supported features, and so on. +interface terminal-input { + /// The input side of a terminal. + resource terminal-input; +} + +/// Terminal output. +/// +/// In the future, this may include functions for querying the terminal +/// size, being notified of terminal size changes, querying supported +/// features, and so on. +interface terminal-output { + /// The output side of a terminal. + resource terminal-output; +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +interface terminal-stdin { + use terminal-input.{terminal-input}; + + /// If stdin is connected to a terminal, return a `terminal-input` handle + /// allowing further interaction with it. + get-terminal-stdin: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stdout as a +/// link-time authority. +interface terminal-stdout { + use terminal-output.{terminal-output}; + + /// If stdout is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + get-terminal-stdout: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stderr as a +/// link-time authority. +interface terminal-stderr { + use terminal-output.{terminal-output}; + + /// If stderr is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + get-terminal-stderr: func() -> option; +} + +world imports { + import environment; + import exit; + import wasi:io/error@0.2.0; + import wasi:io/poll@0.2.0; + import wasi:io/streams@0.2.0; + import stdin; + import stdout; + import stderr; + import terminal-input; + import terminal-output; + import terminal-stdin; + import terminal-stdout; + import terminal-stderr; + import wasi:clocks/monotonic-clock@0.2.0; + import wasi:clocks/wall-clock@0.2.0; + import wasi:filesystem/types@0.2.0; + import wasi:filesystem/preopens@0.2.0; + import wasi:sockets/network@0.2.0; + import wasi:sockets/instance-network@0.2.0; + import wasi:sockets/udp@0.2.0; + import wasi:sockets/udp-create-socket@0.2.0; + import wasi:sockets/tcp@0.2.0; + import wasi:sockets/tcp-create-socket@0.2.0; + import wasi:sockets/ip-name-lookup@0.2.0; + import wasi:random/random@0.2.0; + import wasi:random/insecure@0.2.0; + import wasi:random/insecure-seed@0.2.0; +} +world command { + import environment; + import exit; + import wasi:io/error@0.2.0; + import wasi:io/poll@0.2.0; + import wasi:io/streams@0.2.0; + import stdin; + import stdout; + import stderr; + import terminal-input; + import terminal-output; + import terminal-stdin; + import terminal-stdout; + import terminal-stderr; + import wasi:clocks/monotonic-clock@0.2.0; + import wasi:clocks/wall-clock@0.2.0; + import wasi:filesystem/types@0.2.0; + import wasi:filesystem/preopens@0.2.0; + import wasi:sockets/network@0.2.0; + import wasi:sockets/instance-network@0.2.0; + import wasi:sockets/udp@0.2.0; + import wasi:sockets/udp-create-socket@0.2.0; + import wasi:sockets/tcp@0.2.0; + import wasi:sockets/tcp-create-socket@0.2.0; + import wasi:sockets/ip-name-lookup@0.2.0; + import wasi:random/random@0.2.0; + import wasi:random/insecure@0.2.0; + import wasi:random/insecure-seed@0.2.0; + + export run; +} diff --git a/plugins/keyvalue-filesystem/wit/deps/wasi-clocks-0.2.0/package.wit b/plugins/keyvalue-filesystem/wit/deps/wasi-clocks-0.2.0/package.wit new file mode 100644 index 00000000..9e0ba3dc --- /dev/null +++ b/plugins/keyvalue-filesystem/wit/deps/wasi-clocks-0.2.0/package.wit @@ -0,0 +1,29 @@ +package wasi:clocks@0.2.0; + +interface monotonic-clock { + use wasi:io/poll@0.2.0.{pollable}; + + type instant = u64; + + type duration = u64; + + now: func() -> instant; + + resolution: func() -> duration; + + subscribe-instant: func(when: instant) -> pollable; + + subscribe-duration: func(when: duration) -> pollable; +} + +interface wall-clock { + record datetime { + seconds: u64, + nanoseconds: u32, + } + + now: func() -> datetime; + + resolution: func() -> datetime; +} + diff --git a/plugins/keyvalue-filesystem/wit/deps/wasi-filesystem-0.2.0/package.wit b/plugins/keyvalue-filesystem/wit/deps/wasi-filesystem-0.2.0/package.wit new file mode 100644 index 00000000..cb6a2beb --- /dev/null +++ b/plugins/keyvalue-filesystem/wit/deps/wasi-filesystem-0.2.0/package.wit @@ -0,0 +1,158 @@ +package wasi:filesystem@0.2.0; + +interface types { + use wasi:io/streams@0.2.0.{input-stream, output-stream, error}; + use wasi:clocks/wall-clock@0.2.0.{datetime}; + + type filesize = u64; + + enum descriptor-type { + unknown, + block-device, + character-device, + directory, + fifo, + symbolic-link, + regular-file, + socket, + } + + flags descriptor-flags { + read, + write, + file-integrity-sync, + data-integrity-sync, + requested-write-sync, + mutate-directory, + } + + flags path-flags { + symlink-follow, + } + + flags open-flags { + create, + directory, + exclusive, + truncate, + } + + type link-count = u64; + + record descriptor-stat { + %type: descriptor-type, + link-count: link-count, + size: filesize, + data-access-timestamp: option, + data-modification-timestamp: option, + status-change-timestamp: option, + } + + variant new-timestamp { + no-change, + now, + timestamp(datetime), + } + + record directory-entry { + %type: descriptor-type, + name: string, + } + + enum error-code { + access, + would-block, + already, + bad-descriptor, + busy, + deadlock, + quota, + exist, + file-too-large, + illegal-byte-sequence, + in-progress, + interrupted, + invalid, + io, + is-directory, + loop, + too-many-links, + message-size, + name-too-long, + no-device, + no-entry, + no-lock, + insufficient-memory, + insufficient-space, + not-directory, + not-empty, + not-recoverable, + unsupported, + no-tty, + no-such-device, + overflow, + not-permitted, + pipe, + read-only, + invalid-seek, + text-file-busy, + cross-device, + } + + enum advice { + normal, + sequential, + random, + will-need, + dont-need, + no-reuse, + } + + record metadata-hash-value { + lower: u64, + upper: u64, + } + + resource descriptor { + read-via-stream: func(offset: filesize) -> result; + write-via-stream: func(offset: filesize) -> result; + append-via-stream: func() -> result; + advise: func(offset: filesize, length: filesize, advice: advice) -> result<_, error-code>; + sync-data: func() -> result<_, error-code>; + get-flags: func() -> result; + get-type: func() -> result; + set-size: func(size: filesize) -> result<_, error-code>; + set-times: func(data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; + read: func(length: filesize, offset: filesize) -> result, bool>, error-code>; + write: func(buffer: list, offset: filesize) -> result; + read-directory: func() -> result; + sync: func() -> result<_, error-code>; + create-directory-at: func(path: string) -> result<_, error-code>; + stat: func() -> result; + stat-at: func(path-flags: path-flags, path: string) -> result; + set-times-at: func(path-flags: path-flags, path: string, data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; + link-at: func(old-path-flags: path-flags, old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; + open-at: func(path-flags: path-flags, path: string, open-flags: open-flags, %flags: descriptor-flags) -> result; + readlink-at: func(path: string) -> result; + remove-directory-at: func(path: string) -> result<_, error-code>; + rename-at: func(old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; + symlink-at: func(old-path: string, new-path: string) -> result<_, error-code>; + unlink-file-at: func(path: string) -> result<_, error-code>; + is-same-object: func(other: borrow) -> bool; + metadata-hash: func() -> result; + metadata-hash-at: func(path-flags: path-flags, path: string) -> result; + } + + resource directory-entry-stream { + read-directory-entry: func() -> result, error-code>; + } + + filesystem-error-code: func(err: borrow) -> option; +} + +interface preopens { + use types.{descriptor}; + + get-directories: func() -> list>; +} + diff --git a/plugins/keyvalue-filesystem/wit/deps/wasi-http-0.2.0/package.wit b/plugins/keyvalue-filesystem/wit/deps/wasi-http-0.2.0/package.wit new file mode 100644 index 00000000..11f7ff44 --- /dev/null +++ b/plugins/keyvalue-filesystem/wit/deps/wasi-http-0.2.0/package.wit @@ -0,0 +1,571 @@ +package wasi:http@0.2.0; + +/// This interface defines all of the types and methods for implementing +/// HTTP Requests and Responses, both incoming and outgoing, as well as +/// their headers, trailers, and bodies. +interface types { + use wasi:clocks/monotonic-clock@0.2.0.{duration}; + use wasi:io/streams@0.2.0.{input-stream, output-stream}; + use wasi:io/error@0.2.0.{error as io-error}; + use wasi:io/poll@0.2.0.{pollable}; + + /// This type corresponds to HTTP standard Methods. + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string), + } + + /// This type corresponds to HTTP standard Related Schemes. + variant scheme { + HTTP, + HTTPS, + other(string), + } + + /// Defines the case payload type for `DNS-error` above: + record DNS-error-payload { + rcode: option, + info-code: option, + } + + /// Defines the case payload type for `TLS-alert-received` above: + record TLS-alert-received-payload { + alert-id: option, + alert-message: option, + } + + /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + record field-size-payload { + field-name: option, + field-size: option, + } + + /// These cases are inspired by the IANA HTTP Proxy Error Types: + /// https://www.iana.org/assignments/http-proxy-status/http-proxy-status.xhtml#table-http-proxy-error-types + variant error-code { + DNS-timeout, + DNS-error(DNS-error-payload), + destination-not-found, + destination-unavailable, + destination-IP-prohibited, + destination-IP-unroutable, + connection-refused, + connection-terminated, + connection-timeout, + connection-read-timeout, + connection-write-timeout, + connection-limit-reached, + TLS-protocol-error, + TLS-certificate-error, + TLS-alert-received(TLS-alert-received-payload), + HTTP-request-denied, + HTTP-request-length-required, + HTTP-request-body-size(option), + HTTP-request-method-invalid, + HTTP-request-URI-invalid, + HTTP-request-URI-too-long, + HTTP-request-header-section-size(option), + HTTP-request-header-size(option), + HTTP-request-trailer-section-size(option), + HTTP-request-trailer-size(field-size-payload), + HTTP-response-incomplete, + HTTP-response-header-section-size(option), + HTTP-response-header-size(field-size-payload), + HTTP-response-body-size(option), + HTTP-response-trailer-section-size(option), + HTTP-response-trailer-size(field-size-payload), + HTTP-response-transfer-coding(option), + HTTP-response-content-coding(option), + HTTP-response-timeout, + HTTP-upgrade-failed, + HTTP-protocol-error, + loop-detected, + configuration-error, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. It also includes an optional string for an + /// unstructured description of the error. Users should not depend on the + /// string for diagnosing errors, as it's not required to be consistent + /// between implementations. + internal-error(option), + } + + /// This type enumerates the different kinds of errors that may occur when + /// setting or appending to a `fields` resource. + variant header-error { + /// This error indicates that a `field-key` or `field-value` was + /// syntactically invalid when used with an operation that sets headers in a + /// `fields`. + invalid-syntax, + /// This error indicates that a forbidden `field-key` was used when trying + /// to set a header in a `fields`. + forbidden, + /// This error indicates that the operation on the `fields` was not + /// permitted because the fields are immutable. + immutable, + } + + /// Field keys are always strings. + type field-key = string; + + /// Field values should always be ASCII strings. However, in + /// reality, HTTP implementations often have to interpret malformed values, + /// so they are provided as a list of bytes. + type field-value = list; + + /// This following block defines the `fields` resource which corresponds to + /// HTTP standard Fields. Fields are a common representation used for both + /// Headers and Trailers. + /// + /// A `fields` may be mutable or immutable. A `fields` created using the + /// constructor, `from-list`, or `clone` will be mutable, but a `fields` + /// resource given by other means (including, but not limited to, + /// `incoming-request.headers`, `outgoing-request.headers`) might be be + /// immutable. In an immutable fields, the `set`, `append`, and `delete` + /// operations will fail with `header-error.immutable`. + resource fields { + /// Construct an empty HTTP Fields. + /// + /// The resulting `fields` is mutable. + constructor(); + /// Construct an HTTP Fields. + /// + /// The resulting `fields` is mutable. + /// + /// The list represents each key-value pair in the Fields. Keys + /// which have multiple values are represented by multiple entries in this + /// list with the same key. + /// + /// The tuple is a pair of the field key, represented as a string, and + /// Value, represented as a list of bytes. In a valid Fields, all keys + /// and values are valid UTF-8 strings. However, values are not always + /// well-formed, so they are represented as a raw list of bytes. + /// + /// An error result will be returned if any header or value was + /// syntactically invalid, or if a header was forbidden. + from-list: static func(entries: list>) -> result; + /// Get all of the values corresponding to a key. If the key is not present + /// in this `fields`, an empty list is returned. However, if the key is + /// present but empty, this is represented by a list with one or more + /// empty field-values present. + get: func(name: field-key) -> list; + /// Returns `true` when the key is present in this `fields`. If the key is + /// syntactically invalid, `false` is returned. + has: func(name: field-key) -> bool; + /// Set all of the values for a key. Clears any existing values for that + /// key, if they have been set. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + set: func(name: field-key, value: list) -> result<_, header-error>; + /// Delete all values for a key. Does nothing if no values for the key + /// exist. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + delete: func(name: field-key) -> result<_, header-error>; + /// Append a value for a key. Does not change or delete any existing + /// values for that key. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + append: func(name: field-key, value: field-value) -> result<_, header-error>; + /// Retrieve the full set of keys and values in the Fields. Like the + /// constructor, the list represents each key-value pair. + /// + /// The outer list represents each key-value pair in the Fields. Keys + /// which have multiple values are represented by multiple entries in this + /// list with the same key. + entries: func() -> list>; + /// Make a deep copy of the Fields. Equivelant in behavior to calling the + /// `fields` constructor on the return value of `entries`. The resulting + /// `fields` is mutable. + clone: func() -> fields; + } + + /// Headers is an alias for Fields. + type headers = fields; + + /// Trailers is an alias for Fields. + type trailers = fields; + + /// Represents an incoming HTTP Request. + resource incoming-request { + /// Returns the method of the incoming request. + method: func() -> method; + /// Returns the path with query parameters from the request, as a string. + path-with-query: func() -> option; + /// Returns the protocol scheme from the request. + scheme: func() -> option; + /// Returns the authority from the request, if it was present. + authority: func() -> option; + /// Get the `headers` associated with the request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// The `headers` returned are a child resource: it must be dropped before + /// the parent `incoming-request` is dropped. Dropping this + /// `incoming-request` before all children are dropped will trap. + headers: func() -> headers; + /// Gives the `incoming-body` associated with this request. Will only + /// return success at most once, and subsequent calls will return error. + consume: func() -> result; + } + + /// Represents an outgoing HTTP Request. + resource outgoing-request { + /// Construct a new `outgoing-request` with a default `method` of `GET`, and + /// `none` values for `path-with-query`, `scheme`, and `authority`. + /// + /// * `headers` is the HTTP Headers for the Request. + /// + /// It is possible to construct, or manipulate with the accessor functions + /// below, an `outgoing-request` with an invalid combination of `scheme` + /// and `authority`, or `headers` which are not permitted to be sent. + /// It is the obligation of the `outgoing-handler.handle` implementation + /// to reject invalid constructions of `outgoing-request`. + constructor(headers: headers); + /// Returns the resource corresponding to the outgoing Body for this + /// Request. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-request` can be retrieved at most once. Subsequent + /// calls will return error. + body: func() -> result; + /// Get the Method for the Request. + method: func() -> method; + /// Set the Method for the Request. Fails if the string present in a + /// `method.other` argument is not a syntactically valid method. + set-method: func(method: method) -> result; + /// Get the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. + path-with-query: func() -> option; + /// Set the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. Fails is the + /// string given is not a syntactically valid path and query uri component. + set-path-with-query: func(path-with-query: option) -> result; + /// Get the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + scheme: func() -> option; + /// Set the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. Fails if the + /// string given is not a syntactically valid uri scheme. + set-scheme: func(scheme: option) -> result; + /// Get the HTTP Authority for the Request. A value of `none` may be used + /// with Related Schemes which do not require an Authority. The HTTP and + /// HTTPS schemes always require an authority. + authority: func() -> option; + /// Set the HTTP Authority for the Request. A value of `none` may be used + /// with Related Schemes which do not require an Authority. The HTTP and + /// HTTPS schemes always require an authority. Fails if the string given is + /// not a syntactically valid uri authority. + set-authority: func(authority: option) -> result; + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transfered to + /// another component by e.g. `outgoing-handler.handle`. + headers: func() -> headers; + } + + /// Parameters for making an HTTP Request. Each of these parameters is + /// currently an optional timeout applicable to the transport layer of the + /// HTTP protocol. + /// + /// These timeouts are separate from any the user may use to bound a + /// blocking call to `wasi:io/poll.poll`. + resource request-options { + /// Construct a default `request-options` value. + constructor(); + /// The timeout for the initial connect to the HTTP Server. + connect-timeout: func() -> option; + /// Set the timeout for the initial connect to the HTTP Server. An error + /// return value indicates that this timeout is not supported. + set-connect-timeout: func(duration: option) -> result; + /// The timeout for receiving the first byte of the Response body. + first-byte-timeout: func() -> option; + /// Set the timeout for receiving the first byte of the Response body. An + /// error return value indicates that this timeout is not supported. + set-first-byte-timeout: func(duration: option) -> result; + /// The timeout for receiving subsequent chunks of bytes in the Response + /// body stream. + between-bytes-timeout: func() -> option; + /// Set the timeout for receiving subsequent chunks of bytes in the Response + /// body stream. An error return value indicates that this timeout is not + /// supported. + set-between-bytes-timeout: func(duration: option) -> result; + } + + /// Represents the ability to send an HTTP Response. + /// + /// This resource is used by the `wasi:http/incoming-handler` interface to + /// allow a Response to be sent corresponding to the Request provided as the + /// other argument to `incoming-handler.handle`. + resource response-outparam { + /// Set the value of the `response-outparam` to either send a response, + /// or indicate an error. + /// + /// This method consumes the `response-outparam` to ensure that it is + /// called at most once. If it is never called, the implementation + /// will respond with an error. + /// + /// The user may provide an `error` to `response` to allow the + /// implementation determine how to respond with an HTTP error response. + set: static func(param: response-outparam, response: result); + } + + /// This type corresponds to the HTTP standard Status Code. + type status-code = u16; + + /// Represents an incoming HTTP Response. + resource incoming-response { + /// Returns the status code from the incoming response. + status: func() -> status-code; + /// Returns the headers from the incoming response. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `incoming-response` is dropped. + headers: func() -> headers; + /// Returns the incoming body. May be called at most once. Returns error + /// if called additional times. + consume: func() -> result; + } + + /// Represents an incoming HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, indicating that the full contents of the + /// body have been received. This resource represents the contents as + /// an `input-stream` and the delivery of trailers as a `future-trailers`, + /// and ensures that the user of this interface may only be consuming either + /// the body contents or waiting on trailers at any given time. + resource incoming-body { + /// Returns the contents of the body, as a stream of bytes. + /// + /// Returns success on first call: the stream representing the contents + /// can be retrieved at most once. Subsequent calls will return error. + /// + /// The returned `input-stream` resource is a child: it must be dropped + /// before the parent `incoming-body` is dropped, or consumed by + /// `incoming-body.finish`. + /// + /// This invariant ensures that the implementation can determine whether + /// the user is consuming the contents of the body, waiting on the + /// `future-trailers` to be ready, or neither. This allows for network + /// backpressure is to be applied when the user is consuming the body, + /// and for that backpressure to not inhibit delivery of the trailers if + /// the user does not read the entire body. + %stream: func() -> result; + /// Takes ownership of `incoming-body`, and returns a `future-trailers`. + /// This function will trap if the `input-stream` child is still alive. + finish: static func(this: incoming-body) -> future-trailers; + } + + /// Represents a future which may eventaully return trailers, or an error. + /// + /// In the case that the incoming HTTP Request or Response did not have any + /// trailers, this future will resolve to the empty set of trailers once the + /// complete Request or Response body has been received. + resource future-trailers { + /// Returns a pollable which becomes ready when either the trailers have + /// been received, or an error has occured. When this pollable is ready, + /// the `get` method will return `some`. + subscribe: func() -> pollable; + /// Returns the contents of the trailers, or an error which occured, + /// once the future is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the trailers or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the HTTP Request or Response + /// body, as well as any trailers, were received successfully, or that an + /// error occured receiving them. The optional `trailers` indicates whether + /// or not trailers were present in the body. + /// + /// When some `trailers` are returned by this method, the `trailers` + /// resource is immutable, and a child. Use of the `set`, `append`, or + /// `delete` methods will return an error, and the resource must be + /// dropped before the parent `future-trailers` is dropped. + get: func() -> option, error-code>>>; + } + + /// Represents an outgoing HTTP Response. + resource outgoing-response { + /// Construct an `outgoing-response`, with a default `status-code` of `200`. + /// If a different `status-code` is needed, it must be set via the + /// `set-status-code` method. + /// + /// * `headers` is the HTTP Headers for the Response. + constructor(headers: headers); + /// Get the HTTP Status Code for the Response. + status-code: func() -> status-code; + /// Set the HTTP Status Code for the Response. Fails if the status-code + /// given is not a valid http status code. + set-status-code: func(status-code: status-code) -> result; + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transfered to + /// another component by e.g. `outgoing-handler.handle`. + headers: func() -> headers; + /// Returns the resource corresponding to the outgoing Body for this Response. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-response` can be retrieved at most once. Subsequent + /// calls will return error. + body: func() -> result; + } + + /// Represents an outgoing HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, inducating the full contents of the body + /// have been sent. This resource represents the contents as an + /// `output-stream` child resource, and the completion of the body (with + /// optional trailers) with a static function that consumes the + /// `outgoing-body` resource, and ensures that the user of this interface + /// may not write to the body contents after the body has been finished. + /// + /// If the user code drops this resource, as opposed to calling the static + /// method `finish`, the implementation should treat the body as incomplete, + /// and that an error has occured. The implementation should propogate this + /// error to the HTTP protocol by whatever means it has available, + /// including: corrupting the body on the wire, aborting the associated + /// Request, or sending a late status code for the Response. + resource outgoing-body { + /// Returns a stream for writing the body contents. + /// + /// The returned `output-stream` is a child resource: it must be dropped + /// before the parent `outgoing-body` resource is dropped (or finished), + /// otherwise the `outgoing-body` drop or `finish` will trap. + /// + /// Returns success on the first call: the `output-stream` resource for + /// this `outgoing-body` may be retrieved at most once. Subsequent calls + /// will return error. + write: func() -> result; + /// Finalize an outgoing body, optionally providing trailers. This must be + /// called to signal that the response is complete. If the `outgoing-body` + /// is dropped without calling `outgoing-body.finalize`, the implementation + /// should treat the body as corrupted. + /// + /// Fails if the body's `outgoing-request` or `outgoing-response` was + /// constructed with a Content-Length header, and the contents written + /// to the body (via `write`) does not match the value given in the + /// Content-Length. + finish: static func(this: outgoing-body, trailers: option) -> result<_, error-code>; + } + + /// Represents a future which may eventaully return an incoming HTTP + /// Response, or an error. + /// + /// This resource is returned by the `wasi:http/outgoing-handler` interface to + /// provide the HTTP Response corresponding to the sent Request. + resource future-incoming-response { + /// Returns a pollable which becomes ready when either the Response has + /// been received, or an error has occured. When this pollable is ready, + /// the `get` method will return `some`. + subscribe: func() -> pollable; + /// Returns the incoming HTTP Response, or an error, once one is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the response or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the incoming HTTP Response + /// status and headers have recieved successfully, or that an error + /// occured. Errors may also occur while consuming the response body, + /// but those will be reported by the `incoming-body` and its + /// `output-stream` child. + get: func() -> option>>; + } + + /// Attempts to extract a http-related `error` from the wasi:io `error` + /// provided. + /// + /// Stream operations which return + /// `wasi:io/stream/stream-error::last-operation-failed` have a payload of + /// type `wasi:io/error/error` with more information about the operation + /// that failed. This payload can be passed through to this function to see + /// if there's http-related information about the error to return. + /// + /// Note that this function is fallible because not all io-errors are + /// http-related errors. + http-error-code: func(err: borrow) -> option; +} + +/// This interface defines a handler of incoming HTTP Requests. It should +/// be exported by components which can respond to HTTP Requests. +interface incoming-handler { + use types.{incoming-request, response-outparam}; + + /// This function is invoked with an incoming HTTP Request, and a resource + /// `response-outparam` which provides the capability to reply with an HTTP + /// Response. The response is sent by calling the `response-outparam.set` + /// method, which allows execution to continue after the response has been + /// sent. This enables both streaming to the response body, and performing other + /// work. + /// + /// The implementor of this function must write a response to the + /// `response-outparam` before returning, or else the caller will respond + /// with an error on its behalf. + handle: func(request: incoming-request, response-out: response-outparam); +} + +/// This interface defines a handler of outgoing HTTP Requests. It should be +/// imported by components which wish to make HTTP Requests. +interface outgoing-handler { + use types.{outgoing-request, request-options, future-incoming-response, error-code}; + + /// This function is invoked with an outgoing HTTP Request, and it returns + /// a resource `future-incoming-response` which represents an HTTP Response + /// which may arrive in the future. + /// + /// The `options` argument accepts optional parameters for the HTTP + /// protocol's transport layer. + /// + /// This function may return an error if the `outgoing-request` is invalid + /// or not allowed to be made. Otherwise, protocol errors are reported + /// through the `future-incoming-response`. + handle: func(request: outgoing-request, options: option) -> result; +} + +/// The `wasi:http/proxy` world captures a widely-implementable intersection of +/// hosts that includes HTTP forward and reverse proxies. Components targeting +/// this world may concurrently stream in and out any number of incoming and +/// outgoing HTTP requests. +world proxy { + import wasi:random/random@0.2.0; + import wasi:io/error@0.2.0; + import wasi:io/poll@0.2.0; + import wasi:io/streams@0.2.0; + import wasi:cli/stdout@0.2.0; + import wasi:cli/stderr@0.2.0; + import wasi:cli/stdin@0.2.0; + import wasi:clocks/monotonic-clock@0.2.0; + import types; + import outgoing-handler; + import wasi:clocks/wall-clock@0.2.0; + + export incoming-handler; +} diff --git a/plugins/keyvalue-filesystem/wit/deps/wasi-io-0.2.0/package.wit b/plugins/keyvalue-filesystem/wit/deps/wasi-io-0.2.0/package.wit new file mode 100644 index 00000000..18400299 --- /dev/null +++ b/plugins/keyvalue-filesystem/wit/deps/wasi-io-0.2.0/package.wit @@ -0,0 +1,48 @@ +package wasi:io@0.2.0; + +interface error { + resource error { + to-debug-string: func() -> string; + } +} + +interface poll { + resource pollable { + ready: func() -> bool; + block: func(); + } + + poll: func(in: list>) -> list; +} + +interface streams { + use error.{error}; + use poll.{pollable}; + + variant stream-error { + last-operation-failed(error), + closed, + } + + resource input-stream { + read: func(len: u64) -> result, stream-error>; + blocking-read: func(len: u64) -> result, stream-error>; + skip: func(len: u64) -> result; + blocking-skip: func(len: u64) -> result; + subscribe: func() -> pollable; + } + + resource output-stream { + check-write: func() -> result; + write: func(contents: list) -> result<_, stream-error>; + blocking-write-and-flush: func(contents: list) -> result<_, stream-error>; + flush: func() -> result<_, stream-error>; + blocking-flush: func() -> result<_, stream-error>; + subscribe: func() -> pollable; + write-zeroes: func(len: u64) -> result<_, stream-error>; + blocking-write-zeroes-and-flush: func(len: u64) -> result<_, stream-error>; + splice: func(src: borrow, len: u64) -> result; + blocking-splice: func(src: borrow, len: u64) -> result; + } +} + diff --git a/plugins/keyvalue-filesystem/wit/deps/wasi-io-0.2.1/package.wit b/plugins/keyvalue-filesystem/wit/deps/wasi-io-0.2.1/package.wit new file mode 100644 index 00000000..5a47b2a0 --- /dev/null +++ b/plugins/keyvalue-filesystem/wit/deps/wasi-io-0.2.1/package.wit @@ -0,0 +1,48 @@ +package wasi:io@0.2.1; + +interface error { + resource error { + to-debug-string: func() -> string; + } +} + +interface poll { + resource pollable { + ready: func() -> bool; + block: func(); + } + + poll: func(in: list>) -> list; +} + +interface streams { + use error.{error}; + use poll.{pollable}; + + variant stream-error { + last-operation-failed(error), + closed, + } + + resource input-stream { + read: func(len: u64) -> result, stream-error>; + blocking-read: func(len: u64) -> result, stream-error>; + skip: func(len: u64) -> result; + blocking-skip: func(len: u64) -> result; + subscribe: func() -> pollable; + } + + resource output-stream { + check-write: func() -> result; + write: func(contents: list) -> result<_, stream-error>; + blocking-write-and-flush: func(contents: list) -> result<_, stream-error>; + flush: func() -> result<_, stream-error>; + blocking-flush: func() -> result<_, stream-error>; + subscribe: func() -> pollable; + write-zeroes: func(len: u64) -> result<_, stream-error>; + blocking-write-zeroes-and-flush: func(len: u64) -> result<_, stream-error>; + splice: func(src: borrow, len: u64) -> result; + blocking-splice: func(src: borrow, len: u64) -> result; + } +} + diff --git a/plugins/keyvalue-filesystem/wit/deps/wasi-keyvalue-0.2.0-draft/package.wit b/plugins/keyvalue-filesystem/wit/deps/wasi-keyvalue-0.2.0-draft/package.wit new file mode 100644 index 00000000..412280a9 --- /dev/null +++ b/plugins/keyvalue-filesystem/wit/deps/wasi-keyvalue-0.2.0-draft/package.wit @@ -0,0 +1,240 @@ +package wasi:keyvalue@0.2.0-draft; + +/// A keyvalue interface that provides eventually consistent key-value operations. +/// +/// Each of these operations acts on a single key-value pair. +/// +/// The value in the key-value pair is defined as a `u8` byte array and the intention is that it is +/// the common denominator for all data types defined by different key-value stores to handle data, +/// ensuring compatibility between different key-value stores. Note: the clients will be expecting +/// serialization/deserialization overhead to be handled by the key-value store. The value could be +/// a serialized object from JSON, HTML or vendor-specific data types like AWS S3 objects. +/// +/// Data consistency in a key value store refers to the guarantee that once a write operation +/// completes, all subsequent read operations will return the value that was written. +/// +/// Any implementation of this interface must have enough consistency to guarantee "reading your +/// writes." In particular, this means that the client should never get a value that is older than +/// the one it wrote, but it MAY get a newer value if one was written around the same time. These +/// guarantees only apply to the same client (which will likely be provided by the host or an +/// external capability of some kind). In this context a "client" is referring to the caller or +/// guest that is consuming this interface. Once a write request is committed by a specific client, +/// all subsequent read requests by the same client will reflect that write or any subsequent +/// writes. Another client running in a different context may or may not immediately see the result +/// due to the replication lag. As an example of all of this, if a value at a given key is A, and +/// the client writes B, then immediately reads, it should get B. If something else writes C in +/// quick succession, then the client may get C. However, a client running in a separate context may +/// still see A or B +interface store { + /// The set of errors which may be raised by functions in this package + variant error { + /// The host does not recognize the store identifier requested. + no-such-store, + /// The requesting component does not have access to the specified store + /// (which may or may not exist). + access-denied, + /// Some implementation-specific error has occurred (e.g. I/O) + other(string), + } + + /// A response to a `list-keys` operation. + record key-response { + /// The list of keys returned by the query. + keys: list, + /// The continuation token to use to fetch the next page of keys. If this is `null`, then + /// there are no more keys to fetch. + cursor: option, + } + + /// A bucket is a collection of key-value pairs. Each key-value pair is stored as a entry in the + /// bucket, and the bucket itself acts as a collection of all these entries. + /// + /// It is worth noting that the exact terminology for bucket in key-value stores can very + /// depending on the specific implementation. For example: + /// + /// 1. Amazon DynamoDB calls a collection of key-value pairs a table + /// 2. Redis has hashes, sets, and sorted sets as different types of collections + /// 3. Cassandra calls a collection of key-value pairs a column family + /// 4. MongoDB calls a collection of key-value pairs a collection + /// 5. Riak calls a collection of key-value pairs a bucket + /// 6. Memcached calls a collection of key-value pairs a slab + /// 7. Azure Cosmos DB calls a collection of key-value pairs a container + /// + /// In this interface, we use the term `bucket` to refer to a collection of key-value pairs + resource bucket { + /// Get the value associated with the specified `key` + /// + /// The value is returned as an option. If the key-value pair exists in the + /// store, it returns `Ok(value)`. If the key does not exist in the + /// store, it returns `Ok(none)`. + /// + /// If any other error occurs, it returns an `Err(error)`. + get: func(key: string) -> result>, error>; + /// Set the value associated with the key in the store. If the key already + /// exists in the store, it overwrites the value. + /// + /// If the key does not exist in the store, it creates a new key-value pair. + /// + /// If any other error occurs, it returns an `Err(error)`. + set: func(key: string, value: list) -> result<_, error>; + /// Delete the key-value pair associated with the key in the store. + /// + /// If the key does not exist in the store, it does nothing. + /// + /// If any other error occurs, it returns an `Err(error)`. + delete: func(key: string) -> result<_, error>; + /// Check if the key exists in the store. + /// + /// If the key exists in the store, it returns `Ok(true)`. If the key does + /// not exist in the store, it returns `Ok(false)`. + /// + /// If any other error occurs, it returns an `Err(error)`. + exists: func(key: string) -> result; + /// Get all the keys in the store with an optional cursor (for use in pagination). It + /// returns a list of keys. Please note that for most KeyValue implementations, this is a + /// can be a very expensive operation and so it should be used judiciously. Implementations + /// can return any number of keys in a single response, but they should never attempt to + /// send more data than is reasonable (i.e. on a small edge device, this may only be a few + /// KB, while on a large machine this could be several MB). Any response should also return + /// a cursor that can be used to fetch the next page of keys. See the `key-response` record + /// for more information. + /// + /// Note that the keys are not guaranteed to be returned in any particular order. + /// + /// If the store is empty, it returns an empty list. + /// + /// MAY show an out-of-date list of keys if there are concurrent writes to the store. + /// + /// If any error occurs, it returns an `Err(error)`. + list-keys: func(cursor: option) -> result; + } + + /// Get the bucket with the specified identifier. + /// + /// `identifier` must refer to a bucket provided by the host. + /// + /// `error::no-such-store` will be raised if the `identifier` is not recognized. + open: func(identifier: string) -> result; +} + +/// A keyvalue interface that provides atomic operations. +/// +/// Atomic operations are single, indivisible operations. When a fault causes an atomic operation to +/// fail, it will appear to the invoker of the atomic operation that the action either completed +/// successfully or did nothing at all. +/// +/// Please note that this interface is bare functions that take a reference to a bucket. This is to +/// get around the current lack of a way to "extend" a resource with additional methods inside of +/// wit. Future version of the interface will instead extend these methods on the base `bucket` +/// resource. +interface atomics { + use store.{bucket, error}; + + /// Atomically increment the value associated with the key in the store by the given delta. It + /// returns the new value. + /// + /// If the key does not exist in the store, it creates a new key-value pair with the value set + /// to the given delta. + /// + /// If any other error occurs, it returns an `Err(error)`. + increment: func(bucket: borrow, key: string, delta: u64) -> result; +} + +/// A keyvalue interface that provides batch operations. +/// +/// A batch operation is an operation that operates on multiple keys at once. +/// +/// Batch operations are useful for reducing network round-trip time. For example, if you want to +/// get the values associated with 100 keys, you can either do 100 get operations or you can do 1 +/// batch get operation. The batch operation is faster because it only needs to make 1 network call +/// instead of 100. +/// +/// A batch operation does not guarantee atomicity, meaning that if the batch operation fails, some +/// of the keys may have been modified and some may not. +/// +/// This interface does has the same consistency guarantees as the `store` interface, meaning that +/// you should be able to "read your writes." +/// +/// Please note that this interface is bare functions that take a reference to a bucket. This is to +/// get around the current lack of a way to "extend" a resource with additional methods inside of +/// wit. Future version of the interface will instead extend these methods on the base `bucket` +/// resource. +interface batch { + use store.{bucket, error}; + + /// Get the key-value pairs associated with the keys in the store. It returns a list of + /// key-value pairs. + /// + /// If any of the keys do not exist in the store, it returns a `none` value for that pair in the + /// list. + /// + /// MAY show an out-of-date value if there are concurrent writes to the store. + /// + /// If any other error occurs, it returns an `Err(error)`. + get-many: func(bucket: borrow, keys: list) -> result>>>, error>; + + /// Set the values associated with the keys in the store. If the key already exists in the + /// store, it overwrites the value. + /// + /// Note that the key-value pairs are not guaranteed to be set in the order they are provided. + /// + /// If any of the keys do not exist in the store, it creates a new key-value pair. + /// + /// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not + /// rollback the key-value pairs that were already set. Thus, this batch operation does not + /// guarantee atomicity, implying that some key-value pairs could be set while others might + /// fail. + /// + /// Other concurrent operations may also be able to see the partial results. + set-many: func(bucket: borrow, key-values: list>>) -> result<_, error>; + + /// Delete the key-value pairs associated with the keys in the store. + /// + /// Note that the key-value pairs are not guaranteed to be deleted in the order they are + /// provided. + /// + /// If any of the keys do not exist in the store, it skips the key. + /// + /// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not + /// rollback the key-value pairs that were already deleted. Thus, this batch operation does not + /// guarantee atomicity, implying that some key-value pairs could be deleted while others might + /// fail. + /// + /// Other concurrent operations may also be able to see the partial results. + delete-many: func(bucket: borrow, keys: list) -> result<_, error>; +} + +/// A keyvalue interface that provides watch operations. +/// +/// This interface is used to provide event-driven mechanisms to handle +/// keyvalue changes. +interface watcher { + use store.{bucket}; + + /// Handle the `set` event for the given bucket and key. It includes a reference to the `bucket` + /// that can be used to interact with the store. + on-set: func(bucket: bucket, key: string, value: list); + + /// Handle the `delete` event for the given bucket and key. It includes a reference to the + /// `bucket` that can be used to interact with the store. + on-delete: func(bucket: bucket, key: string); +} + +/// The `wasi:keyvalue/imports` world provides common APIs for interacting with key-value stores. +/// Components targeting this world will be able to do: +/// +/// 1. CRUD (create, read, update, delete) operations on key-value stores. +/// 2. Atomic `increment` and CAS (compare-and-swap) operations. +/// 3. Batch operations that can reduce the number of round trips to the network. +world imports { + import store; + import atomics; + import batch; +} +world watch-service { + import store; + import atomics; + import batch; + + export watcher; +} diff --git a/plugins/keyvalue-filesystem/wit/deps/wasi-logging-0.1.0-draft/package.wit b/plugins/keyvalue-filesystem/wit/deps/wasi-logging-0.1.0-draft/package.wit new file mode 100644 index 00000000..164cb5b9 --- /dev/null +++ b/plugins/keyvalue-filesystem/wit/deps/wasi-logging-0.1.0-draft/package.wit @@ -0,0 +1,36 @@ +package wasi:logging@0.1.0-draft; + +/// WASI Logging is a logging API intended to let users emit log messages with +/// simple priority levels and context values. +interface logging { + /// A log level, describing a kind of message. + enum level { + /// Describes messages about the values of variables and the flow of + /// control within a program. + trace, + /// Describes messages likely to be of interest to someone debugging a + /// program. + debug, + /// Describes messages likely to be of interest to someone monitoring a + /// program. + info, + /// Describes messages indicating hazardous situations. + warn, + /// Describes messages indicating serious errors. + error, + /// Describes messages indicating fatal errors. + critical, + } + + /// Emit a log message. + /// + /// A log message has a `level` describing what kind of message is being + /// sent, a context, which is an uninterpreted string meant to help + /// consumers group similar messages, and a string containing the message + /// text. + log: func(level: level, context: string, message: string); +} + +world imports { + import logging; +} diff --git a/plugins/keyvalue-filesystem/wit/deps/wasi-random-0.2.0/package.wit b/plugins/keyvalue-filesystem/wit/deps/wasi-random-0.2.0/package.wit new file mode 100644 index 00000000..58c179e1 --- /dev/null +++ b/plugins/keyvalue-filesystem/wit/deps/wasi-random-0.2.0/package.wit @@ -0,0 +1,18 @@ +package wasi:random@0.2.0; + +interface random { + get-random-bytes: func(len: u64) -> list; + + get-random-u64: func() -> u64; +} + +interface insecure { + get-insecure-random-bytes: func(len: u64) -> list; + + get-insecure-random-u64: func() -> u64; +} + +interface insecure-seed { + insecure-seed: func() -> tuple; +} + diff --git a/plugins/keyvalue-filesystem/wit/deps/wasi-sockets-0.2.0/package.wit b/plugins/keyvalue-filesystem/wit/deps/wasi-sockets-0.2.0/package.wit new file mode 100644 index 00000000..0602b855 --- /dev/null +++ b/plugins/keyvalue-filesystem/wit/deps/wasi-sockets-0.2.0/package.wit @@ -0,0 +1,179 @@ +package wasi:sockets@0.2.0; + +interface network { + resource network; + + enum error-code { + unknown, + access-denied, + not-supported, + invalid-argument, + out-of-memory, + timeout, + concurrency-conflict, + not-in-progress, + would-block, + invalid-state, + new-socket-limit, + address-not-bindable, + address-in-use, + remote-unreachable, + connection-refused, + connection-reset, + connection-aborted, + datagram-too-large, + name-unresolvable, + temporary-resolver-failure, + permanent-resolver-failure, + } + + enum ip-address-family { + ipv4, + ipv6, + } + + type ipv4-address = tuple; + + type ipv6-address = tuple; + + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + record ipv4-socket-address { + port: u16, + address: ipv4-address, + } + + record ipv6-socket-address { + port: u16, + flow-info: u32, + address: ipv6-address, + scope-id: u32, + } + + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } +} + +interface instance-network { + use network.{network}; + + instance-network: func() -> network; +} + +interface udp { + use wasi:io/poll@0.2.0.{pollable}; + use network.{network, error-code, ip-socket-address, ip-address-family}; + + record incoming-datagram { + data: list, + remote-address: ip-socket-address, + } + + record outgoing-datagram { + data: list, + remote-address: option, + } + + resource udp-socket { + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + finish-bind: func() -> result<_, error-code>; + %stream: func(remote-address: option) -> result, error-code>; + local-address: func() -> result; + remote-address: func() -> result; + address-family: func() -> ip-address-family; + unicast-hop-limit: func() -> result; + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + receive-buffer-size: func() -> result; + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + send-buffer-size: func() -> result; + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + subscribe: func() -> pollable; + } + + resource incoming-datagram-stream { + receive: func(max-results: u64) -> result, error-code>; + subscribe: func() -> pollable; + } + + resource outgoing-datagram-stream { + check-send: func() -> result; + send: func(datagrams: list) -> result; + subscribe: func() -> pollable; + } +} + +interface udp-create-socket { + use network.{network, error-code, ip-address-family}; + use udp.{udp-socket}; + + create-udp-socket: func(address-family: ip-address-family) -> result; +} + +interface tcp { + use wasi:io/streams@0.2.0.{input-stream, output-stream}; + use wasi:io/poll@0.2.0.{pollable}; + use wasi:clocks/monotonic-clock@0.2.0.{duration}; + use network.{network, error-code, ip-socket-address, ip-address-family}; + + enum shutdown-type { + receive, + send, + both, + } + + resource tcp-socket { + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + finish-bind: func() -> result<_, error-code>; + start-connect: func(network: borrow, remote-address: ip-socket-address) -> result<_, error-code>; + finish-connect: func() -> result, error-code>; + start-listen: func() -> result<_, error-code>; + finish-listen: func() -> result<_, error-code>; + accept: func() -> result, error-code>; + local-address: func() -> result; + remote-address: func() -> result; + is-listening: func() -> bool; + address-family: func() -> ip-address-family; + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + keep-alive-enabled: func() -> result; + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + keep-alive-idle-time: func() -> result; + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + keep-alive-interval: func() -> result; + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + keep-alive-count: func() -> result; + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + hop-limit: func() -> result; + set-hop-limit: func(value: u8) -> result<_, error-code>; + receive-buffer-size: func() -> result; + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + send-buffer-size: func() -> result; + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + subscribe: func() -> pollable; + shutdown: func(shutdown-type: shutdown-type) -> result<_, error-code>; + } +} + +interface tcp-create-socket { + use network.{network, error-code, ip-address-family}; + use tcp.{tcp-socket}; + + create-tcp-socket: func(address-family: ip-address-family) -> result; +} + +interface ip-name-lookup { + use wasi:io/poll@0.2.0.{pollable}; + use network.{network, error-code, ip-address}; + + resource resolve-address-stream { + resolve-next-address: func() -> result, error-code>; + subscribe: func() -> pollable; + } + + resolve-addresses: func(network: borrow, name: string) -> result; +} + diff --git a/plugins/keyvalue-filesystem/wit/deps/wasmcloud-wash-0.0.2/package.wit b/plugins/keyvalue-filesystem/wit/deps/wasmcloud-wash-0.0.2/package.wit new file mode 100644 index 00000000..e88e4420 --- /dev/null +++ b/plugins/keyvalue-filesystem/wit/deps/wasmcloud-wash-0.0.2/package.wit @@ -0,0 +1,264 @@ +package wasmcloud:wash@0.0.2; + +interface types { + /// Hook types that can be registered by plugins + /// Hooks are executed at specific times during the wash lifecycle + /// and can be used to modify behavior, perform preflight checks, or handle errors. + enum hook-type { + /// Default fallback + unknown, + /// Called before `wash doctor` runs, useful for preflight checks for plugins + before-doctor, + /// Called after `wash doctor` runs, useful to examine results and offer recommendations + after-doctor, + /// Called before `wash build` runs + before-build, + /// Called after `wash build` runs + after-build, + /// Called before `wash push` runs + before-push, + /// Called after `wash push` runs + after-push, + /// Called before `wash dev` runs and starts the development loop + before-dev, + /// Called after `wash dev` ends the development loop + after-dev, + /// Called to register a component's exports for use in `wash dev` + dev-register, + } + + /// Used for flags and arguments in commands + /// Flags are registered as `wash -- ` + /// Arguments are registered in order as `wash ` + /// + /// Flags and arguments can be optional, and the final value is populated by the CLI parser. If you set default to None, + /// then the final value should always be Some(value) before the command is executed. This should still be validated. + record command-argument { + /// Human readable name for the argument + name: string, + /// Short description of the argument + description: string, + /// An environment variable that can be used to set this argument. It's strongly recommended to namespace + /// this value to avoid conflicts with other plugins. + env: option, + /// Default value, if any. If omitted, the argument is required. If present, the argument is optional. + default: option, + /// Final value for this argument, populated by the CLI parser. + value: option, + } + + /// A command is a registered action that can be executed by the user. + /// Commands are registered as `wash `, and can have flags and arguments. + record command { + /// Unique identifier for the command, usually the same as the command name + id: string, + /// The command name, registered as `wash ` + name: string, + /// Short human-friendly description of the command + description: string, + /// Command flags, registered as `wash -- ` + %flags: list>, + /// Command positional arguments, registered in order as `wash ` + arguments: list, + /// List of sample command usage + usage: list, + } + + /// Metadata for a plugin, used to register the plugin with wash. + /// This metadata is used to display information about the plugin in the CLI and to register this plugins + /// place in the wash command lifecycle, including commands and hooks. + record metadata { + /// Internal Unique identifier for the plugin + /// e.g. `dev.wasmcloud.oci` + id: string, + /// The plugin name as referred to in the CLI. For this reason, this name should not contain spaces or special characters. + /// Ex: "oci" + name: string, + /// Short human-friendly description of the plugin + description: string, + /// The plugin author contact information + /// This is usually a name and email address + /// Ex: "WasmCloud Team " + contact: string, + /// Source or Documentation URL for the Plugin + /// Ex: https://github.com/wasmcloud/wash-oci-plugin + url: string, + /// The plugin license + /// Ex: "Apache-2.0" + license: string, + /// The plugin version + /// Ex: "0.1.0" + version: string, + /// The top level command for this plugin. + /// If set, this plugin will be registered as 'wash ' and will not have a command name. + /// If omitted, this plugin will be registered as 'wash ' for each + /// command in the `sub-commands` list. + command: option, + /// All subcommands for this plugin. If `command` is set, this list should be empty and will be ignored. + sub-commands: list, + /// Hooks to register for this plugin. This list opts a plugin into the wash command lifecycle + hooks: list, + } + + /// Plugin config is a key/value store that can be used to share information between instances of the plugin. + /// This is useful for passing information between commands and hooks, or for storing state that's accessed + /// across multiple invocations of the plugin. + /// + /// Plugin config key/value pairs are available both in this object and through wasi:config/runtime calls. + /// + /// This is a global store for all instances of the plugin and race contentions should be handled with care. + /// The contents of the store are not persisted after wash's execution ends. + resource plugin-config { + /// Get the value for a given key in the plugin config. + get: func(key: string) -> option; + /// Set the value for a given plugin config key. Returns the value if it already existed, or none if it was newly created. + set: func(key: string, value: string) -> option; + /// Delete the value for a given key in the plugin config, if it exists. + /// Returns the deleted value if it existed, or none if it did not. + delete: func(key: string) -> option; + /// List all keys in the plugin config. + %list: func() -> list; + } + + /// TODO(ISSUE#5): Expose project configuration to plugins + resource project-config { + /// Current wash version + /// Ex: 0.39.3 + version: func() -> string; + } + + /// Shared context between the plugin and wash, enabling both to read and write key/value pairs + /// for dynamic behavior modification. This allows plugins to influence wash operations by updating + /// context values (e.g., modifying OCI artifact annotations before a push). + /// + /// The context is accessible to both wash and plugins during command and hook execution, supporting + /// collaborative state changes and behavioral overrides. + /// + /// Context data is not persisted beyond the current wash execution and should be treated as ephemeral. + resource context { + /// Get the value for a given context key. + /// Ex: After wash build: context.get("tinygo.path") -> "/opt/homebrew/bin/tinygo" + get: func(key: string) -> option; + /// Set the value for a given context key. Returns the value if it already existed, or none if it was newly created. + /// Ex: Before wash push: context.set("oci.annotations", "foo=bar") + set: func(key: string, value: string) -> option; + /// Delete the value for a given context key, if it exists. Can be used to remove keys from the context. + /// Returns the deleted value if it existed, or none if it did not. + /// Ex: Before wash push: context.delete("oci.password") + delete: func(key: string) -> option; + /// List all keys in the context. + %list: func() -> list; + } + + /// The runner resource provides access to the wash runtime and its configuration. + /// It allows plugins to interact with the wash environment, execute commands, and manage configuration. + resource runner { + /// Project configuration is provided when running in a project context, for example during the `build` or `dev` + /// commands. If the plugin is not running in a project context, this will return none. + /// project-config: func() -> option; + /// Shared context between the plugin and wash, enabling both to read and write key/value pairs + /// for dynamic behavior modification. This allows plugins to influence wash operations by updating + /// context values (e.g., modifying OCI artifact annotations before a push). + /// + /// The context is accessible to both wash and plugins during command and hook execution, supporting + /// collaborative state changes and behavioral overrides. + context: func() -> result; + /// Plugin config is a key/value store that can be used to share information between instances of the plugin. + /// This is useful for passing information between commands and hooks, or for storing state that's accessed + /// across multiple invocations of the plugin. + /// + /// Plugin config key/value pairs are available both in this object and through wasi:config/runtime calls. + /// + /// This is a global store for all instances of the plugin and race contentions should be handled with care. + /// The contents of the store are not persisted after wash's execution ends. + plugin-config: func() -> result; + /// Executes a host native binary on behalf of a plugin + /// Commands are executed under 'sh -c' and inherit the environment and working directory. + /// wash will ask the user for confirmation before executing this function. + /// + /// Returns the stdout and stderr of the command as a tuple. + host-exec: func(bin: string, args: list) -> result, string>; + /// Executes a host native binary on behalf of a plugin in the background. + /// Commands are executed under 'sh -c' and inherit the environment and working directory. + /// wash will ask the user for confirmation before executing this function. + /// + /// The lifecycle of this command is tied to the CLI execution context. This means it will + /// continue to run until the CLI session ends and then be killed. + host-exec-background: func(bin: string, args: list) -> result<_, string>; + } +} + +interface plugin { + use types.{metadata, runner, command, hook-type}; + + /// Called by wash to retrieve the plugin metadata. It's recommended to avoid + /// any computation or logging in this function as it's called on each command execution. + info: func() -> metadata; + + /// Called before any commands or hooks are executed so that the plugin could take preflight actions + /// such as checking the environment, validating configuration, or preparing resources. Note that + /// any in-memory state will _not_ be persisted in this component, and the plugin-config store + /// should be used for any state that needs to be shared across commands or hooks. + initialize: func(runner: runner) -> result; + + /// Handle the execution of a given command. The resulting value should be the string that will + /// be printed to the user, or an error message if the command failed. + run: func(runner: runner, cmd: command) -> result; + + /// Handle the execution of a given hook type. The resulting value should be the string that will + /// be printed to the user, or an error message if the hook failed. + hook: func(runner: runner, hook: hook-type) -> result; +} + +/// The world for exported bindings for wash plugins. Exporting the plugin interface is the +/// only requirement for a component to be used as a plugin in wash. +world wash-plugin { + import types; + + export plugin; +} +/// An example world showing the list of interfaces that are available to plugins. +world plugin-guest { + import wasi:logging/logging@0.1.0-draft; + import wasi:cli/environment@0.2.0; + import wasi:cli/exit@0.2.0; + import wasi:io/error@0.2.0; + import wasi:io/poll@0.2.0; + import wasi:io/streams@0.2.0; + import wasi:cli/stdin@0.2.0; + import wasi:cli/stdout@0.2.0; + import wasi:cli/stderr@0.2.0; + import wasi:cli/terminal-input@0.2.0; + import wasi:cli/terminal-output@0.2.0; + import wasi:cli/terminal-stdin@0.2.0; + import wasi:cli/terminal-stdout@0.2.0; + import wasi:cli/terminal-stderr@0.2.0; + import wasi:clocks/monotonic-clock@0.2.0; + import wasi:clocks/wall-clock@0.2.0; + import wasi:filesystem/types@0.2.0; + import wasi:filesystem/preopens@0.2.0; + import wasi:sockets/network@0.2.0; + import wasi:sockets/instance-network@0.2.0; + import wasi:sockets/udp@0.2.0; + import wasi:sockets/udp-create-socket@0.2.0; + import wasi:sockets/tcp@0.2.0; + import wasi:sockets/tcp-create-socket@0.2.0; + import wasi:sockets/ip-name-lookup@0.2.0; + import wasi:random/random@0.2.0; + import wasi:random/insecure@0.2.0; + import wasi:random/insecure-seed@0.2.0; + import types; + + export plugin; +} +/// An example world of a plugin that can be used in the hot reload loop, namely exporting the HTTP bindings +world dev { + import wasi:logging/logging@0.1.0-draft; + import wasi:io/poll@0.2.0; + import wasi:clocks/monotonic-clock@0.2.0; + import wasi:io/error@0.2.0; + import wasi:io/streams@0.2.0; + import wasi:http/types@0.2.0; + + export wasi:http/incoming-handler@0.2.0; +} diff --git a/plugins/keyvalue-filesystem/wit/world.wit b/plugins/keyvalue-filesystem/wit/world.wit new file mode 100644 index 00000000..b5ec5fa7 --- /dev/null +++ b/plugins/keyvalue-filesystem/wit/world.wit @@ -0,0 +1,9 @@ +package wasmcloud:plugin@0.1.0; + +world keyvalue-filesystem { + + export wasmcloud:wash/plugin@0.0.2; + export wasi:keyvalue/store@0.2.0-draft; + export wasi:keyvalue/atomics@0.2.0-draft; + export wasi:keyvalue/batch@0.2.0-draft; +}