diff --git a/README.md b/README.md index ab88c0a..b1aec1d 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,9 @@ [![License](https://img.shields.io/badge/license-Apache_2.0-blue)](LICENSE) [![Rust](https://img.shields.io/badge/built_with-Rust-orange)](https://www.rust-lang.org/) +https://github.com/user-attachments/assets/539f7c72-6847-4a25-9a76-d55028267cd7 -https://github.com/user-attachments/assets/dc0043d5-da42-4ead-b66e-0b7025046aa1 - -[Getting Started](#-quick-start) • [Examples](#-examples) • [Documentation](#-documentation) • [Contributing](#-contributing) +[Getting Started](#-quick-start) • [Documentation](#-documentation) • [Contributing](#-contributing) @@ -18,7 +17,7 @@ https://github.com/user-attachments/assets/dc0043d5-da42-4ead-b66e-0b7025046aa1 ## 🎯 What is Capsule? -Modern AI agents are evolving from simple, single-threaded scripts into **distributed multi-agent architectures** that coordinate dozens of sub-agents in parallel. These systems handle long-running workflows, large-scale data processing, and complex decision-making tasks that require: +Modern AI agents are evolving from simple, single-threaded scripts into **distributed multi-agent architectures** that coordinate sub-agents in parallel. These systems handle long-running workflows, large-scale data processing, and complex decision-making tasks that require: - 🔒 **Robust isolation** between untrusted code and your host system - 📊 **Fine-grained resource control** (CPU, RAM, timeout limits) @@ -30,25 +29,19 @@ Modern AI agents are evolving from simple, single-threaded scripts into **distri ## 🚀 How It Works -Capsule leverages **WebAssembly (Wasm)** to create secure, isolated execution environments for each task. Here's what makes it powerful: - -1. **Decorator-Based**: Simply annotate your Python functions with `@task` -2. **Automatic Compilation**: Python code is compiled to Wasm on-the-fly -3. **Isolated Execution**: Each task runs in its own sandboxed Wasm instance -4. **Resource Limits**: Enforce CPU, RAM, and time constraints per task -5. **Fault Recovery**: Automatic retries on failure without affecting other tasks +Capsule leverages **WebAssembly (Wasm)** to create secure, isolated execution environments for each task. Simply annotate your Python functions with `@task` decorator : ```python from capsule import task -@task(name="analyze_data", compute="MEDIUM", ram="512MB", timeout="30s") +@task(name="analyze_data", compute="MEDIUM", ram="512MB", timeout="30s", max_retries=1) def analyze_data(dataset: list) -> dict: """Process data in an isolated, resource-controlled environment.""" # Your code runs safely in a Wasm sandbox return {"processed": len(dataset), "status": "complete"} ``` -This diagram illustrates the complete journey of a task from your command line invocation to execution within the Rust core: +This diagram illustrates the complete journey of a task from your command line invocation to execution within the Rust core ![Process Flow](./assets/task-execution-flow.png) @@ -59,7 +52,7 @@ When you run `capsule run main.py`, your Python code is compiled into a WebAssem ### Prerequisites - **Rust** (latest stable) - [Install Rust](https://rustup.rs/) -- **Python 3.11+** - [Install Python](https://www.python.org/downloads/) (Only for the V0.1) +- **Python 3.13+** - [Install Python](https://www.python.org/downloads/) ### Installation @@ -96,77 +89,6 @@ capsule run hello.py That's it! Your Python function just ran in a secure WebAssembly sandbox. 🎉 -## 📖 Examples - -### Basic Task with Resource Limits - -```python -from capsule import task - -@task( - name="process_batch", - compute="MEDIUM", - ram="256MB", - timeout="60s", - max_retries=3 -) -def process_batch(items: list) -> dict: - """Process a batch of items with resource constraints.""" - results = [] - for item in items: - # Process each item - results.append(process_item(item)) - - return {"processed": len(results), "results": results} -``` - -### Making HTTP Requests - -Standard libraries like `requests` are disabled for security. Use Capsule's built-in HTTP client: - -```python -from capsule import task -from capsule.http import get, post - -@task(name="fetch_data", compute="MEDIUM", timeout="30s") -def fetch_data(api_url: str) -> dict: - """Fetch data from an external API.""" - response = get(api_url) - - if response.ok(): - data = response.json() - print(f"✅ Fetched {len(data)} items") - return data - else: - print(f"❌ Request failed: {response.status_code}") - return {"error": response.status_code} - -@task(name="post_results", compute="LOW", timeout="15s") -def post_results(api_url: str, payload: dict) -> bool: - """Post results to an API endpoint.""" - response = post(api_url, json=payload) - return response.ok() -``` - -### Task with Environment Variables - -```python -from capsule import task - -@task( - name="authenticated_task", - compute="LOW", - env_vars={"API_KEY": "your-api-key", "ENVIRONMENT": "production"} -) -def authenticated_task() -> str: - """Access environment variables within the task.""" - import os - api_key = os.getenv("API_KEY") - env = os.getenv("ENVIRONMENT") - - return f"Running in {env} with key: {api_key[:4]}..." -``` - ## 📚 Documentation ### Task Configuration Options @@ -195,19 +117,26 @@ Capsule controls CPU usage through WebAssembly's **fuel mechanism**, which meter Standard Python networking relies on sockets, which aren't natively compatible with WebAssembly's sandbox model. For security and portability, Capsule provides its own HTTP client that works seamlessly within the Wasm environment while maintaining strict isolation boundaries: ```python +from capsule import task from capsule.http import get, post, put, delete -# GET request -response = get("https://api.example.com/data") +@task(name="http_example", compute="MEDIUM", timeout="30s") +def main() -> dict: + """Example demonstrating HTTP client usage within a task.""" -# POST with JSON body -response = post("https://api.example.com/submit", json={"key": "value"}) + # GET request + response = get("https://api.example.com/data") -# Response methods -response.ok() # Returns True if status code is 2xx -response.status_code # Get the HTTP status code -response.json() # Parse response as JSON -response.text() # Get response as text + # POST with JSON body + response = post("https://api.example.com/submit", json={"key": "value"}) + + # Response methods + is_ok = response.ok() # Returns True if status code is 2xx + status = response.status_code # Get the HTTP status code + data = response.json() # Parse response as JSON + text = response.text() # Get response as text + + return {"status": status, "success": is_ok} ``` ## 🔧 Compatibility @@ -224,10 +153,12 @@ response.text() # Get response as text ### Important Limitations -Packages with C extensions like `numpy` and `pandas` are not yet supported in the current version. Support for compiled extensions is planned for future releases as we expand WebAssembly compatibility. +Packages with C extensions like `numpy` and `pandas` are not yet supported in the current version. Support for compiled extensions is planned for future releases as Capsule expands WebAssembly compatibility. ## 📅 What's Next +> 💡 Community Driven: The path from v0.2 onwards is flexible. While the initial vision is for a Daemon mode your feedback defines the priority. + #### v0.2.0: The Orchestrator **Status:** Planned @@ -250,14 +181,14 @@ Want to follow along with Capsule's development journey? I share detailed engine ## 🤝 Contributing -We welcome contributions from the community! Here's how you can help: +Contributions are welcome! Here's how you can help: 1. **Fork** the repository 2. **Create** a feature branch: `git checkout -b feature/amazing-feature` 3. **Add tests** if applicable: `cargo test` 4. **Open** a Pull Request -Need help or have questions? [Open an issue](https://github.com/mavdol/capsule/issues) ! +Need help or have questions? [Open an issue](https://github.com/mavdol/capsule/issues) ## 📄 License diff --git a/ROADMAP.md b/ROADMAP.md index 337eca4..c6668c9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,12 +1,14 @@ # 📌 Roadmap -This document tracks the development status of Capsule. We follow a "Release Early, Release Often" philosophy. +This document tracks the development status of Capsule. + +> Note: Features marked as Planned are subject to change based on community feedback and real-world usage patterns. --- ## v0.1.0: The Python Runner (MVP) -**Status:** 🚧 In Progress +**Status:** ✅ Done **Goal:** Validating the core Host/Guest technology and Python interoperability. diff --git a/crates/capsule-cli/Cargo.lock b/crates/capsule-cli/Cargo.lock index 6abf42d..c7464be 100644 --- a/crates/capsule-cli/Cargo.lock +++ b/crates/capsule-cli/Cargo.lock @@ -273,6 +273,7 @@ name = "capsule-core" version = "0.1.0" dependencies = [ "anyhow", + "colored", "humantime", "indicatif", "nanoid", @@ -364,6 +365,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "console" version = "0.15.11" @@ -1255,6 +1266,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "leb128" version = "0.2.5" diff --git a/crates/capsule-core/Cargo.lock b/crates/capsule-core/Cargo.lock index c386172..12ccff3 100644 --- a/crates/capsule-core/Cargo.lock +++ b/crates/capsule-core/Cargo.lock @@ -249,6 +249,7 @@ name = "capsule-core" version = "0.1.0" dependencies = [ "anyhow", + "colored", "humantime", "indicatif", "nanoid", @@ -304,6 +305,16 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "console" version = "0.15.11" diff --git a/crates/capsule-core/Cargo.toml b/crates/capsule-core/Cargo.toml index bd3af5d..637ee71 100644 --- a/crates/capsule-core/Cargo.toml +++ b/crates/capsule-core/Cargo.toml @@ -18,6 +18,7 @@ tokio = "1.48.0" wasmtime = { version = "29.0.0", features = ["component-model", "async"] } wasmtime-wasi = "29.0.0" indicatif = "0.17" +colored = "2.1" [dev-dependencies] rustfmt = "0.10.0" diff --git a/crates/capsule-core/src/wasm/compiler/python.rs b/crates/capsule-core/src/wasm/compiler/python.rs index a9ea1c5..d6fd95f 100644 --- a/crates/capsule-core/src/wasm/compiler/python.rs +++ b/crates/capsule-core/src/wasm/compiler/python.rs @@ -41,6 +41,35 @@ pub struct PythonWasmCompiler { } impl PythonWasmCompiler { + fn get_python_command() -> String { + let candidates = vec!["python3", "python"]; + + for cmd in candidates { + if Command::new(cmd) + .arg("--version") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_ok() + { + return cmd.to_string(); + } + } + + "python3".to_string() + } + + fn normalize_path_for_command(path: &Path) -> PathBuf { + #[cfg(windows)] + { + let path_str = path.to_string_lossy(); + if path_str.starts_with(r"\\?\") { + return PathBuf::from(&path_str[4..]); + } + } + path.to_path_buf() + } + pub fn new(source_path: &Path) -> Result { let source_path = source_path.canonicalize().map_err(|e| { PythonWasmCompilerError::FsError(format!("Cannot resolve source path: {}", e)) @@ -123,21 +152,27 @@ from capsule.app import TaskRunner, exports fs::write(&bootloader_path, bootloader_content)?; + let wit_path_normalized = Self::normalize_path_for_command(&wit_path); + let cache_dir_normalized = Self::normalize_path_for_command(&self.cache_dir); + let python_path_normalized = Self::normalize_path_for_command(python_path); + let sdk_path_normalized = Self::normalize_path_for_command(&sdk_path); + let output_wasm_normalized = Self::normalize_path_for_command(&self.output_wasm); + let output = Command::new("componentize-py") .arg("-d") - .arg(&wit_path) + .arg(&wit_path_normalized) .arg("-w") .arg("capsule-agent") .arg("componentize") .arg("_capsule_boot") .arg("-p") - .arg(&self.cache_dir) + .arg(&cache_dir_normalized) .arg("-p") - .arg(python_path) + .arg(&python_path_normalized) .arg("-p") - .arg(&sdk_path) + .arg(&sdk_path_normalized) .arg("-o") - .arg(&self.output_wasm) + .arg(&output_wasm_normalized) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output()?; @@ -261,12 +296,13 @@ from capsule.app import TaskRunner, exports } fn find_sdk_via_python(&self) -> Result { - let output = Command::new("python3") + let python_cmd = Self::get_python_command(); + let output = Command::new(&python_cmd) .arg("-c") .arg("import capsule; import os; print(os.path.dirname(os.path.dirname(capsule.__file__)), end='')") .output() .map_err(|e| { - PythonWasmCompilerError::FsError(format!("Failed to execute python3: {}", e)) + PythonWasmCompilerError::FsError(format!("Failed to execute {}: {}", python_cmd, e)) })?; if !output.status.success() { diff --git a/crates/capsule-core/src/wasm/utilities/task_reporter.rs b/crates/capsule-core/src/wasm/utilities/task_reporter.rs index 1311e61..82cefb6 100644 --- a/crates/capsule-core/src/wasm/utilities/task_reporter.rs +++ b/crates/capsule-core/src/wasm/utilities/task_reporter.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; +use colored::Colorize; use indicatif::{ProgressBar, ProgressStyle}; pub struct TaskReporter { @@ -38,7 +39,12 @@ impl TaskReporter { self.finish_spinner(); self.start_time = Instant::now(); if self.verbose { - println!("Capsule log: ▶ Starting task '{}' ({})", task_name, task_id); + println!( + "Capsule log: {} Starting task {} ({})", + "▶".cyan(), + format!("'{}'", task_name).cyan(), + task_id + ); } } @@ -48,8 +54,11 @@ impl TaskReporter { let elapsed = self.start_time.elapsed(); let time_str = self.format_duration(elapsed); println!( - "Capsule log: ✔ Task '{}' completed ({})", - task_name, time_str + "Capsule log: {} Task {} {} ({})", + "✔".green(), + format!("'{}'", task_name).cyan(), + "completed".green(), + time_str ); } } @@ -59,8 +68,11 @@ impl TaskReporter { if self.verbose { let time_str = self.format_duration(elapsed); println!( - "Capsule log: ✔ Task '{}' completed ({})", - task_name, time_str + "Capsule log: {} Task {} {} ({})", + "✔".green(), + format!("'{}'", task_name).cyan(), + "completed".green(), + time_str ); } } @@ -68,18 +80,28 @@ impl TaskReporter { pub fn task_failed(&mut self, task_name: &str, error: &str) { self.finish_spinner(); if self.verbose { - println!("Capsule log: ✗ Task '{}' failed: {}", task_name, error); + println!( + "Capsule log: {} Task {} failed: {}", + "✗".red(), + format!("'{}'", task_name).cyan(), + error.red() + ); } else { - eprintln!("Capsule log: ✗ {}", error); + eprintln!("Capsule log: {} {}", "✗".red(), error.red()); } } pub fn task_timeout(&mut self, task_name: &str) { self.finish_spinner(); if self.verbose { - println!("Capsule log: ✗ Task '{}' timed out", task_name); + println!( + "Capsule log: {} Task {} {}", + "✗".red(), + format!("'{}'", task_name).cyan(), + "timed out".red() + ); } else { - eprintln!("Capsule log: ✗ Task timed out"); + eprintln!("Capsule log: {} {}", "✗".red(), "Task timed out".red()); } } @@ -105,7 +127,7 @@ impl TaskReporter { if let Some(msg) = completion_message && self.verbose { - println!("✓ {} ({})", msg, time_str); + println!("{} {} ({})", "✓".green(), msg.green(), time_str); } } @@ -120,7 +142,7 @@ impl TaskReporter { } pub fn error(&self, message: &str) { - eprintln!("{}", message); + eprintln!("{}", message.red()); } pub fn format_duration(&self, duration: std::time::Duration) -> String {