From bcccdd54f2f9fcbc595bb7073a0a25dc0a6c5602 Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Tue, 24 Mar 2026 11:30:18 +0300 Subject: [PATCH 01/21] feat(task): add function_name field to task struct --- pkg/task/task.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/task/task.go b/pkg/task/task.go index 4d8bdafe..6de87bdc 100644 --- a/pkg/task/task.go +++ b/pkg/task/task.go @@ -68,6 +68,7 @@ type Task struct { Kind TaskKind `json:"kind,omitempty"` State State `json:"state"` ImageURL string `json:"image_url,omitempty"` + FunctionName string `json:"function_name,omitempty"` File []byte `json:"file,omitempty"` CLIArgs []string `json:"cli_args"` Inputs []uint64 `json:"inputs,omitempty"` From c582200099da0c88d38d2a1c295255bd49eb3047 Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Tue, 24 Mar 2026 11:33:32 +0300 Subject: [PATCH 02/21] feat(storage): add function_name column to postgres and sqlite task repos --- pkg/storage/postgres/init.go | 9 +++++++++ pkg/storage/postgres/tasks.go | 22 ++++++++++++++-------- pkg/storage/sqlite/init.go | 9 +++++++++ pkg/storage/sqlite/tasks.go | 16 ++++++++++------ 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/pkg/storage/postgres/init.go b/pkg/storage/postgres/init.go index 6465d4b6..b48f1eb5 100644 --- a/pkg/storage/postgres/init.go +++ b/pkg/storage/postgres/init.go @@ -250,6 +250,15 @@ func (db *Database) Migrate() error { `ALTER TABLE proplets DROP COLUMN IF EXISTS metadata`, }, }, + { + Id: "4_add_function_name", + Up: []string{ + `ALTER TABLE tasks ADD COLUMN IF NOT EXISTS function_name TEXT`, + }, + Down: []string{ + `ALTER TABLE tasks DROP COLUMN IF EXISTS function_name`, + }, + }, }, } diff --git a/pkg/storage/postgres/tasks.go b/pkg/storage/postgres/tasks.go index 4fb4b685..a40ccf29 100644 --- a/pkg/storage/postgres/tasks.go +++ b/pkg/storage/postgres/tasks.go @@ -22,6 +22,7 @@ type dbTask struct { Name string `db:"name"` State uint8 `db:"state"` ImageURL *string `db:"image_url"` + FunctionName *string `db:"function_name"` File []byte `db:"file"` CLIArgs []byte `db:"cli_args"` Inputs []byte `db:"inputs"` @@ -45,13 +46,13 @@ type dbTask struct { Mode *string `db:"mode"` } -const taskColumns = `id, name, state, image_url, file, cli_args, inputs, env, daemon, encrypted, +const taskColumns = `id, name, state, image_url, function_name, file, cli_args, inputs, env, daemon, encrypted, kbs_resource_path, proplet_id, results, error, monitoring_profile, start_time, finish_time, created_at, updated_at, workflow_id, job_id, depends_on, run_if, kind, mode` func (r *taskRepo) Create(ctx context.Context, t task.Task) (task.Task, error) { query := `INSERT INTO tasks (` + taskColumns + `) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)` + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)` cliArgs, err := jsonBytes(t.CLIArgs) if err != nil { @@ -86,6 +87,7 @@ func (r *taskRepo) Create(ctx context.Context, t task.Task) (task.Task, error) { _, err = r.db.ExecContext(ctx, query, t.ID, t.Name, uint8(t.State), nullString(t.ImageURL), + nullString(t.FunctionName), t.File, cliArgs, inputs, @@ -131,11 +133,11 @@ func (r *taskRepo) Get(ctx context.Context, id string) (task.Task, error) { func (r *taskRepo) Update(ctx context.Context, t task.Task) error { query := `UPDATE tasks SET - name = $2, state = $3, image_url = $4, file = $5, cli_args = $6, inputs = $7, - env = $8, daemon = $9, encrypted = $10, kbs_resource_path = $11, proplet_id = $12, - results = $13, error = $14, monitoring_profile = $15, start_time = $16, - finish_time = $17, updated_at = $18, workflow_id = $19, job_id = $20, - depends_on = $21, run_if = $22, kind = $23, mode = $24 + name = $2, state = $3, image_url = $4, function_name = $5, file = $6, cli_args = $7, inputs = $8, + env = $9, daemon = $10, encrypted = $11, kbs_resource_path = $12, proplet_id = $13, + results = $14, error = $15, monitoring_profile = $16, start_time = $17, + finish_time = $18, updated_at = $19, workflow_id = $20, job_id = $21, + depends_on = $22, run_if = $23, kind = $24, mode = $25 WHERE id = $1` cliArgs, err := jsonBytes(t.CLIArgs) @@ -171,6 +173,7 @@ func (r *taskRepo) Update(ctx context.Context, t task.Task) error { _, err = r.db.ExecContext(ctx, query, t.ID, t.Name, uint8(t.State), nullString(t.ImageURL), + nullString(t.FunctionName), t.File, cliArgs, inputs, env, t.Daemon, t.Encrypted, @@ -246,7 +249,7 @@ func (r *taskRepo) scanTasks(ctx context.Context, query string, args ...any) ([] for rows.Next() { var dbt dbTask if err := rows.Scan( - &dbt.ID, &dbt.Name, &dbt.State, &dbt.ImageURL, + &dbt.ID, &dbt.Name, &dbt.State, &dbt.ImageURL, &dbt.FunctionName, &dbt.File, &dbt.CLIArgs, &dbt.Inputs, &dbt.Env, &dbt.Daemon, &dbt.Encrypted, &dbt.KBSResourcePath, &dbt.PropletID, &dbt.Results, &dbt.Error, &dbt.MonitoringProfile, @@ -287,6 +290,9 @@ func (r *taskRepo) toTask(dbt dbTask) (task.Task, error) { if dbt.ImageURL != nil { t.ImageURL = *dbt.ImageURL } + if dbt.FunctionName != nil { + t.FunctionName = *dbt.FunctionName + } if err := jsonUnmarshal(dbt.CLIArgs, &t.CLIArgs); err != nil { return task.Task{}, err } diff --git a/pkg/storage/sqlite/init.go b/pkg/storage/sqlite/init.go index 2ad7da03..ec07a98a 100644 --- a/pkg/storage/sqlite/init.go +++ b/pkg/storage/sqlite/init.go @@ -243,6 +243,15 @@ func (db *Database) Migrate() error { `ALTER TABLE proplets DROP COLUMN metadata`, }, }, + { + Id: "4_add_function_name", + Up: []string{ + `ALTER TABLE tasks ADD COLUMN function_name TEXT`, + }, + Down: []string{ + `ALTER TABLE tasks DROP COLUMN function_name`, + }, + }, }, } diff --git a/pkg/storage/sqlite/tasks.go b/pkg/storage/sqlite/tasks.go index 99c46427..9a4b037f 100644 --- a/pkg/storage/sqlite/tasks.go +++ b/pkg/storage/sqlite/tasks.go @@ -24,6 +24,7 @@ type dbTask struct { Name string `db:"name"` State uint8 `db:"state"` ImageURL *string `db:"image_url"` + FunctionName *string `db:"function_name"` File []byte `db:"file"` CLIArgs []byte `db:"cli_args"` Inputs []byte `db:"inputs"` @@ -47,13 +48,13 @@ type dbTask struct { Mode *string `db:"mode"` } -const taskColumns = `id, name, state, image_url, file, cli_args, inputs, env, daemon, encrypted, +const taskColumns = `id, name, state, image_url, function_name, file, cli_args, inputs, env, daemon, encrypted, kbs_resource_path, proplet_id, results, error, monitoring_profile, start_time, finish_time, created_at, updated_at, workflow_id, job_id, depends_on, run_if, kind, mode` func (r *taskRepo) Create(ctx context.Context, t task.Task) (task.Task, error) { query := `INSERT INTO tasks (` + taskColumns + `) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` cliArgs, err := jsonBytes(t.CLIArgs) if err != nil { @@ -86,7 +87,7 @@ func (r *taskRepo) Create(ctx context.Context, t task.Task) (task.Task, error) { } _, err = r.db.ExecContext(ctx, query, - t.ID, t.Name, uint8(t.State), nullString(t.ImageURL), + t.ID, t.Name, uint8(t.State), nullString(t.ImageURL), nullString(t.FunctionName), t.File, cliArgs, inputs, env, t.Daemon, t.Encrypted, nullString(t.KBSResourcePath), nullString(t.PropletID), @@ -122,7 +123,7 @@ func (r *taskRepo) Get(ctx context.Context, id string) (task.Task, error) { func (r *taskRepo) Update(ctx context.Context, t task.Task) error { query := `UPDATE tasks SET - name = ?, state = ?, image_url = ?, file = ?, cli_args = ?, inputs = ?, + name = ?, state = ?, image_url = ?, function_name = ?, file = ?, cli_args = ?, inputs = ?, env = ?, daemon = ?, encrypted = ?, kbs_resource_path = ?, proplet_id = ?, results = ?, error = ?, monitoring_profile = ?, start_time = ?, finish_time = ?, updated_at = ?, workflow_id = ?, job_id = ?, @@ -160,7 +161,7 @@ func (r *taskRepo) Update(ctx context.Context, t task.Task) error { } _, err = r.db.ExecContext(ctx, query, - t.Name, uint8(t.State), nullString(t.ImageURL), + t.Name, uint8(t.State), nullString(t.ImageURL), nullString(t.FunctionName), t.File, cliArgs, inputs, env, t.Daemon, t.Encrypted, nullString(t.KBSResourcePath), nullString(t.PropletID), @@ -228,7 +229,7 @@ func (r *taskRepo) scanTasks(ctx context.Context, query string, args ...any) ([] for rows.Next() { var dbt dbTask if err := rows.Scan( - &dbt.ID, &dbt.Name, &dbt.State, &dbt.ImageURL, + &dbt.ID, &dbt.Name, &dbt.State, &dbt.ImageURL, &dbt.FunctionName, &dbt.File, &dbt.CLIArgs, &dbt.Inputs, &dbt.Env, &dbt.Daemon, &dbt.Encrypted, &dbt.KBSResourcePath, &dbt.PropletID, &dbt.Results, &dbt.Error, &dbt.MonitoringProfile, @@ -269,6 +270,9 @@ func (r *taskRepo) toTask(dbt dbTask) (task.Task, error) { if dbt.ImageURL != nil { t.ImageURL = *dbt.ImageURL } + if dbt.FunctionName != nil { + t.FunctionName = *dbt.FunctionName + } if dbt.CLIArgs != nil { if err := jsonUnmarshal(dbt.CLIArgs, &t.CLIArgs); err != nil { return task.Task{}, err From 9305bac1a7df8729f3e95332ec1693ac951c5abc Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Tue, 24 Mar 2026 11:34:15 +0300 Subject: [PATCH 03/21] feat(manager): include function_name in task dispatch payload --- manager/service.go | 1 + 1 file changed, 1 insertion(+) diff --git a/manager/service.go b/manager/service.go index c06d40f1..6839792b 100644 --- a/manager/service.go +++ b/manager/service.go @@ -1677,6 +1677,7 @@ func (svc *service) publishStart(ctx context.Context, t task.Task, propletID str payload := map[string]any{ "id": t.ID, "name": t.Name, + "function_name": t.FunctionName, "state": t.State, "image_url": t.ImageURL, "file": t.File, From 8b8155fad7319162d728b4e30529603919b682e3 Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Tue, 24 Mar 2026 11:36:13 +0300 Subject: [PATCH 04/21] feat(proplet): support custom component exports via function_name --- proplet/src/runtime/wasmtime_runtime.rs | 167 ++++++++++++++++++++++++ proplet/src/service.rs | 6 +- proplet/src/types.rs | 2 + 3 files changed, 174 insertions(+), 1 deletion(-) diff --git a/proplet/src/runtime/wasmtime_runtime.rs b/proplet/src/runtime/wasmtime_runtime.rs index 9bd1940b..3fb9cef1 100644 --- a/proplet/src/runtime/wasmtime_runtime.rs +++ b/proplet/src/runtime/wasmtime_runtime.rs @@ -118,8 +118,14 @@ impl Runtime for WasmtimeRuntime { is_proxy, ); + let has_custom_export = !config.function_name.is_empty() + && config.function_name != "_start" + && !config.function_name.starts_with("fl-round-"); + if is_proxy { self.start_app_proxy(config).await + } else if is_component && has_custom_export { + self.start_app_component_export(config).await } else if is_component { self.start_app_component(config).await } else { @@ -317,6 +323,167 @@ impl WasmtimeRuntime { } } + async fn start_app_component_export(&self, config: StartConfig) -> Result> { + info!( + "Compiling WASM component for custom export '{}' for task: {}", + config.function_name, config.id + ); + + let component = match component::Component::from_binary(&self.engine, &config.wasm_binary) { + Ok(c) => c, + Err(e) => { + return Err(anyhow::anyhow!( + "Failed to compile WASM component from binary: {e}" + )) + } + }; + + let mut wasi_builder = WasiCtxBuilder::new(); + wasi_builder.inherit_stdio(); + for (key, value) in &config.env { + wasi_builder.env(key, value); + } + for dir in &self.preopened_dirs { + let _ = wasi_builder + .preopened_dir(dir, dir, DirPerms::all(), FilePerms::all()) + .map_err(|e| format!("Failed to preopen directory '{dir}': {e}")); + } + let wasi = wasi_builder.build(); + + let store_data = StoreData { + wasi, + http: WasiHttpCtx::new(), + table: ResourceTable::new(), + }; + + let mut store = Store::new(&self.engine, store_data); + + let mut linker: component::Linker = component::Linker::new(&self.engine); + let _ = wasmtime_wasi::p2::add_to_linker_sync(&mut linker) + .map_err(|e| format!("Failed to add WASI P2 to component linker: {e}")); + + let task_id = config.id.clone(); + let task_id_for_cleanup = task_id.clone(); + let function_name = config.function_name.clone(); + let args = config.args.clone(); + let tasks = self.tasks.clone(); + + let (result_tx, result_rx) = oneshot::channel(); + + let handle = tokio::task::spawn(async move { + let result = tokio::task::spawn_blocking(move || { + let instance = match linker.instantiate(&mut store, &component) { + Ok(i) => i, + Err(e) => { + return Err(anyhow::anyhow!( + "Failed to instantiate WASM component: {e}" + )) + } + }; + + let func = instance + .get_func(&mut store, &function_name) + .ok_or_else(|| { + anyhow::anyhow!( + "Export '{}' not found in component for task {}", + function_name, + task_id + ) + })?; + + let func_ty = func.ty(&store); + let param_types: Vec<_> = func_ty.params().collect(); + let result_count = func_ty.results().count(); + + if args.len() != param_types.len() { + return Err(anyhow::anyhow!( + "Argument count mismatch for '{}': expected {} but got {}", + function_name, + param_types.len(), + args.len() + )); + } + + let wasm_args: Vec = args + .iter() + .zip(param_types.iter()) + .map(|(arg, (_, ty))| match ty { + component::Type::Bool => Ok(component::Val::Bool(*arg != 0)), + component::Type::S8 => Ok(component::Val::S8(*arg as i8)), + component::Type::U8 => Ok(component::Val::U8(*arg as u8)), + component::Type::S16 => Ok(component::Val::S16(*arg as i16)), + component::Type::U16 => Ok(component::Val::U16(*arg as u16)), + component::Type::S32 => Ok(component::Val::S32(*arg as i32)), + component::Type::U32 => Ok(component::Val::U32(*arg as u32)), + component::Type::S64 => Ok(component::Val::S64(*arg as i64)), + component::Type::U64 => Ok(component::Val::U64(*arg)), + component::Type::Float32 => { + Ok(component::Val::Float32(f32::from_bits(*arg as u32))) + } + component::Type::Float64 => { + Ok(component::Val::Float64(f64::from_bits(*arg))) + } + _ => Err(anyhow::anyhow!( + "Unsupported WIT parameter type for export '{}'", + function_name + )), + }) + .collect::>>()?; + + let mut results: Vec = + (0..result_count).map(|_| component::Val::Bool(false)).collect(); + + func.call(&mut store, &wasm_args, &mut results).map_err(|e| { + anyhow::anyhow!("Failed to call export '{}': {e}", function_name) + })?; + + let result_string = results.first().map(|v| match v { + component::Val::Bool(b) => b.to_string(), + component::Val::S8(n) => n.to_string(), + component::Val::U8(n) => n.to_string(), + component::Val::S16(n) => n.to_string(), + component::Val::U16(n) => n.to_string(), + component::Val::S32(n) => n.to_string(), + component::Val::U32(n) => n.to_string(), + component::Val::S64(n) => n.to_string(), + component::Val::U64(n) => n.to_string(), + component::Val::Float32(f) => f.to_string(), + component::Val::Float64(f) => f.to_string(), + component::Val::String(s) => s.clone(), + _ => String::new(), + }).unwrap_or_default(); + + info!( + "Export '{}' for task {} completed, result: {}", + function_name, task_id, result_string + ); + + Ok(result_string.into_bytes()) + }) + .await; + + tasks.lock().await.remove(&task_id_for_cleanup); + + let final_result = match result { + Ok(Ok(data)) => Ok(data), + Ok(Err(e)) => Err(e), + Err(e) => Err(anyhow::anyhow!("Task join error: {e}")), + }; + + let _ = result_tx.send(final_result); + }); + + { + let mut tasks_map = self.tasks.lock().await; + tasks_map.insert(config.id.clone(), handle); + } + + match result_rx.await { + Ok(result) => result, + Err(_) => Err(anyhow::anyhow!("Task was cancelled or panicked")), + } + } + async fn start_app_proxy(&self, config: StartConfig) -> Result> { if !self.http_enabled { return Err(anyhow::anyhow!( diff --git a/proplet/src/service.rs b/proplet/src/service.rs index aa9e1775..addd02eb 100644 --- a/proplet/src/service.rs +++ b/proplet/src/service.rs @@ -572,7 +572,11 @@ impl PropletService { // so they can be included in the environment variables let mut config = StartConfig { id: task_id.clone(), - function_name: task_name.clone(), + function_name: req.function_name + .as_deref() + .filter(|s| !s.is_empty()) + .map(String::from) + .unwrap_or_else(|| task_name.clone()), daemon, wasm_binary, cli_args, diff --git a/proplet/src/types.rs b/proplet/src/types.rs index 80cc1270..aa264d97 100644 --- a/proplet/src/types.rs +++ b/proplet/src/types.rs @@ -66,6 +66,8 @@ pub struct StartRequest { pub mode: Option, #[serde(default)] pub proplet_id: Option, + #[serde(default)] + pub function_name: Option, } fn deserialize_null_default<'de, D, T>(deserializer: D) -> std::result::Result From a976de7c5dd5fe0ab6319f8803019c64c3ae0b9d Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Tue, 24 Mar 2026 12:04:53 +0300 Subject: [PATCH 05/21] feat(task): change inputs from []uint64 to []string for WAVE encoding --- pkg/task/task.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/task/task.go b/pkg/task/task.go index 6de87bdc..201a5f46 100644 --- a/pkg/task/task.go +++ b/pkg/task/task.go @@ -71,7 +71,7 @@ type Task struct { FunctionName string `json:"function_name,omitempty"` File []byte `json:"file,omitempty"` CLIArgs []string `json:"cli_args"` - Inputs []uint64 `json:"inputs,omitempty"` + Inputs []string `json:"inputs,omitempty"` Env map[string]string `json:"env,omitempty"` Daemon bool `json:"daemon"` Encrypted bool `json:"encrypted"` From 1bd3a803ff0e5128a0396f33fe967b92b8b9b491 Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Tue, 24 Mar 2026 12:04:58 +0300 Subject: [PATCH 06/21] feat(proplet): change inputs to Vec for WAVE-encoded arguments --- proplet/src/runtime/mod.rs | 2 +- proplet/src/types.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/proplet/src/runtime/mod.rs b/proplet/src/runtime/mod.rs index b14523ca..cf4da0aa 100644 --- a/proplet/src/runtime/mod.rs +++ b/proplet/src/runtime/mod.rs @@ -14,7 +14,7 @@ pub struct StartConfig { pub wasm_binary: Vec, pub cli_args: Vec, pub env: HashMap, - pub args: Vec, + pub args: Vec, #[allow(dead_code)] pub mode: Option, } diff --git a/proplet/src/types.rs b/proplet/src/types.rs index aa264d97..509e18de 100644 --- a/proplet/src/types.rs +++ b/proplet/src/types.rs @@ -51,7 +51,7 @@ pub struct StartRequest { #[serde(default, deserialize_with = "deserialize_null_default")] pub image_url: String, #[serde(default, deserialize_with = "deserialize_null_default")] - pub inputs: Vec, + pub inputs: Vec, #[serde(default)] pub daemon: bool, #[serde(default)] @@ -306,7 +306,7 @@ mod tests { state: 0, file: "base64encodeddata".to_string(), image_url: String::new(), - inputs: vec![1, 2, 3], + inputs: vec!["1".to_string(), "2".to_string(), "3".to_string()], daemon: false, env: Some(HashMap::new()), monitoring_profile: None, @@ -527,7 +527,7 @@ mod tests { "cli_args": ["--arg1", "value1"], "file": "ZGF0YQ==", "image_url": "", - "inputs": [10, 20, 30], + "inputs": ["10", "20", "30"], "daemon": true, "env": { "KEY1": "value1", @@ -540,7 +540,7 @@ mod tests { assert_eq!(req.id, "task-complete"); assert_eq!(req.cli_args.len(), 2); - assert_eq!(req.inputs, vec![10, 20, 30]); + assert_eq!(req.inputs, vec!["10", "20", "30"]); assert!(req.daemon); assert_eq!(req.env.as_ref().unwrap().len(), 2); } From eb549a6a4bc75ad5f0f2c232ad33ebe759421c77 Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Tue, 24 Mar 2026 12:05:02 +0300 Subject: [PATCH 07/21] feat(proplet): use WAVE encoding for component export arguments --- proplet/Cargo.lock | 52 ++++++++++++++++++ proplet/Cargo.toml | 3 +- proplet/src/runtime/wasmtime_runtime.rs | 72 +++++++++++-------------- 3 files changed, 85 insertions(+), 42 deletions(-) diff --git a/proplet/Cargo.lock b/proplet/Cargo.lock index 9e6cfb06..eb647498 100644 --- a/proplet/Cargo.lock +++ b/proplet/Cargo.lock @@ -425,6 +425,12 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + [[package]] name = "bincode" version = "1.3.3" @@ -2996,6 +3002,39 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "logos" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7251356ef8cb7aec833ddf598c6cb24d17b689d20b993f9d11a3d764e34e6458" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f80069600c0d66734f5ff52cc42f2dabd6b29d205f333d61fd7832e9e9963f" +dependencies = [ + "beef", + "fnv", + "lazy_static", + "proc-macro2", + "quote", + "regex-syntax", + "syn 2.0.117", +] + +[[package]] +name = "logos-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24fb722b06a9dc12adb0963ed585f19fc61dc5413e6a9be9422ef92c091e731d" +dependencies = [ + "logos-codegen", +] + [[package]] name = "loopdev" version = "0.5.0" @@ -3927,6 +3966,7 @@ dependencies = [ "tracing-subscriber", "url", "uuid", + "wasm-wave", "wasmtime", "wasmtime-wasi", "wasmtime-wasi-http", @@ -5989,6 +6029,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-wave" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c34a4c10a1b9260f8131929d680e36edf00836bb8e76524d3004522bd6f287" +dependencies = [ + "logos", + "thiserror 2.0.18", + "wit-parser 0.244.0", +] + [[package]] name = "wasmparser" version = "0.215.0" @@ -6073,6 +6124,7 @@ dependencies = [ "tempfile", "wasm-compose", "wasm-encoder 0.244.0", + "wasm-wave", "wasmparser 0.244.0", "wasmtime-environ", "wasmtime-internal-cache", diff --git a/proplet/Cargo.toml b/proplet/Cargo.toml index 7c1b37a3..8af09636 100644 --- a/proplet/Cargo.toml +++ b/proplet/Cargo.toml @@ -9,7 +9,7 @@ rumqttc = "0.25.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.9.8" -wasmtime = { version = "42.0.1", features = ["component-model", "async"] } +wasmtime = { version = "42.0.1", features = ["component-model", "async", "wave"] } wasmtime-wasi = "42.0.1" wasmtime-wasi-http = "42.0.1" hyper = { version = "1.7", features = ["full"] } @@ -41,6 +41,7 @@ futures-util = { version = "0.3" } # ELASTIC TEE HAL — hardware abstraction layer for TEE workloads elastic-tee-hal = { git = "https://github.com/elasticproject-eu/wasmhal", default-features = false, features = ["amd-sev"] } +wasm-wave = "0.244.0" [features] default = [] diff --git a/proplet/src/runtime/wasmtime_runtime.rs b/proplet/src/runtime/wasmtime_runtime.rs index 3fb9cef1..93d5e799 100644 --- a/proplet/src/runtime/wasmtime_runtime.rs +++ b/proplet/src/runtime/wasmtime_runtime.rs @@ -1,6 +1,7 @@ use super::{Runtime, RuntimeContext, StartConfig}; use crate::hal_linker; use anyhow::{Context, Result}; +use wasm_wave; use async_trait::async_trait; use elastic_tee_hal::interfaces::HalProvider; use hyper::server::conn::http1; @@ -407,26 +408,14 @@ impl WasmtimeRuntime { let wasm_args: Vec = args .iter() .zip(param_types.iter()) - .map(|(arg, (_, ty))| match ty { - component::Type::Bool => Ok(component::Val::Bool(*arg != 0)), - component::Type::S8 => Ok(component::Val::S8(*arg as i8)), - component::Type::U8 => Ok(component::Val::U8(*arg as u8)), - component::Type::S16 => Ok(component::Val::S16(*arg as i16)), - component::Type::U16 => Ok(component::Val::U16(*arg as u16)), - component::Type::S32 => Ok(component::Val::S32(*arg as i32)), - component::Type::U32 => Ok(component::Val::U32(*arg as u32)), - component::Type::S64 => Ok(component::Val::S64(*arg as i64)), - component::Type::U64 => Ok(component::Val::U64(*arg)), - component::Type::Float32 => { - Ok(component::Val::Float32(f32::from_bits(*arg as u32))) - } - component::Type::Float64 => { - Ok(component::Val::Float64(f64::from_bits(*arg))) - } - _ => Err(anyhow::anyhow!( - "Unsupported WIT parameter type for export '{}'", - function_name - )), + .map(|(wave_str, (_, ty))| { + wasm_wave::from_str::(ty, wave_str).map_err(|e| { + anyhow::anyhow!( + "Failed to parse WAVE argument '{}' for export '{}': {e}", + wave_str, + function_name + ) + }) }) .collect::>>()?; @@ -437,21 +426,10 @@ impl WasmtimeRuntime { anyhow::anyhow!("Failed to call export '{}': {e}", function_name) })?; - let result_string = results.first().map(|v| match v { - component::Val::Bool(b) => b.to_string(), - component::Val::S8(n) => n.to_string(), - component::Val::U8(n) => n.to_string(), - component::Val::S16(n) => n.to_string(), - component::Val::U16(n) => n.to_string(), - component::Val::S32(n) => n.to_string(), - component::Val::U32(n) => n.to_string(), - component::Val::S64(n) => n.to_string(), - component::Val::U64(n) => n.to_string(), - component::Val::Float32(f) => f.to_string(), - component::Val::Float64(f) => f.to_string(), - component::Val::String(s) => s.clone(), - _ => String::new(), - }).unwrap_or_default(); + let result_string = results + .first() + .and_then(|v| wasm_wave::to_string(v).ok()) + .unwrap_or_default(); info!( "Export '{}' for task {} completed, result: {}", @@ -760,13 +738,25 @@ impl WasmtimeRuntime { .iter() .zip(param_types.iter()) .map(|(arg, param_type)| match param_type { - ValType::I32 => Val::I32(*arg as i32), - ValType::I64 => Val::I64(*arg as i64), - ValType::F32 => Val::F32((*arg as f32).to_bits()), - ValType::F64 => Val::F64((*arg as f64).to_bits()), - _ => Val::I32(*arg as i32), + ValType::I32 => arg + .parse::() + .map(Val::I32) + .map_err(|e| anyhow::anyhow!("Failed to parse '{}' as i32: {e}", arg)), + ValType::I64 => arg + .parse::() + .map(Val::I64) + .map_err(|e| anyhow::anyhow!("Failed to parse '{}' as i64: {e}", arg)), + ValType::F32 => arg + .parse::() + .map(|f| Val::F32(f.to_bits())) + .map_err(|e| anyhow::anyhow!("Failed to parse '{}' as f32: {e}", arg)), + ValType::F64 => arg + .parse::() + .map(|f| Val::F64(f.to_bits())) + .map_err(|e| anyhow::anyhow!("Failed to parse '{}' as f64: {e}", arg)), + _ => Err(anyhow::anyhow!("Unsupported Wasm value type for arg '{}'", arg)), }) - .collect(); + .collect::>>()?; info!( "Calling function '{}' with {} params, expects {} results", From 6a5010b643f84b7c4b7b98893b3b88b5dcddca57 Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Tue, 24 Mar 2026 12:05:06 +0300 Subject: [PATCH 08/21] feat(proplet): support WAVE invoke format for component exports in host runtime --- proplet/src/runtime/host.rs | 25 +++++++++++++++++++------ proplet/src/runtime/tee_runtime.rs | 2 +- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/proplet/src/runtime/host.rs b/proplet/src/runtime/host.rs index 463bf2e7..6f073091 100644 --- a/proplet/src/runtime/host.rs +++ b/proplet/src/runtime/host.rs @@ -52,6 +52,10 @@ impl HostRuntime { } Ok(()) } + + fn is_wasm_component(binary: &[u8]) -> bool { + binary.len() >= 8 && binary[4..8] == [0x0a, 0x00, 0x01, 0x00] + } } #[async_trait] @@ -73,13 +77,20 @@ impl Runtime for HostRuntime { cmd.arg("run"); + let is_component = Self::is_wasm_component(&config.wasm_binary); let cli_args_has_invoke = config.cli_args.iter().any(|a| a == "--invoke"); - if !config.function_name.is_empty() + let has_custom_export = !config.function_name.is_empty() && config.function_name != "_start" && !config.function_name.starts_with("fl-round-") - && !cli_args_has_invoke - { - cmd.arg("--invoke").arg(&config.function_name); + && !cli_args_has_invoke; + + if has_custom_export { + if is_component { + let wave_call = format!("{}({})", config.function_name, config.args.join(", ")); + cmd.arg("--invoke").arg(&wave_call); + } else { + cmd.arg("--invoke").arg(&config.function_name); + } } for arg in &config.cli_args { @@ -103,8 +114,10 @@ impl Runtime for HostRuntime { cmd.arg(&temp_file); - for arg in &config.args { - cmd.arg(arg.to_string()); + if !is_component || !has_custom_export { + for arg in &config.args { + cmd.arg(arg); + } } cmd.envs(&config.env); diff --git a/proplet/src/runtime/tee_runtime.rs b/proplet/src/runtime/tee_runtime.rs index 83282e7e..d26d14aa 100644 --- a/proplet/src/runtime/tee_runtime.rs +++ b/proplet/src/runtime/tee_runtime.rs @@ -242,7 +242,7 @@ impl TeeWasmRuntime { cmd.arg(wasm_path); for arg in &config.args { - cmd.arg(arg.to_string()); + cmd.arg(arg); } cmd.stdout(std::process::Stdio::piped()); From b5d6d272b2816b6e9ed919514b8c6fb6c39dc846 Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Tue, 24 Mar 2026 12:44:36 +0300 Subject: [PATCH 09/21] test(proplet): fix StartRequest struct literals and add function_name tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new function_name field on StartRequest was missing from 10 test struct literals, causing cargo test --release to fail with E0063. Added function_name: None to all affected test fixtures. Also added three targeted tests for the new field: - test_start_request_function_name_present: deserialize with field set - test_start_request_function_name_absent_defaults_to_none: backward compat — old payloads without the field still deserialize cleanly - test_start_request_function_name_roundtrip: ser/deser with WAVE-encoded inputs (Vec) and custom function_name round-trips correctly --- proplet/src/types.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/proplet/src/types.rs b/proplet/src/types.rs index 509e18de..43e67088 100644 --- a/proplet/src/types.rs +++ b/proplet/src/types.rs @@ -313,6 +313,7 @@ mod tests { encrypted: false, kbs_resource_path: None, mode: None, + function_name: None, proplet_id: None, }; @@ -335,6 +336,7 @@ mod tests { encrypted: false, kbs_resource_path: None, mode: None, + function_name: None, proplet_id: None, }; @@ -357,6 +359,7 @@ mod tests { encrypted: false, kbs_resource_path: None, mode: None, + function_name: None, proplet_id: None, }; @@ -381,6 +384,7 @@ mod tests { encrypted: false, kbs_resource_path: None, mode: None, + function_name: None, proplet_id: None, }; @@ -405,6 +409,7 @@ mod tests { encrypted: false, kbs_resource_path: None, mode: None, + function_name: None, proplet_id: None, }; @@ -432,6 +437,7 @@ mod tests { encrypted: true, kbs_resource_path: Some("default/key1/value".to_string()), mode: None, + function_name: None, proplet_id: None, }; @@ -454,6 +460,7 @@ mod tests { encrypted: true, kbs_resource_path: Some("default/key1/value".to_string()), mode: None, + function_name: None, proplet_id: None, }; @@ -481,6 +488,7 @@ mod tests { encrypted: true, kbs_resource_path: Some("default/key1/value".to_string()), mode: None, + function_name: None, proplet_id: None, }; @@ -545,6 +553,55 @@ mod tests { assert_eq!(req.env.as_ref().unwrap().len(), 2); } + #[test] + fn test_start_request_function_name_present() { + let json_data = json!({ + "id": "task-fn", + "name": "my_module", + "file": "ZGF0YQ==", + "image_url": "", + "function_name": "compute_sum" + }); + let req: StartRequest = serde_json::from_value(json_data).unwrap(); + assert_eq!(req.function_name, Some("compute_sum".to_string())); + } + + #[test] + fn test_start_request_function_name_absent_defaults_to_none() { + let json_data = json!({ + "id": "task-no-fn", + "name": "my_module", + "file": "ZGF0YQ==" + }); + let req: StartRequest = serde_json::from_value(json_data).unwrap(); + assert_eq!(req.function_name, None); + } + + #[test] + fn test_start_request_function_name_roundtrip() { + let req = StartRequest { + id: "task-rt".to_string(), + cli_args: vec![], + name: "module".to_string(), + state: 0, + file: "ZGF0YQ==".to_string(), + image_url: String::new(), + inputs: vec!["1u32".to_string(), "2u32".to_string()], + daemon: false, + env: None, + monitoring_profile: None, + encrypted: false, + kbs_resource_path: None, + mode: None, + function_name: Some("add".to_string()), + proplet_id: None, + }; + let json = serde_json::to_string(&req).unwrap(); + let deserialized: StartRequest = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.function_name, Some("add".to_string())); + assert_eq!(deserialized.inputs, vec!["1u32", "2u32"]); + } + #[test] fn test_stop_request_validate_success() { let req = StopRequest { @@ -738,6 +795,7 @@ mod tests { encrypted: false, kbs_resource_path: None, mode: None, + function_name: None, proplet_id: None, }; @@ -803,6 +861,7 @@ mod tests { encrypted: false, kbs_resource_path: None, mode: None, + function_name: None, proplet_id: None, }; From 38862bd31b9e3633df5e5c7921c8421c84d46a56 Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Tue, 24 Mar 2026 13:11:38 +0300 Subject: [PATCH 10/21] fix(proplet): detect both wasm-tools component layer bytes and add tests Extend is_wasm_component to accept 0x0d (wasm-tools >= 0.200 / wit-component 0.241.2) in addition to 0x0a (older wasm-tools). Without this, components built with modern tooling were misidentified as core modules, causing the --invoke flag to be called without WAVE-encoded arguments and producing a parse error at runtime. Adds four regression tests covering old format, new format, core module rejection, and too-short binary rejection. --- proplet/src/runtime/host.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/proplet/src/runtime/host.rs b/proplet/src/runtime/host.rs index 6f073091..8d050ed1 100644 --- a/proplet/src/runtime/host.rs +++ b/proplet/src/runtime/host.rs @@ -54,7 +54,10 @@ impl HostRuntime { } fn is_wasm_component(binary: &[u8]) -> bool { - binary.len() >= 8 && binary[4..8] == [0x0a, 0x00, 0x01, 0x00] + // Component model binary layer marker: 0x0a (older wasm-tools) or 0x0d (wasm-tools >= 0.200) + binary.len() >= 8 + && (binary[4..8] == [0x0a, 0x00, 0x01, 0x00] + || binary[4..8] == [0x0d, 0x00, 0x01, 0x00]) } } @@ -404,4 +407,28 @@ mod tests { runtime.cleanup_temp_file(file_path).await.unwrap(); } + + #[test] + fn test_is_wasm_component_old_format() { + let binary = [0x00, 0x61, 0x73, 0x6d, 0x0a, 0x00, 0x01, 0x00, 0x00]; + assert!(HostRuntime::is_wasm_component(&binary)); + } + + #[test] + fn test_is_wasm_component_new_format() { + let binary = [0x00, 0x61, 0x73, 0x6d, 0x0d, 0x00, 0x01, 0x00, 0x00]; + assert!(HostRuntime::is_wasm_component(&binary)); + } + + #[test] + fn test_is_wasm_component_rejects_core_module() { + let binary = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x00]; + assert!(!HostRuntime::is_wasm_component(&binary)); + } + + #[test] + fn test_is_wasm_component_rejects_too_short() { + let binary = [0x00, 0x61, 0x73, 0x6d]; + assert!(!HostRuntime::is_wasm_component(&binary)); + } } From 1e45177354882613d52aad2fcc7bcd55c2778040 Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Tue, 24 Mar 2026 13:56:31 +0300 Subject: [PATCH 11/21] style(proplet): fix rustfmt violations in wasmtime_runtime and service --- proplet/src/runtime/wasmtime_runtime.rs | 18 +++++++++--------- proplet/src/service.rs | 3 ++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/proplet/src/runtime/wasmtime_runtime.rs b/proplet/src/runtime/wasmtime_runtime.rs index 93d5e799..55639757 100644 --- a/proplet/src/runtime/wasmtime_runtime.rs +++ b/proplet/src/runtime/wasmtime_runtime.rs @@ -1,7 +1,6 @@ use super::{Runtime, RuntimeContext, StartConfig}; use crate::hal_linker; use anyhow::{Context, Result}; -use wasm_wave; use async_trait::async_trait; use elastic_tee_hal::interfaces::HalProvider; use hyper::server::conn::http1; @@ -15,6 +14,7 @@ use tokio::sync::watch; use tokio::sync::Mutex; use tokio::task::JoinHandle; use tracing::{error, info, warn}; +use wasm_wave; use wasmtime::component::ResourceTable; use wasmtime::*; use wasmtime_wasi::p2::bindings::sync::Command; @@ -376,9 +376,7 @@ impl WasmtimeRuntime { let instance = match linker.instantiate(&mut store, &component) { Ok(i) => i, Err(e) => { - return Err(anyhow::anyhow!( - "Failed to instantiate WASM component: {e}" - )) + return Err(anyhow::anyhow!("Failed to instantiate WASM component: {e}")) } }; @@ -419,12 +417,14 @@ impl WasmtimeRuntime { }) .collect::>>()?; - let mut results: Vec = - (0..result_count).map(|_| component::Val::Bool(false)).collect(); + let mut results: Vec = (0..result_count) + .map(|_| component::Val::Bool(false)) + .collect(); - func.call(&mut store, &wasm_args, &mut results).map_err(|e| { - anyhow::anyhow!("Failed to call export '{}': {e}", function_name) - })?; + func.call(&mut store, &wasm_args, &mut results) + .map_err(|e| { + anyhow::anyhow!("Failed to call export '{}': {e}", function_name) + })?; let result_string = results .first() diff --git a/proplet/src/service.rs b/proplet/src/service.rs index addd02eb..d4557a8b 100644 --- a/proplet/src/service.rs +++ b/proplet/src/service.rs @@ -572,7 +572,8 @@ impl PropletService { // so they can be included in the environment variables let mut config = StartConfig { id: task_id.clone(), - function_name: req.function_name + function_name: req + .function_name .as_deref() .filter(|s| !s.is_empty()) .map(String::from) From c69465e359873d3b349a2bce679a3277e37312fc Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Wed, 25 Mar 2026 11:38:56 +0300 Subject: [PATCH 12/21] refactor: remove function_name field and use name as the function identifier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit name already served as the function name (validated with 'name is required'). function_name was redundant — the only purpose was as an optional override that fell back to name anyway. Simplify by using name directly throughout. --- manager/service.go | 1 - pkg/storage/postgres/init.go | 9 ----- pkg/storage/postgres/tasks.go | 22 +++++------- pkg/storage/sqlite/init.go | 9 ----- pkg/storage/sqlite/tasks.go | 16 ++++----- pkg/task/task.go | 1 - proplet/src/service.rs | 7 +--- proplet/src/types.rs | 63 +---------------------------------- 8 files changed, 16 insertions(+), 112 deletions(-) diff --git a/manager/service.go b/manager/service.go index 6839792b..c06d40f1 100644 --- a/manager/service.go +++ b/manager/service.go @@ -1677,7 +1677,6 @@ func (svc *service) publishStart(ctx context.Context, t task.Task, propletID str payload := map[string]any{ "id": t.ID, "name": t.Name, - "function_name": t.FunctionName, "state": t.State, "image_url": t.ImageURL, "file": t.File, diff --git a/pkg/storage/postgres/init.go b/pkg/storage/postgres/init.go index b48f1eb5..17c9eb2d 100644 --- a/pkg/storage/postgres/init.go +++ b/pkg/storage/postgres/init.go @@ -250,16 +250,7 @@ func (db *Database) Migrate() error { `ALTER TABLE proplets DROP COLUMN IF EXISTS metadata`, }, }, - { - Id: "4_add_function_name", - Up: []string{ - `ALTER TABLE tasks ADD COLUMN IF NOT EXISTS function_name TEXT`, - }, - Down: []string{ - `ALTER TABLE tasks DROP COLUMN IF EXISTS function_name`, - }, }, - }, } _, err := migrate.Exec(db.DB.DB, "postgres", migrations, migrate.Up) diff --git a/pkg/storage/postgres/tasks.go b/pkg/storage/postgres/tasks.go index a40ccf29..4fb4b685 100644 --- a/pkg/storage/postgres/tasks.go +++ b/pkg/storage/postgres/tasks.go @@ -22,7 +22,6 @@ type dbTask struct { Name string `db:"name"` State uint8 `db:"state"` ImageURL *string `db:"image_url"` - FunctionName *string `db:"function_name"` File []byte `db:"file"` CLIArgs []byte `db:"cli_args"` Inputs []byte `db:"inputs"` @@ -46,13 +45,13 @@ type dbTask struct { Mode *string `db:"mode"` } -const taskColumns = `id, name, state, image_url, function_name, file, cli_args, inputs, env, daemon, encrypted, +const taskColumns = `id, name, state, image_url, file, cli_args, inputs, env, daemon, encrypted, kbs_resource_path, proplet_id, results, error, monitoring_profile, start_time, finish_time, created_at, updated_at, workflow_id, job_id, depends_on, run_if, kind, mode` func (r *taskRepo) Create(ctx context.Context, t task.Task) (task.Task, error) { query := `INSERT INTO tasks (` + taskColumns + `) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)` + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)` cliArgs, err := jsonBytes(t.CLIArgs) if err != nil { @@ -87,7 +86,6 @@ func (r *taskRepo) Create(ctx context.Context, t task.Task) (task.Task, error) { _, err = r.db.ExecContext(ctx, query, t.ID, t.Name, uint8(t.State), nullString(t.ImageURL), - nullString(t.FunctionName), t.File, cliArgs, inputs, @@ -133,11 +131,11 @@ func (r *taskRepo) Get(ctx context.Context, id string) (task.Task, error) { func (r *taskRepo) Update(ctx context.Context, t task.Task) error { query := `UPDATE tasks SET - name = $2, state = $3, image_url = $4, function_name = $5, file = $6, cli_args = $7, inputs = $8, - env = $9, daemon = $10, encrypted = $11, kbs_resource_path = $12, proplet_id = $13, - results = $14, error = $15, monitoring_profile = $16, start_time = $17, - finish_time = $18, updated_at = $19, workflow_id = $20, job_id = $21, - depends_on = $22, run_if = $23, kind = $24, mode = $25 + name = $2, state = $3, image_url = $4, file = $5, cli_args = $6, inputs = $7, + env = $8, daemon = $9, encrypted = $10, kbs_resource_path = $11, proplet_id = $12, + results = $13, error = $14, monitoring_profile = $15, start_time = $16, + finish_time = $17, updated_at = $18, workflow_id = $19, job_id = $20, + depends_on = $21, run_if = $22, kind = $23, mode = $24 WHERE id = $1` cliArgs, err := jsonBytes(t.CLIArgs) @@ -173,7 +171,6 @@ func (r *taskRepo) Update(ctx context.Context, t task.Task) error { _, err = r.db.ExecContext(ctx, query, t.ID, t.Name, uint8(t.State), nullString(t.ImageURL), - nullString(t.FunctionName), t.File, cliArgs, inputs, env, t.Daemon, t.Encrypted, @@ -249,7 +246,7 @@ func (r *taskRepo) scanTasks(ctx context.Context, query string, args ...any) ([] for rows.Next() { var dbt dbTask if err := rows.Scan( - &dbt.ID, &dbt.Name, &dbt.State, &dbt.ImageURL, &dbt.FunctionName, + &dbt.ID, &dbt.Name, &dbt.State, &dbt.ImageURL, &dbt.File, &dbt.CLIArgs, &dbt.Inputs, &dbt.Env, &dbt.Daemon, &dbt.Encrypted, &dbt.KBSResourcePath, &dbt.PropletID, &dbt.Results, &dbt.Error, &dbt.MonitoringProfile, @@ -290,9 +287,6 @@ func (r *taskRepo) toTask(dbt dbTask) (task.Task, error) { if dbt.ImageURL != nil { t.ImageURL = *dbt.ImageURL } - if dbt.FunctionName != nil { - t.FunctionName = *dbt.FunctionName - } if err := jsonUnmarshal(dbt.CLIArgs, &t.CLIArgs); err != nil { return task.Task{}, err } diff --git a/pkg/storage/sqlite/init.go b/pkg/storage/sqlite/init.go index ec07a98a..bd04b805 100644 --- a/pkg/storage/sqlite/init.go +++ b/pkg/storage/sqlite/init.go @@ -243,16 +243,7 @@ func (db *Database) Migrate() error { `ALTER TABLE proplets DROP COLUMN metadata`, }, }, - { - Id: "4_add_function_name", - Up: []string{ - `ALTER TABLE tasks ADD COLUMN function_name TEXT`, - }, - Down: []string{ - `ALTER TABLE tasks DROP COLUMN function_name`, - }, }, - }, } _, err := migrate.Exec(db.DB.DB, "sqlite3", migrations, migrate.Up) diff --git a/pkg/storage/sqlite/tasks.go b/pkg/storage/sqlite/tasks.go index 9a4b037f..99c46427 100644 --- a/pkg/storage/sqlite/tasks.go +++ b/pkg/storage/sqlite/tasks.go @@ -24,7 +24,6 @@ type dbTask struct { Name string `db:"name"` State uint8 `db:"state"` ImageURL *string `db:"image_url"` - FunctionName *string `db:"function_name"` File []byte `db:"file"` CLIArgs []byte `db:"cli_args"` Inputs []byte `db:"inputs"` @@ -48,13 +47,13 @@ type dbTask struct { Mode *string `db:"mode"` } -const taskColumns = `id, name, state, image_url, function_name, file, cli_args, inputs, env, daemon, encrypted, +const taskColumns = `id, name, state, image_url, file, cli_args, inputs, env, daemon, encrypted, kbs_resource_path, proplet_id, results, error, monitoring_profile, start_time, finish_time, created_at, updated_at, workflow_id, job_id, depends_on, run_if, kind, mode` func (r *taskRepo) Create(ctx context.Context, t task.Task) (task.Task, error) { query := `INSERT INTO tasks (` + taskColumns + `) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` cliArgs, err := jsonBytes(t.CLIArgs) if err != nil { @@ -87,7 +86,7 @@ func (r *taskRepo) Create(ctx context.Context, t task.Task) (task.Task, error) { } _, err = r.db.ExecContext(ctx, query, - t.ID, t.Name, uint8(t.State), nullString(t.ImageURL), nullString(t.FunctionName), + t.ID, t.Name, uint8(t.State), nullString(t.ImageURL), t.File, cliArgs, inputs, env, t.Daemon, t.Encrypted, nullString(t.KBSResourcePath), nullString(t.PropletID), @@ -123,7 +122,7 @@ func (r *taskRepo) Get(ctx context.Context, id string) (task.Task, error) { func (r *taskRepo) Update(ctx context.Context, t task.Task) error { query := `UPDATE tasks SET - name = ?, state = ?, image_url = ?, function_name = ?, file = ?, cli_args = ?, inputs = ?, + name = ?, state = ?, image_url = ?, file = ?, cli_args = ?, inputs = ?, env = ?, daemon = ?, encrypted = ?, kbs_resource_path = ?, proplet_id = ?, results = ?, error = ?, monitoring_profile = ?, start_time = ?, finish_time = ?, updated_at = ?, workflow_id = ?, job_id = ?, @@ -161,7 +160,7 @@ func (r *taskRepo) Update(ctx context.Context, t task.Task) error { } _, err = r.db.ExecContext(ctx, query, - t.Name, uint8(t.State), nullString(t.ImageURL), nullString(t.FunctionName), + t.Name, uint8(t.State), nullString(t.ImageURL), t.File, cliArgs, inputs, env, t.Daemon, t.Encrypted, nullString(t.KBSResourcePath), nullString(t.PropletID), @@ -229,7 +228,7 @@ func (r *taskRepo) scanTasks(ctx context.Context, query string, args ...any) ([] for rows.Next() { var dbt dbTask if err := rows.Scan( - &dbt.ID, &dbt.Name, &dbt.State, &dbt.ImageURL, &dbt.FunctionName, + &dbt.ID, &dbt.Name, &dbt.State, &dbt.ImageURL, &dbt.File, &dbt.CLIArgs, &dbt.Inputs, &dbt.Env, &dbt.Daemon, &dbt.Encrypted, &dbt.KBSResourcePath, &dbt.PropletID, &dbt.Results, &dbt.Error, &dbt.MonitoringProfile, @@ -270,9 +269,6 @@ func (r *taskRepo) toTask(dbt dbTask) (task.Task, error) { if dbt.ImageURL != nil { t.ImageURL = *dbt.ImageURL } - if dbt.FunctionName != nil { - t.FunctionName = *dbt.FunctionName - } if dbt.CLIArgs != nil { if err := jsonUnmarshal(dbt.CLIArgs, &t.CLIArgs); err != nil { return task.Task{}, err diff --git a/pkg/task/task.go b/pkg/task/task.go index 201a5f46..147c3982 100644 --- a/pkg/task/task.go +++ b/pkg/task/task.go @@ -68,7 +68,6 @@ type Task struct { Kind TaskKind `json:"kind,omitempty"` State State `json:"state"` ImageURL string `json:"image_url,omitempty"` - FunctionName string `json:"function_name,omitempty"` File []byte `json:"file,omitempty"` CLIArgs []string `json:"cli_args"` Inputs []string `json:"inputs,omitempty"` diff --git a/proplet/src/service.rs b/proplet/src/service.rs index d4557a8b..aa9e1775 100644 --- a/proplet/src/service.rs +++ b/proplet/src/service.rs @@ -572,12 +572,7 @@ impl PropletService { // so they can be included in the environment variables let mut config = StartConfig { id: task_id.clone(), - function_name: req - .function_name - .as_deref() - .filter(|s| !s.is_empty()) - .map(String::from) - .unwrap_or_else(|| task_name.clone()), + function_name: task_name.clone(), daemon, wasm_binary, cli_args, diff --git a/proplet/src/types.rs b/proplet/src/types.rs index 43e67088..bd07c7c7 100644 --- a/proplet/src/types.rs +++ b/proplet/src/types.rs @@ -66,8 +66,6 @@ pub struct StartRequest { pub mode: Option, #[serde(default)] pub proplet_id: Option, - #[serde(default)] - pub function_name: Option, } fn deserialize_null_default<'de, D, T>(deserializer: D) -> std::result::Result @@ -85,7 +83,7 @@ impl StartRequest { return Err(anyhow::anyhow!("id is required")); } if self.name.is_empty() { - return Err(anyhow::anyhow!("function name is required")); + return Err(anyhow::anyhow!("name is required")); } if self.encrypted { @@ -313,7 +311,6 @@ mod tests { encrypted: false, kbs_resource_path: None, mode: None, - function_name: None, proplet_id: None, }; @@ -336,7 +333,6 @@ mod tests { encrypted: false, kbs_resource_path: None, mode: None, - function_name: None, proplet_id: None, }; @@ -359,7 +355,6 @@ mod tests { encrypted: false, kbs_resource_path: None, mode: None, - function_name: None, proplet_id: None, }; @@ -384,7 +379,6 @@ mod tests { encrypted: false, kbs_resource_path: None, mode: None, - function_name: None, proplet_id: None, }; @@ -409,7 +403,6 @@ mod tests { encrypted: false, kbs_resource_path: None, mode: None, - function_name: None, proplet_id: None, }; @@ -437,7 +430,6 @@ mod tests { encrypted: true, kbs_resource_path: Some("default/key1/value".to_string()), mode: None, - function_name: None, proplet_id: None, }; @@ -460,7 +452,6 @@ mod tests { encrypted: true, kbs_resource_path: Some("default/key1/value".to_string()), mode: None, - function_name: None, proplet_id: None, }; @@ -488,7 +479,6 @@ mod tests { encrypted: true, kbs_resource_path: Some("default/key1/value".to_string()), mode: None, - function_name: None, proplet_id: None, }; @@ -553,55 +543,6 @@ mod tests { assert_eq!(req.env.as_ref().unwrap().len(), 2); } - #[test] - fn test_start_request_function_name_present() { - let json_data = json!({ - "id": "task-fn", - "name": "my_module", - "file": "ZGF0YQ==", - "image_url": "", - "function_name": "compute_sum" - }); - let req: StartRequest = serde_json::from_value(json_data).unwrap(); - assert_eq!(req.function_name, Some("compute_sum".to_string())); - } - - #[test] - fn test_start_request_function_name_absent_defaults_to_none() { - let json_data = json!({ - "id": "task-no-fn", - "name": "my_module", - "file": "ZGF0YQ==" - }); - let req: StartRequest = serde_json::from_value(json_data).unwrap(); - assert_eq!(req.function_name, None); - } - - #[test] - fn test_start_request_function_name_roundtrip() { - let req = StartRequest { - id: "task-rt".to_string(), - cli_args: vec![], - name: "module".to_string(), - state: 0, - file: "ZGF0YQ==".to_string(), - image_url: String::new(), - inputs: vec!["1u32".to_string(), "2u32".to_string()], - daemon: false, - env: None, - monitoring_profile: None, - encrypted: false, - kbs_resource_path: None, - mode: None, - function_name: Some("add".to_string()), - proplet_id: None, - }; - let json = serde_json::to_string(&req).unwrap(); - let deserialized: StartRequest = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.function_name, Some("add".to_string())); - assert_eq!(deserialized.inputs, vec!["1u32", "2u32"]); - } - #[test] fn test_stop_request_validate_success() { let req = StopRequest { @@ -795,7 +736,6 @@ mod tests { encrypted: false, kbs_resource_path: None, mode: None, - function_name: None, proplet_id: None, }; @@ -861,7 +801,6 @@ mod tests { encrypted: false, kbs_resource_path: None, mode: None, - function_name: None, proplet_id: None, }; From 3e38762503cce0e520a6b62bf23d379c3b77852f Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Wed, 25 Mar 2026 11:44:53 +0300 Subject: [PATCH 13/21] style: fix gci formatting in postgres and sqlite init files --- pkg/storage/postgres/init.go | 2 +- pkg/storage/sqlite/init.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/storage/postgres/init.go b/pkg/storage/postgres/init.go index 17c9eb2d..6465d4b6 100644 --- a/pkg/storage/postgres/init.go +++ b/pkg/storage/postgres/init.go @@ -250,7 +250,7 @@ func (db *Database) Migrate() error { `ALTER TABLE proplets DROP COLUMN IF EXISTS metadata`, }, }, - }, + }, } _, err := migrate.Exec(db.DB.DB, "postgres", migrations, migrate.Up) diff --git a/pkg/storage/sqlite/init.go b/pkg/storage/sqlite/init.go index bd04b805..2ad7da03 100644 --- a/pkg/storage/sqlite/init.go +++ b/pkg/storage/sqlite/init.go @@ -243,7 +243,7 @@ func (db *Database) Migrate() error { `ALTER TABLE proplets DROP COLUMN metadata`, }, }, - }, + }, } _, err := migrate.Exec(db.DB.DB, "sqlite3", migrations, migrate.Up) From 96c8e65d29ab9e4d3529bcf9f62b54117b8a21b1 Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Wed, 25 Mar 2026 11:55:35 +0300 Subject: [PATCH 14/21] test(proplet): update validate_empty_name assertion to match renamed error message --- proplet/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proplet/src/types.rs b/proplet/src/types.rs index bd07c7c7..f97d7fef 100644 --- a/proplet/src/types.rs +++ b/proplet/src/types.rs @@ -384,7 +384,7 @@ mod tests { let result = req.validate(); assert!(result.is_err()); - assert_eq!(result.unwrap_err().to_string(), "function name is required"); + assert_eq!(result.unwrap_err().to_string(), "name is required"); } #[test] From 3fb8b4ad112a720d3b621a10b08cb8bd0beecc2f Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Wed, 25 Mar 2026 12:54:25 +0300 Subject: [PATCH 15/21] feat(proplet): route http/https image_url to HTTP fetch instead of registry --- proplet/src/service.rs | 57 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/proplet/src/service.rs b/proplet/src/service.rs index aa9e1775..d7231d31 100644 --- a/proplet/src/service.rs +++ b/proplet/src/service.rs @@ -5,7 +5,10 @@ use crate::mqtt::{build_topic, MqttMessage, PubSub}; use crate::runtime::{Runtime, RuntimeContext, StartConfig}; use crate::types::*; use anyhow::{Context, Result}; +use futures_util::StreamExt; use reqwest::Client as HttpClient; + +const WASM_FETCH_MAX_BYTES: usize = 100 * 1024 * 1024; use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use std::time::SystemTime; @@ -482,6 +485,17 @@ impl PropletService { if req.encrypted { info!("Encrypted workload with image_url: {}", req.image_url); Vec::new() + } else if req.image_url.starts_with("http://") || req.image_url.starts_with("https://") { + match self.fetch_wasm_from_http(&req.image_url).await { + Ok(binary) => binary, + Err(e) => { + error!("Failed to fetch wasm for task {}: {}", req.id, e); + self.running_tasks.lock().await.remove(&req.id); + self.publish_result(&req.id, Vec::new(), Some(e.to_string())) + .await?; + return Err(e); + } + } } else { info!("Requesting binary from registry: {}", req.image_url); self.request_binary_from_registry(&req.image_url).await?; @@ -1028,6 +1042,49 @@ impl PropletService { Ok(()) } + async fn fetch_wasm_from_http(&self, url: &str) -> Result> { + let response = self + .http_client + .get(url) + .send() + .await + .with_context(|| format!("Failed to connect to {}", url))?; + + let status = response.status(); + if status.is_client_error() || status.is_server_error() { + return Err(anyhow::anyhow!("HTTP {} fetching wasm from {}", status, url)); + } + + if let Some(content_length) = response.content_length() { + if content_length as usize > WASM_FETCH_MAX_BYTES { + return Err(anyhow::anyhow!( + "wasm response from {} exceeds size limit ({} > {} bytes)", + url, + content_length, + WASM_FETCH_MAX_BYTES + )); + } + } + + let mut binary = Vec::new(); + let mut stream = response.bytes_stream(); + while let Some(chunk) = stream.next().await { + let chunk = + chunk.with_context(|| format!("Failed to read response body from {}", url))?; + binary.extend_from_slice(&chunk); + if binary.len() > WASM_FETCH_MAX_BYTES { + return Err(anyhow::anyhow!( + "wasm response from {} exceeds size limit ({} bytes)", + url, + WASM_FETCH_MAX_BYTES + )); + } + } + + info!("Fetched wasm from {}, size: {} bytes", url, binary.len()); + Ok(binary) + } + #[allow(dead_code)] fn parse_http_requests_from_stderr(stderr: &str) -> Vec { let mut requests = Vec::new(); From f11fc7eaf32437fc22e0bbec59f6441914652fef Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Wed, 25 Mar 2026 13:13:52 +0300 Subject: [PATCH 16/21] fix(proplet): cargo fmt formatting --- proplet/src/service.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/proplet/src/service.rs b/proplet/src/service.rs index d7231d31..b132affb 100644 --- a/proplet/src/service.rs +++ b/proplet/src/service.rs @@ -485,7 +485,8 @@ impl PropletService { if req.encrypted { info!("Encrypted workload with image_url: {}", req.image_url); Vec::new() - } else if req.image_url.starts_with("http://") || req.image_url.starts_with("https://") { + } else if req.image_url.starts_with("http://") || req.image_url.starts_with("https://") + { match self.fetch_wasm_from_http(&req.image_url).await { Ok(binary) => binary, Err(e) => { @@ -1052,7 +1053,11 @@ impl PropletService { let status = response.status(); if status.is_client_error() || status.is_server_error() { - return Err(anyhow::anyhow!("HTTP {} fetching wasm from {}", status, url)); + return Err(anyhow::anyhow!( + "HTTP {} fetching wasm from {}", + status, + url + )); } if let Some(content_length) = response.content_length() { From 484a31ce2188423f266a7fab9bd82eb8f8eedcb9 Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Thu, 26 Mar 2026 11:22:58 +0300 Subject: [PATCH 17/21] revert: remove http/https image_url routing (belongs in #169) --- proplet/src/service.rs | 62 ------------------------------------------ 1 file changed, 62 deletions(-) diff --git a/proplet/src/service.rs b/proplet/src/service.rs index b132affb..aa9e1775 100644 --- a/proplet/src/service.rs +++ b/proplet/src/service.rs @@ -5,10 +5,7 @@ use crate::mqtt::{build_topic, MqttMessage, PubSub}; use crate::runtime::{Runtime, RuntimeContext, StartConfig}; use crate::types::*; use anyhow::{Context, Result}; -use futures_util::StreamExt; use reqwest::Client as HttpClient; - -const WASM_FETCH_MAX_BYTES: usize = 100 * 1024 * 1024; use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use std::time::SystemTime; @@ -485,18 +482,6 @@ impl PropletService { if req.encrypted { info!("Encrypted workload with image_url: {}", req.image_url); Vec::new() - } else if req.image_url.starts_with("http://") || req.image_url.starts_with("https://") - { - match self.fetch_wasm_from_http(&req.image_url).await { - Ok(binary) => binary, - Err(e) => { - error!("Failed to fetch wasm for task {}: {}", req.id, e); - self.running_tasks.lock().await.remove(&req.id); - self.publish_result(&req.id, Vec::new(), Some(e.to_string())) - .await?; - return Err(e); - } - } } else { info!("Requesting binary from registry: {}", req.image_url); self.request_binary_from_registry(&req.image_url).await?; @@ -1043,53 +1028,6 @@ impl PropletService { Ok(()) } - async fn fetch_wasm_from_http(&self, url: &str) -> Result> { - let response = self - .http_client - .get(url) - .send() - .await - .with_context(|| format!("Failed to connect to {}", url))?; - - let status = response.status(); - if status.is_client_error() || status.is_server_error() { - return Err(anyhow::anyhow!( - "HTTP {} fetching wasm from {}", - status, - url - )); - } - - if let Some(content_length) = response.content_length() { - if content_length as usize > WASM_FETCH_MAX_BYTES { - return Err(anyhow::anyhow!( - "wasm response from {} exceeds size limit ({} > {} bytes)", - url, - content_length, - WASM_FETCH_MAX_BYTES - )); - } - } - - let mut binary = Vec::new(); - let mut stream = response.bytes_stream(); - while let Some(chunk) = stream.next().await { - let chunk = - chunk.with_context(|| format!("Failed to read response body from {}", url))?; - binary.extend_from_slice(&chunk); - if binary.len() > WASM_FETCH_MAX_BYTES { - return Err(anyhow::anyhow!( - "wasm response from {} exceeds size limit ({} bytes)", - url, - WASM_FETCH_MAX_BYTES - )); - } - } - - info!("Fetched wasm from {}, size: {} bytes", url, binary.len()); - Ok(binary) - } - #[allow(dead_code)] fn parse_http_requests_from_stderr(stderr: &str) -> Vec { let mut requests = Vec::new(); From 6bb8cca46025a867436e0fee03487e272f99ed8e Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Thu, 26 Mar 2026 12:19:16 +0300 Subject: [PATCH 18/21] feat(task): add FlexStrings type to accept both JSON numbers and strings for inputs --- pkg/task/task.go | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/pkg/task/task.go b/pkg/task/task.go index 147c3982..0dd0e5e9 100644 --- a/pkg/task/task.go +++ b/pkg/task/task.go @@ -1,11 +1,38 @@ package task import ( + "encoding/json" + "fmt" "time" "github.com/absmach/propeller/pkg/proplet" ) +// FlexStrings is a []string that accepts both JSON strings and JSON numbers +// when unmarshaling, coercing numbers to their string representation. +type FlexStrings []string + +func (f *FlexStrings) UnmarshalJSON(data []byte) error { + var raw []json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + *f = make(FlexStrings, len(raw)) + for i, r := range raw { + var s string + if err := json.Unmarshal(r, &s); err == nil { + (*f)[i] = s + continue + } + var n json.Number + if err := json.Unmarshal(r, &n); err != nil { + return fmt.Errorf("inputs[%d]: expected string or number", i) + } + (*f)[i] = n.String() + } + return nil +} + type State uint8 const ( @@ -70,7 +97,7 @@ type Task struct { ImageURL string `json:"image_url,omitempty"` File []byte `json:"file,omitempty"` CLIArgs []string `json:"cli_args"` - Inputs []string `json:"inputs,omitempty"` + Inputs FlexStrings `json:"inputs,omitempty"` Env map[string]string `json:"env,omitempty"` Daemon bool `json:"daemon"` Encrypted bool `json:"encrypted"` From 182869aef188e4bde989c982f1d017411fababf6 Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Thu, 26 Mar 2026 12:20:26 +0300 Subject: [PATCH 19/21] style(task): remove comment from FlexStrings type --- pkg/task/task.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/task/task.go b/pkg/task/task.go index 0dd0e5e9..e7f9de31 100644 --- a/pkg/task/task.go +++ b/pkg/task/task.go @@ -8,8 +8,6 @@ import ( "github.com/absmach/propeller/pkg/proplet" ) -// FlexStrings is a []string that accepts both JSON strings and JSON numbers -// when unmarshaling, coercing numbers to their string representation. type FlexStrings []string func (f *FlexStrings) UnmarshalJSON(data []byte) error { From 96b7990e79e20ec2d1fcad11a0684ca7abf2513e Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Thu, 26 Mar 2026 13:24:06 +0300 Subject: [PATCH 20/21] feat(examples): add greet-component wasm example with string input --- examples/greet-component/Cargo.lock | 442 +++++++++++++++++++++++++ examples/greet-component/Cargo.toml | 10 + examples/greet-component/src/lib.rs | 11 + examples/greet-component/wit/world.wit | 5 + 4 files changed, 468 insertions(+) create mode 100644 examples/greet-component/Cargo.lock create mode 100644 examples/greet-component/Cargo.toml create mode 100644 examples/greet-component/src/lib.rs create mode 100644 examples/greet-component/wit/world.wit diff --git a/examples/greet-component/Cargo.lock b/examples/greet-component/Cargo.lock new file mode 100644 index 00000000..20925d93 --- /dev/null +++ b/examples/greet-component/Cargo.lock @@ -0,0 +1,442 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "greet-component" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[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.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[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.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[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-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", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/greet-component/Cargo.toml b/examples/greet-component/Cargo.toml new file mode 100644 index 00000000..4656cb58 --- /dev/null +++ b/examples/greet-component/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "greet-component" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = "0.43.0" diff --git a/examples/greet-component/src/lib.rs b/examples/greet-component/src/lib.rs new file mode 100644 index 00000000..65a3b020 --- /dev/null +++ b/examples/greet-component/src/lib.rs @@ -0,0 +1,11 @@ +wit_bindgen::generate!({ world: "greet-world" }); + +struct Component; + +impl Guest for Component { + fn greet(name: String) -> String { + format!("Hello, {}!", name) + } +} + +export!(Component); diff --git a/examples/greet-component/wit/world.wit b/examples/greet-component/wit/world.wit new file mode 100644 index 00000000..8a40f184 --- /dev/null +++ b/examples/greet-component/wit/world.wit @@ -0,0 +1,5 @@ +package propeller:greet-component; + +world greet-world { + export greet: func(name: string) -> string; +} From 240ecd97d0c2c76a20142297d00bb2ab57f3fb64 Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Fri, 27 Mar 2026 11:26:28 +0300 Subject: [PATCH 21/21] fix(ci): fix nlreturn lint in FlexStrings and serialize env-var tests --- pkg/task/task.go | 2 ++ proplet/src/config.rs | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/pkg/task/task.go b/pkg/task/task.go index e7f9de31..32961ce0 100644 --- a/pkg/task/task.go +++ b/pkg/task/task.go @@ -20,6 +20,7 @@ func (f *FlexStrings) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(r, &s); err == nil { (*f)[i] = s + continue } var n json.Number @@ -28,6 +29,7 @@ func (f *FlexStrings) UnmarshalJSON(data []byte) error { } (*f)[i] = n.String() } + return nil } diff --git a/proplet/src/config.rs b/proplet/src/config.rs index bdc7c7fe..223b13d0 100644 --- a/proplet/src/config.rs +++ b/proplet/src/config.rs @@ -382,6 +382,13 @@ impl PropletConfig { mod tests { use super::*; use std::env; + use std::sync::{Mutex, OnceLock}; + + static ENV_MUTEX: OnceLock> = OnceLock::new(); + + fn env_lock() -> std::sync::MutexGuard<'static, ()> { + ENV_MUTEX.get_or_init(|| Mutex::new(())).lock().unwrap() + } #[test] fn test_proplet_config_default() { @@ -469,6 +476,7 @@ mod tests { #[test] fn test_proplet_config_from_env_log_level() { + let _lock = env_lock(); env::set_var("PROPLET_LOG_LEVEL", "debug"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_LOG_LEVEL"); @@ -478,6 +486,7 @@ mod tests { #[test] fn test_proplet_config_from_env_mqtt_address() { + let _lock = env_lock(); env::set_var("PROPLET_MQTT_ADDRESS", "tcp://mqtt.example.com:1883"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_MQTT_ADDRESS"); @@ -487,6 +496,7 @@ mod tests { #[test] fn test_proplet_config_from_env_mqtt_timeout() { + let _lock = env_lock(); env::set_var("PROPLET_MQTT_TIMEOUT", "120"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_MQTT_TIMEOUT"); @@ -496,6 +506,7 @@ mod tests { #[test] fn test_proplet_config_from_env_mqtt_qos() { + let _lock = env_lock(); env::set_var("PROPLET_MQTT_QOS", "1"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_MQTT_QOS"); @@ -505,6 +516,7 @@ mod tests { #[test] fn test_proplet_config_from_env_liveliness_interval() { + let _lock = env_lock(); env::set_var("PROPLET_LIVELINESS_INTERVAL", "20"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_LIVELINESS_INTERVAL"); @@ -514,6 +526,7 @@ mod tests { #[test] fn test_proplet_config_from_env_domain_id() { + let _lock = env_lock(); env::set_var("PROPLET_DOMAIN_ID", "domain-123"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_DOMAIN_ID"); @@ -523,6 +536,7 @@ mod tests { #[test] fn test_proplet_config_from_env_channel_id() { + let _lock = env_lock(); env::set_var("PROPLET_CHANNEL_ID", "channel-456"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_CHANNEL_ID"); @@ -532,6 +546,7 @@ mod tests { #[test] fn test_proplet_config_from_env_client_id() { + let _lock = env_lock(); env::set_var("PROPLET_CLIENT_ID", "client-789"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_CLIENT_ID"); @@ -541,6 +556,7 @@ mod tests { #[test] fn test_proplet_config_from_env_client_key() { + let _lock = env_lock(); env::set_var("PROPLET_CLIENT_KEY", "secret-key"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_CLIENT_KEY"); @@ -550,6 +566,7 @@ mod tests { #[test] fn test_proplet_config_from_env_k8s_namespace() { + let _lock = env_lock(); env::set_var("PROPLET_MANAGER_K8S_NAMESPACE", "production"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_MANAGER_K8S_NAMESPACE"); @@ -559,6 +576,7 @@ mod tests { #[test] fn test_proplet_config_from_env_external_runtime() { + let _lock = env_lock(); env::set_var("PROPLET_EXTERNAL_WASM_RUNTIME", "/usr/local/bin/wasmtime"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_EXTERNAL_WASM_RUNTIME"); @@ -571,6 +589,7 @@ mod tests { #[test] fn test_proplet_config_from_env_mqtt_timeout_invalid() { + let _lock = env_lock(); env::set_var("PROPLET_MQTT_TIMEOUT", "not-a-number"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_MQTT_TIMEOUT"); @@ -580,6 +599,7 @@ mod tests { #[test] fn test_proplet_config_from_env_mqtt_qos_invalid() { + let _lock = env_lock(); env::set_var("PROPLET_MQTT_QOS", "invalid"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_MQTT_QOS"); @@ -589,6 +609,7 @@ mod tests { #[test] fn test_proplet_config_from_env_no_env_vars() { + let _lock = env_lock(); let vars_to_clear = vec![ "PROPLET_LOG_LEVEL", "PROPLET_MQTT_ADDRESS",