From 4e8326c470e0497f8d0769574216714a04007603 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Wed, 21 Jan 2026 11:34:50 +0100 Subject: [PATCH 01/19] add manfest.rs to load a capsule.toml --- crates/capsule-core/Cargo.lock | 57 ++++++++++++- crates/capsule-core/Cargo.toml | 1 + crates/capsule-core/src/config/manifest.rs | 94 ++++++++++++++++++++++ crates/capsule-core/src/config/mod.rs | 1 + 4 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 crates/capsule-core/src/config/manifest.rs diff --git a/crates/capsule-core/Cargo.lock b/crates/capsule-core/Cargo.lock index eff28d5..25b0d6f 100644 --- a/crates/capsule-core/Cargo.lock +++ b/crates/capsule-core/Cargo.lock @@ -260,6 +260,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "toml 0.9.11+spec-1.1.0", "wasmtime", "wasmtime-wasi", "wasmtime-wasi-http", @@ -2189,6 +2190,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2530,11 +2540,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit", ] +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -2544,6 +2569,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -2552,18 +2586,33 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "tower" version = "0.5.2" diff --git a/crates/capsule-core/Cargo.toml b/crates/capsule-core/Cargo.toml index 0832e73..2c2ac60 100644 --- a/crates/capsule-core/Cargo.toml +++ b/crates/capsule-core/Cargo.toml @@ -27,6 +27,7 @@ wasmtime-wasi-http = "29.0.0" indicatif = "0.17" chrono = "0.4" colored = "2.1" +toml = "0.9.11" [dev-dependencies] rustfmt = "0.10.0" diff --git a/crates/capsule-core/src/config/manifest.rs b/crates/capsule-core/src/config/manifest.rs new file mode 100644 index 0000000..1d35538 --- /dev/null +++ b/crates/capsule-core/src/config/manifest.rs @@ -0,0 +1,94 @@ +use serde::Deserialize; +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::wasm::execution_policy::{Compute, ExecutionPolicy}; + +#[derive(Debug)] +pub enum ManifestError { + FsError(String), + ParseError(String), +} + +impl std::fmt::Display for ManifestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ManifestError::FsError(e) => write!(f, "Filesystem error > {}", e), + ManifestError::ParseError(e) => write!(f, "Parse error > {}", e), + } + } +} + +impl std::error::Error for ManifestError {} + +#[derive(Debug, Deserialize)] +pub struct Workflow { + pub name: Option, + pub version: Option, + pub entrypoint: Option, +} + +#[derive(Debug, Deserialize)] +pub struct DefaultPolicy { + pub default_compute: Option, + pub default_ram: Option, + pub default_timeout: Option, + pub default_max_retries: Option, + pub default_allowed_files: Option>, +} + +#[derive(Debug, Deserialize)] +pub struct CapsuleToml { + pub workflow: Option, + pub tasks: Option, +} + +pub struct Manifest { + pub source_path: PathBuf, + pub capsule_toml: CapsuleToml, +} + +impl Manifest { + pub fn load_from_file(path: &Path) -> Result { + let source_path = path + .canonicalize() + .map_err(|e| ManifestError::FsError(format!("Cannot resolve source path: {}", e)))?; + + let capsule_toml_path = Self::find_capsule_toml(&source_path); + + let capsule_toml = match capsule_toml_path { + Some(toml_path) => { + let contents = fs::read_to_string(&toml_path).map_err(|e| { + ManifestError::FsError(format!("Failed to read capsule.toml: {}", e)) + })?; + + toml::from_str(&contents).map_err(|e| { + ManifestError::ParseError(format!("Failed to parse capsule.toml: {}", e)) + })? + } + None => CapsuleToml { + workflow: None, + tasks: None, + }, + }; + + Ok(Self { + source_path, + capsule_toml, + }) + } + + fn find_capsule_toml(dir: &Path) -> Option { + let lowercase = dir.join("capsule.toml"); + if lowercase.exists() { + return Some(lowercase); + } + + let capitalized = dir.join("Capsule.toml"); + if capitalized.exists() { + return Some(capitalized); + } + + None + } +} diff --git a/crates/capsule-core/src/config/mod.rs b/crates/capsule-core/src/config/mod.rs index b6c0d0e..bf23c6b 100644 --- a/crates/capsule-core/src/config/mod.rs +++ b/crates/capsule-core/src/config/mod.rs @@ -1,2 +1,3 @@ pub mod database; pub mod log; +pub mod manifest; From 4bc16add444a5772b4fc82016eda33d52aba9aeb Mon Sep 17 00:00:00 2001 From: Mavdol Date: Wed, 21 Jan 2026 11:59:30 +0100 Subject: [PATCH 02/19] source_path directly in main manifest method --- crates/capsule-core/src/config/manifest.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/capsule-core/src/config/manifest.rs b/crates/capsule-core/src/config/manifest.rs index 1d35538..57cca47 100644 --- a/crates/capsule-core/src/config/manifest.rs +++ b/crates/capsule-core/src/config/manifest.rs @@ -2,7 +2,7 @@ use serde::Deserialize; use std::fs; use std::path::{Path, PathBuf}; -use crate::wasm::execution_policy::{Compute, ExecutionPolicy}; +use crate::wasm::execution_policy::Compute; #[derive(Debug)] pub enum ManifestError { @@ -49,8 +49,8 @@ pub struct Manifest { } impl Manifest { - pub fn load_from_file(path: &Path) -> Result { - let source_path = path + pub fn new() -> Result { + let source_path = Path::new(".") .canonicalize() .map_err(|e| ManifestError::FsError(format!("Cannot resolve source path: {}", e)))?; From bf0657c0e6774cb8cf13740b1665b431fb626805 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Wed, 21 Jan 2026 12:04:35 +0100 Subject: [PATCH 03/19] get manifest entrypoint function --- crates/capsule-core/src/config/manifest.rs | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/capsule-core/src/config/manifest.rs b/crates/capsule-core/src/config/manifest.rs index 57cca47..d63c6a1 100644 --- a/crates/capsule-core/src/config/manifest.rs +++ b/crates/capsule-core/src/config/manifest.rs @@ -78,6 +78,29 @@ impl Manifest { }) } + pub fn get_entrypoint(&self) -> Result { + let workflow = self + .capsule_toml + .workflow + .as_ref() + .ok_or_else(|| { + ManifestError::ParseError( + "No [workflow] section found in capsule.toml. Please add a workflow configuration or specify a file path.".to_string() + ) + })?; + + let entrypoint = workflow + .entrypoint + .as_ref() + .ok_or_else(|| { + ManifestError::ParseError( + "No 'entrypoint' field found in [workflow] section of capsule.toml.".to_string() + ) + })?; + + Ok(entrypoint.clone()) + } + fn find_capsule_toml(dir: &Path) -> Option { let lowercase = dir.join("capsule.toml"); if lowercase.exists() { From 58d16f870e4731f464d194e126548e379c17e3e8 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Wed, 21 Jan 2026 12:19:53 +0100 Subject: [PATCH 04/19] makes file path optional with run command --- crates/capsule-cli/Cargo.lock | 59 +++++++++++++++++++++++++++++++--- crates/capsule-cli/src/cli.rs | 2 +- crates/capsule-cli/src/main.rs | 3 +- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/crates/capsule-cli/Cargo.lock b/crates/capsule-cli/Cargo.lock index bcc21a3..d4e31d7 100644 --- a/crates/capsule-cli/Cargo.lock +++ b/crates/capsule-cli/Cargo.lock @@ -279,6 +279,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "toml 0.9.11+spec-1.1.0", "wasmtime", "wasmtime-wasi", "wasmtime-wasi-http", @@ -2018,6 +2019,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2299,11 +2309,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit", ] +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -2313,6 +2338,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -2321,18 +2355,33 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "tower" version = "0.5.2" @@ -2721,7 +2770,7 @@ dependencies = [ "serde", "serde_derive", "sha2", - "toml", + "toml 0.8.23", "windows-sys 0.59.0", "zstd", ] diff --git a/crates/capsule-cli/src/cli.rs b/crates/capsule-cli/src/cli.rs index 80733d1..1a4cb29 100644 --- a/crates/capsule-cli/src/cli.rs +++ b/crates/capsule-cli/src/cli.rs @@ -12,7 +12,7 @@ pub struct Cli { #[derive(Subcommand)] pub enum Commands { Run { - file: String, + file: Option, #[arg(short, long)] verbose: bool, diff --git a/crates/capsule-cli/src/main.rs b/crates/capsule-cli/src/main.rs index bc55b8a..44b1630 100644 --- a/crates/capsule-cli/src/main.rs +++ b/crates/capsule-cli/src/main.rs @@ -37,7 +37,8 @@ async fn main() -> Result<(), CliError> { verbose, args, } => { - run::execute(Path::new(&file), args, verbose).await?; + let file_path = file.as_deref().map(Path::new); + run::execute(file_path, args, verbose).await?; } } From c430c2c5b644c9cd1ddbcbe08f89bb96abc45965 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Wed, 21 Jan 2026 12:20:30 +0100 Subject: [PATCH 05/19] capsule.toml entrypoint manage --- crates/capsule-cli/src/commands/run.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/capsule-cli/src/commands/run.rs b/crates/capsule-cli/src/commands/run.rs index 469d1e4..ad29fd7 100644 --- a/crates/capsule-cli/src/commands/run.rs +++ b/crates/capsule-cli/src/commands/run.rs @@ -2,6 +2,8 @@ use std::fmt; use std::path::{Path, PathBuf}; use std::time::Instant; +use capsule_core::config::manifest::{Manifest, ManifestError}; + use capsule_core::wasm::commands::create::CreateInstance; use capsule_core::wasm::commands::run::RunInstance; use capsule_core::wasm::compiler::javascript::{ @@ -52,6 +54,12 @@ impl From for RunError { } } +impl From for RunError { + fn from(err: ManifestError) -> Self { + RunError::ExecutionFailed(err.to_string()) + } +} + struct CompileResult { wasm_path: PathBuf, cache_dir: PathBuf, @@ -88,14 +96,20 @@ fn compile_to_wasm(file_path: &Path) -> Result { } pub async fn execute( - file_path: &Path, + file_path: Option<&Path>, args: Vec, verbose: bool, ) -> Result { + let manifest = Manifest::new()?; let mut reporter = TaskReporter::new(true); + let file_path: PathBuf = match file_path { + Some(path) => path.to_path_buf(), + None => PathBuf::from(manifest.get_entrypoint()?), + }; + reporter.start_progress("Preparing environment"); - let compile_result = compile_to_wasm(file_path)?; + let compile_result = compile_to_wasm(&file_path)?; reporter.finish_progress(Some("Environment ready")); reporter.start_progress("Initializing runtime"); From 244d7d38c4d64d5bc86b8c523af2bf10c2be91bc Mon Sep 17 00:00:00 2001 From: Mavdol Date: Wed, 21 Jan 2026 19:44:17 +0100 Subject: [PATCH 06/19] Update runtime --- crates/capsule-cli/src/commands/run.rs | 2 +- crates/capsule-core/src/wasm/runtime.rs | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/capsule-cli/src/commands/run.rs b/crates/capsule-cli/src/commands/run.rs index ad29fd7..30514b3 100644 --- a/crates/capsule-cli/src/commands/run.rs +++ b/crates/capsule-cli/src/commands/run.rs @@ -117,7 +117,7 @@ pub async fn execute( cache_dir: compile_result.cache_dir, verbose, }; - let runtime = Runtime::with_config(runtime_config)?; + let runtime = Runtime::new(runtime_config, manifest.capsule_toml)?; let execution_policy = ExecutionPolicy::default() .compute(Some(Compute::Custom(u64::MAX))) diff --git a/crates/capsule-core/src/wasm/runtime.rs b/crates/capsule-core/src/wasm/runtime.rs index 465c2ee..0b51051 100644 --- a/crates/capsule-core/src/wasm/runtime.rs +++ b/crates/capsule-core/src/wasm/runtime.rs @@ -7,6 +7,7 @@ use wasmtime::component::Component; use wasmtime::{Config, Engine}; use crate::config::log::{Log, LogError}; +use crate::config::manifest::CapsuleToml; use crate::wasm::utilities::task_reporter::TaskReporter; pub enum WasmRuntimeError { @@ -78,14 +79,11 @@ pub struct Runtime { component: RwLock>, pub task_reporter: Arc>, + pub capsule_toml: CapsuleToml } impl Runtime { - pub fn new() -> Result, WasmRuntimeError> { - Self::with_config(RuntimeConfig::default()) - } - - pub fn with_config(config: RuntimeConfig) -> Result, WasmRuntimeError> { + pub fn new(config: RuntimeConfig, capsule_toml: CapsuleToml) -> Result, WasmRuntimeError> { let mut engine_config = Config::new(); let db_path = config.cache_dir.join("trace.db"); let log = Log::new( @@ -116,6 +114,7 @@ impl Runtime { verbose: config.verbose, component: RwLock::new(None), task_reporter, + capsule_toml })) } From 5c9b92da1bd053bd61c1570755f9ecb34f93b288 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Wed, 21 Jan 2026 23:25:10 +0100 Subject: [PATCH 07/19] manager capsule toml default values for task config --- crates/capsule-core/src/config/manifest.rs | 15 +- crates/capsule-core/src/wasm/runtime.rs | 9 +- crates/capsule-core/src/wasm/state.rs | 2 +- .../src/wasm/utilities/task_config.rs | 151 ++++++++++++++++-- 4 files changed, 152 insertions(+), 25 deletions(-) diff --git a/crates/capsule-core/src/config/manifest.rs b/crates/capsule-core/src/config/manifest.rs index d63c6a1..f1e6931 100644 --- a/crates/capsule-core/src/config/manifest.rs +++ b/crates/capsule-core/src/config/manifest.rs @@ -37,7 +37,7 @@ pub struct DefaultPolicy { pub default_allowed_files: Option>, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Default, Deserialize)] pub struct CapsuleToml { pub workflow: Option, pub tasks: Option, @@ -89,14 +89,11 @@ impl Manifest { ) })?; - let entrypoint = workflow - .entrypoint - .as_ref() - .ok_or_else(|| { - ManifestError::ParseError( - "No 'entrypoint' field found in [workflow] section of capsule.toml.".to_string() - ) - })?; + let entrypoint = workflow.entrypoint.as_ref().ok_or_else(|| { + ManifestError::ParseError( + "No 'entrypoint' field found in [workflow] section of capsule.toml.".to_string(), + ) + })?; Ok(entrypoint.clone()) } diff --git a/crates/capsule-core/src/wasm/runtime.rs b/crates/capsule-core/src/wasm/runtime.rs index 0b51051..347b33e 100644 --- a/crates/capsule-core/src/wasm/runtime.rs +++ b/crates/capsule-core/src/wasm/runtime.rs @@ -79,11 +79,14 @@ pub struct Runtime { component: RwLock>, pub task_reporter: Arc>, - pub capsule_toml: CapsuleToml + pub capsule_toml: CapsuleToml, } impl Runtime { - pub fn new(config: RuntimeConfig, capsule_toml: CapsuleToml) -> Result, WasmRuntimeError> { + pub fn new( + config: RuntimeConfig, + capsule_toml: CapsuleToml, + ) -> Result, WasmRuntimeError> { let mut engine_config = Config::new(); let db_path = config.cache_dir.join("trace.db"); let log = Log::new( @@ -114,7 +117,7 @@ impl Runtime { verbose: config.verbose, component: RwLock::new(None), task_reporter, - capsule_toml + capsule_toml, })) } diff --git a/crates/capsule-core/src/wasm/state.rs b/crates/capsule-core/src/wasm/state.rs index 4fd44e4..c8ee9d6 100644 --- a/crates/capsule-core/src/wasm/state.rs +++ b/crates/capsule-core/src/wasm/state.rs @@ -64,7 +64,7 @@ impl Host for State { }; let task_config: TaskConfig = serde_json::from_str(&config).unwrap_or_default(); - let policy = task_config.to_execution_policy(); + let policy = task_config.to_execution_policy(&runtime.capsule_toml); let max_retries = policy.max_retries; let mut last_error: Option = None; diff --git a/crates/capsule-core/src/wasm/utilities/task_config.rs b/crates/capsule-core/src/wasm/utilities/task_config.rs index f8207cd..50be2f1 100644 --- a/crates/capsule-core/src/wasm/utilities/task_config.rs +++ b/crates/capsule-core/src/wasm/utilities/task_config.rs @@ -1,5 +1,6 @@ use serde::Deserialize; +use crate::config::manifest::CapsuleToml; use crate::wasm::execution_policy::{Compute, ExecutionPolicy}; #[derive(Debug, Deserialize, Default)] @@ -17,7 +18,9 @@ pub struct TaskConfig { } impl TaskConfig { - pub fn to_execution_policy(&self) -> ExecutionPolicy { + pub fn to_execution_policy(&self, capsule_toml: &CapsuleToml) -> ExecutionPolicy { + let default_policy = capsule_toml.tasks.as_ref(); + let compute = self .compute .as_ref() @@ -29,17 +32,41 @@ impl TaskConfig { .parse::() .map(Compute::Custom) .unwrap_or(Compute::Medium), + }) + .or_else(|| default_policy.and_then(|p| p.default_compute.clone())); + + let ram = self + .ram + .as_ref() + .and_then(|r| Self::parse_ram_string(r)) + .or_else(|| { + default_policy + .and_then(|p| p.default_ram.as_ref()) + .and_then(|r| Self::parse_ram_string(r)) }); - let ram = self.ram.as_ref().and_then(|r| Self::parse_ram_string(r)); + let timeout = self + .timeout + .clone() + .or_else(|| default_policy.and_then(|p| p.default_timeout.clone())); + + let max_retries = self + .max_retries + .or_else(|| default_policy.and_then(|p| p.default_max_retries)); + + let allowed_files = self + .allowed_files + .clone() + .or_else(|| default_policy.and_then(|p| p.default_allowed_files.clone())) + .unwrap_or_default(); ExecutionPolicy::new() .name(self.name.clone()) .compute(compute) .ram(ram) - .timeout(self.timeout.clone()) - .max_retries(self.max_retries) - .allowed_files(self.allowed_files.clone().unwrap_or_default()) + .timeout(timeout) + .max_retries(max_retries) + .allowed_files(allowed_files) } pub fn parse_ram_string(s: &str) -> Option { @@ -71,6 +98,7 @@ impl TaskConfig { #[cfg(test)] mod tests { use super::*; + use crate::config::manifest::DefaultPolicy; #[test] fn test_parse_ram_string() { @@ -118,7 +146,7 @@ mod tests { #[test] fn test_to_execution_policy_default() { let config = TaskConfig::default(); - let policy = config.to_execution_policy(); + let policy = config.to_execution_policy(&CapsuleToml::default()); assert_eq!(policy.name, "default"); assert_eq!(policy.compute, Compute::Medium); @@ -135,10 +163,10 @@ mod tests { ram: Some("2GB".to_string()), timeout: Some("30s".to_string()), max_retries: Some(3), - allowed_files: Some(vec!["./data/input.txt".to_string()]), + allowed_files: Some(vec!["./data".to_string()]), }; - let policy = config.to_execution_policy(); + let policy = config.to_execution_policy(&CapsuleToml::default()); assert_eq!(policy.name, "test_task"); assert_eq!(policy.compute, Compute::High); @@ -153,24 +181,123 @@ mod tests { compute: Some("LOW".to_string()), ..Default::default() }; - assert_eq!(low.to_execution_policy().compute, Compute::Low); + assert_eq!( + low.to_execution_policy(&CapsuleToml::default()).compute, + Compute::Low + ); let medium = TaskConfig { compute: Some("MEDIUM".to_string()), ..Default::default() }; - assert_eq!(medium.to_execution_policy().compute, Compute::Medium); + assert_eq!( + medium.to_execution_policy(&CapsuleToml::default()).compute, + Compute::Medium + ); let high = TaskConfig { compute: Some("HIGH".to_string()), ..Default::default() }; - assert_eq!(high.to_execution_policy().compute, Compute::High); + assert_eq!( + high.to_execution_policy(&CapsuleToml::default()).compute, + Compute::High + ); let invalid = TaskConfig { compute: Some("INVALID".to_string()), ..Default::default() }; - assert_eq!(invalid.to_execution_policy().compute, Compute::Medium); + assert_eq!( + invalid.to_execution_policy(&CapsuleToml::default()).compute, + Compute::Medium + ); + } + + #[test] + fn test_to_execution_policy_uses_capsule_toml_defaults() { + let capsule_toml = CapsuleToml { + workflow: None, + tasks: Some(DefaultPolicy { + default_compute: Some(Compute::High), + default_ram: Some("1GB".to_string()), + default_timeout: Some("60s".to_string()), + default_max_retries: Some(5), + default_allowed_files: Some(vec!["./default".to_string()]), + }), + }; + + let config = TaskConfig::default(); + let policy = config.to_execution_policy(&capsule_toml); + + assert_eq!(policy.compute, Compute::High); + assert_eq!(policy.ram, Some(1024 * 1024 * 1024)); + assert_eq!(policy.timeout, Some("60s".to_string())); + assert_eq!(policy.max_retries, 5); + assert_eq!(policy.allowed_files, vec!["./default".to_string()]); + } + + #[test] + fn test_task_config_overrides_capsule_toml_defaults() { + let capsule_toml = CapsuleToml { + workflow: None, + tasks: Some(DefaultPolicy { + default_compute: Some(Compute::Low), + default_ram: Some("512MB".to_string()), + default_timeout: Some("30s".to_string()), + default_max_retries: Some(2), + default_allowed_files: Some(vec!["./default.txt".to_string()]), + }), + }; + + let config = TaskConfig { + name: Some("override_task".to_string()), + compute: Some("HIGH".to_string()), + ram: Some("4GB".to_string()), + timeout: Some("120s".to_string()), + max_retries: Some(10), + allowed_files: Some(vec!["./custom".to_string()]), + }; + + let policy = config.to_execution_policy(&capsule_toml); + + assert_eq!(policy.name, "override_task"); + assert_eq!(policy.compute, Compute::High); + assert_eq!(policy.ram, Some(4 * 1024 * 1024 * 1024)); + assert_eq!(policy.timeout, Some("120s".to_string())); + assert_eq!(policy.max_retries, 10); + assert_eq!(policy.allowed_files, vec!["./custom".to_string()]); + } + + #[test] + fn test_partial_task_config_with_capsule_toml_defaults() { + let capsule_toml = CapsuleToml { + workflow: None, + tasks: Some(DefaultPolicy { + default_compute: Some(Compute::Medium), + default_ram: Some("2GB".to_string()), + default_timeout: Some("45s".to_string()), + default_max_retries: Some(3), + default_allowed_files: Some(vec!["./default".to_string()]), + }), + }; + + let config = TaskConfig { + name: Some("partial_task".to_string()), + compute: Some("LOW".to_string()), + ram: None, + timeout: None, + max_retries: Some(1), + allowed_files: None, + }; + + let policy = config.to_execution_policy(&capsule_toml); + + assert_eq!(policy.name, "partial_task"); + assert_eq!(policy.compute, Compute::Low); + assert_eq!(policy.ram, Some(2 * 1024 * 1024 * 1024)); + assert_eq!(policy.timeout, Some("45s".to_string())); + assert_eq!(policy.max_retries, 1); + assert_eq!(policy.allowed_files, vec!["./default".to_string()]); } } From 1c478a821868060a966ddbcc8810d0f3dfc17c16 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Thu, 22 Jan 2026 13:52:03 +0100 Subject: [PATCH 08/19] change task running message --- crates/capsule-core/src/wasm/utilities/task_reporter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/capsule-core/src/wasm/utilities/task_reporter.rs b/crates/capsule-core/src/wasm/utilities/task_reporter.rs index c95273a..76abc32 100644 --- a/crates/capsule-core/src/wasm/utilities/task_reporter.rs +++ b/crates/capsule-core/src/wasm/utilities/task_reporter.rs @@ -41,7 +41,7 @@ impl TaskReporter { self.start_time = Instant::now(); if self.verbose { println!( - "{} {} [{}] Starting task", + "{} {} [{}] Started", Self::timestamp(), "INFO".cyan(), task_name.magenta() From 6b1480a845788b5f7f74fa15af6837b26ab5a645 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Thu, 22 Jan 2026 14:11:35 +0100 Subject: [PATCH 09/19] clear examples --- .../javascript/limit-testing/limits-test.ts | 51 +----- examples/python/hello-world/main.py | 16 -- examples/python/http-request/main.py | 2 +- examples/python/limit-testing/limits_test.py | 167 ++++++++++++++++++ examples/python/recursive-tasks/main.py | 54 ------ .../python/voice-of-customer/requirements.txt | 4 - examples/python/voice-of-customer/src/main.py | 73 -------- .../voice-of-customer/src/model/__init__.py | 1 - .../voice-of-customer/src/model/feedback.py | 20 --- .../voice-of-customer/src/tasks/analyze.py | 65 ------- .../voice-of-customer/src/tasks/fetch.py | 66 ------- 11 files changed, 169 insertions(+), 350 deletions(-) delete mode 100644 examples/python/hello-world/main.py create mode 100644 examples/python/limit-testing/limits_test.py delete mode 100644 examples/python/recursive-tasks/main.py delete mode 100644 examples/python/voice-of-customer/requirements.txt delete mode 100644 examples/python/voice-of-customer/src/main.py delete mode 100644 examples/python/voice-of-customer/src/model/__init__.py delete mode 100644 examples/python/voice-of-customer/src/model/feedback.py delete mode 100644 examples/python/voice-of-customer/src/tasks/analyze.py delete mode 100644 examples/python/voice-of-customer/src/tasks/fetch.py diff --git a/examples/javascript/limit-testing/limits-test.ts b/examples/javascript/limit-testing/limits-test.ts index dbd9e16..518a632 100644 --- a/examples/javascript/limit-testing/limits-test.ts +++ b/examples/javascript/limit-testing/limits-test.ts @@ -1,9 +1,7 @@ import { task } from "@capsule-run/sdk"; + /** * This task should TIMEOUT because it sleeps longer than the configured timeout. - * - * Expected: Timeout error after 2 seconds - * Actual behavior: Sleeps for 10 seconds */ export const testTimeout = task( { @@ -12,10 +10,6 @@ export const testTimeout = task( compute: "HIGH" }, async (): Promise => { - console.log("Starting timeout test..."); - console.log("Configured timeout: 2 seconds"); - console.log("Will attempt to sleep for: 10 seconds"); - await new Promise(resolve => setTimeout(resolve, 10000)); console.log("Sleep completed (this should NOT print)"); @@ -25,10 +19,6 @@ export const testTimeout = task( /** * This task should EXCEED compute limits with intensive CPU operations. - * - * LOW compute: ~100M fuel - * Expected: Out of fuel error - * Actual behavior: Performs billions of operations */ export const testComputeLimitLow = task( { @@ -37,10 +27,6 @@ export const testComputeLimitLow = task( timeout: "60s" }, (): string => { - console.log("Starting LOW compute limit test..."); - console.log("Configured compute: LOW (~100M fuel)"); - console.log("Will perform intensive CPU operations..."); - let result = 0; for (let i = 0; i < 100000000; i++) { @@ -58,9 +44,6 @@ export const testComputeLimitLow = task( /** * This task should EXCEED MEDIUM compute limits. - * - * MEDIUM compute: ~2B fuel - * Expected: Out of fuel error */ export const testComputeLimitMedium = task( { @@ -69,10 +52,6 @@ export const testComputeLimitMedium = task( timeout: "60s" }, (): string => { - console.log("๐Ÿ”ฅ Starting MEDIUM compute limit test..."); - console.log("โšก Configured compute: MEDIUM (~2B fuel)"); - console.log("โšก Will perform VERY intensive CPU operations..."); - let result = 0; for (let i = 0; i < 10000000000; i++) { @@ -90,9 +69,6 @@ export const testComputeLimitMedium = task( /** * This task should EXCEED RAM limits by allocating large arrays. - * - * Expected: Out of memory error - * Actual behavior: Allocates ~1GB+ of memory */ export const testRAMLimit = task( { @@ -102,10 +78,6 @@ export const testRAMLimit = task( timeout: "30s" }, (): string => { - console.log("Starting RAM limit test..."); - console.log("Configured RAM: 10MB"); - console.log("Will attempt to allocate: ~100MB+"); - const arrays: number[][] = []; try { @@ -132,8 +104,6 @@ export const testRAMLimit = task( * - Very low timeout * - Low compute * - Low RAM - * - * Expected: Should fail on whichever limit is hit first */ export const testCombinedLimits = task( { @@ -143,18 +113,10 @@ export const testCombinedLimits = task( ram: "5MB" }, async (): Promise => { - console.log("Starting combined limits test..."); - console.log("Timeout: 3s"); - console.log("Compute: LOW"); - console.log("RAM: 5MB"); - console.log(""); - const data: number[][] = []; let computeResult = 0; for (let i = 0; i < 100; i++) { - console.log(`๐Ÿ”„ Iteration ${i}...`); - data.push(new Array(100000).fill(0).map(() => Math.random())); for (let j = 0; j < 1000000; j++) { @@ -171,8 +133,6 @@ export const testCombinedLimits = task( /** * Tests timeout with multiple async operations. - * - * Expected: Timeout error after 5 seconds */ export const testAsyncTimeout = task( { @@ -181,13 +141,9 @@ export const testAsyncTimeout = task( compute: "MEDIUM" }, async (): Promise => { - console.log("Starting async timeout test..."); - console.log("Configured timeout: 5 seconds"); - const delays = [2000, 2000, 2000, 2000, 2000]; for (let i = 0; i < delays.length; i++) { - console.log(`Delay ${i + 1}/${delays.length} (${delays[i]}ms)...`); await new Promise(resolve => setTimeout(resolve, delays[i])); console.log(`โœ“ Delay ${i + 1} completed`); } @@ -204,14 +160,9 @@ export const testImmediateFuelExhaustion = task( timeout: "60s" }, (): string => { - console.log("Starting immediate fuel exhaustion test..."); - console.log("This should fail almost immediately with LOW compute"); - let result = 0; const iterations = 1000000000; - console.log(`Starting ${iterations.toLocaleString()} iterations...`); - for (let i = 0; i < iterations; i++) { result += Math.pow(Math.sqrt(i), 2) * Math.sin(i) * Math.cos(i) * Math.tan(i) * Math.log(i + 1); diff --git a/examples/python/hello-world/main.py b/examples/python/hello-world/main.py deleted file mode 100644 index 32de9e5..0000000 --- a/examples/python/hello-world/main.py +++ /dev/null @@ -1,16 +0,0 @@ -from capsule import task - - -@task(name="First_task", compute="MEDIUM", ram="128MB") -def hello() -> str: - """Simple main task that just returns a message.""" - return "Pre task like! 1" - -@task(name="de", compute="MEDIUM", ram="128MB") -def main() -> str: - """Simple main task that just returns a message.""" - hellomessage = hello() - print(hellomessage) - return "Hello from caps! ๐Ÿš€" - - diff --git a/examples/python/http-request/main.py b/examples/python/http-request/main.py index c1f702a..c56e12d 100644 --- a/examples/python/http-request/main.py +++ b/examples/python/http-request/main.py @@ -43,7 +43,7 @@ def create_post(): print(f"โŒ Request failed with status: {response.status_code}") return {"error": response.status_code} - +@task(name="main") def main(): """Run HTTP examples.""" print("๐ŸŒ HTTP Request Example") diff --git a/examples/python/limit-testing/limits_test.py b/examples/python/limit-testing/limits_test.py new file mode 100644 index 0000000..7c5e636 --- /dev/null +++ b/examples/python/limit-testing/limits_test.py @@ -0,0 +1,167 @@ +import math +import time +from capsule import task + + +@task(name="test_timeout", timeout="2s", compute="HIGH") +def test_timeout() -> str: + """ + This task should TIMEOUT because it sleeps longer than the configured timeout. + """ + time.sleep(10) + + print("Sleep completed (this should NOT print)") + return "TIMEOUT TEST FAILED - Should have timed out!" + + +@task(name="test_compute_limit_low", compute="LOW", timeout="60s") +def test_compute_limit_low() -> str: + """ + This task should EXCEED compute limits with intensive CPU operations. + """ + result = 0.0 + + for i in range(100_000_000): + result += math.sqrt(i) * math.sin(i) * math.cos(i) + + if i % 1000 == 0: + result = result % 1_000_000 + + print("Computation completed (this should NOT print)") + return f"COMPUTE TEST FAILED - Should have run out of fuel! Result: {result}" + + +@task(name="test_compute_limit_medium", compute="MEDIUM", timeout="60s") +def test_compute_limit_medium() -> str: + """ + This task should EXCEED MEDIUM compute limits. + """ + result = 0.0 + + for i in range(10_000_000_000): + result += math.sqrt(i) * math.sin(i) * math.cos(i) * math.tan(i) + + if i % 10000 == 0: + result = result % 1_000_000 + + print("Computation completed (this should NOT print)") + return f"COMPUTE TEST FAILED - Should have run out of fuel! Result: {result}" + + +@task(name="test_ram_limit", ram="10MB", compute="HIGH", timeout="30s") +def test_ram_limit() -> str: + """ + This task should EXCEED RAM limits by allocating large lists. + """ + arrays = [] + + try: + for i in range(1000): + large_array = [idx * 0.5 for idx in range(1_000_000)] + arrays.append(large_array) + + if i % 10 == 0: + print(f"๐Ÿ“Š Allocated {i} arrays (~{i * 8}MB)") + + print("Memory allocation completed (this should NOT print)") + return f"RAM TEST FAILED - Should have run out of memory! Arrays: {len(arrays)}" + except Exception as error: + print(f"Python error: {error}") + raise error + + +@task(name="test_combined_limits", timeout="3s", compute="LOW", ram="5MB") +def test_combined_limits() -> str: + """ + This task tests multiple limits simultaneously: + - Very low timeout + - Low compute + - Low RAM + """ + data = [] + compute_result = 0.0 + + for i in range(100): + data.append([j * 0.5 for j in range(100_000)]) + + for j in range(1_000_000): + compute_result += math.sqrt(j) * math.sin(j) + + time.sleep(0.1) + + print("All operations completed (this should NOT print)") + return f"COMBINED TEST FAILED - Should have hit a limit! Data: {len(data)}, Compute: {compute_result}" + + +@task(name="test_async_timeout", timeout="5s", compute="MEDIUM") +def test_async_timeout() -> str: + """ + Tests timeout with multiple sleep operations. + """ + delays = [2.0, 2.0, 2.0, 2.0, 2.0] + + for i, delay in enumerate(delays): + time.sleep(delay) + + print("All delays completed (this should NOT print)") + return "ASYNC TIMEOUT TEST FAILED - Should have timed out!" + + +@task(name="test_immediate_fuel_exhaustion", compute="LOW", timeout="60s") +def test_immediate_fuel_exhaustion() -> str: + """ + This task should run out of fuel almost immediately. + """ + result = 0.0 + iterations = 1_000_000_000 + + for i in range(iterations): + result += ( + math.pow(math.sqrt(i), 2) + * math.sin(i) + * math.cos(i) + * math.tan(i) + * math.log(i + 1) + ) + + print("Completed (this should NOT print)") + return f"FUEL TEST FAILED - Result: {result}" + + +@task(name="main", compute="HIGH", timeout="120s") +def main() -> None: + """ + Main function to demonstrate all limit tests. + + Uncomment individual tests to run them one at a time. + Each test is designed to FAIL with a specific limit error. + """ + # Test 1: Timeout + print("\n๐Ÿงช Running Test 1: Timeout Limit") + test_timeout() + + # Test 2: Low Compute + print("\n๐Ÿงช Running Test 2: LOW Compute Limit") + test_compute_limit_low() + + # Test 3: Medium Compute + print("\n๐Ÿงช Running Test 3: MEDIUM Compute Limit") + test_compute_limit_medium() + + # Test 4: RAM Limit + print("\n๐Ÿงช Running Test 4: RAM Limit") + test_ram_limit() + + # Test 5: Combined Limits + print("\n๐Ÿงช Running Test 5: Combined Limits") + test_combined_limits() + + # Test 6: Async Timeout + print("\n๐Ÿงช Running Test 6: Async Timeout") + test_async_timeout() + + # Test 7: Immediate Fuel Exhaustion + print("\n๐Ÿงช Running Test 7: Immediate Fuel Exhaustion") + test_immediate_fuel_exhaustion() + + print("\nโœ… Limit testing completed.") diff --git a/examples/python/recursive-tasks/main.py b/examples/python/recursive-tasks/main.py deleted file mode 100644 index 1c7da78..0000000 --- a/examples/python/recursive-tasks/main.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -Example demonstrating recursive/nested task execution with config. - -This example shows: -1. How task configuration is properly passed from decorator to host -2. How tasks can call other tasks (recursive execution) -3. How the host creates isolated Wasm instances for each task -""" - -from capsule import task - - -@task(name="add", compute="LOW", ram="64MB", timeout="5s") -def add(a: int, b: int) -> int: - """A simple addition task with LOW compute requirements.""" - return a + b - - -@task(name="multiply", compute="MEDIUM", ram="128MB", timeout="10s") -def multiply(a: int, b: int) -> int: - """A multiplication task with MEDIUM compute requirements.""" - return a * b - - -@task(name="complex_calculation", compute="HIGH", ram="512MB", timeout="30s", max_retries=3) -def complex_calculation(x: int, y: int) -> dict: - """ - A complex calculation that spawns sub-tasks. - - This demonstrates recursive task execution: - 1. The host creates a Wasm instance for this task - 2. This task calls add() and multiply() - 3. Each call creates a NEW Wasm instance with its own resource limits - 4. Results are returned back up the chain - """ - sum_result = add(x, y) - product_result = multiply(x, y) - - return { - "sum": sum_result, - "product": product_result, - "total": sum_result + product_result - } - - -@task(name="main", compute="MEDIUM", ram="256MB") -def main() -> dict: - """Main entry point - orchestrates the workflow.""" - print("Starting complex calculation workflow...") - - result = complex_calculation(10, 5) - - print(f"Calculation complete: {result}") - return result diff --git a/examples/python/voice-of-customer/requirements.txt b/examples/python/voice-of-customer/requirements.txt deleted file mode 100644 index ca9b9b6..0000000 --- a/examples/python/voice-of-customer/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -openai>=1.0.0 -anthropic>=0.18.0 -pydantic>=2.0.0 -faker>=20.0.0 diff --git a/examples/python/voice-of-customer/src/main.py b/examples/python/voice-of-customer/src/main.py deleted file mode 100644 index 5a625c6..0000000 --- a/examples/python/voice-of-customer/src/main.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -Voice of Customer - Example with Module Imports - -This example demonstrates: -1. Task registration across multiple imported modules -2. Nested directory structure with tasks/ -3. Each @task creates an isolated Wasm instance -""" - -from capsule import task -from tasks.fetch import ( - fetch_twitter_messages, - fetch_zendesk_tickets, - fetch_appstore_reviews -) -from tasks.analyze import analyze_feedback - -def main() -> dict: - print("๐Ÿš€ Voice of Customer Engine - Starting...") - print() - - print("๐Ÿ“ฅ Fetching feedback from multiple channels...") - - twitter_messages = fetch_twitter_messages(limit=50) - zendesk_tickets = fetch_zendesk_tickets(limit=50) - appstore_reviews = fetch_appstore_reviews(limit=50) - - print(f"โœ“ Fetched {len(twitter_messages)} Twitter messages") - print(f"โœ“ Fetched {len(zendesk_tickets)} Zendesk tickets") - print(f"โœ“ Fetched {len(appstore_reviews)} App Store reviews") - print() - - all_feedback = twitter_messages + zendesk_tickets + appstore_reviews - print(f"๐Ÿ“Š Total feedback messages: {len(all_feedback)}") - print() - - print("๐Ÿ” Analyzing feedback (each creates isolated Wasm instance)...") - print() - - results = [] - for msg in all_feedback[:10]: - result = analyze_feedback(msg) - results.append(result) - - print(f"โœ… Completed: {len(results)} analyses") - print() - - print("๐Ÿ“ˆ Top Insights:") - - critical_issues = [r for r in results if r["urgency"] == "critical"] - bugs = [r for r in results if r["category"] == "bug"] - - print(f" โ€ข Critical issues found: {len(critical_issues)}") - print(f" โ€ข Bugs reported: {len(bugs)}") - print() - - by_source = {} - for result in results: - source = result["source"] - by_source.setdefault(source, []).append(result) - - print("๐Ÿ“Š Results by source:") - for source, items in by_source.items(): - print(f" โ€ข {source}: {len(items)} analyzed") - print() - - return { - "total_fetched": len(all_feedback), - "total_analyzed": len(results), - "critical_issues": len(critical_issues), - "bugs": len(bugs), - "by_source": {k: len(v) for k, v in by_source.items()} - } diff --git a/examples/python/voice-of-customer/src/model/__init__.py b/examples/python/voice-of-customer/src/model/__init__.py deleted file mode 100644 index 577d28c..0000000 --- a/examples/python/voice-of-customer/src/model/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Empty __init__.py to make this a package diff --git a/examples/python/voice-of-customer/src/model/feedback.py b/examples/python/voice-of-customer/src/model/feedback.py deleted file mode 100644 index 1a93769..0000000 --- a/examples/python/voice-of-customer/src/model/feedback.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Literal -from pydantic import BaseModel - -FeedbackSource = Literal["twitter", "zendesk", "appstore"] - -class FeedbackMessage(BaseModel): - """Raw feedback from a customer channel.""" - id: str - source: FeedbackSource - content: str - metadata: dict = {} - -class AnalysisResult(BaseModel): - """Analysis output for a single feedback message.""" - message_id: str - source: FeedbackSource - sentiment: Literal["positive", "negative", "neutral"] - category: str - urgency: Literal["low", "medium", "high", "critical"] - summary: str diff --git a/examples/python/voice-of-customer/src/tasks/analyze.py b/examples/python/voice-of-customer/src/tasks/analyze.py deleted file mode 100644 index d110e70..0000000 --- a/examples/python/voice-of-customer/src/tasks/analyze.py +++ /dev/null @@ -1,65 +0,0 @@ -from capsule import task - -@task( - name="analyze_feedback", - compute="HIGH", - ram="512MB", - timeout="30s" -) -def analyze_feedback(message: dict) -> dict: - source = message["source"] - content = message["content"] - - sentiment = _simulate_sentiment_analysis(content) - category = _simulate_categorization(content, source) - urgency = _simulate_urgency_detection(content, category) - summary = _simulate_summarization(content) - - return { - "message_id": message["id"], - "source": source, - "sentiment": sentiment, - "category": category, - "urgency": urgency, - "summary": summary - } - - -def _simulate_sentiment_analysis(content: str) -> str: - keywords_negative = ["bug", "crash", "broken", "awful", "hate"] - keywords_positive = ["great", "love", "amazing", "excellent"] - - content_lower = content.lower() - if any(word in content_lower for word in keywords_negative): - return "negative" - elif any(word in content_lower for word in keywords_positive): - return "positive" - return "neutral" - -def _simulate_categorization(content: str, source: str) -> str: - content_lower = content.lower() - - if "bug" in content_lower or "crash" in content_lower: - return "bug" - elif "feature" in content_lower or "add" in content_lower: - return "feature_request" - elif "slow" in content_lower or "performance" in content_lower: - return "performance" - else: - return "general_feedback" - -def _simulate_urgency_detection(content: str, category: str) -> str: - content_lower = content.lower() - - if "critical" in content_lower or "urgent" in content_lower: - return "critical" - elif category == "bug" and "crash" in content_lower: - return "high" - elif category == "bug": - return "medium" - else: - return "low" - -def _simulate_summarization(content: str) -> str: - words = content.split()[:15] - return " ".join(words) + "..." diff --git a/examples/python/voice-of-customer/src/tasks/fetch.py b/examples/python/voice-of-customer/src/tasks/fetch.py deleted file mode 100644 index 7bdbb03..0000000 --- a/examples/python/voice-of-customer/src/tasks/fetch.py +++ /dev/null @@ -1,66 +0,0 @@ -from capsule import task - -@task( - name="fetch_twitter", - compute="HIGH", - ram="128MB", - timeout="10s" -) -def fetch_twitter_messages(limit: int = 3000) -> list: - """ - Simulates fetching messages from Twitter/X API. - In production, this would call the real Twitter API. - """ - messages = [] - print(f"๐Ÿ“ฅ Fetching {limit} Twitter messages...") - for i in range(limit): - messages.append({ - "id": f"tw_{i}", - "source": "twitter", - "content": f"Sample tweet message {i} about product feedback", - "metadata": {"likes": i * 10} - }) - return messages - -@task( - name="fetch_zendesk", - compute="HIGH", - ram="256MB", - timeout="15s" -) -def fetch_zendesk_tickets(limit: int = 6000) -> list: - """ - Simulates fetching support tickets from Zendesk API. - In production, this would call the real Zendesk API. - """ - messages = [] - priorities = ["low", "medium", "high"] - for i in range(limit): - messages.append({ - "id": f"zd_{i}", - "source": "zendesk", - "content": f"Support ticket {i}: Customer reported issue with feature X. Details: Problem description goes here.", - "metadata": {"ticket_priority": priorities[i % 3]} - }) - return messages - -@task( - name="fetch_appstore", - compute="HIGH", - ram="128MB", - timeout="10s" -) -def fetch_appstore_reviews(limit: int = 1000) -> list: - """ - Simulates fetching reviews from App Store API. - In production, this would call the real App Store Connect API. - """ - messages = [] - for i in range(limit): - messages.append({ - "id": f"as_{i}", - "source": "appstore", - "content": f"App Store review {i}: User feedback about the application experience.", - "metadata": {"rating": (i % 5) + 1} - }) - return messages From 3e1223bdf4ab6dfaa1c4de19bc35948a5dae8e0d Mon Sep 17 00:00:00 2001 From: Mavdol Date: Thu, 22 Jan 2026 14:19:44 +0100 Subject: [PATCH 10/19] add a http example for typescrupt --- .../javascript/http-request/http-request.ts | 94 +++++++++++++++++++ examples/javascript/http-request/package.json | 16 ++++ .../javascript/http-request/tsconfig.json | 10 ++ 3 files changed, 120 insertions(+) create mode 100644 examples/javascript/http-request/http-request.ts create mode 100644 examples/javascript/http-request/package.json create mode 100644 examples/javascript/http-request/tsconfig.json diff --git a/examples/javascript/http-request/http-request.ts b/examples/javascript/http-request/http-request.ts new file mode 100644 index 0000000..acfbf36 --- /dev/null +++ b/examples/javascript/http-request/http-request.ts @@ -0,0 +1,94 @@ +import { task } from "@capsule-run/sdk"; + +interface Post { + id: number; + title: string; + body: string; + userId: number; +} + +/** + * Fetch posts from JSONPlaceholder API. + */ +export const fetchPosts = task( + { + name: "fetchPosts", + compute: "MEDIUM" + }, + async (): Promise => { + const response = await fetch("https://jsonplaceholder.typicode.com/posts/1"); + + if (response.ok) { + const data: Post = await response.json(); + console.log(`โœ… Fetched post: ${data.title.slice(0, 50)}...`); + return data; + } else { + console.log(`โŒ Request failed with status: ${response.status}`); + return { error: response.status }; + } + } +); + +/** + * Create a new post via POST request. + */ +export const createPost = task( + { + name: "createPost", + compute: "MEDIUM" + }, + async (): Promise => { + const response = await fetch("https://jsonplaceholder.typicode.com/posts", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + title: "Hello from Capsule!", + body: "This post was created from a Wasm task.", + userId: 1 + }) + }); + + if (response.ok) { + const data: Post = await response.json(); + console.log(`โœ… Created post with id: ${data.id}`); + return data; + } else { + console.log(`โŒ Request failed with status: ${response.status}`); + return { error: response.status }; + } + } +); + +/** + * Main function to run HTTP examples. + */ +export const main = task( + { + name: "main", + compute: "MEDIUM" + }, + async (): Promise<{ fetched: Post | { error: number }; created: Post | { error: number } }> => { + console.log("๐ŸŒ HTTP Request Example"); + console.log("========================================"); + console.log(); + + console.log("๐Ÿ“ฅ Fetching a post..."); + const postData = await fetchPosts(); + if ("title" in postData) { + console.log(` Title: ${postData.title}`); + } + console.log(); + + console.log("๐Ÿ“ค Creating a new post..."); + const newPost = await createPost(); + if ("id" in newPost) { + console.log(` New post ID: ${newPost.id}`); + } + console.log(); + + console.log("โœจ Done!"); + return { fetched: postData, created: newPost }; + } +); diff --git a/examples/javascript/http-request/package.json b/examples/javascript/http-request/package.json new file mode 100644 index 0000000..354c6e1 --- /dev/null +++ b/examples/javascript/http-request/package.json @@ -0,0 +1,16 @@ +{ + "name": "http-request", + "version": "1.0.0", + "description": "HTTP request example using fetch API", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "typescript": "^5.9.3" + } +} diff --git a/examples/javascript/http-request/tsconfig.json b/examples/javascript/http-request/tsconfig.json new file mode 100644 index 0000000..f2047f1 --- /dev/null +++ b/examples/javascript/http-request/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Node", + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true + } +} From 7479218bd1fe0078e65fc75b1660690fdc2f407f Mon Sep 17 00:00:00 2001 From: Mavdol Date: Thu, 22 Jan 2026 15:54:25 +0100 Subject: [PATCH 11/19] Start the v0.4.0 --- ROADMAP.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index f6c3a64..c3dc908 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -45,11 +45,11 @@ This document tracks the development status of Capsule. ## v0.4.0: Configuration & Observability -**Status:** ๐Ÿšง Planned +**Status:** In progress **Goal:** Improve developer workflow with project configuration and richer task feedback. -- [ ] **Config File:** `capsule.toml` for default task options. +- [x] **Config File:** `capsule.toml` for default task options. - [ ] **Structured Output:** Tasks return a detailed JSON envelope with execution metadata: ```json From 036ac4e9ab1214895f5f3b5be1795f001c5b07e6 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Thu, 22 Jan 2026 16:33:26 +0100 Subject: [PATCH 12/19] Fix file access example --- examples/javascript/file-access/data/output.txt | 2 ++ examples/javascript/file-access/main.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 examples/javascript/file-access/data/output.txt diff --git a/examples/javascript/file-access/data/output.txt b/examples/javascript/file-access/data/output.txt new file mode 100644 index 0000000..d1133ac --- /dev/null +++ b/examples/javascript/file-access/data/output.txt @@ -0,0 +1,2 @@ +Processed 2 lines +First line: Hello from Capsule! diff --git a/examples/javascript/file-access/main.ts b/examples/javascript/file-access/main.ts index 8763e59..c4949f8 100644 --- a/examples/javascript/file-access/main.ts +++ b/examples/javascript/file-access/main.ts @@ -20,7 +20,7 @@ export const main = task({ name: "main" }, async () => { const lines = content.trim().split("\n"); const lineCount = lines.length; - const result = restrictedWriter( + const result = await restrictedWriter( `Processed ${lineCount} lines\nFirst line: ${lines[0]}\n` ); From 128004af080f239e379d0ed55ccf45e4d3e38bd2 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Thu, 22 Jan 2026 16:39:32 +0100 Subject: [PATCH 13/19] switch from 0.3.2 to 0.4.0 --- crates/capsule-cli/Cargo.lock | 2 +- crates/capsule-cli/Cargo.toml | 2 +- crates/capsule-cli/npm/package.json | 10 +++++----- crates/capsule-cli/pyproject.toml | 2 +- crates/capsule-core/Cargo.lock | 2 +- crates/capsule-core/Cargo.toml | 2 +- crates/capsule-sdk/javascript/package-lock.json | 4 ++-- crates/capsule-sdk/javascript/package.json | 2 +- crates/capsule-sdk/python/pyproject.toml | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/capsule-cli/Cargo.lock b/crates/capsule-cli/Cargo.lock index d4e31d7..6a6e98d 100644 --- a/crates/capsule-cli/Cargo.lock +++ b/crates/capsule-cli/Cargo.lock @@ -266,7 +266,7 @@ dependencies = [ [[package]] name = "capsule-core" -version = "0.3.2" +version = "0.4.0" dependencies = [ "anyhow", "chrono", diff --git a/crates/capsule-cli/Cargo.toml b/crates/capsule-cli/Cargo.toml index 26a02fe..c6e6cea 100644 --- a/crates/capsule-cli/Cargo.toml +++ b/crates/capsule-cli/Cargo.toml @@ -16,6 +16,6 @@ path = "src/main.rs" [dependencies] clap = { version = "4.5.53", features = ["derive"] } -capsule-core = { version= "0.3.2", path = "../capsule-core" } +capsule-core = { version= "0.4.0", path = "../capsule-core" } tokio = { version = "1.48.0", features = ["rt", "rt-multi-thread", "macros"] } serde_json = "1" diff --git a/crates/capsule-cli/npm/package.json b/crates/capsule-cli/npm/package.json index 7973cc8..b0539cd 100644 --- a/crates/capsule-cli/npm/package.json +++ b/crates/capsule-cli/npm/package.json @@ -1,6 +1,6 @@ { "name": "@capsule-run/cli", - "version": "0.3.2", + "version": "0.4.0", "description": "Secure WASM runtime to isolate and manage AI agent tasks", "bin": { "capsule": "./bin/capsule.js" @@ -29,9 +29,9 @@ "node": ">=18" }, "optionalDependencies": { - "@capsule-run/cli-darwin-arm64": "0.3.2", - "@capsule-run/cli-darwin-x64": "0.3.2", - "@capsule-run/cli-linux-x64": "0.3.2", - "@capsule-run/cli-win32-x64": "0.3.2" + "@capsule-run/cli-darwin-arm64": "0.4.0", + "@capsule-run/cli-darwin-x64": "0.4.0", + "@capsule-run/cli-linux-x64": "0.4.0", + "@capsule-run/cli-win32-x64": "0.4.0" } } diff --git a/crates/capsule-cli/pyproject.toml b/crates/capsule-cli/pyproject.toml index 20434b4..1a127d0 100644 --- a/crates/capsule-cli/pyproject.toml +++ b/crates/capsule-cli/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "capsule-run" -version = "0.3.2" +version = "0.4.0" description = "Secure WASM runtime to isolate and manage AI agent tasks" readme = "docs/README-pypi.md" license = {text = "Apache-2.0"} diff --git a/crates/capsule-core/Cargo.lock b/crates/capsule-core/Cargo.lock index 25b0d6f..dca16f3 100644 --- a/crates/capsule-core/Cargo.lock +++ b/crates/capsule-core/Cargo.lock @@ -246,7 +246,7 @@ dependencies = [ [[package]] name = "capsule-core" -version = "0.3.2" +version = "0.4.0" dependencies = [ "anyhow", "chrono", diff --git a/crates/capsule-core/Cargo.toml b/crates/capsule-core/Cargo.toml index 2c2ac60..24031e2 100644 --- a/crates/capsule-core/Cargo.toml +++ b/crates/capsule-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "capsule-core" -version = "0.3.2" +version = "0.4.0" edition = "2024" description = "Core library for Capsule - WASM runtime for AI agent isolation" license = "Apache-2.0" diff --git a/crates/capsule-sdk/javascript/package-lock.json b/crates/capsule-sdk/javascript/package-lock.json index 0b0c678..f5814b3 100644 --- a/crates/capsule-sdk/javascript/package-lock.json +++ b/crates/capsule-sdk/javascript/package-lock.json @@ -1,12 +1,12 @@ { "name": "@capsule-run/sdk", - "version": "0.3.2", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@capsule-run/sdk", - "version": "0.3.2", + "version": "0.4.0", "license": "Apache-2.0", "dependencies": { "@bytecodealliance/jco": "^1.0.0", diff --git a/crates/capsule-sdk/javascript/package.json b/crates/capsule-sdk/javascript/package.json index 984a7fa..655e707 100644 --- a/crates/capsule-sdk/javascript/package.json +++ b/crates/capsule-sdk/javascript/package.json @@ -1,6 +1,6 @@ { "name": "@capsule-run/sdk", - "version": "0.3.2", + "version": "0.4.0", "description": "Capsule JavaScript SDK - run AI agent tasks in secure WASM sandboxes", "type": "module", "main": "./dist/index.js", diff --git a/crates/capsule-sdk/python/pyproject.toml b/crates/capsule-sdk/python/pyproject.toml index 3161ea8..6c21c50 100644 --- a/crates/capsule-sdk/python/pyproject.toml +++ b/crates/capsule-sdk/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "capsule" -version = "0.3.2" +version = "0.4.0" description = "Capsule Python SDK - run AI agent tasks in secure WASM sandboxes" readme = "README.md" requires-python = ">=3.8" From 324a7a2f21e457fb822b1a2577570bf1bf473eb9 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Thu, 22 Jan 2026 16:40:26 +0100 Subject: [PATCH 14/19] add project configuration to documentation --- crates/capsule-cli/docs/README-pypi.md | 18 ++++++++++++++++++ crates/capsule-cli/npm/README.md | 20 +++++++++++++++++++- crates/capsule-sdk/javascript/README.md | 20 +++++++++++++++++++- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/capsule-cli/docs/README-pypi.md b/crates/capsule-cli/docs/README-pypi.md index 40f613f..3e2fa93 100644 --- a/crates/capsule-cli/docs/README-pypi.md +++ b/crates/capsule-cli/docs/README-pypi.md @@ -65,6 +65,24 @@ When you run `capsule run main.py`, your code is compiled into a WebAssembly mod | `max_retries` | Retry attempts on failure | `int` | `0` | `3` | | `allowed_files` | Folders accessible in the sandbox | `list` | `[]` | `["./data", "./output"]` | +### Project Configuration (Optional) + +Create a `capsule.toml` file in your project root to set default options: + +```toml +[workflow] +name = "My AI Workflow" +version = "1.0.0" +entrypoint = "src/main.py" # Run `capsule run` without specifying a file + +[tasks] +default_compute = "MEDIUM" +default_ram = "256MB" +default_timeout = "30s" +``` + +Task-level options always override these defaults. + ### Compute Levels - **LOW**: Minimal allocation for lightweight tasks diff --git a/crates/capsule-cli/npm/README.md b/crates/capsule-cli/npm/README.md index 6a0b9b0..17e58bc 100644 --- a/crates/capsule-cli/npm/README.md +++ b/crates/capsule-cli/npm/README.md @@ -84,6 +84,24 @@ When you run `capsule run main.ts`, your code is compiled into a WebAssembly mod | `maxRetries` | Retry attempts on failure | `number` | `0` | `3` | | `allowedFiles` | Folders accessible in the sandbox | `string[]` | `[]` | `["./data", "./output"]` | +### Project Configuration (Optional) + +Create a `capsule.toml` file in your project root to set default options: + +```toml +[workflow] +name = "My AI Workflow" +version = "1.0.0" +entrypoint = "src/main.ts" # Run `capsule run` without specifying a file + +[tasks] +default_compute = "MEDIUM" +default_ram = "256MB" +default_timeout = "30s" +``` + +Task-level options always override these defaults. + ### Compute Levels - **LOW**: Minimal allocation for lightweight tasks @@ -108,7 +126,7 @@ export const restrictedWriter = task({ }); export const main = task({ name: "main" }, async () => { - restrictedWriter(); + await restrictedWriter(); return await files.readText("./data/input.txt"); }); ``` diff --git a/crates/capsule-sdk/javascript/README.md b/crates/capsule-sdk/javascript/README.md index 4d38e12..dbfbf9f 100644 --- a/crates/capsule-sdk/javascript/README.md +++ b/crates/capsule-sdk/javascript/README.md @@ -84,6 +84,24 @@ When you run `capsule run main.ts`, your code is compiled into a WebAssembly mod | `maxRetries` | Retry attempts on failure | `number` | `0` | `3` | | `allowedFiles` | Folders accessible in the sandbox | `string[]` | `[]` | `["./data", "./output"]` | +### Project Configuration (Optional) + +Create a `capsule.toml` file in your project root to set default options: + +```toml +[workflow] +name = "My AI Workflow" +version = "1.0.0" +entrypoint = "src/main.ts" # Run `capsule run` without specifying a file + +[tasks] +default_compute = "MEDIUM" +default_ram = "256MB" +default_timeout = "30s" +``` + +Task-level options always override these defaults. + ### Compute Levels - **LOW**: Minimal allocation for lightweight tasks @@ -108,7 +126,7 @@ export const restrictedWriter = task({ }); export const main = task({ name: "main" }, async () => { - restrictedWriter(); + await restrictedWriter(); return await files.readText("./data/input.txt"); }); ``` From e04c73cefe19ee94551f7a942353bc49b4c313cc Mon Sep 17 00:00:00 2001 From: Mavdol Date: Thu, 22 Jan 2026 16:40:31 +0100 Subject: [PATCH 15/19] add project configuration to documentation --- README.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f6ce8ca..45e6c70 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,33 @@ Configure your tasks with these parameters: | `max_retries` / `maxRetries` | Number of retry attempts on failure | `int` | `0` | `3` | | `allowed_files` / `allowedFiles` | Folders accessible in the sandbox | `list` | `[]` | `["./data", "./output"]` | +### Project Configuration (Optional) + +You can create a `capsule.toml` file in your project root to set default options for all tasks and define workflow metadata: + +```toml +# capsule.toml + +[workflow] +name = "My AI Workflow" +version = "1.0.0" +entrypoint = "src/main.py" # Default file when running `capsule run` + +[tasks] +default_compute = "MEDIUM" +default_ram = "256MB" +default_timeout = "30s" +default_max_retries = 2 +``` + +With an entrypoint defined, you can simply run: + +```bash +capsule run +``` + +Task-level options always override these defaults when specified. + ### Compute Levels Capsule controls CPU usage through WebAssembly's **fuel mechanism**, which meters instruction execution. The compute level determines how much fuel your task receives. @@ -237,7 +264,7 @@ export const restrictedWriter = task({ }); export const main = task({ name: "main" }, async () => { - restrictedWriter(); + await restrictedWriter(); return await files.readText("./data/input.txt"); }); ``` From 40b0576104d140d22163cd61be397a1576d7852b Mon Sep 17 00:00:00 2001 From: Mavdol Date: Thu, 22 Jan 2026 16:41:41 +0100 Subject: [PATCH 16/19] try a different configuration --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 45e6c70..056409f 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,14 @@ Configure your tasks with these parameters: | `max_retries` / `maxRetries` | Number of retry attempts on failure | `int` | `0` | `3` | | `allowed_files` / `allowedFiles` | Folders accessible in the sandbox | `list` | `[]` | `["./data", "./output"]` | +### Compute Levels + +Capsule controls CPU usage through WebAssembly's **fuel mechanism**, which meters instruction execution. The compute level determines how much fuel your task receives. +- **LOW** provides minimal allocation for lightweight tasks +- **MEDIUM** offers balanced resources for typical workloads +- **HIGH** grants maximum fuel for compute-intensive operations +- **CUSTOM** to specify an exact fuel value (e.g., `compute="1000000"`) for precise control over execution limits. + ### Project Configuration (Optional) You can create a `capsule.toml` file in your project root to set default options for all tasks and define workflow metadata: @@ -172,14 +180,6 @@ capsule run Task-level options always override these defaults when specified. -### Compute Levels - -Capsule controls CPU usage through WebAssembly's **fuel mechanism**, which meters instruction execution. The compute level determines how much fuel your task receives. -- **LOW** provides minimal allocation for lightweight tasks -- **MEDIUM** offers balanced resources for typical workloads -- **HIGH** grants maximum fuel for compute-intensive operations -- **CUSTOM** to specify an exact fuel value (e.g., `compute="1000000"`) for precise control over execution limits. - ### HTTP Client API #### Python From 36a59624e603d25832c8a8eacc3035b44030ead6 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Thu, 22 Jan 2026 16:52:57 +0100 Subject: [PATCH 17/19] better documentation structure --- crates/capsule-cli/docs/README-pypi.md | 14 +++++++------- crates/capsule-cli/npm/README.md | 14 +++++++------- crates/capsule-sdk/javascript/README.md | 14 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/crates/capsule-cli/docs/README-pypi.md b/crates/capsule-cli/docs/README-pypi.md index 3e2fa93..bbdb214 100644 --- a/crates/capsule-cli/docs/README-pypi.md +++ b/crates/capsule-cli/docs/README-pypi.md @@ -65,6 +65,13 @@ When you run `capsule run main.py`, your code is compiled into a WebAssembly mod | `max_retries` | Retry attempts on failure | `int` | `0` | `3` | | `allowed_files` | Folders accessible in the sandbox | `list` | `[]` | `["./data", "./output"]` | +### Compute Levels + +- **LOW**: Minimal allocation for lightweight tasks +- **MEDIUM**: Balanced resources for typical workloads +- **HIGH**: Maximum fuel for compute-intensive operations +- **CUSTOM**: Specify exact fuel value (e.g., `compute="1000000"`) + ### Project Configuration (Optional) Create a `capsule.toml` file in your project root to set default options: @@ -83,13 +90,6 @@ default_timeout = "30s" Task-level options always override these defaults. -### Compute Levels - -- **LOW**: Minimal allocation for lightweight tasks -- **MEDIUM**: Balanced resources for typical workloads -- **HIGH**: Maximum fuel for compute-intensive operations -- **CUSTOM**: Specify exact fuel value (e.g., `compute="1000000"`) - ### HTTP Client Standard `requests` library isn't compatible with WASM. Use Capsule's HTTP client: diff --git a/crates/capsule-cli/npm/README.md b/crates/capsule-cli/npm/README.md index 17e58bc..b82cef7 100644 --- a/crates/capsule-cli/npm/README.md +++ b/crates/capsule-cli/npm/README.md @@ -84,6 +84,13 @@ When you run `capsule run main.ts`, your code is compiled into a WebAssembly mod | `maxRetries` | Retry attempts on failure | `number` | `0` | `3` | | `allowedFiles` | Folders accessible in the sandbox | `string[]` | `[]` | `["./data", "./output"]` | +### Compute Levels + +- **LOW**: Minimal allocation for lightweight tasks +- **MEDIUM**: Balanced resources for typical workloads +- **HIGH**: Maximum fuel for compute-intensive operations +- **CUSTOM**: Specify exact fuel value (e.g., `compute="1000000"`) + ### Project Configuration (Optional) Create a `capsule.toml` file in your project root to set default options: @@ -102,13 +109,6 @@ default_timeout = "30s" Task-level options always override these defaults. -### Compute Levels - -- **LOW**: Minimal allocation for lightweight tasks -- **MEDIUM**: Balanced resources for typical workloads -- **HIGH**: Maximum fuel for compute-intensive operations -- **CUSTOM**: Specify exact fuel value (e.g., `compute="1000000"`) - ### File Access The **entry point task** (main) has access to the entire project directory. Sub-tasks have **no filesystem access by default** and must declare `allowedFiles` to access specific paths. diff --git a/crates/capsule-sdk/javascript/README.md b/crates/capsule-sdk/javascript/README.md index dbfbf9f..e862013 100644 --- a/crates/capsule-sdk/javascript/README.md +++ b/crates/capsule-sdk/javascript/README.md @@ -84,6 +84,13 @@ When you run `capsule run main.ts`, your code is compiled into a WebAssembly mod | `maxRetries` | Retry attempts on failure | `number` | `0` | `3` | | `allowedFiles` | Folders accessible in the sandbox | `string[]` | `[]` | `["./data", "./output"]` | +### Compute Levels + +- **LOW**: Minimal allocation for lightweight tasks +- **MEDIUM**: Balanced resources for typical workloads +- **HIGH**: Maximum fuel for compute-intensive operations +- **CUSTOM**: Specify exact fuel value (e.g., `compute="1000000"`) + ### Project Configuration (Optional) Create a `capsule.toml` file in your project root to set default options: @@ -102,13 +109,6 @@ default_timeout = "30s" Task-level options always override these defaults. -### Compute Levels - -- **LOW**: Minimal allocation for lightweight tasks -- **MEDIUM**: Balanced resources for typical workloads -- **HIGH**: Maximum fuel for compute-intensive operations -- **CUSTOM**: Specify exact fuel value (e.g., `compute="1000000"`) - ### File Access The entry point task (main) has access to the entire project directory. Sub-tasks have no filesystem access by default and must declare `allowedFiles` to access specific paths. From 7b98c64080cda2660c06593b0b4500ca222a37e4 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Thu, 22 Jan 2026 17:12:04 +0100 Subject: [PATCH 18/19] delete debug file --- examples/javascript/file-access/data/output.txt | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 examples/javascript/file-access/data/output.txt diff --git a/examples/javascript/file-access/data/output.txt b/examples/javascript/file-access/data/output.txt deleted file mode 100644 index d1133ac..0000000 --- a/examples/javascript/file-access/data/output.txt +++ /dev/null @@ -1,2 +0,0 @@ -Processed 2 lines -First line: Hello from Capsule! From 7d90083bc5b54db573a9c29818ce55663729ea72 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Thu, 22 Jan 2026 18:23:50 +0100 Subject: [PATCH 19/19] fix cache dir duplication and delete unused pycache --- .../src/wasm/compiler/javascript.rs | 7 ++-- .../capsule-core/src/wasm/compiler/python.rs | 33 +++++++++++++++---- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/crates/capsule-core/src/wasm/compiler/javascript.rs b/crates/capsule-core/src/wasm/compiler/javascript.rs index 1452b95..abcfb41 100644 --- a/crates/capsule-core/src/wasm/compiler/javascript.rs +++ b/crates/capsule-core/src/wasm/compiler/javascript.rs @@ -49,9 +49,10 @@ impl JavascriptWasmCompiler { JavascriptWasmCompilerError::FsError(format!("Cannot resolve source path: {}", e)) })?; - let cache_dir = source_path - .parent() - .ok_or_else(|| JavascriptWasmCompilerError::FsError("Invalid source path".to_string()))? + let cache_dir = std::env::current_dir() + .map_err(|e| { + JavascriptWasmCompilerError::FsError(format!("Cannot get current directory: {}", e)) + })? .join(".capsule"); fs::create_dir_all(&cache_dir)?; diff --git a/crates/capsule-core/src/wasm/compiler/python.rs b/crates/capsule-core/src/wasm/compiler/python.rs index 817ba61..bd1b156 100644 --- a/crates/capsule-core/src/wasm/compiler/python.rs +++ b/crates/capsule-core/src/wasm/compiler/python.rs @@ -46,13 +46,12 @@ impl PythonWasmCompiler { PythonWasmCompilerError::FsError(format!("Cannot resolve source path: {}", e)) })?; - let source_dir = source_path - .parent() - .ok_or(PythonWasmCompilerError::FsError( - "Cannot determine source directory".to_string(), - ))?; + let cache_dir = std::env::current_dir() + .map_err(|e| { + PythonWasmCompilerError::FsError(format!("Cannot get current directory: {}", e)) + })? + .join(".capsule"); - let cache_dir = source_dir.join(".capsule"); let output_wasm = cache_dir.join("capsule.wasm"); if !cache_dir.exists() { @@ -174,6 +173,8 @@ from capsule.app import TaskRunner, exports ))); } + self.cleanup_pycache(python_path); + Ok(self.output_wasm.clone()) } @@ -322,4 +323,24 @@ from capsule.app import TaskRunner, exports Ok(sdk_path) } + + fn cleanup_pycache(&self, source_dir: &Path) { + Self::remove_pycache_recursive(source_dir); + Self::remove_pycache_recursive(&self.cache_dir); + } + + fn remove_pycache_recursive(dir: &Path) { + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + if path.file_name().is_some_and(|n| n == "__pycache__") { + let _ = fs::remove_dir_all(&path); + } else { + Self::remove_pycache_recursive(&path); + } + } + } + } + } }