diff --git a/Cargo.lock b/Cargo.lock index 11339c11..95baf04c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -403,7 +403,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -1547,6 +1547,23 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.3.1", + "hyper 1.6.0", + "hyper-util", + "rustls 0.23.31", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.2", + "tower-service", ] [[package]] @@ -1562,20 +1579,45 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" dependencies = [ + "base64 0.22.1", "bytes", + "futures-channel", "futures-util", "http 1.3.1", "http-body 1.0.1", "hyper 1.6.0", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", + "system-configuration 0.6.1", "tokio", "tower-service", + "tracing", + "windows-registry", ] [[package]] @@ -1751,6 +1793,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1867,7 +1919,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.5", ] [[package]] @@ -2718,7 +2770,7 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", - "hyper-tls", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -2732,7 +2784,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-native-tls", "tower-service", @@ -2743,6 +2795,46 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls", + "hyper-tls 0.6.0", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-native-tls", + "tower", + "tower-http 0.6.7", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -3125,6 +3217,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple-theater" +version = "0.2.1" +dependencies = [ + "anyhow", + "serde", + "serde_json", + "theater", + "tokio", + "tracing", +] + [[package]] name = "slab" version = "0.4.9" @@ -3263,6 +3367,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3283,7 +3390,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "system-configuration-sys 0.6.0", ] [[package]] @@ -3296,6 +3414,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "target-lexicon" version = "0.13.2" @@ -3385,7 +3513,7 @@ dependencies = [ "pretty_assertions", "rand 0.8.5", "rand_chacha 0.3.1", - "reqwest", + "reqwest 0.11.27", "rustls 0.23.31", "rustls-pemfile 2.2.0", "serde", @@ -3393,6 +3521,7 @@ dependencies = [ "sha1", "tempfile", "test-log", + "theater-chain", "thiserror 1.0.69", "tokio", "tokio-rustls 0.26.2", @@ -3405,6 +3534,30 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "theater-chain" +version = "0.2.1" +dependencies = [ + "base64 0.21.7", + "bytes", + "chrono", + "futures", + "hex", + "lazy_static", + "md5", + "pin-utils", + "rand 0.8.5", + "rand_chacha 0.3.1", + "serde", + "sha1", + "thiserror 1.0.69", + "tokio-util", + "toml", + "tracing", + "wasmtime", + "wit-bindgen", +] + [[package]] name = "theater-cli" version = "0.2.1" @@ -3451,7 +3604,7 @@ dependencies = [ "anyhow", "bytes", "futures", - "reqwest", + "reqwest 0.11.27", "serde", "serde_json", "theater", @@ -3462,6 +3615,169 @@ dependencies = [ "uuid", ] +[[package]] +name = "theater-handler-environment" +version = "0.2.1" +dependencies = [ + "anyhow", + "chrono", + "theater", + "thiserror 1.0.69", + "tracing", + "wasmtime", +] + +[[package]] +name = "theater-handler-filesystem" +version = "0.2.1" +dependencies = [ + "anyhow", + "chrono", + "dunce", + "pretty_assertions", + "rand 0.8.5", + "serde", + "serde_json", + "tempfile", + "test-log", + "theater", + "thiserror 1.0.69", + "tokio", + "tracing", + "wasmtime", +] + +[[package]] +name = "theater-handler-http-client" +version = "0.2.1" +dependencies = [ + "anyhow", + "chrono", + "reqwest 0.12.24", + "serde", + "serde_json", + "theater", + "thiserror 1.0.69", + "tracing", + "wasmtime", +] + +[[package]] +name = "theater-handler-http-framework" +version = "0.2.1" + +[[package]] +name = "theater-handler-message-server" +version = "0.2.1" +dependencies = [ + "anyhow", + "chrono", + "serde", + "serde_json", + "theater", + "thiserror 2.0.12", + "tokio", + "tracing", + "uuid", + "wasmtime", +] + +[[package]] +name = "theater-handler-process" +version = "0.2.1" +dependencies = [ + "anyhow", + "chrono", + "serde", + "serde_json", + "theater", + "thiserror 2.0.12", + "tokio", + "tracing", + "wasmtime", +] + +[[package]] +name = "theater-handler-random" +version = "0.2.1" +dependencies = [ + "anyhow", + "chrono", + "pretty_assertions", + "rand 0.8.5", + "rand_chacha 0.3.1", + "serde", + "tempfile", + "test-log", + "theater", + "thiserror 1.0.69", + "tokio", + "tracing", + "uuid", + "wasmtime", +] + +[[package]] +name = "theater-handler-runtime" +version = "0.2.1" +dependencies = [ + "anyhow", + "chrono", + "pretty_assertions", + "serde", + "tempfile", + "test-log", + "theater", + "thiserror 1.0.69", + "tokio", + "tracing", + "wasmtime", +] + +[[package]] +name = "theater-handler-store" +version = "0.2.1" +dependencies = [ + "anyhow", + "chrono", + "serde", + "serde_json", + "theater", + "thiserror 2.0.12", + "tokio", + "tracing", + "wasmtime", +] + +[[package]] +name = "theater-handler-supervisor" +version = "0.2.1" +dependencies = [ + "anyhow", + "chrono", + "serde", + "serde_json", + "theater", + "thiserror 2.0.12", + "tokio", + "tracing", + "wasmtime", +] + +[[package]] +name = "theater-handler-timing" +version = "0.2.1" +dependencies = [ + "anyhow", + "chrono", + "futures", + "theater", + "thiserror 1.0.69", + "tokio", + "tracing", + "wasmtime", +] + [[package]] name = "theater-server" version = "0.2.1" @@ -3476,7 +3792,7 @@ dependencies = [ "theater", "tokio", "tokio-util", - "tower-http", + "tower-http 0.5.2", "tracing", "uuid", "warp", @@ -3727,6 +4043,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -4570,9 +4904,9 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.1", "windows-result", - "windows-strings", + "windows-strings 0.4.2", ] [[package]] @@ -4603,13 +4937,39 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings 0.3.1", + "windows-targets 0.53.5", +] + [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.1", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.1", ] [[package]] @@ -4618,7 +4978,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -4672,13 +5032,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4691,6 +5068,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4703,6 +5086,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4715,12 +5104,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4733,6 +5134,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4745,6 +5152,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4757,6 +5170,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4769,6 +5188,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.10" diff --git a/Cargo.toml b/Cargo.toml index 2ecfc23f..998a962c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,21 @@ [workspace] members = [ "crates/theater", - "crates/theater-cli", + "crates/theater-cli", "crates/theater-server", "crates/theater-server-cli", - "crates/theater-client" + "crates/theater-client", + "crates/theater-handler-environment", + "crates/theater-handler-filesystem", + "crates/theater-handler-http-client", + "crates/theater-handler-http-framework", + "crates/theater-handler-message-server", + "crates/theater-handler-process", + "crates/theater-handler-random", + "crates/theater-handler-runtime", + "crates/theater-handler-store", + "crates/theater-handler-supervisor", + "crates/theater-handler-timing", "crates/simple-theater", ] resolver = "2" diff --git a/HANDLER_MIGRATION.md b/HANDLER_MIGRATION.md new file mode 100644 index 00000000..85b79a4c --- /dev/null +++ b/HANDLER_MIGRATION.md @@ -0,0 +1,121 @@ +# Handler Migration Summary: Random Handler + +## What We Did + +Successfully migrated the `random` handler from the Theater core runtime into a standalone `theater-handler-random` crate. + +## Key Changes + +### 1. Created New Crate Structure +- `/crates/theater-handler-random/` + - `Cargo.toml` - Dependencies and metadata + - `src/lib.rs` - Handler implementation + - `README.md` - Documentation + +### 2. Trait Simplification +**Before:** +```rust +fn setup_host_functions( + &mut self, + actor_component: &mut ActorComponent, +) -> Pin> + Send + '_>>; +``` + +**After:** +```rust +fn setup_host_functions( + &mut self, + actor_component: &mut ActorComponent, +) -> Result<()>; +``` + +**Why:** None of the handlers actually used `.await` in their setup functions. Making them synchronous: +- Eliminated complex lifetime issues +- Made the code more honest about what it does +- Simplified implementation for all future handlers + +### 3. Handler Implementation +- Renamed `RandomHost` → `RandomHandler` +- Implemented the `Handler` trait with synchronous `setup_host_functions` and `add_export_functions` +- Kept the async closures for actual random operations (those ARE async) +- Maintained all existing functionality and chain event logging + +### 4. Dependencies +The handler crate depends on: +- Core theater crate (for trait definitions and types) +- Wasmtime (for WASM integration) +- Random generation (`rand`, `rand_chacha`) +- Standard async/logging tools + +## Migration Pattern for Other Handlers + +Based on this migration, here's the pattern for migrating other handlers: + +1. **Create the crate** with proper Cargo.toml +2. **Copy the host implementation** from `/crates/theater/src/host/` +3. **Rename** `XxxHost` → `XxxHandler` +4. **Implement the `Handler` trait:** + - `create_instance()` - Clone yourself + - `start()` - Async startup (keep as-is) + - `setup_host_functions()` - Now synchronous! + - `add_export_functions()` - Now synchronous! + - `name()`, `imports()`, `exports()` - Metadata +5. **Update imports** to use `theater::` prefix +6. **Test** and document + +## Benefits + +✅ **Cleaner architecture** - Handlers are independent modules +✅ **Easier to maintain** - Each handler can evolve separately +✅ **Better testing** - Test handlers in isolation +✅ **Simpler lifetimes** - Synchronous trait methods avoid lifetime complexity +✅ **Third-party handlers** - Clear pattern for custom handlers + +## Next Steps + +Recommended order for migrating remaining handlers: +1. ✅ `random` - DONE! +2. ✅ `environment` - DONE! +3. `timing` - Also straightforward +4. `http-client` - Moderate complexity +5. `filesystem` - Larger but well-isolated +6. `process`, `supervisor`, `store` - More complex, do last +7. `message-server`, `http-framework` - Most complex + +## Testing + +The migrated handlers: +- ✅ Compile without errors +- ✅ All unit tests pass +- ✅ Maintain backward compatibility +- ✅ Integrate with Theater runtime via `Handler` trait + +## Completed Migrations + +### 1. Random Handler +- **Crate**: `theater-handler-random` +- **Status**: ✅ Complete +- **Notes**: First migration, served as the documented example + +### 2. Timing Handler +- **Crate**: `theater-handler-timing` +- **Status**: ✅ Complete +- **Notes**: Completed prior to environment handler + +### 3. Environment Handler +- **Crate**: `theater-handler-environment` +- **Status**: ✅ Complete (2025-11-30) +- **Notes**: + - Fixed wasmtime version (26.0 → 31.0) + - Corrected closure signatures for func_wrap + - Updated all config fields in tests and docs + - All tests passing + +### 4. HTTP Client Handler +- **Crate**: `theater-handler-http-client` +- **Status**: ✅ Complete (2025-11-30) +- **Notes**: + - Migrated component types (HttpRequest, HttpResponse) + - Preserved async operations with func_wrap_async + - Permission checking maintained + - All tests passing (3 unit + 1 doc) diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 8ed25fea..00000000 --- a/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,87 +0,0 @@ -# Theater Variable Substitution Implementation Summary - -## ✅ **Successfully Implemented Variable Substitution** - -After evaluating multiple libraries (`subst`, `tera`, `handlebars`), we've implemented a production-ready variable substitution system using **Handlebars**. - -## **What We Implemented** - -### **Core Features** -- ✅ **Variable Syntax**: `{{variable_name}}` (Handlebars syntax) -- ✅ **Nested Access**: `{{app.database.host}}` -- ✅ **Array Access**: `{{servers.[0].hostname}}` -- ✅ **Default Values**: `{{default server.port "8080"}}` (via helper) -- ✅ **Two-Stage Loading**: Extract init_state → resolve → substitute → parse -- ✅ **Backward Compatibility**: All existing manifests work unchanged - -### **Files Created/Modified** -1. ✅ `crates/theater/src/utils/template.rs` - Handlebars-based substitution engine -2. ✅ `crates/theater/src/utils/mod.rs` - Added template module -3. ✅ `crates/theater/src/config/actor_manifest.rs` - Added substitution methods -4. ✅ `crates/theater-cli/src/commands/start.rs` - Updated CLI with variable detection -5. ✅ `crates/theater/Cargo.toml` - Added handlebars dependency -6. ✅ `crates/theater/tests/integration/manifest_substitution_tests.rs` - Integration tests -7. ✅ `VARIABLE_SUBSTITUTION_GUIDE.md` - Complete documentation - -## **Example Usage** - -**Dynamic Manifest**: -```toml -name = "{{app.name}}" -component = "{{build.component_path}}" -save_chain = {{logging.save_events}} - -[[handler]] -type = "filesystem" -path = "{{workspace.data_dir}}" - -[[handler]] -type = "http-client" -base_url = "{{api.endpoint}}" -timeout = {{default api.timeout_ms "5000"}} -``` - -**CLI Usage** (unchanged): -```bash -theater start manifest.toml --initial-state config.json -``` - -## **Why Handlebars?** - -1. **Production Ready**: Used by rust-lang.org, 6M+ downloads -2. **Perfect Syntax Match**: Supports `{{nested.object.access}}` -3. **Extensible**: Custom helpers for defaults and future features -4. **Familiar**: Standard templating syntax developers know -5. **Robust**: Handles edge cases, escaping, error reporting - -## **Syntax Changes from Original Spec** - -| Original Spec | Implemented | Reason | -|---------------|-------------|---------| -| `${var}` | `{{var}}` | Handlebars standard syntax | -| `${var:default}` | `{{default var "default"}}` | More flexible helper system | - -## **Current Status** - -✅ **Core Implementation**: Complete and working -✅ **CLI Integration**: Automatic variable detection -✅ **Backward Compatibility**: All existing code works -✅ **Error Handling**: Comprehensive error messages -🔧 **Test Suite**: 8/9 tests passing (one formatting issue) -✅ **Documentation**: Complete migration guide - -## **Next Steps** - -1. **Fix final test**: The formatting issue in the complete manifest test -2. **Integration testing**: End-to-end testing with real manifests -3. **Performance testing**: Benchmark against non-variable manifests -4. **Documentation**: Update README with variable substitution examples - -## **Migration Impact** - -- **Zero Breaking Changes**: All existing manifests work unchanged -- **Opt-in Feature**: Only manifests with `{{}}` syntax are processed -- **Performance**: Minimal overhead for non-variable manifests -- **Security**: Variables scoped to init_state only - -This implementation provides a robust, production-ready variable substitution system that perfectly matches the original specification while using battle-tested Handlebars templating. diff --git a/README.md b/README.md index a03fbf98..23e58a6f 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,11 @@ # Theater -A WebAssembly actor system designed for secure, transparent, and reliable AI agent infrastructure. +A WebAssembly actor runtime for reproducible, isolated, and observable programs. -> [!NOTE] -> This documentation is incomplete, please reach out to me at colinrozzi@gmail.com. I very much appreciate your interest and would love to hear from you! This project is in early development, breaking changes are expected, security is not guarenteed +Every run in Theater produces a chain, created by hashing all of the information that crosses the wasm sandbox. Right now, this chain is mostly used for debugging, but there are many exciting -## The Challenge +An actor's chain can be used for many things. First and currently most important, debugging. When an actor fails, you have a complete and reproducible record of everything that led up to that failure. Here is a sample run: -AI agents present incredible opportunities for automation and intelligence, but they also introduce significant challenges. As autonomous AI agents become more capable and ubiquitous, we need infrastructure that can: - -1. **Contain and secure** agents with precise permission boundaries -2. **Trace and verify** all agent actions for auditability and debugging -3. **Manage failures** gracefully in complex agent systems -4. **Orchestrate cooperation** between specialized agents with different capabilities - -## The Theater Approach - -Theater provides infrastructure specifically designed for AI agent systems. It moves trust from the individual agents to the system itself, providing guarantees at the infrastructure level: - -1. **WebAssembly Components** provide sandboxing and deterministic execution, ensuring agents can only access explicitly granted capabilities -2. **Actor Model with Supervision** implements an Erlang-style supervision hierarchy for fault-tolerance and structured agent cooperation -3. **Event Chain** captures every agent action in a verifiable record, enabling complete traceability and deterministic replay - -### Read more - -- **[Guide](https://colinrozzi.github.io/theater/guide)** - A comprehensive guide to Theater -- **[Reference](https://colinrozzi.github.io/theater/api/theater)** - Full rustdoc documentation - -### To get started: -1. Download [theater-server-cli](crates/theater-server-cli) - ```bash - cargo install theater-server-cli - ``` -2. Download [theater-cli](crates/theater-cli) - ```bash - cargo install theater-cli - ``` -3. Start the Theater server and leave it running in the background - ```bash - theater-server --log-stdout - ``` -4. Start an actor in a new terminal (this requires an actor, I will update this shortly with a link to a sample actor) - ```bash - theater start manifest.toml - ``` -5. Look at the running actors - ```bash - theater list - ``` -6. Look at an actor's event chain - ```bash - theater events - ``` -7. Stop an actor - ```bash - theater stop - ``` - - -### Manual Setup - -Most people will have these things, move on to cloning and download the dependencies as you need, but just for clarity you will need: - -1. Rust 1.81.0 or newer -2. LLVM and Clang for building wasmtime -3. CMake -4. OpenSSL and pkg-config - -Then: - -1. Clone the repository: -```bash -git clone https://github.com/colinrozzi/theater.git -cd theater ``` -2. Build the project: -```bash -cargo build ``` diff --git a/VARIABLE_SUBSTITUTION_GUIDE.md b/VARIABLE_SUBSTITUTION_GUIDE.md deleted file mode 100644 index de214d6c..00000000 --- a/VARIABLE_SUBSTITUTION_GUIDE.md +++ /dev/null @@ -1,301 +0,0 @@ -# Theater Manifest Variable Substitution - Migration Guide - -## Overview - -Theater v0.2.2 introduces variable substitution in manifest files, allowing dynamic configuration based on initial state values. This feature is **fully backward compatible** - existing manifests will continue to work without changes. - -## What's New - -### Variable Syntax -- `${variable_name}` - Simple variable reference -- `${nested.object.path}` - Nested object access using dot notation -- `${variable_name:default_value}` - Variable with default value - -### Example Usage - -**Before (static configuration):** -```toml -name = "my-processor" -component = "./dist/processor.wasm" -save_chain = true - -[[handler]] -type = "filesystem" -path = "/tmp/data" - -[[handler]] -type = "http-client" -base_url = "https://api.example.com" -timeout = 5000 -``` - -**After (dynamic configuration):** -```toml -name = "${app.name}" -component = "${build.component_path}" -save_chain = ${logging.save_events:true} - -[[handler]] -type = "filesystem" -path = "${workspace.data_dir}" - -[[handler]] -type = "http-client" -base_url = "${api.endpoint:https://api.default.com}" -timeout = ${api.timeout_ms:5000} -``` - -**Initial State (config.json):** -```json -{ - "app": { - "name": "production-processor" - }, - "build": { - "component_path": "./dist/processor-v2.wasm" - }, - "workspace": { - "data_dir": "/var/data/prod" - }, - "api": { - "endpoint": "https://prod-api.example.com/v1", - "timeout_ms": 10000 - }, - "logging": { - "save_events": true - } -} -``` - -## Migration Steps - -### 1. No Changes Required for Existing Manifests -Your existing manifests will continue to work exactly as before. No migration is required. - -### 2. Optional: Add Variable Substitution -To use the new feature: - -1. **Identify Dynamic Values**: Look for configuration values that change between environments (dev/staging/prod) or deployments. - -2. **Create Initial State File**: Extract these values into a JSON file: - ```json - { - "environment": "production", - "database": { - "host": "prod-db.example.com", - "port": 5432 - }, - "api": { - "base_url": "https://prod-api.example.com" - } - } - ``` - -3. **Update Manifest**: Replace static values with variable references: - ```toml - name = "app-${environment}" - - [[handler]] - type = "database" - host = "${database.host}" - port = ${database.port} - - [[handler]] - type = "http-client" - base_url = "${api.base_url}" - ``` - -4. **Reference Initial State**: Add the init_state field: - ```toml - init_state = "config.json" - # or init_state = "https://config-server.com/prod-config.json" - # or init_state = "store://my-store/prod-config" - ``` - -### 3. CLI Usage Updates - -**No changes required** - the CLI automatically detects and processes variables: - -```bash -# Works with or without variables -theater start manifest.toml - -# Override state still works -theater start manifest.toml --initial-state override.json -``` - -## New API Methods - -### For Library Users - -If you're using Theater as a library, new methods are available: - -```rust -use theater::config::actor_manifest::ManifestConfig; -use serde_json::json; - -// Original method (still works) -let config = ManifestConfig::from_str(toml_content)?; - -// New method with variable substitution -let override_state = json!({"env": "staging"}); -let config = ManifestConfig::from_str_with_substitution( - toml_content, - Some(&override_state) -).await?; -``` - -## Best Practices - -### 1. Use Defaults for Optional Values -```toml -debug_mode = ${features.debug:false} -timeout = ${api.timeout:30000} -``` - -### 2. Group Related Configuration -```json -{ - "database": { - "host": "localhost", - "port": 5432, - "name": "myapp" - }, - "api": { - "base_url": "https://api.example.com", - "timeout": 5000 - } -} -``` - -### 3. Use Environment-Specific Config Files -```bash -# Development -theater start manifest.toml --initial-state config/dev.json - -# Production -theater start manifest.toml --initial-state config/prod.json -``` - -### 4. Secure Sensitive Values -For production deployments, consider using Theater's store:// references for sensitive configuration: - -```toml -init_state = "store://secure-store/prod-config" -``` - -## Error Handling - -### Variable Not Found -``` -Error: Variable substitution failed: Variable 'missing.variable' not found and no default provided -``` - -**Solution**: Add a default value or ensure the variable exists in your initial state: -```toml -# Add default -value = ${missing.variable:default_value} - -# Or add to initial state -{"missing": {"variable": "value"}} -``` - -### Invalid Path -``` -Error: Variable substitution failed: Invalid JSON path 'string.field': Cannot traverse into string value -``` - -**Solution**: Check your JSON structure and variable paths: -```toml -# If your state is: {"config": "simple_string"} -# This won't work: -value = ${config.field} - -# This will work: -value = ${config} -``` - -## Troubleshooting - -### Variables Not Being Substituted -1. **Check syntax**: Ensure variables use `${...}` format -2. **Verify initial state**: Make sure your JSON is valid and contains the referenced paths -3. **Check file paths**: Ensure init_state file paths are correct - -### Performance Considerations -- Variable substitution adds minimal overhead -- Initial state is cached during the actor's lifetime -- No impact on manifests without variables - -## Advanced Features - -### Array Access -```toml -primary_server = "${servers.0.hostname}" -backup_server = "${servers.1.hostname}" -``` - -```json -{ - "servers": [ - {"hostname": "primary.example.com"}, - {"hostname": "backup.example.com"} - ] -} -``` - -### Complex Data Types -```toml -# Arrays -tags = ${metadata.tags} - -# Objects (converted to inline TOML tables) -config = ${server.settings} -``` - -## Security Considerations - -### Restrictions -- The `init_state` field cannot contain variables (prevents circular dependencies) -- Variables can only access data from the resolved initial state -- No access to environment variables or external data sources - -### Safe Practices -- Store sensitive configuration in Theater's secure store -- Use file permissions to protect initial state files -- Validate initial state JSON structure in your deployment pipeline - -## Future Enhancements - -The variable substitution system is designed to be extensible. Future versions may include: - -- Function calls: `${env("HOME")}`, `${uuid()}` -- Conditional logic: `${debug_mode ? "verbose" : "quiet"}` -- Template includes: `${include("common.toml")}` - -## Getting Help - -If you encounter issues with variable substitution: - -1. Check this migration guide -2. Review the error messages for specific guidance -3. Test with simple variable references first -4. Reach out to colinrozzi@gmail.com for support - -## Changelog - -### v0.2.2 -- ✅ Added variable substitution with `${var}`, `${var:default}`, and `${nested.path}` syntax -- ✅ Support for array indexing and complex data types -- ✅ Full backward compatibility with existing manifests -- ✅ New `ManifestConfig::from_str_with_substitution()` API method -- ✅ Comprehensive error handling and validation -- ✅ Integration tests and documentation - -## Implementation Details - -This feature is powered by the excellent [`subst`](https://lib.rs/crates/subst) crate, providing: -- Shell-like variable substitution with `${}` syntax -- Built-in default value support -- Recursive substitution in default values -- Production-tested reliability with 1M+ downloads diff --git a/changes/README.md b/changes/README.md new file mode 100644 index 00000000..b022b561 --- /dev/null +++ b/changes/README.md @@ -0,0 +1,34 @@ +# Theater Project Changes + +This directory tracks proposed and in-progress changes to the Theater project at the workspace level. + +## Directory Structure + +``` +changes/ +├── proposals/ # Detailed change proposals +│ └── 2025-11-30-handler-migration.md +├── in-progress/ # Active work tracking +│ └── handler-migration.md +└── README.md # This file +``` + +## Change Process + +1. **Proposal**: Create a detailed proposal in `proposals/` describing the change, motivation, design, and implementation plan +2. **In Progress**: Track active work in `in-progress/` with detailed status updates +3. **Completion**: Update the in-progress document with completion notes and learnings + +## Active Changes + +| Date | Change | Status | Tracking | +|------|--------|--------|----------| +| 2025-11-30 | Handler Migration | In Progress | [handler-migration.md](in-progress/handler-migration.md) | + +## Completed Changes + +None yet at the workspace level. + +## Note on Crate-Level Changes + +Individual crates (like `theater`, `theater-client`, etc.) may have their own `changes/` directories for tracking crate-specific changes. This top-level directory is for workspace-wide changes that affect multiple crates or the overall project structure. diff --git a/changes/in-progress/environment-handler-migration.md b/changes/in-progress/environment-handler-migration.md new file mode 100644 index 00000000..bcba9d70 --- /dev/null +++ b/changes/in-progress/environment-handler-migration.md @@ -0,0 +1,127 @@ +# Environment Handler Migration Summary + +**Date**: 2025-11-30 +**Handler**: `environment` +**Crate**: `theater-handler-environment` +**Status**: ✅ Complete + +## Overview + +Successfully migrated the environment handler from the Theater core runtime into a standalone `theater-handler-environment` crate, following the pattern established by the random handler migration. + +## Changes Made + +### 1. Created New Crate Structure +- `/crates/theater-handler-environment/` + - `Cargo.toml` - Dependencies and metadata + - `src/lib.rs` - Handler implementation + - `README.md` - Documentation + +### 2. Implementation Details + +**Renamed**: `EnvironmentHost` → `EnvironmentHandler` + +**Implemented Handler Trait**: +- `create_instance()` - Clones the handler for reuse +- `start()` - Async startup that waits for shutdown +- `setup_host_functions()` - Synchronous setup (was async but never awaited) +- `add_export_functions()` - No-op for read-only handler +- `name()` - Returns "environment" +- `imports()` - Returns "theater:simple/environment" +- `exports()` - Returns None (read-only handler) + +**Added `Clone` derive**: Handler can now be cloned for multiple actor instances + +### 3. Fixed Dependencies + +**Issue**: Cargo.toml initially had `wasmtime = "26.0"` while rest of project uses 31.0 + +**Fix**: Updated to `wasmtime = { version = "31.0", features = ["component-model", "async"] }` + +**Impact**: This was causing type mismatches in closure signatures + +### 4. Closure Signature Corrections + +**Issue**: Initial migration used incorrect parameter types: +```rust +// ❌ Wrong (caused type errors) +move |mut ctx, var_name: String| -> Result<...> + +// ✅ Correct (matches wasmtime 31.0 API) +move |mut ctx, (var_name,): (String,)| -> Result<...> +``` + +**Pattern**: Parameters must be tuples that implement `ComponentNamedList` +- Single parameter: `(param,): (Type,)` +- No parameters: `()` +- Multiple parameters: `(p1, p2): (Type1, Type2)` + +### 5. Test and Documentation Updates + +**Config Fields**: Updated all examples to include complete `EnvironmentHandlerConfig`: +```rust +let config = EnvironmentHandlerConfig { + allowed_vars: None, + denied_vars: None, + allow_list_all: false, + allowed_prefixes: None, +}; +``` + +**Tests Added**: +- `test_handler_creation` - Verifies handler instantiation +- `test_handler_clone` - Verifies clone functionality +- Doc test - Compiles example from module documentation + +## Key Learnings + +1. **wasmtime version matters**: Version mismatches cause subtle type errors in closure signatures +2. **Tuple destructuring required**: Parameters to `func_wrap` must use tuple destructuring syntax +3. **Complete config structs**: All fields must be specified, even Optional ones set to None +4. **Synchronous is simpler**: Making setup_host_functions synchronous eliminated async complexity without losing functionality + +## Files Modified + +### New Files +- `/crates/theater-handler-environment/Cargo.toml` +- `/crates/theater-handler-environment/src/lib.rs` +- `/crates/theater-handler-environment/README.md` + +### Updated Files +- `/changes/in-progress/handler-migration.md` - Progress tracking +- `/HANDLER_MIGRATION.md` - Completed migrations list + +## Testing Results + +``` +running 2 tests +test tests::test_handler_clone ... ok +test tests::test_handler_creation ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored + +running 1 test +test crates/theater-handler-environment/src/lib.rs - (line 16) - compile ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored +``` + +✅ All tests passing! + +## Next Steps + +The environment handler is now complete and ready for: +1. Integration testing with actual actors +2. Removal of old implementation from `/crates/theater/src/host/environment.rs` +3. Updates to core runtime to use new handler crate + +## Migration Pattern Validation + +This migration confirms the pattern established by the random handler: +- ✅ Handler trait implementation is straightforward +- ✅ Synchronous setup functions work well +- ✅ Clone derive enables handler reuse +- ✅ Tests validate basic functionality +- ✅ Documentation is clear and complete + +The pattern is solid and ready for the remaining 8 handlers! diff --git a/changes/in-progress/filesystem-handler-migration.md b/changes/in-progress/filesystem-handler-migration.md new file mode 100644 index 00000000..b546c38a --- /dev/null +++ b/changes/in-progress/filesystem-handler-migration.md @@ -0,0 +1,238 @@ +# Filesystem Handler Migration Summary + +**Date**: 2025-11-30 +**Handler**: `filesystem` +**Crate**: `theater-handler-filesystem` +**Status**: ✅ Complete + +## Overview + +Successfully migrated the filesystem handler from the Theater core runtime into a standalone `theater-handler-filesystem` crate. This was the largest handler migration to date, requiring modular architecture to manage the complexity. The handler provides comprehensive filesystem access with permission-based security. + +## Changes Made + +### 1. Modular Crate Structure + +Due to the handler's size (~1,125 lines in original), we split it into logical modules: + +``` +theater-handler-filesystem/ +├── src/ +│ ├── lib.rs # Main handler + Handler trait impl +│ ├── types.rs # Error types and internal types +│ ├── path_validation.rs # Path resolution and permissions +│ ├── command_execution.rs # Command execution logic +│ └── operations/ +│ ├── mod.rs # Orchestrates all operations +│ ├── basic_ops.rs # File/dir operations (7 functions) +│ └── commands.rs # Command execution setup (2 functions) +├── Cargo.toml +└── README.md +``` + +This modular approach improves: +- **Maintainability**: Each module has a single responsibility +- **Testability**: Operations can be tested independently +- **Readability**: Functions are grouped logically +- **Scalability**: New operations can be added without file bloat + +### 2. Handler Implementation + +**Renamed**: `FileSystemHost` → `FilesystemHandler` + +**Implemented Handler Trait**: +- `create_instance()` - Clones the handler for reuse +- `start()` - Simple async startup, waits for shutdown +- `setup_host_functions()` - Delegates to operations module +- `add_export_functions()` - No-op (no exports needed) +- `name()` - Returns "filesystem" +- `imports()` - Returns "theater:simple/filesystem" +- `exports()` - Returns None + +**Added `Clone` derive**: Handler can be cloned for multiple actor instances + +### 3. Operations Implemented + +**Basic File Operations** (synchronous): +1. `read-file` - Read file contents as bytes +2. `write-file` - Write string contents to file +3. `delete-file` - Remove a file + +**Directory Operations** (synchronous): +4. `list-files` - List directory contents +5. `create-dir` - Create new directory +6. `delete-dir` - Remove directory and contents +7. `path-exists` - Check if path exists + +**Command Operations** (asynchronous): +8. `execute-command` - Execute shell commands with restrictions +9. `execute-nix-command` - Execute nix development commands + +All operations use `func_wrap` except commands which use `func_wrap_async`. + +### 4. Path Validation System + +Created comprehensive path validation in `path_validation.rs`: + +```rust +pub fn resolve_and_validate_path( + base_path: &Path, + requested_path: &str, + operation: &str, // "read", "write", "delete", "execute" + permissions: &Option, +) -> Result +``` + +**Validation Logic**: +1. Append requested path to base path +2. Determine operation type (creation vs access) +3. For creation: validate parent directory +4. For access: validate target path +5. Canonicalize using `dunce` (robust cross-platform) +6. Check against allowed_paths permissions +7. Return validated path + +**Security Features**: +- Prevents directory traversal (`../`, symlinks, etc.) +- Uses `dunce` for Windows/Unix compatibility +- Validates parent for creation operations +- All paths canonicalized before use + +### 5. Permission System + +**FileSystemPermissions** structure: +- `allowed_paths`: Whitelist of accessible paths +- All operations check permissions before execution +- Permission denials logged as events + +**Path Checking**: +- Resolved path must match or start with allowed path +- Allowed paths are also canonicalized for comparison +- Creation operations check parent directory permissions + +### 6. Command Execution Security + +Commands are heavily restricted: +- Only `nix` command allowed +- Args whitelist: `["flake", "init"]` or specific cargo component build +- Directory must be within allowed paths +- All execution logged to chain + +Implemented in `command_execution.rs`: +```rust +pub async fn execute_command(...) -> Result +pub async fn execute_nix_command(...) -> Result +``` + +### 7. Event Recording + +Comprehensive event logging for observability: +- **Setup events**: Handler initialization, linker creation +- **Operation events**: Call and result for each operation +- **Permission events**: Denials with detailed reasons +- **Command events**: Execution and completion +- **Error events**: All failures with context + +Event types: `filesystem-setup`, `theater:simple/filesystem/*`, `permission-denied` + +### 8. Test Coverage + +**Tests Added**: +- `test_handler_creation` - Verifies handler with explicit path +- `test_handler_clone` - Verifies clone functionality +- `test_temp_dir_creation` - Verifies temporary directory creation + +All 3 tests passing! ✅ + +## Key Learnings + +1. **Modular architecture essential**: Large handlers benefit from splitting into modules +2. **Path validation is complex**: Creation vs access operations require different validation +3. **dunce for path handling**: Better than std::fs::canonicalize for cross-platform +4. **Security layers**: Permissions, command whitelists, path validation all important +5. **LinkerInstance not Instance**: Need to use correct wasmtime type +6. **Event recording everywhere**: Every operation and error should be logged + +## Dependencies + +New dependencies beyond standard handler deps: +- `dunce = "1.0"` - Robust path canonicalization +- `rand = "0.8"` - Random temp directory names +- `serde_json = "1.0"` - Error serialization + +## Files Modified + +### New Files +- `/crates/theater-handler-filesystem/Cargo.toml` +- `/crates/theater-handler-filesystem/src/lib.rs` +- `/crates/theater-handler-filesystem/src/types.rs` +- `/crates/theater-handler-filesystem/src/path_validation.rs` +- `/crates/theater-handler-filesystem/src/command_execution.rs` +- `/crates/theater-handler-filesystem/src/operations/mod.rs` +- `/crates/theater-handler-filesystem/src/operations/basic_ops.rs` +- `/crates/theater-handler-filesystem/src/operations/commands.rs` +- `/crates/theater-handler-filesystem/README.md` + +### Updated Files +- `/changes/in-progress/handler-migration.md` - Progress tracking +- `/changes/in-progress/filesystem-handler-migration.md` - This document + +## Testing Results + +``` +running 3 tests +test tests::test_handler_clone ... ok +test tests::test_handler_creation ... ok +test tests::test_temp_dir_creation ... ok + +test result: ok. 3 passed; 0 failed; 0 ignored +``` + +✅ All tests passing! + +## Pattern Validation + +This migration validates modular architecture for large handlers: +- ✅ Module organization improves readability +- ✅ Separate concerns (validation, execution, operations) +- ✅ Each module can be tested independently +- ✅ Easy to add new operations +- ✅ Clear separation of sync vs async operations + +## Unique Aspects + +Compared to previous handlers, the filesystem handler is unique because: + +1. **Largest handler**: ~1,125 lines, required modular split +2. **Complex permissions**: Path validation with creation vs access logic +3. **Security critical**: Multiple layers of security checks +4. **Mixed operations**: 7 sync + 2 async operations +5. **External dependencies**: Uses `dunce` for path handling +6. **Command execution**: Shell command capability with restrictions + +## Next Steps + +The filesystem handler is now complete and ready for: +1. Integration testing with actual actors +2. Removal of old implementation from `/crates/theater/src/host/filesystem.rs` +3. Updates to core runtime to use new handler crate + +## Migration Progress + +**Phase 2 Complete!** ✅ + +All Phase 1 and Phase 2 handlers are now migrated: + +**Phase 1** (Simple): +- ✅ random +- ✅ timing +- ✅ environment +- ✅ runtime + +**Phase 2** (Medium): +- ✅ http-client +- ✅ filesystem + +**Total**: 6/11 handlers complete (55%) + +Next: Phase 3 - Complex handlers (process, store, supervisor) diff --git a/changes/in-progress/handler-migration.md b/changes/in-progress/handler-migration.md new file mode 100644 index 00000000..49210161 --- /dev/null +++ b/changes/in-progress/handler-migration.md @@ -0,0 +1,164 @@ +# Handler Migration Progress + +This document tracks the progress of migrating handlers from the core `theater` crate to separate `theater-handler-*` crates. + +See the full proposal: [2025-11-30-handler-migration.md](../proposals/2025-11-30-handler-migration.md) + +## Migration Status + +### ✅ Phase 1: Simple Handlers + +| Handler | Status | Crate | Old File | Notes | +|---------|--------|-------|----------|-------| +| random | ✅ COMPLETE | `theater-handler-random` | `src/host/random.rs` | Documented example migration | +| timing | ✅ COMPLETE | `theater-handler-timing` | `src/host/timing.rs` | Fully migrated | +| environment | ✅ COMPLETE | `theater-handler-environment` | `src/host/environment.rs` | Migrated 2025-11-30 | +| runtime | ✅ COMPLETE | `theater-handler-runtime` | `src/host/runtime.rs` | Migrated 2025-11-30 | + +### ❌ Phase 2: Medium Complexity + +| Handler | Status | Crate | Old File | Notes | +|---------|--------|-------|----------|-------| +| http-client | ✅ COMPLETE | `theater-handler-http-client` | `src/host/http_client.rs` | Migrated 2025-11-30 | +| filesystem | ✅ COMPLETE | `theater-handler-filesystem` | `src/host/filesystem.rs` | Migrated 2025-11-30 | + +### ⚙️ Phase 3: Complex Handlers + +| Handler | Status | Crate | Old File | Notes | +|---------|--------|-------|----------|-------| +| process | ✅ COMPLETE | `theater-handler-process` | `src/host/process.rs` | Migrated 2025-12-07 | +| store | ✅ COMPLETE | `theater-handler-store` | `src/host/store.rs` | Migrated 2025-12-07 | +| supervisor | ✅ COMPLETE | `theater-handler-supervisor` | `src/host/supervisor.rs` | Migrated 2025-12-08 | + +### ⚙️ Phase 4: Framework Handlers + +| Handler | Status | Crate | Old File | Notes | +|---------|--------|-------|----------|-------| +| message-server | ✅ COMPLETE | `theater-handler-message-server` | `src/host/message_server.rs` | Compilation fixed 2025-12-09 | +| http-framework | ❌ NOT STARTED | `theater-handler-http-framework` | `src/host/framework/` | Depends on others | + +## Overall Progress + +- **Completed**: 10/11 (91%) +- **Blocked**: 0/11 (0%) +- **In Progress**: 0/11 (0%) +- **Not Started**: 1/11 (9%) + +## Current Sprint + +### Active Work +- No active work at the moment + +### Blocked +- None! All blockers resolved. + +### Next Up +1. Complete Phase 4: Migrate http-framework handler +2. Update core theater crate to use new handlers +3. Complete handler migration project + +## Cleanup Checklist + +For each completed handler migration: +- [ ] New handler crate fully implemented +- [ ] All tests passing +- [ ] Documentation complete +- [ ] Old implementation removed from `/crates/theater/src/host/` +- [ ] References updated in core crate +- [ ] HANDLER_MIGRATION.md updated with learnings + +## Migration Log + +### 2025-11-30 +- Created change tracking structure +- Created proposal document +- Identified that random and timing are complete +- ✅ **Completed environment handler migration** + - Implemented EnvironmentHandler struct with Handler trait + - Fixed wasmtime version from 26.0 to 31.0 to match rest of project + - Fixed closure signatures for func_wrap (tuples for parameters) + - Updated tests and documentation with all config fields + - All tests passing (2 unit tests + 1 doc test) + - Ready for integration +- ✅ **Completed runtime handler migration** + - Implemented RuntimeHandler struct with Handler trait + - Migrated log, get-state, and shutdown functions + - Async shutdown operation with theater command channel + - Comprehensive event recording for chain + - All tests passing (1 unit test) + - Ready for integration +- ✅ **Completed filesystem handler migration** + - Split into modular structure (lib, types, path_validation, operations) + - Implemented all 9 filesystem operations + - Comprehensive path validation with dunce canonicalization + - Permission system with allowed/denied paths + - Command execution with security restrictions + - All tests passing (3 unit tests) + - Ready for integration +- ✅ **Completed http-client handler migration** + - Implemented HttpClientHandler struct with Handler trait + - Migrated HttpRequest and HttpResponse component types + - All async operations properly wrapped with func_wrap_async + - Permission checking preserved + - All tests passing (3 unit tests + 1 doc test) + - Ready for integration + +### 2025-12-07 (Continued) +- ✅ **Completed process handler migration** (most complex handler yet!) + - Implemented ProcessHandler struct with Handler trait + - Migrated all 5 operations (os-spawn, os-write-stdin, os-status, os-kill, os-signal) + - Added 3 export functions for callbacks (handle-stdout, handle-stderr, handle-exit) + - Complex process lifecycle management with ManagedProcess struct + - Async I/O handling for stdin/stdout/stderr with 4 output modes (raw, line-by-line, JSON, chunked) + - Process timeout monitoring with automatic kill + - Comprehensive permission checking + - Fixed multiple Send/lifetime issues with careful lock management + - Fixed event data structure mismatches (ProcessSpawn, StdinWrite, Error, etc.) + - Fixed wasmtime version to 31.0 + - All tests passing (3 unit tests + 1 doc test) + - Complete README with architecture and usage documentation + - ~990 lines migrated from ~1408 line original + - Ready for integration + +### 2025-12-07 (Morning) +- ✅ **Completed store handler migration** + - Implemented StoreHandler struct with Handler trait + - Migrated all 13 store operations (new, store, get, exists, label operations, list operations) + - Fixed wasmtime version from 26.0 to 31.0 to match rest of project + - Content-addressed storage with SHA1 hashing preserved + - Label management system fully functional + - Comprehensive event recording for all operations + - All tests passing (2 unit tests + 1 doc test) + - Complete README documentation with all operations listed + - Ready for integration + +### 2025-12-08 +- ✅ **Completed supervisor handler migration** (last Phase 3 handler!) + - Implemented SupervisorHandler struct with Handler trait + - Migrated all 7 supervisor operations (spawn, resume, list-children, restart-child, stop-child, get-child-state, get-child-events) + - Added 3 export functions for callbacks (handle-child-error, handle-child-exit, handle-child-external-stop) + - Unique architecture with background task for receiving child actor results + - Used Arc>> to manage channel receiver in cloneable handler + - Fixed Handler trait compliance (add_export_functions takes &self, start returns Pin>) + - All tests passing (2 unit tests) + - Complete README with lifecycle documentation + - ~1230 lines migrated from ~1079 line original + - Ready for integration + - **Phase 3 now 100% complete! 🎉** + +### 2025-12-09 +- ✅ **Resolved message-server handler compilation blocker** + - Added MessageCommand enum (separate from TheaterCommand for future lifecycle integration) + - Fixed ActorChannelOpen struct: added `initiator_id` field, renamed `data` to `initial_msg` + - Fixed ActorChannelMessage struct: renamed `data` to `msg` + - Added ChannelId::parse() method for parsing channel IDs from strings + - Added temporary TheaterCommand variants (SendMessage, ChannelOpen, ChannelMessage, ChannelClose) + - These are marked TEMPORARY and will be replaced with MessageCommand routing in lifecycle integration + - Fixed MutexGuard Send issue in handler by adding proper scope + - All tests passing (2 unit tests) + - Handler now compiles successfully! + - Ready for lifecycle integration (separate PR) + +### Earlier +- 2025-11-30: Random handler migration completed (documented) +- 2025-11-29: Timing handler migration completed diff --git a/changes/in-progress/http-client-handler-migration.md b/changes/in-progress/http-client-handler-migration.md new file mode 100644 index 00000000..ed46ea41 --- /dev/null +++ b/changes/in-progress/http-client-handler-migration.md @@ -0,0 +1,185 @@ +# HTTP Client Handler Migration Summary + +**Date**: 2025-11-30 +**Handler**: `http-client` +**Crate**: `theater-handler-http-client` +**Status**: ✅ Complete + +## Overview + +Successfully migrated the HTTP client handler from the Theater core runtime into a standalone `theater-handler-http-client` crate. This handler enables actors to make HTTP requests to external services with permission-based access control. + +## Changes Made + +### 1. Created New Crate Structure +- `/crates/theater-handler-http-client/` + - `Cargo.toml` - Dependencies including reqwest + - `src/lib.rs` - Handler implementation + - `README.md` - Documentation + +### 2. Implementation Details + +**Renamed**: `HttpClientHost` → `HttpClientHandler` + +**Implemented Handler Trait**: +- `create_instance()` - Clones the handler for reuse +- `start()` - Simple async startup (no background tasks needed) +- `setup_host_functions()` - Synchronous wrapper around async HTTP operations +- `add_export_functions()` - No-op (no exports needed) +- `name()` - Returns "http-client" +- `imports()` - Returns "theater:simple/http-client" +- `exports()` - Returns None + +**Added `Clone` derive**: Handler can now be cloned for multiple actor instances + +### 3. Component Types Migrated + +**HttpRequest**: +```rust +#[derive(ComponentType, Lift, Lower)] +pub struct HttpRequest { + method: String, + uri: String, + headers: Vec<(String, String)>, + body: Option>, +} +``` + +**HttpResponse**: +```rust +#[derive(ComponentType, Lift, Lower)] +pub struct HttpResponse { + status: u16, + headers: Vec<(String, String)>, + body: Option>, +} +``` + +These types implement the Wasmtime component model traits for seamless WASM integration. + +### 4. Async Operations Preserved + +The HTTP client uses `func_wrap_async` for the `send-http` function since actual HTTP requests are asynchronous: + +```rust +interface.func_wrap_async( + "send-http", + move |ctx, (req,): (HttpRequest,)| -> Box> { + // Permission checking + // HTTP request execution + // Response handling + } +) +``` + +This is different from environment/timing handlers which use synchronous `func_wrap`. + +### 5. Permission System + +Permission checking is performed **before** any HTTP operations: + +1. Parse URL to extract host +2. Check against `HttpClientPermissions` +3. If denied, log event and return error +4. If allowed, proceed with request + +Permissions control: +- **allowed_hosts**: Whitelist of accessible hosts +- **denied_hosts**: Blacklist (takes precedence) +- **allowed_methods**: Permitted HTTP methods (GET, POST, etc.) + +### 6. Error Handling + +Multiple levels of error handling: +- Invalid HTTP methods +- Network errors +- Response body read errors +- Permission denials + +All errors are logged to the chain and returned as `Result`. + +### 7. Test Coverage + +**Tests Added**: +- `test_handler_creation` - Verifies handler instantiation +- `test_handler_clone` - Verifies clone functionality +- `test_http_request_structures` - Validates component types +- Doc test - Compiles usage example + +All 4 tests passing! ✅ + +## Key Learnings + +1. **func_wrap_async for I/O**: HTTP requests require `func_wrap_async` not `func_wrap` +2. **Component types**: Types crossing WASM boundary need `ComponentType`, `Lift`, `Lower` derives +3. **Permission checks before operations**: Check permissions BEFORE starting async work +4. **Comprehensive error logging**: Log errors at multiple stages (permission, method parse, request, body read) + +## Dependencies + +New dependencies added: +- `reqwest = "0.12"` - HTTP client library +- Existing: `wasmtime`, `theater`, `serde`, `tracing`, `anyhow`, `thiserror` + +## Files Modified + +### New Files +- `/crates/theater-handler-http-client/Cargo.toml` +- `/crates/theater-handler-http-client/src/lib.rs` +- `/crates/theater-handler-http-client/README.md` + +### Updated Files +- `/changes/in-progress/handler-migration.md` - Progress tracking +- `/HANDLER_MIGRATION.md` - Completed migrations list + +## Testing Results + +``` +running 3 tests +test tests::test_handler_clone ... ok +test tests::test_handler_creation ... ok +test tests::test_http_request_structures ... ok + +test result: ok. 3 passed; 0 failed; 0 ignored + +running 1 test +test crates/theater-handler-http-client/src/lib.rs - (line 16) - compile ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored +``` + +✅ All tests passing! + +## Pattern Confirmation + +This migration confirms the pattern works for async operations: +- ✅ Handler trait implementation straightforward +- ✅ `func_wrap_async` for I/O operations +- ✅ `setup_host_functions` can be sync even with async closures +- ✅ Component types work seamlessly +- ✅ Permission checking integrates cleanly +- ✅ Error handling is comprehensive + +## Next Steps + +The http-client handler is now complete and ready for: +1. Integration testing with actors making real HTTP requests +2. Removal of old implementation from `/crates/theater/src/host/http_client.rs` +3. Updates to core runtime to use new handler crate + +## Comparison with Previous Handlers + +| Handler | Type | Complexity | Async Ops | +|---------|------|------------|-----------| +| random | Simple | Low | Yes (RNG) | +| timing | Simple | Low | No | +| environment | Simple | Low | No | +| http-client | Medium | Medium | Yes (HTTP) | + +The HTTP client adds complexity with: +- Component model types +- Network I/O +- Error handling from multiple sources +- Permission checks on parsed URLs + +All successfully migrated! Ready for filesystem next. diff --git a/changes/in-progress/message-server-blocker.md b/changes/in-progress/message-server-blocker.md new file mode 100644 index 00000000..f13b50f7 --- /dev/null +++ b/changes/in-progress/message-server-blocker.md @@ -0,0 +1,236 @@ +# Message-Server Handler Migration Blocker + +**Status**: Handler code complete, but **blocked on core theater infrastructure** + +**Date**: 2025-12-08 + +## Problem Summary + +The message-server handler has been fully implemented (~1,315 lines) with all operations, state management, and message processing. However, it **cannot compile** because required `TheaterCommand` enum variants are missing from the core theater crate. + +## What Was Completed + +✅ **Handler Implementation** (~1,315 lines) +- `MessageServerHandler` struct with all state management +- Background message processing loop (handles all 6 `ActorMessage` types) +- All 5 export functions (handle-send, handle-request, handle-channel-open, handle-channel-message, handle-channel-close) +- Complete error handling and event recording + +✅ **All 8 Operations Implemented**: +1. `send` - One-way message to another actor +2. `request` - Request-response RPC pattern +3. `list-outstanding-requests` - Query pending requests +4. `respond-to-request` - Respond to incoming request +5. `cancel-request` - Cancel pending request +6. `open-channel` - Create bidirectional channel +7. `send-on-channel` - Send message over channel +8. `close-channel` - Close channel + +✅ **State Management**: +- `Arc>>` - Track open channels +- `Arc>>>>` - Pending requests +- `Arc>>>` - Mailbox pattern (like supervisor) + +## The Blocker + +### Missing TheaterCommand Variants + +The handler code uses these `TheaterCommand` variants that **don't exist** in `/crates/theater/src/messages.rs`: + +```rust +// Used in operations 1-2 (send, request): +TheaterCommand::SendMessage { + actor_id: TheaterId, + actor_message: ActorMessage, // ActorMessage::Send or ActorMessage::Request +} + +// Used in operation 6 (open-channel): +TheaterCommand::ChannelOpen { + initiator_id: ChannelParticipant, + target_id: ChannelParticipant, + channel_id: ChannelId, + initial_message: Vec, + response_tx: oneshot::Sender>, +} + +// Used in operation 7 (send-on-channel): +TheaterCommand::ChannelMessage { + channel_id: ChannelId, + message: Vec, +} + +// Used in operation 8 (close-channel): +TheaterCommand::ChannelClose { + channel_id: ChannelId, +} +``` + +### Struct Field Mismatches + +Additionally, message structs have different field names than expected: + +**`ActorChannelOpen`** (in `/crates/theater/src/messages.rs:639`): +```rust +pub struct ActorChannelOpen { + pub channel_id: ChannelId, + pub response_tx: oneshot::Sender>, + pub data: Vec, // Handler expects: initial_msg + // Handler also expects: initiator_id field (missing) +} +``` + +**`ActorChannelMessage`** (in `/crates/theater/src/messages.rs:662`): +```rust +pub struct ActorChannelMessage { + pub channel_id: ChannelId, + pub data: Vec, // Handler expects: msg +} +``` + +### Helper Method Missing + +**`ChannelId`** lacks a `parse()` method: +```rust +// Handler uses but doesn't exist: +ChannelId::parse(&channel_id_str) +``` + +## Compilation Errors + +``` +error[E0599]: no variant named `SendMessage` found for enum `TheaterCommand` +error[E0599]: no variant named `ChannelOpen` found for enum `TheaterCommand` +error[E0599]: no variant named `ChannelMessage` found for enum `TheaterCommand` +error[E0599]: no variant named `ChannelClose` found for enum `TheaterCommand` +error[E0599]: no function or associated item named `parse` found for struct `ChannelId` +error[E0026]: struct `ActorChannelOpen` does not have fields named `initiator_id`, `initial_msg` +error[E0027]: pattern does not mention field `data` +error[E0026]: struct `ActorChannelMessage` does not have a field named `msg` +error[E0027]: pattern does not mention field `data` +``` + +Total: **11 compilation errors** + +## Why This Happened + +During the earlier handler migrations, the messaging infrastructure was likely: +1. Never fully implemented in the core `TheaterCommand` enum, OR +2. Removed/refactored but the old `message_server.rs` still referenced the old API + +The original `/crates/theater/src/host/message_server.rs` (1,280 lines) uses these variants, which suggests they existed at some point or were planned but never added. + +## Solutions + +### Option 1: Add Missing Infrastructure to Core Theater (Recommended) + +**File**: `/crates/theater/src/messages.rs` + +Add to `TheaterCommand` enum: +```rust +pub enum TheaterCommand { + // ... existing variants ... + + /// Send a message to an actor's mailbox + SendMessage { + actor_id: TheaterId, + actor_message: ActorMessage, + }, + + /// Open a bidirectional channel between actors + ChannelOpen { + initiator_id: ChannelParticipant, + target_id: ChannelParticipant, + channel_id: ChannelId, + initial_message: Vec, + response_tx: oneshot::Sender>, + }, + + /// Send a message over an existing channel + ChannelMessage { + channel_id: ChannelId, + message: Vec, + }, + + /// Close a channel + ChannelClose { + channel_id: ChannelId, + }, +} +``` + +Update struct field names: +```rust +// In ActorChannelOpen +pub struct ActorChannelOpen { + pub channel_id: ChannelId, + pub initiator_id: ChannelParticipant, // Add + pub response_tx: oneshot::Sender>, + pub initial_msg: Vec, // Rename from: data +} + +// In ActorChannelMessage +pub struct ActorChannelMessage { + pub channel_id: ChannelId, + pub msg: Vec, // Rename from: data +} +``` + +Add helper method: +```rust +impl ChannelId { + pub fn parse(s: &str) -> Result { + // Parse channel ID from string representation + // Implementation depends on ChannelId structure + } +} +``` + +Then add handler for these commands in `TheaterRuntime`: +- Route `SendMessage` to actor's mailbox +- Route `ChannelOpen/Message/Close` to appropriate actors + +### Option 2: Refactor Handler to Use Different Routing + +Instead of using `TheaterCommand`, directly route messages through actor mailboxes. This would require: +- Access to the theater runtime's actor registry +- Direct mailbox sending instead of command-based routing +- More coupling between handler and runtime internals + +**Not recommended** - defeats the purpose of the modular handler architecture. + +### Option 3: Defer Message-Server Migration + +Leave message-server in the core `theater` crate for now and complete the other handler (http-framework). The messaging system may need more architectural design before extraction. + +## Impact + +**Cannot complete migration without**: +- Adding 4 `TheaterCommand` variants +- Updating 2 struct definitions +- Adding 1 helper method +- Implementing command handlers in `TheaterRuntime` + +**Estimated work**: 2-4 hours to add infrastructure + integration testing + +## Files + +**Handler (complete)**: +- `/crates/theater-handler-message-server/src/lib.rs` (1,315 lines) +- `/crates/theater-handler-message-server/Cargo.toml` (dependencies configured) + +**Core changes needed**: +- `/crates/theater/src/messages.rs` (add variants, update structs, add parse method) +- `/crates/theater/src/theater_runtime.rs` (handle new command variants) + +## Recommendation + +**Add the missing infrastructure** to properly support actor-to-actor messaging. The message-server handler is architecturally sound and complete - it just needs the core theater crate to provide the command routing infrastructure. + +This is a good opportunity to design the messaging system properly as a first-class feature of the theater runtime. + +## Next Steps + +1. ✅ Document this blocker (this file) +2. ✅ Update migration tracking to show message-server as "BLOCKED" +3. Consider moving to http-framework handler (last Phase 4 handler) +4. Return to message-server after infrastructure is added diff --git a/changes/in-progress/runtime-handler-migration.md b/changes/in-progress/runtime-handler-migration.md new file mode 100644 index 00000000..235c81fc --- /dev/null +++ b/changes/in-progress/runtime-handler-migration.md @@ -0,0 +1,181 @@ +# Runtime Handler Migration Summary + +**Date**: 2025-11-30 +**Handler**: `runtime` +**Crate**: `theater-handler-runtime` +**Status**: ✅ Complete + +## Overview + +Successfully migrated the runtime handler from the Theater core runtime into a standalone `theater-handler-runtime` crate. This handler provides runtime information and control capabilities to WebAssembly actors, including logging, state retrieval, and graceful shutdown. + +## Changes Made + +### 1. Created New Crate Structure +- `/crates/theater-handler-runtime/` + - `Cargo.toml` - Dependencies including theater, wasmtime, tokio, chrono + - `src/lib.rs` - Handler implementation + - `README.md` - Documentation + +### 2. Implementation Details + +**Renamed**: `RuntimeHost` → `RuntimeHandler` + +**Implemented Handler Trait**: +- `create_instance()` - Clones the handler for reuse +- `start()` - Async startup that waits for shutdown signal +- `setup_host_functions()` - Sets up log, get-state, and shutdown functions +- `add_export_functions()` - Registers the `theater:simple/actor` init function +- `name()` - Returns "runtime" +- `imports()` - Returns "theater:simple/runtime" +- `exports()` - Returns "theater:simple/actor" + +**Added `Clone` derive**: Handler can now be cloned for multiple actor instances + +### 3. Constructor Differences + +The runtime handler requires theater integration: + +```rust +pub fn new( + config: RuntimeHostConfig, + theater_tx: Sender, // For sending shutdown commands + permissions: Option, +) -> Self +``` + +This is different from simpler handlers (environment, timing, random) which only need their config. + +### 4. Host Functions Implemented + +**1. Log Function (Synchronous)**: +```rust +func_wrap("log", move |ctx, (msg,): (String,)| { + // Record log event + // Output to tracing logger +}) +``` + +**2. Get State Function (Synchronous)**: +```rust +func_wrap("get-state", move |ctx, ()| -> Result<(Vec,)> { + // Record state request + // Return last event data from actor store +}) +``` + +**3. Shutdown Function (Async)**: +```rust +func_wrap_async("shutdown", + move |ctx, (data,): (Option>,)| -> Box> { + // Record shutdown call + // Send shutdown command to theater runtime + // Return success/error + } +) +``` + +### 5. Event Recording + +The runtime handler extensively records events to the actor's chain: + +- **Setup events**: `runtime-setup` with success/error details +- **Log events**: `theater:simple/runtime/log` with level and message +- **State events**: `theater:simple/runtime/get-state` with request and result +- **Shutdown events**: `theater:simple/runtime/shutdown` with call and result + +Event recording happens at every significant step, providing complete observability. + +### 6. Export Function Registration + +Unlike read-only handlers, the runtime handler also registers an export function: + +```rust +fn add_export_functions(&self, actor_instance: &mut ActorInstance) -> Result<()> { + actor_instance.register_function_no_result::<(String,)>( + "theater:simple/actor", + "init" + ) +} +``` + +This allows actors to be initialized by the runtime. + +### 7. Test Coverage + +**Tests Added**: +- `test_runtime_handler_creation` - Verifies handler instantiation with proper name, imports, and exports + +All tests passing! ✅ + +## Key Learnings + +1. **Theater integration required**: Runtime handler needs the `theater_tx` channel to communicate with the main runtime +2. **Mixed sync/async operations**: Log and get-state are sync, shutdown is async +3. **Export functions**: Runtime handler is the first to implement `add_export_functions` with actual functionality +4. **Comprehensive event recording**: Every operation records multiple events (call, result, errors) for complete traceability +5. **Error handling at every step**: Linker instance creation, function wrapping, and runtime operations all have specific error events + +## Dependencies + +Dependencies added beyond standard handler deps: +- `chrono = "0.4"` - For timestamp generation in events + +## Files Modified + +### New Files +- `/crates/theater-handler-runtime/Cargo.toml` +- `/crates/theater-handler-runtime/src/lib.rs` +- `/crates/theater-handler-runtime/README.md` + +### Updated Files +- `/changes/in-progress/handler-migration.md` - Progress tracking +- `/changes/in-progress/runtime-handler-migration.md` - This document + +## Testing Results + +``` +running 1 test +test tests::test_runtime_handler_creation ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored +``` + +✅ All tests passing! + +## Pattern Validation + +This migration confirms the pattern works for handlers with theater integration: +- ✅ Handler trait implementation with external dependencies +- ✅ Mix of synchronous and asynchronous host functions +- ✅ Export function registration +- ✅ Extensive event recording for observability +- ✅ Clean separation despite theater coupling + +## Unique Aspects + +Compared to previous handlers, the runtime handler is unique because: + +1. **Theater dependency**: Requires `Sender` to communicate with runtime +2. **Bidirectional**: Both imports functions for actors AND exports functions to them +3. **Shutdown control**: Can trigger actor shutdown, not just provide data +4. **Most verbose event recording**: Records more detail than other handlers + +## Next Steps + +The runtime handler is now complete and ready for: +1. Integration testing with actual actors +2. Removal of old implementation from `/crates/theater/src/host/runtime.rs` +3. Updates to core runtime to use new handler crate + +## Migration Progress + +**Phase 1 Complete!** ✅ + +With runtime handler done, all Phase 1 simple handlers are migrated: +- ✅ random +- ✅ timing +- ✅ environment +- ✅ runtime + +Next: Phase 2 - filesystem handler diff --git a/changes/proposals/2025-11-30-handler-migration.md b/changes/proposals/2025-11-30-handler-migration.md new file mode 100644 index 00000000..3babf317 --- /dev/null +++ b/changes/proposals/2025-11-30-handler-migration.md @@ -0,0 +1,226 @@ +# Change Request: Handler Migration to Separate Crates + +## Overview +Migrate all handler implementations from the core `theater` crate into separate `theater-handler-*` crates, following the pattern established by the `theater-handler-random` migration. + +## Motivation +Currently, all handler implementations (environment, filesystem, http-client, process, etc.) are embedded within the core `theater` crate in `/crates/theater/src/host/`. This creates several issues: + +1. **Tight coupling**: Handlers are tightly coupled to the core runtime, making them harder to maintain independently +2. **Difficult testing**: Testing handlers in isolation requires building the entire theater runtime +3. **Limited extensibility**: Third-party developers cannot easily create custom handlers following a clear pattern +4. **Complex dependencies**: All handler dependencies are bundled into the core crate, even if only a subset of handlers are used +5. **Harder to evolve**: Changes to individual handlers require rebuilding and testing the entire core crate + +Moving handlers into separate crates provides: +- ✅ **Cleaner architecture** - Handlers are independent modules +- ✅ **Easier maintenance** - Each handler can evolve separately +- ✅ **Better testing** - Test handlers in isolation +- ✅ **Simpler lifetimes** - Synchronous trait methods avoid lifetime complexity +- ✅ **Third-party handlers** - Clear pattern for custom handlers +- ✅ **Modular dependencies** - Users can depend on only the handlers they need + +## Detailed Design + +### 1. Handler Trait Simplification +The core `Handler` trait has been simplified to make implementation easier: + +**Before:** +```rust +fn setup_host_functions( + &mut self, + actor_component: &mut ActorComponent, +) -> Pin> + Send + '_>>; +``` + +**After:** +```rust +fn setup_host_functions( + &mut self, + actor_component: &mut ActorComponent, +) -> Result<()>; +``` + +**Rationale:** None of the handlers actually used `.await` in their setup functions. Making them synchronous: +- Eliminated complex lifetime issues +- Made the code more honest about what it does +- Simplified implementation for all future handlers + +### 2. Migration Pattern + +Each handler migration follows this pattern: + +#### Step 1: Create New Crate Structure +``` +/crates/theater-handler-{name}/ + ├── Cargo.toml # Dependencies and metadata + ├── src/ + │ └── lib.rs # Handler implementation + └── README.md # Documentation +``` + +#### Step 2: Copy and Adapt Implementation +1. Copy the host implementation from `/crates/theater/src/host/{name}.rs` +2. Rename `{Name}Host` → `{Name}Handler` +3. Update imports to use `theater::` prefix +4. Implement the `Handler` trait: + - `create_instance()` - Clone yourself + - `start()` - Async startup (keep as-is) + - `setup_host_functions()` - Now synchronous! + - `add_export_functions()` - Now synchronous! + - `name()`, `imports()`, `exports()` - Metadata + +#### Step 3: Update Dependencies +The handler crate depends on: +- Core theater crate (for trait definitions and types) +- Wasmtime (for WASM integration) +- Handler-specific dependencies +- Standard async/logging tools + +#### Step 4: Remove Old Implementation +Once the new handler crate is tested and working: +1. Remove the old implementation from `/crates/theater/src/host/{name}.rs` +2. Update any references in the core crate to use the new handler crate +3. Update documentation + +### 3. Handler List and Priority + +#### ✅ Completed Migrations +1. **random** - COMPLETE (documented example) +2. **timing** - COMPLETE + +#### 🚧 In Progress +None currently + +#### ❌ Pending Migrations +Recommended order based on complexity: + +**Phase 1: Simple Handlers** +3. **environment** - Provides env var access (simple, no complex state) +4. **runtime** - Runtime information (simple metadata) + +**Phase 2: Medium Complexity** +5. **http-client** - HTTP requests (moderate complexity) +6. **filesystem** - File operations (larger but well-isolated) + +**Phase 3: Complex Handlers** +7. **process** - OS process spawning (complex interactions) +8. **store** - Persistent storage (complex state management) +9. **supervisor** - Actor supervision (complex orchestration) + +**Phase 4: Framework Handlers** +10. **message-server** - Inter-actor messaging (complex, depends on others) +11. **http-framework** - HTTP server framework (complex, depends on others) + +### 4. Testing Strategy + +For each migrated handler: +- ✅ Unit tests compile without errors +- ✅ Handler integrates with Theater runtime via `Handler` trait +- ✅ All existing functionality is preserved +- ✅ Chain events are logged correctly +- ✅ Permissions are enforced properly + +### 5. Documentation Requirements + +Each handler crate must include: +- Comprehensive rustdoc comments +- README.md with usage examples +- Migration notes if behavior changes +- Permission requirements + +## Implementation Plan + +### Phase 1: Foundation (Weeks 1-2) +- [x] Create top-level changes tracking structure +- [ ] Document migration pattern in detail +- [ ] Migrate environment handler +- [ ] Migrate runtime handler +- [ ] Update core theater crate to use new handlers + +### Phase 2: Core Handlers (Weeks 3-4) +- [ ] Migrate http-client handler +- [ ] Migrate filesystem handler +- [ ] Update tests and documentation + +### Phase 3: Complex Handlers (Weeks 5-7) +- [ ] Migrate process handler +- [ ] Migrate store handler +- [ ] Migrate supervisor handler + +### Phase 4: Framework Handlers (Weeks 8-9) +- [ ] Migrate message-server handler +- [ ] Migrate http-framework handler +- [ ] Complete cleanup of old implementations + +### Phase 5: Finalization (Week 10) +- [ ] Update all documentation +- [ ] Update examples +- [ ] Final testing +- [ ] Release notes + +## Breaking Changes + +This migration is designed to be non-breaking: +- The `Handler` trait is simplified but existing handlers can be easily adapted +- Old handler implementations remain until new ones are tested +- Users can gradually migrate to new handler crates +- The runtime behavior remains identical + +However, there will be one breaking change: +- Dependencies on `theater` that use handlers directly will need to add dependencies on the specific `theater-handler-*` crates + +## Migration Example: Random Handler + +See `HANDLER_MIGRATION.md` in the root for the complete documented example of the random handler migration. + +Key takeaways from the random handler migration: +- Trait simplification eliminated lifetime issues +- Clear separation of concerns +- All functionality preserved +- Better testability + +## Success Criteria + +The migration is complete when: +1. All 11 handlers are migrated to separate crates +2. All tests pass +3. Documentation is updated +4. Old implementations are removed from core crate +5. Examples are updated to use new handler crates +6. CI/CD pipeline passes +7. Performance benchmarks show no regression + +## Alternatives Considered + +### Alternative 1: Keep Handlers in Core +**Rejected:** Maintains tight coupling and makes it hard for third parties to create handlers + +### Alternative 2: Dynamic Plugin System +**Rejected:** Adds complexity and runtime overhead. Static linking via Cargo is simpler and more efficient + +### Alternative 3: Async Setup Functions +**Rejected:** None of the handlers need async setup, and it complicates lifetimes unnecessarily + +## Impacts + +### Positive +- Cleaner architecture +- Better modularity +- Easier to extend +- Clearer patterns for third-party handlers +- Simplified dependencies + +### Negative +- More crates to maintain (mitigated by clear patterns) +- Slightly more complex dependency management for users +- Migration effort required + +## Future Enhancements + +After migration completion: +1. Consider versioning handlers independently +2. Create handler registry/marketplace +3. Add handler composition utilities +4. Develop handler testing framework +5. Create handler development guide diff --git a/crates/simple-theater/Cargo.toml b/crates/simple-theater/Cargo.toml new file mode 100644 index 00000000..51f1a54a --- /dev/null +++ b/crates/simple-theater/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "simple-theater" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +rust-version.workspace = true + +[dependencies] +theater.workspace = true + +anyhow.workspace = true +tokio.workspace = true +serde.workspace = true +serde_json.workspace = true +tracing.workspace = true diff --git a/crates/simple-theater/src/lib.rs b/crates/simple-theater/src/lib.rs new file mode 100644 index 00000000..f72ef700 --- /dev/null +++ b/crates/simple-theater/src/lib.rs @@ -0,0 +1,35 @@ +use anyhow::Result; +use theater::chain::ChainEvent; +use theater::config::permissions::HandlerPermission; +use theater::host::SimpleHandler; +use theater::messages::TheaterCommand; +use theater::theater_runtime::TheaterRuntime; +use theater::ChannelEvent; +use tokio::sync::mpsc::{Receiver, Sender}; + +mod simple_handler_wrapper; + +pub struct SimpleTheater { + runtime: TheaterRuntime, +} + +impl SimpleTheater { + pub async fn new( + theater_tx: Sender, + theater_rx: Receiver, + channel_events_tx: Option>, + permissions: HandlerPermission, + ) -> Result { + let simple_handler_wrapper = SimpleHandlerWrapper::new(); + let runtime = TheaterRuntime::new( + theater_tx, + theater_rx, + channel_events_tx, + permissions, + simple_handler_wrapper, + ) + .await?; + + Ok(Self { runtime }) + } +} diff --git a/crates/simple-theater/src/simple_handler_wrapper.rs b/crates/simple-theater/src/simple_handler_wrapper.rs new file mode 100644 index 00000000..56f48b24 --- /dev/null +++ b/crates/simple-theater/src/simple_handler_wrapper.rs @@ -0,0 +1,146 @@ +// okay what are we doing here. +// we want to wrap the existing SimpleHandler host functions so that they can be used with +// HostHandler + +use anyhow::Result; +use std::future::Future; +use theater::handler::HostHandler; +use theater::host::SimpleHandler; +use theater::host::{ + environment::EnvironmentHost, filesystem::FileSystemHost, framework::HttpFramework, + http_client::HttpClientHost, message_server::MessageServerHost, process::ProcessHost, + random::RandomHost, runtime::RuntimeHost, store::StoreHost, supervisor::SupervisorHost, + timing::TimingHost, +}; + +pub enum SimpleHandlerWrapper { + MessageServer(MessageServerHost), + Environment(EnvironmentHost), + FileSystem(FileSystemHost), + HttpClient(HttpClientHost), + HttpFramework(HttpFramework), + Process(ProcessHost), + Runtime(RuntimeHost), + Supervisor(SupervisorHost), + Store(StoreHost), + Timing(TimingHost), + Random(RandomHost), +} + +impl HostHandler for SimpleHandlerWrapper { + fn start( + &mut self, + actor_handle: theater::actor::handle::ActorHandle, + shutdown_receiver: theater::shutdown::ShutdownReceiver, + ) -> impl Future> + Send { + async move { + match self { + SimpleHandlerWrapper::MessageServer(handler) => { + handler.start(actor_handle, shutdown_receiver).await + } + SimpleHandlerWrapper::Environment(handler) => { + handler.start(actor_handle, shutdown_receiver).await + } + SimpleHandlerWrapper::FileSystem(handler) => { + handler.start(actor_handle, shutdown_receiver).await + } + SimpleHandlerWrapper::HttpClient(handler) => { + handler.start(actor_handle, shutdown_receiver).await + } + SimpleHandlerWrapper::HttpFramework(handler) => { + handler.start(actor_handle, shutdown_receiver).await + } + SimpleHandlerWrapper::Process(handler) => { + handler.start(actor_handle, shutdown_receiver).await + } + SimpleHandlerWrapper::Runtime(handler) => { + handler.start(actor_handle, shutdown_receiver).await + } + SimpleHandlerWrapper::Supervisor(handler) => { + handler.start(actor_handle, shutdown_receiver).await + } + SimpleHandlerWrapper::Store(handler) => { + handler.start(actor_handle, shutdown_receiver).await + } + SimpleHandlerWrapper::Timing(handler) => { + handler.start(actor_handle, shutdown_receiver).await + } + SimpleHandlerWrapper::Random(handler) => { + handler.start(actor_handle, shutdown_receiver).await + } + } + } + } + + fn name(&self) -> &str { + match self { + SimpleHandlerWrapper::MessageServer(_handler) => "message_server", + SimpleHandlerWrapper::Environment(_handler) => "environment", + SimpleHandlerWrapper::FileSystem(_handler) => "filesystem", + SimpleHandlerWrapper::HttpClient(_handler) => "http_client", + SimpleHandlerWrapper::HttpFramework(_handler) => "http_framework", + SimpleHandlerWrapper::Process(_handler) => "process", + SimpleHandlerWrapper::Runtime(_handler) => "runtime", + SimpleHandlerWrapper::Supervisor(_handler) => "supervisor", + SimpleHandlerWrapper::Store(_handler) => "store", + SimpleHandlerWrapper::Timing(_handler) => "timing", + SimpleHandlerWrapper::Random(_handler) => "random", + } + } + + fn get_handlers( + &self, + actor_component: &mut theater::wasm::ActorComponent, + ) -> Vec { + let mut handlers = Vec::new(); + for handler in self { + if handler.is_required(actor_component) { + handlers.push(handler.clone()); + } + } + handlers + } + + fn setup_handlers( + &self, + actor_component: &mut theater::wasm::ActorComponent, + ) -> impl std::future::Future> + Send { + let fut = async move { + match self { + SimpleHandlerWrapper::MessageServer(handler) => { + handler.setup_host_functions(actor_component).await + } + SimpleHandlerWrapper::Environment(handler) => { + handler.setup_host_functions(actor_component).await + } + SimpleHandlerWrapper::FileSystem(handler) => { + handler.setup_host_functions(actor_component).await + } + SimpleHandlerWrapper::HttpClient(handler) => { + handler.setup_host_functions(actor_component).await + } + SimpleHandlerWrapper::HttpFramework(handler) => { + handler.setup_host_functions(actor_component).await + } + SimpleHandlerWrapper::Process(handler) => { + handler.setup_host_functions(actor_component).await + } + SimpleHandlerWrapper::Runtime(handler) => { + handler.setup_host_functions(actor_component).await + } + SimpleHandlerWrapper::Supervisor(handler) => { + handler.setup_host_functions(actor_component).await + } + SimpleHandlerWrapper::Store(handler) => { + handler.setup_host_functions(actor_component).await + } + SimpleHandlerWrapper::Timing(handler) => { + handler.setup_host_functions(actor_component).await + } + SimpleHandlerWrapper::Random(handler) => { + handler.setup_host_functions(actor_component).await + } + } + }; + } +} diff --git a/crates/theater-chain/Cargo.toml b/crates/theater-chain/Cargo.toml new file mode 100644 index 00000000..e86edcfe --- /dev/null +++ b/crates/theater-chain/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "theater-chain" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +rust-version.workspace = true + +[dependencies] +tracing.workspace = true + +# WebAssembly runtime +wasmtime = { version = "31.0", features = ["component-model", "async"] } +wit-bindgen = "0.36.0" + +# Additional core deps +chrono = { version = "0.4", features = ["serde"] } +thiserror = "1.0" +toml = "0.8" +lazy_static = "1.4" +futures = "0.3" +pin-utils = "0.1" +rand = "0.8.5" +rand_chacha = "0.3" +tokio-util = { version = "0.7.13", features = ["codec"] } +bytes = "1.0" +base64 = "0.21" +sha1 = "0.10" +hex = "0.4.3" +md5 = "0.7.0" +serde = { workspace = true, features = ["derive"] } diff --git a/crates/theater-chain/src/chain.rs b/crates/theater-chain/src/chain.rs new file mode 100644 index 00000000..f9cc6b34 --- /dev/null +++ b/crates/theater-chain/src/chain.rs @@ -0,0 +1,65 @@ +use crate::event::Event; +use crate::event::EventType; +use tracing::error; + +pub struct Chain { + events: Vec>, + current_hash: Option>, +} + +impl Chain { + pub fn new() -> Self { + Self { + events: Vec::new(), + current_hash: None, + } + } + + pub fn add_typed_event(&mut self, event_data: D) -> Event { + let event = Event::new(self.current_hash.clone(), event_data); + + // Now that we have the hash, store the updated event in memory + self.events.push(event.clone()); + self.current_hash = Some(event.hash.clone()); + + event + } + + pub fn verify(&self) -> bool { + let mut prev_hash = None; + + for event in &self.events { + // Verify the event's hash + if !event.verify() { + error!( + "Event hash verification failed for event {}", + hex::encode(&event.hash) + ); + return false; + } + + // Verify the parent hash linkage + if event.parent_hash != prev_hash { + error!( + "Parent hash mismatch for event {}: expected {:?}, found {:?}", + hex::encode(&event.hash), + prev_hash.as_ref().map(|h| hex::encode(h)), + event.parent_hash.as_ref().map(|h| hex::encode(h)) + ); + return false; + } + + prev_hash = Some(event.hash.clone()); + } + + true + } + + pub fn get_last_event(&self) -> Option<&Event> { + self.events.last() + } + + pub fn get_events(&self) -> &[Event] { + &self.events + } +} diff --git a/crates/theater-chain/src/event.rs b/crates/theater-chain/src/event.rs new file mode 100644 index 00000000..679078a7 --- /dev/null +++ b/crates/theater-chain/src/event.rs @@ -0,0 +1,72 @@ +use serde::{Deserialize, Serialize}; +use sha1::{Digest, Sha1}; +use std::fmt::{Debug, Display, Formatter, Result}; +use std::hash::Hash; +use wasmtime::component::{ComponentType, Lift, Lower}; + +pub trait EventType: + Display + Debug + Send + Sync + ComponentType + Lift + Lower + Hash + Eq + Clone +{ + fn event_type(&self) -> String; + fn len(&self) -> usize; +} + +#[derive(Debug, Clone, Serialize, Deserialize, ComponentType, Lift, Lower, Hash, Eq)] +#[component(record)] +pub struct Event { + pub hash: Vec, + #[component(name = "parent-hash")] + pub parent_hash: Option>, + pub data: D, +} + +impl Event { + pub fn new(parent_hash: Option>, data: D) -> Self { + // Serialize the event data to compute its hash + let mut hasher = Sha1::new(); + if let Some(ref parent) = parent_hash { + hasher.update(parent); + } + hasher.update(data.to_string().as_bytes()); + let hash = hasher.finalize().to_vec(); + + Self { + hash, + parent_hash, + data, + } + } + + pub fn verify(&self) -> bool { + let mut hasher = Sha1::new(); + if let Some(ref parent) = self.parent_hash { + hasher.update(parent); + } + hasher.update(self.data.to_string().as_bytes()); + let computed_hash = hasher.finalize().to_vec(); + self.hash == computed_hash + } +} + +impl Display for Event { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + writeln!(f, "EVENT {}", hex::encode(&self.hash))?; + match &self.parent_hash { + Some(parent) => writeln!(f, "{}", hex::encode(parent))?, + None => writeln!(f, "0000000000000000")?, + } + writeln!(f, "{}", self.data.event_type())?; + writeln!(f, "{}", self.data.len())?; + writeln!(f)?; + writeln!(f, "{}", self.data)?; + writeln!(f)?; + Ok(()) + } +} + +// implement Eq for ChainEvent +impl PartialEq for Event { + fn eq(&self, other: &Self) -> bool { + self.hash == other.hash + } +} diff --git a/crates/theater-chain/src/lib.rs b/crates/theater-chain/src/lib.rs new file mode 100644 index 00000000..3353acd5 --- /dev/null +++ b/crates/theater-chain/src/lib.rs @@ -0,0 +1,2 @@ +pub mod chain; +pub mod event; diff --git a/crates/theater-cli/src/client/connection.rs b/crates/theater-cli/src/client/connection.rs index e1946bad..f848884c 100644 --- a/crates/theater-cli/src/client/connection.rs +++ b/crates/theater-cli/src/client/connection.rs @@ -1,4 +1,3 @@ - use bytes::Bytes; use futures::sink::SinkExt; use futures::stream::StreamExt; diff --git a/crates/theater-cli/src/client/theater_client.rs b/crates/theater-cli/src/client/theater_client.rs index 724b01a5..8ba1d4c8 100644 --- a/crates/theater-cli/src/client/theater_client.rs +++ b/crates/theater-cli/src/client/theater_client.rs @@ -63,7 +63,8 @@ impl TheaterClient { address: conn.address, source: e, }) - }).await + }) + .await } /// Close the connection @@ -95,7 +96,8 @@ impl TheaterClient { response: format!("{:?}", response), }), } - }).await + }) + .await } /// Start an actor from a manifest @@ -119,7 +121,8 @@ impl TheaterClient { address: conn.address, source: e, }) - }).await + }) + .await } /// Stop a running actor (graceful shutdown) @@ -150,7 +153,8 @@ impl TheaterClient { response: format!("{:?}", response), }), } - }).await + }) + .await } /// Terminate an actor immediately (forceful shutdown) @@ -181,7 +185,8 @@ impl TheaterClient { response: format!("{:?}", response), }), } - }).await + }) + .await } /// Get actor state @@ -219,7 +224,8 @@ impl TheaterClient { response: format!("{:?}", response), }), } - }).await + }) + .await } /// Get actor events @@ -250,7 +256,8 @@ impl TheaterClient { response: format!("{:?}", response), }), } - }).await + }) + .await } /// Send a message to an actor (fire and forget) @@ -281,7 +288,8 @@ impl TheaterClient { response: format!("{:?}", response), }), } - }).await + }) + .await } /// Send a request to an actor and wait for response @@ -312,7 +320,8 @@ impl TheaterClient { response: format!("{:?}", response), }), } - }).await + }) + .await } /// Subscribe to events from an actor (returns a stream-like interface) @@ -347,7 +356,8 @@ impl TheaterClient { response: format!("{:?}", response), }), } - }).await + }) + .await } /// Get the next response from the connection (for streaming operations) @@ -360,7 +370,8 @@ impl TheaterClient { address: conn.address.clone(), source: e, }) - }).await + }) + .await } /// Get actor status @@ -388,7 +399,8 @@ impl TheaterClient { response: format!("{:?}", response), }), } - }).await + }) + .await } /// Restart an actor @@ -416,7 +428,8 @@ impl TheaterClient { response: format!("{:?}", response), }), } - }).await + }) + .await } /// Update actor component @@ -447,7 +460,8 @@ impl TheaterClient { response: format!("{:?}", response), }), } - }).await + }) + .await } /// Unsubscribe from actor events @@ -477,7 +491,8 @@ impl TheaterClient { response: format!("{:?}", response), }), } - }).await + }) + .await } /// Open a channel with an actor @@ -507,7 +522,8 @@ impl TheaterClient { response: format!("{:?}", response), }), } - }).await + }) + .await } /// Send a message on a channel @@ -530,7 +546,8 @@ impl TheaterClient { response: format!("{:?}", response), }), } - }).await + }) + .await } /// Close a channel @@ -552,7 +569,8 @@ impl TheaterClient { response: format!("{:?}", response), }), } - }).await + }) + .await } /// Receive channel message (for channel communication) @@ -574,7 +592,8 @@ impl TheaterClient { Box::pin(self.receive_channel_message()).await } } - }).await + }) + .await } } @@ -588,20 +607,22 @@ pub struct EventStream { impl EventStream { /// Get the next event from the stream pub async fn next_event(&self) -> CliResult> { - self.client.with_cancellation(|| async { - let mut conn = self.client.connection.lock().await; - match conn.receive().await? { - ManagementResponse::ActorEvent { event } => Ok(Some(event)), - ManagementResponse::ActorStopped { .. } => Ok(None), - ManagementResponse::Error { error } => Err(CliError::EventStreamError { - reason: format!("{:?}", error), - }), - _ => { - // Ignore other response types in event stream - Box::pin(self.next_event()).await + self.client + .with_cancellation(|| async { + let mut conn = self.client.connection.lock().await; + match conn.receive().await? { + ManagementResponse::ActorEvent { event } => Ok(Some(event)), + ManagementResponse::ActorStopped { .. } => Ok(None), + ManagementResponse::Error { error } => Err(CliError::EventStreamError { + reason: format!("{:?}", error), + }), + _ => { + // Ignore other response types in event stream + Box::pin(self.next_event()).await + } } - } - }).await + }) + .await } /// Get the actor ID this stream is associated with diff --git a/crates/theater-cli/src/commands/create.rs b/crates/theater-cli/src/commands/create.rs index 7bbe8aeb..23ec2d02 100644 --- a/crates/theater-cli/src/commands/create.rs +++ b/crates/theater-cli/src/commands/create.rs @@ -61,13 +61,8 @@ pub async fn execute_async(args: &CreateArgs, ctx: &CommandContext) -> Result<() debug!("Output directory: {}", output_dir.display()); // Get available templates - let templates_list = templates::available_templates().map_err(|e| { - CliError::file_operation_failed( - "load templates", - "templates directory", - e, - ) - })?; + let templates_list = templates::available_templates() + .map_err(|e| CliError::file_operation_failed("load templates", "templates directory", e))?; // Check if the template exists if !templates_list.contains_key(&args.template) { @@ -80,15 +75,15 @@ pub async fn execute_async(args: &CreateArgs, ctx: &CommandContext) -> Result<() // Create the project let project_path = output_dir.join(&args.name); - + // Check if directory already exists if project_path.exists() { return Err(CliError::file_operation_failed( "create project", project_path.display().to_string(), std::io::Error::new( - std::io::ErrorKind::AlreadyExists, - "Directory already exists" + std::io::ErrorKind::AlreadyExists, + "Directory already exists", ), )); } @@ -96,11 +91,7 @@ pub async fn execute_async(args: &CreateArgs, ctx: &CommandContext) -> Result<() // Step 1: Create project from template println!("Creating project structure..."); templates::create_project(&args.template, &args.name, &project_path).map_err(|e| { - CliError::file_operation_failed( - "create project", - project_path.display().to_string(), - e, - ) + CliError::file_operation_failed("create project", project_path.display().to_string(), e) })?; println!("✅ Project created from '{}' template", args.template); @@ -143,14 +134,12 @@ pub async fn execute_async(args: &CreateArgs, ctx: &CommandContext) -> Result<() println!("\nProject '{}' created successfully!", args.name); // Create success result and output - let mut build_instructions = vec![ - format!("cd {}", args.name), - ]; + let mut build_instructions = vec![format!("cd {}", args.name)]; if args.skip_deps { build_instructions.push("wkg wit fetch".to_string()); } - + build_instructions.extend(vec![ "cargo component build --release".to_string(), "theater start manifest.toml".to_string(), @@ -177,15 +166,13 @@ pub async fn execute_async(args: &CreateArgs, ctx: &CommandContext) -> Result<() /// Check if cargo component is installed fn check_cargo_component() -> Result<(), CliError> { debug!("Checking for cargo component..."); - + let output = Command::new("cargo") .args(&["component", "--version"]) .output() - .map_err(|e| { - CliError::MissingTool { - tool: "cargo component".to_string(), - install_command: "cargo install cargo-component".to_string(), - } + .map_err(|e| CliError::MissingTool { + tool: "cargo component".to_string(), + install_command: "cargo install cargo-component".to_string(), })?; if !output.status.success() { @@ -210,10 +197,8 @@ fn fetch_wit_dependencies(project_path: &PathBuf) -> Result<(), CliError> { match child { Ok(mut child) => { - let status = child.wait().map_err(|e| { - CliError::BuildFailed { - output: format!("Failed to wait for wkg wit fetch: {}", e), - } + let status = child.wait().map_err(|e| CliError::BuildFailed { + output: format!("Failed to wait for wkg wit fetch: {}", e), })?; if status.success() { @@ -238,7 +223,7 @@ fn try_wasm_tools_fetch(project_path: &PathBuf) -> Result<(), CliError> { warn!("Please run one of the following manually:"); warn!(" - wkg wit fetch (if you have wkg installed)"); warn!(" - Or manually download theater:simple WIT files to wit/deps/theater-simple/"); - + // Don't fail the creation, just warn Ok(()) } @@ -246,21 +231,23 @@ fn try_wasm_tools_fetch(project_path: &PathBuf) -> Result<(), CliError> { /// Build the project to validate it works fn build_project(project_path: &PathBuf) -> Result<(), CliError> { debug!("Building project at {}", project_path.display()); - + let mut child = Command::new("cargo") - .args(&["component", "build", "--target", "wasm32-unknown-unknown", "--release"]) + .args(&[ + "component", + "build", + "--target", + "wasm32-unknown-unknown", + "--release", + ]) .current_dir(project_path) .spawn() - .map_err(|e| { - CliError::BuildFailed { - output: format!("Failed to execute cargo component build: {}", e), - } + .map_err(|e| CliError::BuildFailed { + output: format!("Failed to execute cargo component build: {}", e), })?; - let status = child.wait().map_err(|e| { - CliError::BuildFailed { - output: format!("Failed to wait for cargo component build: {}", e), - } + let status = child.wait().map_err(|e| CliError::BuildFailed { + output: format!("Failed to wait for cargo component build: {}", e), })?; if !status.success() { @@ -293,23 +280,21 @@ fn should_init_git() -> bool { /// Initialize a git repository and make the first commit fn init_git_repo(project_path: &PathBuf, project_name: &str) -> Result<(), CliError> { debug!("Initializing git repository at {}", project_path.display()); - + // Initialize git repository let init_output = Command::new("git") .args(&["init"]) .current_dir(project_path) .output() - .map_err(|e| { - CliError::MissingTool { - tool: "git".to_string(), - install_command: "Install git from https://git-scm.com/".to_string(), - } + .map_err(|e| CliError::MissingTool { + tool: "git".to_string(), + install_command: "Install git from https://git-scm.com/".to_string(), })?; if !init_output.status.success() { return Err(CliError::BuildFailed { output: format!( - "Failed to initialize git repository: {}", + "Failed to initialize git repository: {}", String::from_utf8_lossy(&init_output.stderr) ), }); @@ -320,16 +305,14 @@ fn init_git_repo(project_path: &PathBuf, project_name: &str) -> Result<(), CliEr .args(&["add", "."]) .current_dir(project_path) .output() - .map_err(|e| { - CliError::BuildFailed { - output: format!("Failed to add files to git: {}", e), - } + .map_err(|e| CliError::BuildFailed { + output: format!("Failed to add files to git: {}", e), })?; if !add_output.status.success() { return Err(CliError::BuildFailed { output: format!( - "Failed to add files to git: {}", + "Failed to add files to git: {}", String::from_utf8_lossy(&add_output.stderr) ), }); @@ -341,10 +324,8 @@ fn init_git_repo(project_path: &PathBuf, project_name: &str) -> Result<(), CliEr .args(&["commit", "-m", &commit_message]) .current_dir(project_path) .output() - .map_err(|e| { - CliError::BuildFailed { - output: format!("Failed to make initial commit: {}", e), - } + .map_err(|e| CliError::BuildFailed { + output: format!("Failed to make initial commit: {}", e), })?; if !commit_output.status.success() { @@ -356,10 +337,7 @@ fn init_git_repo(project_path: &PathBuf, project_name: &str) -> Result<(), CliEr }); } else { return Err(CliError::BuildFailed { - output: format!( - "Failed to make initial commit: {}", - stderr - ), + output: format!("Failed to make initial commit: {}", stderr), }); } } diff --git a/crates/theater-cli/src/commands/list.rs b/crates/theater-cli/src/commands/list.rs index 5797556e..b39d528f 100644 --- a/crates/theater-cli/src/commands/list.rs +++ b/crates/theater-cli/src/commands/list.rs @@ -20,7 +20,7 @@ pub async fn execute_async(args: &ListArgs, ctx: &CommandContext) -> CliResult<( // Create a client with the specified address and cancellation token let client = crate::client::TheaterClient::new(args.address, ctx.shutdown_token.clone()); - + // This will now properly respond to Ctrl+C during the network operation let actors = client.list_actors().await?; diff --git a/crates/theater-cli/src/commands/process.rs b/crates/theater-cli/src/commands/process.rs index 8cccabbc..0825e203 100644 --- a/crates/theater-cli/src/commands/process.rs +++ b/crates/theater-cli/src/commands/process.rs @@ -101,7 +101,8 @@ pub async fn execute_async(args: &ProcessArgs, ctx: &CommandContext) -> Result<( args, ctx, address, - ).await; + ) + .await; match result { Ok(()) => { @@ -142,7 +143,12 @@ async fn run_actor_process( // Start the actor client - .start_actor(manifest_content.to_string(), initial_state, parent, subscribe) + .start_actor( + manifest_content.to_string(), + initial_state, + parent, + subscribe, + ) .await .map_err(|e| CliError::actor_not_found(format!("Failed to start actor: {}", e)))?; diff --git a/crates/theater-cli/src/lib.rs b/crates/theater-cli/src/lib.rs index f8d9e229..807bdce6 100644 --- a/crates/theater-cli/src/lib.rs +++ b/crates/theater-cli/src/lib.rs @@ -213,7 +213,10 @@ pub struct CommandContext { impl CommandContext { /// Create a theater client using the configured server address pub fn create_client(&self) -> client::TheaterClient { - client::TheaterClient::new(self.config.server.default_address, self.shutdown_token.clone()) + client::TheaterClient::new( + self.config.server.default_address, + self.shutdown_token.clone(), + ) } /// Get the server address from config or override diff --git a/crates/theater-cli/src/output/formatters.rs b/crates/theater-cli/src/output/formatters.rs index f26f35e5..88ce8e20 100644 --- a/crates/theater-cli/src/output/formatters.rs +++ b/crates/theater-cli/src/output/formatters.rs @@ -1604,4 +1604,4 @@ mod tests { assert_eq!(truncate_string("hello", 10), "hello"); assert_eq!(truncate_string("hello world", 5), "hell…"); } -} \ No newline at end of file +} diff --git a/crates/theater-cli/src/templates/mod.rs b/crates/theater-cli/src/templates/mod.rs index 7e5388ac..d91bbf72 100644 --- a/crates/theater-cli/src/templates/mod.rs +++ b/crates/theater-cli/src/templates/mod.rs @@ -1,10 +1,10 @@ +use handlebars::Handlebars; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::io; use std::path::{Path, PathBuf}; use tracing::{debug, info}; -use handlebars::Handlebars; -use serde::{Deserialize, Serialize}; /// Template metadata loaded from template.toml #[derive(Debug, Clone, Deserialize)] @@ -37,7 +37,7 @@ pub struct TemplateData { /// Get the path to the templates directory fn templates_dir() -> Result { // Try multiple possible locations for templates - + // 1. First try relative to the current executable (for installed binaries) if let Ok(exe_path) = std::env::current_exe() { if let Some(exe_dir) = exe_path.parent() { @@ -49,7 +49,7 @@ fn templates_dir() -> Result { } } } - + // 2. Try relative to current working directory (for development) let cwd_templates = std::env::current_dir()?.join("templates"); debug!("Trying current working dir: {}", cwd_templates.display()); @@ -57,27 +57,33 @@ fn templates_dir() -> Result { debug!("Found templates at: {}", cwd_templates.display()); return Ok(cwd_templates); } - + // 3. Try in the CLI crate directory (for development from project root) - let cli_crate_templates = std::env::current_dir()?.join("crates").join("theater-cli").join("templates"); + let cli_crate_templates = std::env::current_dir()? + .join("crates") + .join("theater-cli") + .join("templates"); debug!("Trying CLI crate dir: {}", cli_crate_templates.display()); if cli_crate_templates.exists() { debug!("Found templates at: {}", cli_crate_templates.display()); return Ok(cli_crate_templates); } - + // 4. Fallback to compile-time path (for development) let manifest_dir = env!("CARGO_MANIFEST_DIR"); let compile_time_templates = PathBuf::from(manifest_dir).join("templates"); - debug!("Trying compile-time dir: {}", compile_time_templates.display()); + debug!( + "Trying compile-time dir: {}", + compile_time_templates.display() + ); if compile_time_templates.exists() { debug!("Found templates at: {}", compile_time_templates.display()); return Ok(compile_time_templates); } - + Err(io::Error::new( io::ErrorKind::NotFound, - "Templates directory not found in any expected location" + "Templates directory not found in any expected location", )) } @@ -85,11 +91,14 @@ fn templates_dir() -> Result { pub fn available_templates() -> Result, io::Error> { let mut templates = HashMap::new(); let templates_path = templates_dir()?; - + if !templates_path.exists() { return Err(io::Error::new( io::ErrorKind::NotFound, - format!("Templates directory not found: {}", templates_path.display()) + format!( + "Templates directory not found: {}", + templates_path.display() + ), )); } @@ -97,21 +106,24 @@ pub fn available_templates() -> Result, io::Error> { for entry in fs::read_dir(&templates_path)? { let entry = entry?; let path = entry.path(); - + if path.is_dir() { - let template_name = path.file_name() - .and_then(|n| n.to_str()) - .ok_or_else(|| io::Error::new( + let template_name = path.file_name().and_then(|n| n.to_str()).ok_or_else(|| { + io::Error::new( io::ErrorKind::InvalidData, - "Invalid template directory name" - ))?; - + "Invalid template directory name", + ) + })?; + // Load template.toml let metadata_path = path.join("template.toml"); if metadata_path.exists() { match load_template_metadata(&metadata_path) { Ok(metadata) => { - debug!("Loaded template: {} - {}", template_name, metadata.template.description); + debug!( + "Loaded template: {} - {}", + template_name, metadata.template.description + ); let template = Template { name: metadata.template.name, description: metadata.template.description, @@ -132,7 +144,7 @@ pub fn available_templates() -> Result, io::Error> { if templates.is_empty() { return Err(io::Error::new( io::ErrorKind::NotFound, - "No valid templates found" + "No valid templates found", )); } @@ -145,7 +157,7 @@ fn load_template_metadata(path: &Path) -> Result { toml::from_str(&content).map_err(|e| { io::Error::new( io::ErrorKind::InvalidData, - format!("Invalid template.toml: {}", e) + format!("Invalid template.toml: {}", e), ) }) } @@ -174,21 +186,35 @@ pub fn create_project( // Setup Handlebars renderer let mut handlebars = Handlebars::new(); handlebars.set_strict_mode(true); - + // Register default helper (this should match what's used in the main theater crate) - handlebars.register_helper("default", Box::new(|h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, out: &mut dyn handlebars::Output| -> handlebars::HelperResult { - let value = h.param(0).and_then(|v| v.value().as_str()); - let default = h.param(1).and_then(|v| v.value().as_str()).unwrap_or(""); - - let result = if let Some(val) = value { - if val.is_empty() { default } else { val } - } else { - default - }; - - out.write(result)?; - Ok(()) - })); + handlebars.register_helper( + "default", + Box::new( + |h: &handlebars::Helper, + _: &Handlebars, + _: &handlebars::Context, + _: &mut handlebars::RenderContext, + out: &mut dyn handlebars::Output| + -> handlebars::HelperResult { + let value = h.param(0).and_then(|v| v.value().as_str()); + let default = h.param(1).and_then(|v| v.value().as_str()).unwrap_or(""); + + let result = if let Some(val) = value { + if val.is_empty() { + default + } else { + val + } + } else { + default + }; + + out.write(result)?; + Ok(()) + }, + ), + ); // Prepare template data let template_data = TemplateData { @@ -198,7 +224,7 @@ pub fn create_project( // Get template directory let template_dir = templates_dir()?.join(template_name); - + // Create all template files for (target_path, template_file) in &template.files { let source_file_path = template_dir.join(template_file); @@ -212,19 +238,26 @@ pub fn create_project( } // Read template content - let template_content = fs::read_to_string(&source_file_path) - .map_err(|e| io::Error::new( + let template_content = fs::read_to_string(&source_file_path).map_err(|e| { + io::Error::new( io::ErrorKind::NotFound, - format!("Template file not found: {} ({})", source_file_path.display(), e) - ))?; + format!( + "Template file not found: {} ({})", + source_file_path.display(), + e + ), + ) + })?; // Render template with Handlebars let rendered_content = handlebars .render_template(&template_content, &template_data) - .map_err(|e| io::Error::new( - io::ErrorKind::InvalidData, - format!("Template rendering failed for {}: {}", template_file, e) - ))?; + .map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("Template rendering failed for {}: {}", template_file, e), + ) + })?; debug!( "Creating file: {} ({} bytes)", @@ -238,18 +271,18 @@ pub fn create_project( info!("Project '{}' created successfully!", project_name); info!("Note: You may need to run 'wkg wit fetch' to fetch WIT dependencies"); - + Ok(()) } /// List all available templates pub fn list_templates() -> Result<(), io::Error> { let templates = available_templates()?; - + println!("Available templates:"); for (name, template) in templates { println!(" {}: {}", name, template.description); } - + Ok(()) } diff --git a/crates/theater-cli/src/tui/event_explorer/mod.rs b/crates/theater-cli/src/tui/event_explorer/mod.rs index 4c544778..e70d0c37 100644 --- a/crates/theater-cli/src/tui/event_explorer/mod.rs +++ b/crates/theater-cli/src/tui/event_explorer/mod.rs @@ -146,4 +146,4 @@ async fn run_explorer_loop( debug!("Event explorer loop ended"); Ok(()) -} \ No newline at end of file +} diff --git a/crates/theater-handler-environment/Cargo.toml b/crates/theater-handler-environment/Cargo.toml new file mode 100644 index 00000000..665e3918 --- /dev/null +++ b/crates/theater-handler-environment/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "theater-handler-environment" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +rust-version.workspace = true +description = "Environment variable handler for Theater WebAssembly actors" + +[dependencies] +theater = { workspace = true } +anyhow.workspace = true +chrono = "0.4" +thiserror = "1.0" +tracing.workspace = true +wasmtime = { version = "31.0", features = ["component-model", "async"] } diff --git a/crates/theater-handler-environment/README.md b/crates/theater-handler-environment/README.md new file mode 100644 index 00000000..184167bc --- /dev/null +++ b/crates/theater-handler-environment/README.md @@ -0,0 +1,118 @@ +# theater-handler-environment + +Environment variable handler for Theater WebAssembly actors. + +## Overview + +This handler provides read-only access to environment variables for WebAssembly actors running in the Theater runtime. It implements security controls through permission-based access and logs all environment variable operations to the actor's chain for debugging and auditing. + +## Features + +- **Read-only access**: Actors can read but not modify environment variables +- **Permission-based access**: Control which variables actors can access +- **Variable listing**: Optionally allow actors to list all accessible variables +- **Event logging**: All environment variable accesses are logged to the chain +- **Error handling**: Graceful handling of missing variables and permission denials + +## Usage + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +theater-handler-environment = "0.2.1" +``` + +### Basic Example + +```rust +use theater_handler_environment::EnvironmentHandler; +use theater::config::actor_manifest::EnvironmentHandlerConfig; + +// Create handler with default config +let config = EnvironmentHandlerConfig { + allow_list_all: false, // Don't allow listing all variables +}; +let handler = EnvironmentHandler::new(config, None); +``` + +### With Permissions + +```rust +use theater_handler_environment::EnvironmentHandler; +use theater::config::actor_manifest::EnvironmentHandlerConfig; +use theater::config::permissions::EnvironmentPermissions; + +// Create permissions allowing specific variables +let permissions = EnvironmentPermissions { + allowed_variables: vec!["HOME".to_string(), "USER".to_string()], + denied_variables: vec!["SECRET_KEY".to_string()], +}; + +let config = EnvironmentHandlerConfig { + allow_list_all: false, +}; + +let handler = EnvironmentHandler::new(config, Some(permissions)); +``` + +## WIT Interface + +This handler implements the `theater:simple/environment` interface: + +```wit +interface environment { + // Get the value of an environment variable + get-var: func(name: string) -> option; + + // Check if an environment variable exists + exists: func(name: string) -> bool; + + // List all accessible environment variables (if enabled) + list-vars: func() -> list>; +} +``` + +## Configuration + +### EnvironmentHandlerConfig + +- `allow_list_all`: Whether to allow the `list-vars` function (default: `false`) + +### EnvironmentPermissions + +- `allowed_variables`: List of specific variables that actors can access +- `denied_variables`: List of variables that actors cannot access (takes precedence) + +If no permissions are provided, all variables are accessible. + +## Chain Events + +All environment operations are logged as chain events: + +- `HandlerSetupStart`: Handler initialization begins +- `LinkerInstanceSuccess`: WASM linker setup successful +- `GetVar`: Environment variable access attempt +- `PermissionDenied`: Access denied due to permissions +- `HandlerSetupSuccess`: Handler setup completed + +## Security Considerations + +1. **Read-only**: This handler never allows modification of environment variables +2. **Permission checking**: All accesses are checked against configured permissions +3. **Logging**: All accesses are logged for auditing +4. **Fail-safe**: Permission denials return empty/false rather than errors + +## Migration Notes + +This handler was migrated from the core `theater` crate as part of the handler modularization effort. The migration included: + +- Renamed from `EnvironmentHost` to `EnvironmentHandler` +- Implemented the `Handler` trait +- Made `setup_host_functions` synchronous (was async but never awaited) +- Added `Clone` derive for handler reusability +- Improved documentation + +## License + +Apache-2.0 diff --git a/crates/theater-handler-environment/src/lib.rs b/crates/theater-handler-environment/src/lib.rs new file mode 100644 index 00000000..21309341 --- /dev/null +++ b/crates/theater-handler-environment/src/lib.rs @@ -0,0 +1,373 @@ +//! # Environment Variable Handler +//! +//! Provides environment variable access to WebAssembly actors in the Theater system. +//! This handler allows actors to read environment variables while maintaining security +//! boundaries and permission controls. +//! +//! ## Features +//! +//! - **Read-only access**: Actors can read but not modify environment variables +//! - **Permission-based access**: Control which variables actors can access +//! - **Variable listing**: Optionally allow actors to list all accessible variables +//! - **Event logging**: All environment variable accesses are logged to the chain +//! +//! ## Example +//! +//! ```rust,no_run +//! use theater_handler_environment::EnvironmentHandler; +//! use theater::config::actor_manifest::EnvironmentHandlerConfig; +//! +//! let config = EnvironmentHandlerConfig { +//! allowed_vars: None, +//! denied_vars: None, +//! allow_list_all: false, +//! allowed_prefixes: None, +//! }; +//! let handler = EnvironmentHandler::new(config, None); +//! ``` + +use anyhow::Result; +use chrono::Utc; +use std::env; +use std::future::Future; +use std::pin::Pin; +use thiserror::Error; +use tracing::info; +use wasmtime::StoreContextMut; + +use theater::actor::handle::ActorHandle; +use theater::actor::store::ActorStore; +use theater::config::actor_manifest::EnvironmentHandlerConfig; +use theater::config::enforcement::PermissionChecker; +use theater::config::permissions::EnvironmentPermissions; +use theater::events::environment::EnvironmentEventData; +use theater::events::{ChainEventData, EventData}; +use theater::handler::Handler; +use theater::shutdown::ShutdownReceiver; +use theater::wasm::{ActorComponent, ActorInstance}; + +/// Error types for environment operations +#[derive(Error, Debug)] +pub enum EnvironmentError { + #[error("Access denied for environment variable: {0}")] + AccessDenied(String), + + #[error("Environment variable not found: {0}")] + VariableNotFound(String), + + #[error("Invalid variable name: {0}")] + InvalidVariableName(String), +} + +/// Host for providing environment variable access to WebAssembly actors +#[derive(Clone)] +pub struct EnvironmentHandler { + config: EnvironmentHandlerConfig, + permissions: Option, +} + +impl EnvironmentHandler { + /// Create a new environment handler + /// + /// # Arguments + /// + /// * `config` - Configuration for the environment handler + /// * `permissions` - Optional permissions controlling variable access + pub fn new(config: EnvironmentHandlerConfig, permissions: Option) -> Self { + Self { + config, + permissions, + } + } +} + +impl Handler for EnvironmentHandler { + fn create_instance(&self) -> Box { + Box::new(self.clone()) + } + + fn start( + &mut self, + _actor_handle: ActorHandle, + shutdown_receiver: ShutdownReceiver, + ) -> Pin> + Send>> { + info!("Starting Environment handler (read-only)"); + + Box::pin(async move { + // Environment handler doesn't need a background task, but we should wait for shutdown + shutdown_receiver.wait_for_shutdown().await; + info!("Environment handler received shutdown signal"); + info!("Environment handler shut down"); + Ok(()) + }) + } + + fn setup_host_functions( + &mut self, + actor_component: &mut ActorComponent, + ) -> Result<()> { + // Clone what we need for the closures + let permissions_get = self.permissions.clone(); + let permissions_exists = self.permissions.clone(); + let config_list = self.config.clone(); + + // Record setup start + actor_component.actor_store.record_event(ChainEventData { + event_type: "environment-setup".to_string(), + data: EventData::Environment(EnvironmentEventData::HandlerSetupStart), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Starting environment host function setup".to_string()), + }); + + info!("Setting up environment host functions (read-only)"); + + let mut interface = match actor_component + .linker + .instance("theater:simple/environment") + { + Ok(interface) => { + // Record successful linker instance creation + actor_component.actor_store.record_event(ChainEventData { + event_type: "environment-setup".to_string(), + data: EventData::Environment(EnvironmentEventData::LinkerInstanceSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Successfully created linker instance".to_string()), + }); + interface + } + Err(e) => { + // Record the specific error where it happens + actor_component.actor_store.record_event(ChainEventData { + event_type: "environment-setup".to_string(), + data: EventData::Environment(EnvironmentEventData::HandlerSetupError { + error: e.to_string(), + step: "linker_instance".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to create linker instance: {}", e)), + }); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/environment: {}", + e + )); + } + }; + + // get-var implementation + interface.func_wrap( + "get-var", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (var_name,): (String,)| + -> Result<(Option,)> { + let now = Utc::now().timestamp_millis() as u64; + + // PERMISSION CHECK BEFORE OPERATION + if let Err(e) = PermissionChecker::check_env_var_access(&permissions_get, &var_name) { + // Record permission denied event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/environment/permission-denied".to_string(), + data: EventData::Environment(EnvironmentEventData::PermissionDenied { + operation: "get-var".to_string(), + variable_name: var_name.clone(), + reason: e.to_string(), + }), + timestamp: now, + description: Some(format!( + "Permission denied for environment variable access: {}", + e + )), + }); + return Ok((None,)); + } + + let value = env::var(&var_name).ok(); + let value_found = value.is_some(); + + // Record the access attempt + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/environment/get-var".to_string(), + data: EventData::Environment(EnvironmentEventData::GetVar { + variable_name: var_name.clone(), + success: true, + value_found, + timestamp: chrono::Utc::now(), + }), + timestamp: now, + description: Some(format!( + "Environment variable access: {} (found: {})", + var_name, value_found + )), + }); + + Ok((value,)) + }, + )?; + + // exists implementation + interface.func_wrap( + "exists", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (var_name,): (String,)| + -> Result<(bool,)> { + let now = Utc::now().timestamp_millis() as u64; + + // PERMISSION CHECK BEFORE OPERATION + if let Err(e) = + PermissionChecker::check_env_var_access(&permissions_exists, &var_name) + { + // Record permission denied event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/environment/permission-denied".to_string(), + data: EventData::Environment(EnvironmentEventData::PermissionDenied { + operation: "exists".to_string(), + variable_name: var_name.clone(), + reason: e.to_string(), + }), + timestamp: now, + description: Some(format!( + "Permission denied for environment variable exists check: {}", + e + )), + }); + return Ok((false,)); + } + + let exists = env::var(&var_name).is_ok(); + + // Record the check + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/environment/exists".to_string(), + data: EventData::Environment(EnvironmentEventData::GetVar { + variable_name: var_name.clone(), + success: true, + value_found: exists, + timestamp: chrono::Utc::now(), + }), + timestamp: now, + description: Some(format!( + "Environment variable exists check: {} (exists: {})", + var_name, exists + )), + }); + + Ok((exists,)) + }, + )?; + + // list-vars implementation + interface.func_wrap( + "list-vars", + move |mut ctx: StoreContextMut<'_, ActorStore>, + ()| + -> Result<(Vec<(String, String)>,)> { + let now = Utc::now().timestamp_millis() as u64; + + if !config_list.allow_list_all { + // Record denied list attempt + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/environment/list-vars".to_string(), + data: EventData::Environment(EnvironmentEventData::PermissionDenied { + operation: "list-vars".to_string(), + variable_name: "(list-all disabled)".to_string(), + reason: "allow_list_all is false".to_string(), + }), + timestamp: now, + description: Some( + "Environment variable listing denied - allow_list_all is false" + .to_string(), + ), + }); + return Ok((Vec::new(),)); + } + + let mut accessible_vars = Vec::new(); + + for (key, value) in env::vars() { + if config_list.is_variable_allowed(&key) { + accessible_vars.push((key, value)); + } + } + + // Record the list operation + let count = accessible_vars.len(); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/environment/list-vars".to_string(), + data: EventData::Environment(EnvironmentEventData::GetVar { + variable_name: format!("(returned {} variables)", count), + success: true, + value_found: count > 0, + timestamp: chrono::Utc::now(), + }), + timestamp: now, + description: Some(format!( + "Environment variable listing returned {} accessible variables", + count + )), + }); + + Ok((accessible_vars,)) + }, + )?; + + // Record overall setup completion + actor_component.actor_store.record_event(ChainEventData { + event_type: "environment-setup".to_string(), + data: EventData::Environment(EnvironmentEventData::HandlerSetupSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some( + "Environment host functions setup completed successfully".to_string(), + ), + }); + + Ok(()) + } + + fn add_export_functions(&self, _actor_instance: &mut ActorInstance) -> Result<()> { + // Environment handler (read-only) doesn't need export functions + Ok(()) + } + + fn name(&self) -> &str { + "environment" + } + + fn imports(&self) -> Option { + Some("theater:simple/environment".to_string()) + } + + fn exports(&self) -> Option { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_handler_creation() { + let config = EnvironmentHandlerConfig { + allowed_vars: None, + denied_vars: None, + allow_list_all: false, + allowed_prefixes: None, + }; + let handler = EnvironmentHandler::new(config, None); + assert_eq!(handler.name(), "environment"); + assert_eq!(handler.imports(), Some("theater:simple/environment".to_string())); + assert_eq!(handler.exports(), None); + } + + #[test] + fn test_handler_clone() { + let config = EnvironmentHandlerConfig { + allowed_vars: None, + denied_vars: None, + allow_list_all: true, + allowed_prefixes: None, + }; + let handler = EnvironmentHandler::new(config, None); + let cloned = handler.clone(); + assert_eq!(cloned.config.allow_list_all, true); + } +} diff --git a/crates/theater-handler-filesystem/Cargo.toml b/crates/theater-handler-filesystem/Cargo.toml new file mode 100644 index 00000000..14522598 --- /dev/null +++ b/crates/theater-handler-filesystem/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "theater-handler-filesystem" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +rust-version.workspace = true + +[dependencies] +# Core theater dependencies +theater = { path = "../theater" } + +# WebAssembly runtime +wasmtime = { version = "31.0", features = ["component-model", "async"] } + +# Async runtime +tokio = { version = "1.0", features = ["full"] } + +# Error handling +anyhow = "1.0" +thiserror = "1.0" + +# Logging +tracing = "0.1" + +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +# Path canonicalization +dunce = "1.0" + +# Random number generation (for temp dir) +rand = "0.8" + +# Time handling +chrono = "0.4" + +[dev-dependencies] +test-log = "0.2" +pretty_assertions = "1.4" +tempfile = "3.8.1" diff --git a/crates/theater-handler-filesystem/README.md b/crates/theater-handler-filesystem/README.md new file mode 100644 index 00000000..1851f760 --- /dev/null +++ b/crates/theater-handler-filesystem/README.md @@ -0,0 +1,166 @@ +# Theater Filesystem Handler + +Provides filesystem access capabilities to WebAssembly actors in the Theater system. + +## Features + +This handler allows actors to: +- **Read files** - Read file contents as bytes +- **Write files** - Write string contents to files +- **List files** - List files in a directory +- **Delete files** - Remove individual files +- **Create directories** - Create new directories +- **Delete directories** - Remove directories and their contents +- **Check path existence** - Verify if a path exists +- **Execute commands** - Execute shell commands (with restrictions) +- **Execute nix commands** - Run nix development commands + +All operations support permission-based access control with allowed/denied paths. + +## Usage + +Add this handler when creating your Theater runtime: + +```rust +use theater_handler_filesystem::FilesystemHandler; +use theater::config::actor_manifest::FileSystemHandlerConfig; +use theater::config::permissions::FileSystemPermissions; +use std::path::PathBuf; + +// Create handler configuration +let config = FileSystemHandlerConfig { + path: Some(PathBuf::from("/workspace")), + new_dir: Some(false), // Set to true to create a temporary directory + allowed_commands: Some(vec!["nix".to_string()]), // Whitelist commands +}; + +// Optional: Configure permissions +let permissions = Some(FileSystemPermissions { + allowed_paths: Some(vec!["/workspace".to_string()]), + ..Default::default() +}); + +// Create the handler +let filesystem_handler = FilesystemHandler::new(config, permissions); + +// Register with your handler registry +registry.register(filesystem_handler); +``` + +## WIT Interface + +This handler implements the `theater:simple/filesystem` interface: + +```wit +interface filesystem { + // Read file contents + read-file: func(path: string) -> result, string> + + // Write file contents + write-file: func(path: string, contents: string) -> result<_, string> + + // List files in directory + list-files: func(path: string) -> result, string> + + // Delete a file + delete-file: func(path: string) -> result<_, string> + + // Create a directory + create-dir: func(path: string) -> result<_, string> + + // Delete a directory + delete-dir: func(path: string) -> result<_, string> + + // Check if path exists + path-exists: func(path: string) -> result + + // Execute command + execute-command: func(dir: string, command: string, args: list) + -> result + + // Execute nix development command + execute-nix-command: func(dir: string, command: string) + -> result +} +``` + +## Configuration + +### FileSystemHandlerConfig +- `path`: Optional base path for filesystem operations +- `new_dir`: If true, creates a random temporary directory under `/tmp/theater` +- `allowed_commands`: Optional whitelist of commands allowed for execution + +### FileSystemPermissions +- `allowed_paths`: List of paths that actors are allowed to access +- Paths are validated and canonicalized to prevent directory traversal +- Both creation and access operations are checked against permissions + +## Permission Validation + +The handler includes comprehensive path validation: + +1. **Creation operations** (write, create-dir): + - Validates the parent directory exists and is allowed + - Constructs the final path from validated parent + filename + +2. **Access operations** (read, list, delete, path-exists): + - Validates the target path exists and is allowed + - Returns the canonicalized path + +3. **Path canonicalization**: + - Uses `dunce` library for robust Windows/Unix path handling + - Resolves `.`, `..`, symlinks, etc. + - Prevents directory traversal attacks + +## Command Execution + +Command execution is heavily restricted for security: + +- Only `nix` commands are allowed +- Command arguments are validated against a whitelist +- Directory must be within allowed paths +- All execution is logged to the actor's chain + +Currently allowed commands: +- `nix develop --command bash -c "cargo component build --target wasm32-unknown-unknown --release"` +- `nix flake init` + +## Events + +The handler records detailed events to the actor's chain: +- `filesystem-setup` - Handler initialization +- `theater:simple/filesystem/*` - All filesystem operations +- `permission-denied` - Permission violations with detailed reasons +- Command execution and results + +## Architecture + +The handler is split into logical modules for maintainability: + +- `lib.rs` - Main handler implementation and Handler trait +- `types.rs` - Type definitions and error types +- `path_validation.rs` - Path resolution and permission checking +- `operations/basic_ops.rs` - File and directory operations +- `operations/commands.rs` - Command execution functionality + +## Example + +```rust +// Inside a WASM actor +use theater_simple::filesystem; + +// Read a file +let contents = filesystem::read_file("data.txt")?; + +// Write a file +filesystem::write_file("output.txt", "Hello, world!")?; + +// List directory contents +let files = filesystem::list_files(".")?; + +// Check if path exists +if filesystem::path_exists("config.json")? { + // File exists +} +``` diff --git a/crates/theater-handler-filesystem/src/command_execution.rs b/crates/theater-handler-filesystem/src/command_execution.rs new file mode 100644 index 00000000..41196d3b --- /dev/null +++ b/crates/theater-handler-filesystem/src/command_execution.rs @@ -0,0 +1,74 @@ +//! Command execution functionality for filesystem handler + +use std::path::{Path, PathBuf}; +use tokio::process::Command as AsyncCommand; +use tracing::info; + +use theater::events::filesystem::{CommandError, CommandResult, CommandSuccess}; + +/// Execute a command in a directory with the given arguments +pub async fn execute_command( + allowed_path: PathBuf, + dir: &Path, + cmd: &str, + args: &[&str], +) -> anyhow::Result { + // Validate that the directory is within our allowed path + if !dir.starts_with(&allowed_path) { + return Ok(CommandResult::Error(CommandError { + message: "Directory not within allowed path".to_string(), + })); + } + + if cmd != "nix" { + return Ok(CommandResult::Error(CommandError { + message: "Command not allowed".to_string(), + })); + } + + if args + != &[ + "develop", + "--command", + "bash", + "-c", + "cargo component build --target wasm32-unknown-unknown --release", + ] + && args != &["flake", "init"] + { + info!("Args not allowed"); + info!("{:?}", args); + return Ok(CommandResult::Error(CommandError { + message: "Args not allowed".to_string(), + })); + } + + info!("Executing command: {} {:?}", cmd, args); + + // Execute the command + let output = AsyncCommand::new(cmd) + .current_dir(dir) + .args(args) + .output() + .await?; + + info!("Command executed"); + info!("stdout: {}", String::from_utf8_lossy(&output.stdout)); + info!("stderr: {}", String::from_utf8_lossy(&output.stderr)); + info!("exit code: {}", output.status.code().unwrap()); + + Ok(CommandResult::Success(CommandSuccess { + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + exit_code: output.status.code().unwrap_or(-1), + })) +} + +/// Execute a nix development command +pub async fn execute_nix_command( + allowed_path: PathBuf, + dir: &Path, + command: &str, +) -> anyhow::Result { + execute_command(allowed_path, dir, "nix", &["develop", "--command", command]).await +} diff --git a/crates/theater-handler-filesystem/src/lib.rs b/crates/theater-handler-filesystem/src/lib.rs new file mode 100644 index 00000000..23b01103 --- /dev/null +++ b/crates/theater-handler-filesystem/src/lib.rs @@ -0,0 +1,169 @@ +//! # Filesystem Handler +//! +//! Provides filesystem access capabilities to WebAssembly actors in the Theater system. +//! This handler allows actors to read, write, list, delete files and directories with +//! permission-based access control. + +mod path_validation; +mod operations; +mod command_execution; +mod types; + +use std::future::Future; +use std::path::PathBuf; +use std::pin::Pin; +use tracing::info; + +use theater::actor::handle::ActorHandle; +use theater::config::actor_manifest::FileSystemHandlerConfig; +use theater::config::permissions::FileSystemPermissions; +use theater::handler::Handler; +use theater::shutdown::ShutdownReceiver; +use theater::wasm::{ActorComponent, ActorInstance}; + +pub use types::FileSystemError; + +/// Handler for providing filesystem access to WebAssembly actors +#[derive(Clone)] +pub struct FilesystemHandler { + path: PathBuf, + allowed_commands: Option>, + permissions: Option, +} + +impl FilesystemHandler { + pub fn new( + config: FileSystemHandlerConfig, + permissions: Option, + ) -> Self { + let path: PathBuf = match config.new_dir { + Some(true) => Self::create_temp_dir().expect("Failed to create temp directory"), + _ => PathBuf::from(config.path.clone().expect("Path must be provided")), + }; + + info!( + "Creating filesystem handler with path: {:?}, permissions: {:?}", + path, permissions + ); + + Self { + path, + allowed_commands: config.allowed_commands, + permissions, + } + } + + fn create_temp_dir() -> anyhow::Result { + use rand::Rng; + let mut rng = rand::thread_rng(); + let random_num: u32 = rng.gen(); + + let temp_base = PathBuf::from("/tmp/theater"); + std::fs::create_dir_all(&temp_base)?; + + let temp_dir = temp_base.join(random_num.to_string()); + std::fs::create_dir(&temp_dir)?; + + Ok(temp_dir) + } + + pub fn path(&self) -> &PathBuf { + &self.path + } +} + +impl Handler for FilesystemHandler { + fn create_instance(&self) -> Box { + Box::new(self.clone()) + } + + fn start( + &mut self, + _actor_handle: ActorHandle, + shutdown_receiver: ShutdownReceiver, + ) -> Pin> + Send>> { + info!("Starting filesystem handler on path {:?}", self.path); + + Box::pin(async move { + shutdown_receiver.wait_for_shutdown().await; + info!("Filesystem handler received shutdown signal"); + Ok(()) + }) + } + + fn setup_host_functions( + &mut self, + actor_component: &mut ActorComponent, + ) -> anyhow::Result<()> { + operations::setup_host_functions(self, actor_component) + } + + fn add_export_functions( + &self, + _actor_instance: &mut ActorInstance, + ) -> anyhow::Result<()> { + info!("No export functions needed for filesystem handler"); + Ok(()) + } + + fn name(&self) -> &str { + "filesystem" + } + + fn imports(&self) -> Option { + Some("theater:simple/filesystem".to_string()) + } + + fn exports(&self) -> Option { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use theater::config::actor_manifest::FileSystemHandlerConfig; + + #[test] + fn test_handler_creation() { + let config = FileSystemHandlerConfig { + path: Some(std::path::PathBuf::from("/tmp")), + new_dir: Some(false), + allowed_commands: None, + }; + + let handler = FilesystemHandler::new(config, None); + assert_eq!(handler.name(), "filesystem"); + assert_eq!( + handler.imports(), + Some("theater:simple/filesystem".to_string()) + ); + assert_eq!(handler.exports(), None); + } + + #[test] + fn test_handler_clone() { + let config = FileSystemHandlerConfig { + path: Some(std::path::PathBuf::from("/tmp")), + new_dir: Some(false), + allowed_commands: None, + }; + + let handler = FilesystemHandler::new(config, None); + let cloned = handler.clone(); + assert_eq!(handler.path(), cloned.path()); + } + + #[test] + fn test_temp_dir_creation() { + let config = FileSystemHandlerConfig { + path: None, + new_dir: Some(true), + allowed_commands: None, + }; + + let handler = FilesystemHandler::new(config, None); + assert!(handler.path().exists()); + assert!(handler.path().starts_with("/tmp/theater")); + } +} diff --git a/crates/theater-handler-filesystem/src/operations.rs b/crates/theater-handler-filesystem/src/operations.rs new file mode 100644 index 00000000..36f35243 --- /dev/null +++ b/crates/theater-handler-filesystem/src/operations.rs @@ -0,0 +1,79 @@ +//! Filesystem operations implementation + +mod basic_ops; +mod commands; + +pub use basic_ops::*; +pub use commands::*; + +use tracing::info; + +use theater::events::filesystem::FilesystemEventData; +use theater::events::{ChainEventData, EventData}; +use theater::wasm::ActorComponent; + +use crate::FilesystemHandler; + +/// Setup all filesystem host functions +pub fn setup_host_functions( + handler: &FilesystemHandler, + actor_component: &mut ActorComponent, +) -> anyhow::Result<()> { + info!("Setting up filesystem host functions"); + + // Record setup start + actor_component.actor_store.record_event(ChainEventData { + event_type: "filesystem-setup".to_string(), + data: EventData::Filesystem(FilesystemEventData::HandlerSetupStart), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Starting filesystem host function setup".to_string()), + }); + + let mut interface = match actor_component.linker.instance("theater:simple/filesystem") { + Ok(interface) => { + actor_component.actor_store.record_event(ChainEventData { + event_type: "filesystem-setup".to_string(), + data: EventData::Filesystem(FilesystemEventData::LinkerInstanceSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Successfully created linker instance".to_string()), + }); + interface + } + Err(e) => { + actor_component.actor_store.record_event(ChainEventData { + event_type: "filesystem-setup".to_string(), + data: EventData::Filesystem(FilesystemEventData::HandlerSetupError { + error: e.to_string(), + step: "linker_instance".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to create linker instance: {}", e)), + }); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/filesystem: {}", + e + )); + } + }; + + // Setup all the functions + setup_read_file(handler, &mut interface)?; + setup_write_file(handler, &mut interface)?; + setup_list_files(handler, &mut interface)?; + setup_delete_file(handler, &mut interface)?; + setup_create_dir(handler, &mut interface)?; + setup_delete_dir(handler, &mut interface)?; + setup_path_exists(handler, &mut interface)?; + setup_execute_command(handler, &mut interface)?; + setup_execute_nix_command(handler, &mut interface)?; + + // Record overall setup completion + actor_component.actor_store.record_event(ChainEventData { + event_type: "filesystem-setup".to_string(), + data: EventData::Filesystem(FilesystemEventData::HandlerSetupSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Filesystem host functions setup completed successfully".to_string()), + }); + + Ok(()) +} diff --git a/crates/theater-handler-filesystem/src/operations/basic_ops.rs b/crates/theater-handler-filesystem/src/operations/basic_ops.rs new file mode 100644 index 00000000..43bbb9a4 --- /dev/null +++ b/crates/theater-handler-filesystem/src/operations/basic_ops.rs @@ -0,0 +1,690 @@ +//! Basic filesystem operations (read, write, list, delete, create, path-exists) + +use std::fs::File; +use std::io::{BufReader, Read, Write}; +use tracing::{error, info}; +use wasmtime::component::LinkerInstance; +use wasmtime::StoreContextMut; + +use theater::actor::store::ActorStore; +use theater::events::filesystem::FilesystemEventData; +use theater::events::{ChainEventData, EventData}; + +use crate::path_validation::resolve_and_validate_path; +use crate::FilesystemHandler; + +pub fn setup_read_file( + handler: &FilesystemHandler, + interface: &mut LinkerInstance, +) -> anyhow::Result<()> { + let filesystem_handler = handler.clone(); + let permissions = handler.permissions.clone(); + + interface + .func_wrap( + "read-file", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (requested_path,): (String,)| + -> anyhow::Result<(Result, String>,)> { + let file_path = match resolve_and_validate_path( + filesystem_handler.path(), + &requested_path, + "read", + &permissions, + ) { + Ok(path) => path, + Err(e) => { + error!("Filesystem read permission denied: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/permission-denied".to_string(), + data: EventData::Filesystem(FilesystemEventData::PermissionDenied { + operation: "read".to_string(), + path: requested_path.clone(), + reason: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Permission denied for read operation on {}", + requested_path + )), + }); + return Ok((Err(format!("Permission denied: {}", e)),)); + } + }; + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/read-file".to_string(), + data: EventData::Filesystem(FilesystemEventData::FileReadCall { + path: requested_path.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Read file {:?}", requested_path)), + }); + + info!("Reading file {:?}", file_path); + + let file = match File::open(&file_path) { + Ok(f) => f, + Err(e) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/read-file".to_string(), + data: EventData::Filesystem(FilesystemEventData::Error { + operation: "open".to_string(), + path: file_path.to_string_lossy().to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Error opening file {:?}", file_path)), + }); + return Ok((Err(e.to_string()),)); + } + }; + + let mut reader = BufReader::new(file); + let mut contents = Vec::new(); + if let Err(e) = reader.read_to_end(&mut contents) { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/read-file".to_string(), + data: EventData::Filesystem(FilesystemEventData::Error { + operation: "read".to_string(), + path: file_path.to_string_lossy().to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Error reading file {:?}", file_path)), + }); + return Ok((Err(e.to_string()),)); + } + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/read-file".to_string(), + data: EventData::Filesystem(FilesystemEventData::FileReadResult { + contents: contents.clone(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully read {} bytes from file {:?}", + contents.len(), + file_path + )), + }); + + info!("File read successfully"); + Ok((Ok(contents),)) + }, + ) + .map_err(|e| anyhow::anyhow!("Failed to wrap read-file function: {}", e))?; + + Ok(()) +} + +pub fn setup_write_file( + handler: &FilesystemHandler, + interface: &mut LinkerInstance, +) -> anyhow::Result<()> { + let filesystem_handler = handler.clone(); + let permissions = handler.permissions.clone(); + + interface + .func_wrap( + "write-file", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (requested_path, contents): (String, String)| + -> anyhow::Result<(Result<(), String>,)> { + let file_path = match resolve_and_validate_path( + filesystem_handler.path(), + &requested_path, + "write", + &permissions, + ) { + Ok(path) => path, + Err(e) => { + error!("Filesystem write permission denied: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/permission-denied".to_string(), + data: EventData::Filesystem(FilesystemEventData::PermissionDenied { + operation: "write".to_string(), + path: requested_path.clone(), + reason: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Permission denied for write operation on {}", + requested_path + )), + }); + return Ok((Err(format!("Permission denied: {}", e)),)); + } + }; + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/write-file".to_string(), + data: EventData::Filesystem(FilesystemEventData::FileWriteCall { + path: requested_path.clone(), + contents: contents.clone().into(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Writing {} bytes to file {:?}", + contents.len(), + requested_path + )), + }); + + info!("Writing file {:?}", file_path); + + match File::create(&file_path) { + Ok(mut file) => match file.write_all(contents.as_bytes()) { + Ok(_) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/write-file".to_string(), + data: EventData::Filesystem(FilesystemEventData::FileWriteResult { + path: file_path.to_string_lossy().to_string(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully wrote {} bytes to file {:?}", + contents.len(), + file_path + )), + }); + + info!("File written successfully"); + Ok((Ok(()),)) + } + Err(e) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/write-file".to_string(), + data: EventData::Filesystem(FilesystemEventData::Error { + operation: "write".to_string(), + path: file_path.to_string_lossy().to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error writing to file {:?}: {}", + file_path, e + )), + }); + + Ok((Err(e.to_string()),)) + } + }, + Err(e) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/write-file".to_string(), + data: EventData::Filesystem(FilesystemEventData::Error { + operation: "create".to_string(), + path: file_path.to_string_lossy().to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error creating file {:?}: {}", + file_path, e + )), + }); + + Ok((Err(e.to_string()),)) + } + } + }, + ) + .map_err(|e| anyhow::anyhow!("Failed to wrap write-file function: {}", e))?; + + Ok(()) +} + +pub fn setup_list_files( + handler: &FilesystemHandler, + interface: &mut LinkerInstance, +) -> anyhow::Result<()> { + let filesystem_handler = handler.clone(); + let permissions = handler.permissions.clone(); + + interface + .func_wrap( + "list-files", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (requested_path,): (String,)| + -> anyhow::Result<(Result, String>,)> { + let dir_path = match resolve_and_validate_path( + filesystem_handler.path(), + &requested_path, + "read", + &permissions, + ) { + Ok(path) => path, + Err(e) => { + error!("Filesystem list permission denied: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/permission-denied".to_string(), + data: EventData::Filesystem(FilesystemEventData::PermissionDenied { + operation: "list".to_string(), + path: requested_path.clone(), + reason: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Permission denied for list operation on {}", + requested_path + )), + }); + return Ok((Err(format!("Permission denied: {}", e)),)); + } + }; + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/list-files".to_string(), + data: EventData::Filesystem(FilesystemEventData::DirectoryListedCall { + path: requested_path.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Listing files in directory {:?}", requested_path)), + }); + + info!("Listing files in {:?}", dir_path); + + match dir_path.read_dir() { + Ok(entries) => { + let files: Vec = entries + .filter_map(|entry| { + entry.ok().and_then(|e| e.file_name().into_string().ok()) + }) + .collect(); + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/list-files".to_string(), + data: EventData::Filesystem(FilesystemEventData::DirectoryListResult { + entries: files.clone(), + path: dir_path.to_string_lossy().to_string(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully listed {} files in directory {:?}", + files.len(), + dir_path + )), + }); + + info!("Files listed successfully"); + Ok((Ok(files),)) + } + Err(e) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/list-files".to_string(), + data: EventData::Filesystem(FilesystemEventData::Error { + operation: "list".to_string(), + path: dir_path.to_string_lossy().to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error listing files in directory {:?}: {}", + dir_path, e + )), + }); + + Ok((Err(e.to_string()),)) + } + } + }, + ) + .map_err(|e| anyhow::anyhow!("Failed to wrap list-files function: {}", e))?; + + Ok(()) +} + +pub fn setup_delete_file( + handler: &FilesystemHandler, + interface: &mut LinkerInstance, +) -> anyhow::Result<()> { + let filesystem_handler = handler.clone(); + let permissions = handler.permissions.clone(); + + interface + .func_wrap( + "delete-file", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (requested_path,): (String,)| + -> anyhow::Result<(Result<(), String>,)> { + let file_path = match resolve_and_validate_path( + filesystem_handler.path(), + &requested_path, + "delete", + &permissions, + ) { + Ok(path) => path, + Err(e) => { + error!("Filesystem delete permission denied: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/permission-denied".to_string(), + data: EventData::Filesystem(FilesystemEventData::PermissionDenied { + operation: "delete".to_string(), + path: requested_path.clone(), + reason: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Permission denied for delete operation on {}", + requested_path + )), + }); + return Ok((Err(format!("Permission denied: {}", e)),)); + } + }; + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/delete-file".to_string(), + data: EventData::Filesystem(FilesystemEventData::FileDeleteCall { + path: requested_path.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Deleting file {:?}", requested_path)), + }); + + info!("Deleting file {:?}", file_path); + + match std::fs::remove_file(&file_path) { + Ok(_) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/delete-file".to_string(), + data: EventData::Filesystem(FilesystemEventData::FileDeleteResult { + path: file_path.to_string_lossy().to_string(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Successfully deleted file {:?}", file_path)), + }); + + info!("File deleted successfully"); + Ok((Ok(()),)) + } + Err(e) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/delete-file".to_string(), + data: EventData::Filesystem(FilesystemEventData::Error { + operation: "delete".to_string(), + path: file_path.to_string_lossy().to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error deleting file {:?}: {}", + file_path, e + )), + }); + + Ok((Err(e.to_string()),)) + } + } + }, + ) + .map_err(|e| anyhow::anyhow!("Failed to wrap delete-file function: {}", e))?; + + Ok(()) +} + +pub fn setup_create_dir( + handler: &FilesystemHandler, + interface: &mut LinkerInstance, +) -> anyhow::Result<()> { + let filesystem_handler = handler.clone(); + let permissions = handler.permissions.clone(); + + interface + .func_wrap( + "create-dir", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (requested_path,): (String,)| + -> anyhow::Result<(Result<(), String>,)> { + let dir_path = match resolve_and_validate_path( + filesystem_handler.path(), + &requested_path, + "write", + &permissions, + ) { + Ok(path) => path, + Err(e) => { + error!("Filesystem create directory permission denied: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/permission-denied".to_string(), + data: EventData::Filesystem(FilesystemEventData::PermissionDenied { + operation: "create-dir".to_string(), + path: requested_path.clone(), + reason: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Permission denied for create-dir operation on {}", + requested_path + )), + }); + return Ok((Err(format!("Permission denied: {}", e)),)); + } + }; + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/create-dir".to_string(), + data: EventData::Filesystem(FilesystemEventData::DirectoryCreatedCall { + path: requested_path.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Creating directory {:?}", requested_path)), + }); + + info!("Creating directory {:?}", dir_path); + + match std::fs::create_dir(&dir_path) { + Ok(_) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/create-dir".to_string(), + data: EventData::Filesystem( + FilesystemEventData::DirectoryCreatedResult { + success: true, + path: dir_path.to_string_lossy().to_string(), + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully created directory {:?}", + dir_path + )), + }); + + info!("Directory created successfully"); + Ok((Ok(()),)) + } + Err(e) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/create-dir".to_string(), + data: EventData::Filesystem(FilesystemEventData::Error { + operation: "create_dir".to_string(), + path: dir_path.to_string_lossy().to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error creating directory {:?}: {}", + dir_path, e + )), + }); + + Ok((Err(e.to_string()),)) + } + } + }, + ) + .map_err(|e| anyhow::anyhow!("Failed to wrap create-dir function: {}", e))?; + + Ok(()) +} + +pub fn setup_delete_dir( + handler: &FilesystemHandler, + interface: &mut LinkerInstance, +) -> anyhow::Result<()> { + let filesystem_handler = handler.clone(); + let permissions = handler.permissions.clone(); + + interface + .func_wrap( + "delete-dir", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (requested_path,): (String,)| + -> anyhow::Result<(Result<(), String>,)> { + let dir_path = match resolve_and_validate_path( + filesystem_handler.path(), + &requested_path, + "delete", + &permissions, + ) { + Ok(path) => path, + Err(e) => { + error!("Filesystem delete directory permission denied: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/permission-denied".to_string(), + data: EventData::Filesystem(FilesystemEventData::PermissionDenied { + operation: "delete-dir".to_string(), + path: requested_path.clone(), + reason: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Permission denied for delete-dir operation on {}", + requested_path + )), + }); + return Ok((Err(format!("Permission denied: {}", e)),)); + } + }; + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/delete-dir".to_string(), + data: EventData::Filesystem(FilesystemEventData::DirectoryDeletedCall { + path: requested_path.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Deleting directory {:?}", requested_path)), + }); + + info!("Deleting directory {:?}", dir_path); + + match std::fs::remove_dir_all(&dir_path) { + Ok(_) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/delete-dir".to_string(), + data: EventData::Filesystem( + FilesystemEventData::DirectoryDeletedResult { + success: true, + path: dir_path.to_string_lossy().to_string(), + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully deleted directory {:?}", + dir_path + )), + }); + + info!("Directory deleted successfully"); + Ok((Ok(()),)) + } + Err(e) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/delete-dir".to_string(), + data: EventData::Filesystem(FilesystemEventData::Error { + operation: "delete_dir".to_string(), + path: dir_path.to_string_lossy().to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error deleting directory {:?}: {}", + dir_path, e + )), + }); + + Ok((Err(e.to_string()),)) + } + } + }, + ) + .map_err(|e| anyhow::anyhow!("Failed to wrap delete-dir function: {}", e))?; + + Ok(()) +} + +pub fn setup_path_exists( + handler: &FilesystemHandler, + interface: &mut LinkerInstance, +) -> anyhow::Result<()> { + let filesystem_handler = handler.clone(); + let permissions = handler.permissions.clone(); + + interface + .func_wrap( + "path-exists", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (requested_path,): (String,)| + -> anyhow::Result<(Result,)> { + let path = match resolve_and_validate_path( + filesystem_handler.path(), + &requested_path, + "read", + &permissions, + ) { + Ok(path) => path, + Err(e) => { + error!("Filesystem path-exists permission denied: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/permission-denied".to_string(), + data: EventData::Filesystem(FilesystemEventData::PermissionDenied { + operation: "path-exists".to_string(), + path: requested_path.clone(), + reason: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Permission denied for path-exists operation on {}", + requested_path + )), + }); + return Ok((Err(format!("Permission denied: {}", e)),)); + } + }; + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/path-exists".to_string(), + data: EventData::Filesystem(FilesystemEventData::PathExistsCall { + path: requested_path.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Checking if path {:?} exists", requested_path)), + }); + + info!("Checking if path {:?} exists", path); + + let exists = path.exists(); + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/path-exists".to_string(), + data: EventData::Filesystem(FilesystemEventData::PathExistsResult { + path: path.to_string_lossy().to_string(), + exists, + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Path {:?} exists: {}", path, exists)), + }); + + Ok((Ok(exists),)) + }, + ) + .map_err(|e| anyhow::anyhow!("Failed to wrap path-exists function: {}", e))?; + + Ok(()) +} diff --git a/crates/theater-handler-filesystem/src/operations/commands.rs b/crates/theater-handler-filesystem/src/operations/commands.rs new file mode 100644 index 00000000..4b862e47 --- /dev/null +++ b/crates/theater-handler-filesystem/src/operations/commands.rs @@ -0,0 +1,170 @@ +//! Filesystem operations implementation (part 2 - command execution) + +use std::future::Future; +use wasmtime::component::LinkerInstance; +use wasmtime::StoreContextMut; + +use theater::actor::store::ActorStore; +use theater::events::filesystem::{CommandResult, FilesystemEventData}; +use theater::events::{ChainEventData, EventData}; + +use crate::command_execution::{execute_command, execute_nix_command}; +use crate::path_validation::resolve_and_validate_path; +use crate::FilesystemHandler; + +pub fn setup_execute_command( + handler: &FilesystemHandler, + interface: &mut LinkerInstance, +) -> anyhow::Result<()> { + let filesystem_handler = handler.clone(); + let permissions = handler.permissions.clone(); + let allowed_commands = handler.allowed_commands.clone(); + + interface + .func_wrap_async( + "execute-command", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (requested_dir, command, args): (String, String, Vec)| + -> Box,)>> + Send> { + // Validate command if whitelist is configured + if let Some(allowed) = &allowed_commands { + if !allowed.contains(&command) { + return Box::new(async move { + Ok((Err(format!("Command '{}' not in allowed list", command)),)) + }); + } + } + + // RESOLVE AND VALIDATE PATH + let dir_path = match resolve_and_validate_path( + filesystem_handler.path(), + &requested_dir, + "execute", + &permissions, + ) { + Ok(path) => path, + Err(e) => { + return Box::new(async move { + Ok((Err(format!("Permission denied: {}", e)),)) + }); + } + }; + + // Record command execution event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/execute-command".to_string(), + data: EventData::Filesystem(FilesystemEventData::CommandExecuted { + directory: requested_dir.clone(), + command: command.clone(), + args: args.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Executing command '{}' in directory '{}'", + command, requested_dir + )), + }); + + let args_refs: Vec = args.clone(); + let base_path = filesystem_handler.path().clone(); + let command_clone = command.clone(); + + Box::new(async move { + match execute_command( + base_path, + &dir_path, + &command_clone, + &args_refs.iter().map(AsRef::as_ref).collect::>(), + ) + .await + { + Ok(result) => { + // Record successful + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/command-result".to_string(), + data: EventData::Filesystem(FilesystemEventData::CommandCompleted { + result: result.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Command completed".to_string()), + }); + Ok((Ok(result),)) + } + Err(e) => Ok((Err(e.to_string()),)), + } + }) + }, + ) + .map_err(|e| anyhow::anyhow!("Failed to wrap execute-command function: {}", e))?; + + Ok(()) +} + +pub fn setup_execute_nix_command( + handler: &FilesystemHandler, + interface: &mut LinkerInstance, +) -> anyhow::Result<()> { + let filesystem_handler = handler.clone(); + let permissions = handler.permissions.clone(); + + interface + .func_wrap_async( + "execute-nix-command", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (requested_dir, command): (String, String)| + -> Box,)>> + Send> { + // RESOLVE AND VALIDATE PATH + let dir_path = match resolve_and_validate_path( + filesystem_handler.path(), + &requested_dir, + "execute", + &permissions, + ) { + Ok(path) => path, + Err(e) => { + return Box::new(async move { + Ok((Err(format!("Permission denied: {}", e)),)) + }); + } + }; + + // Record nix command execution event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/execute-nix-command".to_string(), + data: EventData::Filesystem(FilesystemEventData::NixCommandExecuted { + directory: requested_dir.clone(), + command: command.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Executing nix command '{}' in directory '{}'", + command, requested_dir + )), + }); + + let base_path = filesystem_handler.path().clone(); + let command_clone = command.clone(); + + Box::new(async move { + match execute_nix_command(base_path, &dir_path, &command_clone).await { + Ok(result) => { + // Record successful execution + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/filesystem/nix-command-result".to_string(), + data: EventData::Filesystem(FilesystemEventData::CommandCompleted { + result: result.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Nix command completed".to_string()), + }); + Ok((Ok(result),)) + } + Err(e) => Ok((Err(e.to_string()),)), + } + }) + }, + ) + .map_err(|e| anyhow::anyhow!("Failed to wrap execute-nix-command function: {}", e))?; + + Ok(()) +} diff --git a/crates/theater-handler-filesystem/src/path_validation.rs b/crates/theater-handler-filesystem/src/path_validation.rs new file mode 100644 index 00000000..8d9bd9bf --- /dev/null +++ b/crates/theater-handler-filesystem/src/path_validation.rs @@ -0,0 +1,110 @@ +//! Path validation and resolution for filesystem operations + +use std::path::{Path, PathBuf}; +use theater::config::permissions::FileSystemPermissions; + +/// Resolve and validate a path against permissions +/// +/// This function: +/// 1. For creation operations (write, create-dir): validates the parent directory exists and is allowed +/// 2. For other operations (read, delete, list, etc.): validates the target path exists and is allowed +/// 3. Resolves the path (handles ., .., etc.) +/// 4. Checks if the resolved path is within allowed paths +/// +/// Returns the resolved path that should be used for the operation +pub fn resolve_and_validate_path( + base_path: &Path, + requested_path: &str, + operation: &str, + permissions: &Option, +) -> Result { + // 1. Append requested path to base path + let full_path = base_path.join(requested_path); + + // 2. Determine if this is a creation operation + let is_creation = matches!(operation, "write" | "create-dir"); + + // 3. For creation operations, validate the parent directory + // For other operations, validate the target path + let path_to_validate = if is_creation { + // For creation, we need to validate the parent directory + full_path.parent().ok_or_else(|| { + "Cannot determine parent directory for creation operation".to_string() + })? + } else { + // For read/delete operations, validate the target path + &full_path + }; + + // 4. Use dunce for robust path canonicalization + let resolved_validation_path = dunce::canonicalize(path_to_validate).map_err(|e| { + if is_creation { + format!( + "Failed to resolve parent directory '{}' for creation operation: {}", + path_to_validate.display(), + e + ) + } else { + format!( + "Failed to resolve path '{}': {}", + path_to_validate.display(), + e + ) + } + })?; + + // 5. Check if resolved path is within allowed paths + if let Some(perms) = permissions { + if let Some(allowed_paths) = &perms.allowed_paths { + let is_allowed = allowed_paths.iter().any(|allowed_path| { + // Canonicalize the allowed path for comparison using dunce + let allowed_canonical = dunce::canonicalize(allowed_path) + .unwrap_or_else(|_| PathBuf::from(allowed_path)); + + // Check if resolved path is within the allowed directory + resolved_validation_path == allowed_canonical + || resolved_validation_path.starts_with(&allowed_canonical) + }); + + if !is_allowed { + return Err(if is_creation { + format!( + "Parent directory '{}' not in allowed paths for creation operation: {:?}", + resolved_validation_path.display(), + allowed_paths + ) + } else { + format!( + "Path '{}' not in allowed paths: {:?}", + resolved_validation_path.display(), + allowed_paths + ) + }); + } + } + } + + // 6. For creation operations, construct the final path from canonicalized parent + filename + // For other operations, return the canonicalized path + if is_creation { + // For creation, we've validated the parent, now construct the target path + // by appending the filename/dirname to the canonicalized parent directory + let final_component = full_path.file_name().ok_or_else(|| { + format!( + "Cannot determine target name for {} operation on path '{}'", + operation, requested_path + ) + })?; + + Ok(resolved_validation_path.join(final_component)) + } else { + // For read/delete, return the canonicalized path + Ok(dunce::canonicalize(&full_path).map_err(|e| { + format!( + "Failed to resolve target path '{}': {}", + full_path.display(), + e + ) + })?) + } +} diff --git a/crates/theater-handler-filesystem/src/types.rs b/crates/theater-handler-filesystem/src/types.rs new file mode 100644 index 00000000..fc86af6b --- /dev/null +++ b/crates/theater-handler-filesystem/src/types.rs @@ -0,0 +1,38 @@ +//! Type definitions for filesystem handler + +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum FileSystemError { + #[error("Path error: {0}")] + PathError(String), + + #[error("IO error: {0}")] + IoError(#[from] std::io::Error), + + #[error("Serialization error: {0}")] + SerializationError(#[from] serde_json::Error), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) enum FileSystemCommand { + ReadFile { path: String }, + WriteFile { path: String, contents: String }, + ListFiles { path: String }, + DeleteFile { path: String }, + CreateDir { path: String }, + DeleteDir { path: String }, + PathExists { path: String }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) enum FileSystemResponse { + ReadFile(Result, String>), + WriteFile(Result<(), String>), + ListFiles(Result, String>), + DeleteFile(Result<(), String>), + CreateDir(Result<(), String>), + DeleteDir(Result<(), String>), + PathExists(Result), +} diff --git a/crates/theater-handler-http-client/Cargo.toml b/crates/theater-handler-http-client/Cargo.toml new file mode 100644 index 00000000..c8aaa94a --- /dev/null +++ b/crates/theater-handler-http-client/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "theater-handler-http-client" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +rust-version.workspace = true +description = "HTTP client handler for Theater WebAssembly actors" + +[dependencies] +theater = { workspace = true } +anyhow.workspace = true +chrono = "0.4" +reqwest = "0.12" +serde = { workspace = true } +serde_json.workspace = true +thiserror = "1.0" +tracing.workspace = true +wasmtime = { version = "31.0", features = ["component-model", "async"] } diff --git a/crates/theater-handler-http-client/README.md b/crates/theater-handler-http-client/README.md new file mode 100644 index 00000000..2a2c02cf --- /dev/null +++ b/crates/theater-handler-http-client/README.md @@ -0,0 +1,162 @@ +# theater-handler-http-client + +HTTP client handler for Theater WebAssembly actors. + +## Overview + +This handler provides HTTP client capabilities for WebAssembly actors running in the Theater runtime. It allows actors to make HTTP requests to external services while maintaining security controls through permission-based access and logging all operations to the actor's chain for debugging and auditing. + +## Features + +- **Full HTTP method support**: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, etc. +- **Headers and body**: Complete control over request headers and body content +- **Permission-based access**: Control which hosts and HTTP methods actors can use +- **Event logging**: All HTTP requests and responses are logged to the chain +- **Error handling**: Graceful handling of network errors and invalid requests + +## Usage + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +theater-handler-http-client = "0.2.1" +``` + +### Basic Example + +```rust +use theater_handler_http_client::HttpClientHandler; +use theater::config::actor_manifest::HttpClientHandlerConfig; + +// Create handler with default config +let config = HttpClientHandlerConfig {}; +let handler = HttpClientHandler::new(config, None); +``` + +### With Permissions + +```rust +use theater_handler_http_client::HttpClientHandler; +use theater::config::actor_manifest::HttpClientHandlerConfig; +use theater::config::permissions::HttpClientPermissions; + +// Create permissions allowing specific hosts and methods +let permissions = HttpClientPermissions { + allowed_hosts: vec!["api.example.com".to_string()], + denied_hosts: vec!["malicious.com".to_string()], + allowed_methods: vec!["GET".to_string(), "POST".to_string()], +}; + +let config = HttpClientHandlerConfig {}; +let handler = HttpClientHandler::new(config, Some(permissions)); +``` + +## WIT Interface + +This handler implements the `theater:simple/http-client` interface: + +```wit +interface http-client { + record http-request { + method: string, + uri: string, + headers: list>, + body: option>, + } + + record http-response { + status: u16, + headers: list>, + body: option>, + } + + // Send an HTTP request and receive a response + send-http: func(request: http-request) -> result; +} +``` + +## Configuration + +### HttpClientHandlerConfig + +Currently has no configuration options. Future versions may add: +- Timeout settings +- Connection pooling options +- TLS configuration + +### HttpClientPermissions + +- `allowed_hosts`: List of specific hosts that actors can access (exact match or wildcard) +- `denied_hosts`: List of hosts that actors cannot access (takes precedence) +- `allowed_methods`: List of HTTP methods that actors can use + +If no permissions are provided, all hosts and methods are accessible. + +## Chain Events + +All HTTP operations are logged as chain events: + +- `HandlerSetupStart`: Handler initialization begins +- `LinkerInstanceSuccess`: WASM linker setup successful +- `HttpClientRequestCall`: HTTP request initiated +- `HttpClientRequestResult`: HTTP response received +- `PermissionDenied`: Access denied due to permissions +- `Error`: Request error occurred +- `HandlerSetupSuccess`: Handler setup completed + +## Security Considerations + +1. **Permission checking**: All requests are checked against configured permissions before execution +2. **Host validation**: URLs are parsed and hosts are validated +3. **Logging**: All requests and responses are logged for auditing +4. **Error handling**: Network errors and invalid requests are handled gracefully +5. **No automatic redirects**: Actors must explicitly handle redirects if needed + +## Example Actor Usage + +From within a WebAssembly actor: + +```rust +// Make a GET request +let request = HttpRequest { + method: "GET".to_string(), + uri: "https://api.example.com/data".to_string(), + headers: vec![ + ("User-Agent".to_string(), "Theater-Actor/1.0".to_string()), + ], + body: None, +}; + +match send_http(request) { + Ok(response) => { + println!("Status: {}", response.status); + if let Some(body) = response.body { + println!("Body: {}", String::from_utf8_lossy(&body)); + } + } + Err(e) => { + println!("Request failed: {}", e); + } +} +``` + +## Migration Notes + +This handler was migrated from the core `theater` crate as part of the handler modularization effort. The migration included: + +- Renamed from `HttpClientHost` to `HttpClientHandler` +- Implemented the `Handler` trait +- Made `setup_host_functions` synchronous (wrapper around async operation) +- Added `Clone` derive for handler reusability +- Improved documentation + +## Dependencies + +- `reqwest` - HTTP client library +- `wasmtime` - WebAssembly runtime with component model support +- `theater` - Core theater types and traits + +## License + +Apache-2.0 diff --git a/crates/theater-handler-http-client/src/lib.rs b/crates/theater-handler-http-client/src/lib.rs new file mode 100644 index 00000000..06608858 --- /dev/null +++ b/crates/theater-handler-http-client/src/lib.rs @@ -0,0 +1,391 @@ +//! # HTTP Client Handler +//! +//! Provides HTTP client capabilities to WebAssembly actors in the Theater system. +//! This handler allows actors to make HTTP requests while maintaining security +//! boundaries and permission controls. +//! +//! ## Features +//! +//! - **Full HTTP method support**: GET, POST, PUT, DELETE, PATCH, etc. +//! - **Headers and body**: Complete control over request headers and body +//! - **Permission-based access**: Control which hosts and methods actors can use +//! - **Event logging**: All HTTP requests are logged to the chain +//! +//! ## Example +//! +//! ```rust,no_run +//! use theater_handler_http_client::HttpClientHandler; +//! use theater::config::actor_manifest::HttpClientHandlerConfig; +//! +//! let config = HttpClientHandlerConfig {}; +//! let handler = HttpClientHandler::new(config, None); +//! ``` + +use anyhow::Result; +use reqwest::Method; +use serde::{Deserialize, Serialize}; +use std::future::Future; +use std::pin::Pin; +use thiserror::Error; +use tracing::{error, info}; +use wasmtime::component::{ComponentType, Lift, Lower}; + +use theater::actor::handle::ActorHandle; +use theater::actor::store::ActorStore; +use theater::actor::types::ActorError; +use theater::config::actor_manifest::HttpClientHandlerConfig; +use theater::config::enforcement::PermissionChecker; +use theater::config::permissions::HttpClientPermissions; +use theater::events::http::HttpEventData; +use theater::events::{ChainEventData, EventData}; +use theater::handler::Handler; +use theater::shutdown::ShutdownReceiver; +use theater::wasm::{ActorComponent, ActorInstance}; + +/// HTTP request structure for component model +#[derive(Debug, Clone, Deserialize, Serialize, ComponentType, Lift, Lower)] +#[component(record)] +pub struct HttpRequest { + method: String, + uri: String, + headers: Vec<(String, String)>, + body: Option>, +} + +/// HTTP response structure for component model +#[derive(Debug, Clone, Deserialize, Serialize, ComponentType, Lift, Lower)] +#[component(record)] +pub struct HttpResponse { + status: u16, + headers: Vec<(String, String)>, + body: Option>, +} + +/// Error types for HTTP client operations +#[derive(Error, Debug)] +pub enum HttpClientError { + #[error("Request error: {0}")] + RequestError(String), + + #[error("Actor error: {0}")] + ActorError(#[from] ActorError), + + #[error("Serialization error: {0}")] + SerializationError(#[from] serde_json::Error), + + #[error("HTTP error: {0}")] + HttpError(#[from] reqwest::Error), + + #[error("Invalid method: {0}")] + InvalidMethod(String), +} + +/// Handler for providing HTTP client capabilities to WebAssembly actors +#[derive(Clone)] +pub struct HttpClientHandler { + permissions: Option, +} + +impl HttpClientHandler { + /// Create a new HTTP client handler + /// + /// # Arguments + /// + /// * `config` - Configuration for the HTTP client handler + /// * `permissions` - Optional permissions controlling HTTP access + pub fn new( + _config: HttpClientHandlerConfig, + permissions: Option, + ) -> Self { + Self { permissions } + } +} + +impl Handler for HttpClientHandler { + fn create_instance(&self) -> Box { + Box::new(self.clone()) + } + + fn start( + &mut self, + _actor_handle: ActorHandle, + _shutdown_receiver: ShutdownReceiver, + ) -> Pin> + Send>> { + Box::pin(async { Ok(()) }) + } + + fn setup_host_functions(&mut self, actor_component: &mut ActorComponent) -> Result<()> { + // Record setup start + actor_component.actor_store.record_event(ChainEventData { + event_type: "http-client-setup".to_string(), + data: EventData::Http(HttpEventData::HandlerSetupStart), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Starting HTTP client host function setup".to_string()), + }); + + info!("Setting up http client host functions"); + + let mut interface = match actor_component + .linker + .instance("theater:simple/http-client") + { + Ok(interface) => { + // Record successful linker instance creation + actor_component.actor_store.record_event(ChainEventData { + event_type: "http-client-setup".to_string(), + data: EventData::Http(HttpEventData::LinkerInstanceSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Successfully created linker instance".to_string()), + }); + interface + } + Err(e) => { + // Record the specific error where it happens + actor_component.actor_store.record_event(ChainEventData { + event_type: "http-client-setup".to_string(), + data: EventData::Http(HttpEventData::HandlerSetupError { + error: e.to_string(), + step: "linker_instance".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to create linker instance: {}", e)), + }); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/http-client: {}", + e + )); + } + }; + + let permissions = self.permissions.clone(); + interface.func_wrap_async( + "send-http", + move |mut ctx: wasmtime::StoreContextMut<'_, ActorStore>, + (req,): (HttpRequest,)| + -> Box,)>> + Send> { + + // Record HTTP client request call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/http-client/send-http".to_string(), + data: EventData::Http(HttpEventData::HttpClientRequestCall { + method: req.method.clone(), + url: req.uri.clone(), + headers_count: req.headers.len(), + body: req.body.clone().map(|b| String::from_utf8_lossy(&b).to_string()), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Sending {} request to {}", req.method, req.uri)), + }); + + // PERMISSION CHECK BEFORE OPERATION + // Extract host from URL for permission checking + let host = if let Ok(parsed_url) = reqwest::Url::parse(&req.uri) { + parsed_url.host_str().unwrap_or(&req.uri).to_string() + } else { + req.uri.clone() + }; + + if let Err(e) = PermissionChecker::check_http_operation( + &permissions, + &req.method, + &host, + ) { + error!("HTTP client permission denied: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/http-client/permission-denied".to_string(), + data: EventData::Http(HttpEventData::PermissionDenied { + operation: "send-http".to_string(), + method: req.method.clone(), + url: req.uri.clone(), + reason: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Permission denied for {} request to {}", req.method, req.uri)), + }); + return Box::new(async move { + Ok((Err(format!("Permission denied: {}", e)),)) + }); + } + + let req_clone = req.clone(); + + Box::new(async move { + let client = reqwest::Client::new(); + + // Parse method or return error + let method = match Method::from_bytes(req_clone.method.as_bytes()) { + Ok(m) => m, + Err(e) => { + let err_msg = format!("Invalid HTTP method: {}", e); + + // Record error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/http-client/send-http".to_string(), + data: EventData::Http(HttpEventData::Error { + operation: "send-http".to_string(), + path: req_clone.uri.clone(), + message: err_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Error sending request to {}: {}", req_clone.uri, err_msg)), + }); + + return Ok((Err(err_msg),)); + } + }; + + let mut request = client.request(method, req_clone.uri.clone()); + + for (key, value) in req_clone.headers { + request = request.header(key, value); + } + if let Some(body) = req_clone.body { + request = request.body(body); + } + + info!("Sending {} request to {}", req_clone.method, req_clone.uri); + + match request.send().await { + Ok(response) => { + let status = response.status().as_u16(); + let headers = response + .headers() + .iter() + .map(|(key, value)| { + ( + key.as_str().to_string(), + value.to_str().unwrap_or_default().to_string(), + ) + }) + .collect(); + + let body = match response.bytes().await { + Ok(bytes) => Some(bytes.to_vec()), + Err(e) => { + // Record error reading response body + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/http-client/send-http".to_string(), + data: EventData::Http(HttpEventData::Error { + operation: "read-response-body".to_string(), + path: req_clone.uri.clone(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Error reading response body from {}: {}", req_clone.uri, e)), + }); + + None + } + }; + + let resp = HttpResponse { + status, + headers, + body: body.clone(), + }; + + // Record HTTP client request result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/http-client/send-http".to_string(), + data: EventData::Http(HttpEventData::HttpClientRequestResult { + status, + headers_count: resp.headers.len(), + body: body.clone().map(|b| String::from_utf8_lossy(&b).to_string()), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Received response from {} with status {}", req_clone.uri, status)), + }); + + Ok((Ok(resp),)) + } + Err(e) => { + let err_msg = e.to_string(); + + // Record HTTP client request error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/http-client/send-http".to_string(), + data: EventData::Http(HttpEventData::Error { + operation: "send-http".to_string(), + path: req_clone.uri.clone(), + message: err_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Error sending request to {}: {}", req_clone.uri, err_msg)), + }); + + Ok((Err(err_msg),)) + } + } + }) + }, + )?; + + // Record overall setup completion + actor_component.actor_store.record_event(ChainEventData { + event_type: "http-client-setup".to_string(), + data: EventData::Http(HttpEventData::HandlerSetupSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some( + "HTTP client host functions setup completed successfully".to_string(), + ), + }); + + info!("Host functions set up for http-client"); + + Ok(()) + } + + fn add_export_functions(&self, _actor_instance: &mut ActorInstance) -> Result<()> { + Ok(()) + } + + fn name(&self) -> &str { + "http-client" + } + + fn imports(&self) -> Option { + Some("theater:simple/http-client".to_string()) + } + + fn exports(&self) -> Option { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_handler_creation() { + let config = HttpClientHandlerConfig {}; + let handler = HttpClientHandler::new(config, None); + assert_eq!(handler.name(), "http-client"); + assert_eq!( + handler.imports(), + Some("theater:simple/http-client".to_string()) + ); + assert_eq!(handler.exports(), None); + } + + #[test] + fn test_handler_clone() { + let config = HttpClientHandlerConfig {}; + let handler = HttpClientHandler::new(config, None); + let cloned = handler.clone(); + assert_eq!(cloned.name(), "http-client"); + } + + #[test] + fn test_http_request_structures() { + let req = HttpRequest { + method: "GET".to_string(), + uri: "https://example.com".to_string(), + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: None, + }; + assert_eq!(req.method, "GET"); + assert_eq!(req.uri, "https://example.com"); + } +} diff --git a/crates/theater-handler-http-framework/Cargo.toml b/crates/theater-handler-http-framework/Cargo.toml new file mode 100644 index 00000000..3cc7b886 --- /dev/null +++ b/crates/theater-handler-http-framework/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "theater-handler-http-framework" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +rust-version.workspace = true + +[dependencies] diff --git a/crates/theater-handler-http-framework/src/lib.rs b/crates/theater-handler-http-framework/src/lib.rs new file mode 100644 index 00000000..b93cf3ff --- /dev/null +++ b/crates/theater-handler-http-framework/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/crates/theater-handler-message-server/Cargo.toml b/crates/theater-handler-message-server/Cargo.toml new file mode 100644 index 00000000..7e8416bc --- /dev/null +++ b/crates/theater-handler-message-server/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "theater-handler-message-server" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +rust-version.workspace = true + +[dependencies] +theater = { workspace = true } +anyhow.workspace = true +tracing.workspace = true +tokio = { workspace = true, features = ["sync", "time"] } +wasmtime = "31.0" +chrono = "0.4" +thiserror = "2.0" +serde = { workspace = true } +serde_json.workspace = true +uuid = { version = "1.11", features = ["v4"] } diff --git a/crates/theater-handler-message-server/src/lib.rs b/crates/theater-handler-message-server/src/lib.rs new file mode 100644 index 00000000..4216dc65 --- /dev/null +++ b/crates/theater-handler-message-server/src/lib.rs @@ -0,0 +1,1494 @@ +//! Theater Message Server Handler +//! +//! Provides actor-to-actor messaging capabilities including: +//! - One-way send messages +//! - Request-response patterns +//! - Bidirectional channels + +use theater::actor::handle::ActorHandle; +use theater::actor::store::ActorStore; +use theater::actor::types::{ActorError, WitActorError}; +use theater::config::permissions::MessageServerPermissions; +use theater::events::message::MessageEventData; +use theater::events::{ChainEventData, EventData}; +use theater::handler::Handler; +use theater::messages::{ + ActorChannelClose, ActorChannelInitiated, ActorChannelMessage, ActorChannelOpen, + ActorLifecycleEvent, ActorMessage, ActorRequest, ActorSend, ChannelId, ChannelParticipant, + MessageCommand, +}; +use theater::shutdown::ShutdownReceiver; +use theater::wasm::{ActorComponent, ActorInstance}; +use theater::TheaterId; + +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, Mutex, RwLock}; +use thiserror::Error; +use tokio::sync::mpsc::{Receiver, Sender}; +use tracing::{debug, error, info, warn}; +use uuid::Uuid; +use wasmtime::component::{ComponentType, Lift, Lower}; +use wasmtime::StoreContextMut; + +/// Errors that can occur during message server operations +#[derive(Error, Debug)] +pub enum MessageServerError { + #[error("Handler error: {0}")] + HandlerError(String), + + #[error("Actor error: {0}")] + ActorError(#[from] ActorError), + + #[error("Serialization error: {0}")] + SerializationError(#[from] serde_json::Error), +} + +/// Channel acceptance response +#[derive(Debug, Deserialize, Serialize, ComponentType, Lift, Lower)] +#[component(record)] +pub struct ChannelAccept { + pub accepted: bool, + pub message: Option>, +} + +/// State for a single channel +#[derive(Clone)] +struct ChannelState { + is_open: bool, +} + +/// Registry entry for each actor managed by the message-server +struct ActorRegistryEntry { + actor_id: TheaterId, + mailbox_tx: Sender, + actor_handle: ActorHandle, + channels: HashSet, +} + +/// The MessageServerHandler provides actor-to-actor communication with complete separation from the runtime. +/// +/// Architecture: +/// - Receives lifecycle events from runtime (ActorSpawned, ActorStopped) +/// - Maintains its own actor registry +/// - Creates and consumes mailboxes for all actors +/// - Routes messages via MessageCommand +/// +/// Enables actors to: +/// - Send one-way messages +/// - Make request-response calls +/// - Open bidirectional channels +/// - Manage outstanding requests +#[derive(Clone)] +pub struct MessageServerHandler { + // Lifecycle event channel (receives notifications from runtime) + lifecycle_rx: Arc>>>, + + // Message command channel (receives routing requests from host functions) + message_command_tx: Sender, + message_command_rx: Arc>>>, + + // Actor registry (message-server owns this) + actor_registry: Arc>>, + + // Channel state tracking + active_channels: Arc>>, + + // Request-response tracking + outstanding_requests: Arc>>>>, + + #[allow(dead_code)] + permissions: Option, +} + +impl MessageServerHandler { + /// Create a new MessageServerHandler + /// + /// Returns (handler, lifecycle_tx, message_tx) tuple: + /// - handler: The MessageServerHandler instance + /// - lifecycle_tx: Channel for runtime to send lifecycle events + /// - message_tx: Channel for host functions to send message commands + /// + /// # Arguments + /// * `permissions` - Optional permission restrictions + pub fn new( + permissions: Option, + ) -> (Self, Sender, Sender) { + let (lifecycle_tx, lifecycle_rx) = tokio::sync::mpsc::channel(100); + let (message_command_tx, message_command_rx) = tokio::sync::mpsc::channel(1000); + + let handler = Self { + lifecycle_rx: Arc::new(Mutex::new(Some(lifecycle_rx))), + message_command_tx: message_command_tx.clone(), + message_command_rx: Arc::new(Mutex::new(Some(message_command_rx))), + actor_registry: Arc::new(RwLock::new(HashMap::new())), + active_channels: Arc::new(Mutex::new(HashMap::new())), + outstanding_requests: Arc::new(Mutex::new(HashMap::new())), + permissions, + }; + + (handler, lifecycle_tx, message_command_tx) + } + + /// Process incoming actor messages + async fn process_message( + &mut self, + msg: ActorMessage, + actor_handle: ActorHandle, + ) -> Result<(), MessageServerError> { + match msg { + ActorMessage::Send(ActorSend { data }) => { + actor_handle + .call_function::<(Vec,), ()>( + "theater:simple/message-server-client.handle-send".to_string(), + (data,), + ) + .await?; + } + ActorMessage::Request(ActorRequest { response_tx, data }) => { + let request_id = Uuid::new_v4().to_string(); + info!("Got request: id={}, data size={}", request_id, data.len()); + + // Store the response sender + { + let mut requests = self.outstanding_requests.lock().unwrap(); + requests.insert(request_id.clone(), response_tx); + } + + // Call the actor's request handler + let response = actor_handle + .call_function::<(String, Vec), (Option>,)>( + "theater:simple/message-server-client.handle-request".to_string(), + (request_id.clone(), data), + ) + .await?; + + // If the actor returned a response immediately, send it + if let Some(response_data) = response.0 { + let mut requests = self.outstanding_requests.lock().unwrap(); + if let Some(tx) = requests.remove(&request_id) { + let _ = tx.send(response_data); + } + } + } + ActorMessage::ChannelOpen(ActorChannelOpen { + initiator_id, + channel_id, + initial_msg, + response_tx, + }) => { + info!("Received channel open request: channel_id={}", channel_id); + + // Call the actor's channel open handler + let response = actor_handle + .call_function::<(String, Vec), (ChannelAccept,)>( + "theater:simple/message-server-client.handle-channel-open".to_string(), + (initiator_id.to_string(), initial_msg), + ) + .await?; + + let channel_accept = response.0; + + if channel_accept.accepted { + // Track the channel as open + let mut channels = self.active_channels.lock().unwrap(); + channels.insert( + channel_id.clone(), + ChannelState { is_open: true }, + ); + } + + // Send the response back + let _ = response_tx.send(Ok(channel_accept.accepted)); + + // If accepted and there's an initial response message, send it + if channel_accept.accepted && channel_accept.message.is_some() { + // The initial response will be handled by the channel flow + } + } + ActorMessage::ChannelMessage(ActorChannelMessage { channel_id, msg }) => { + info!("Received channel message: channel_id={}", channel_id); + actor_handle + .call_function::<(String, Vec), ()>( + "theater:simple/message-server-client.handle-channel-message".to_string(), + (channel_id.as_str().to_string(), msg), + ) + .await?; + } + ActorMessage::ChannelClose(ActorChannelClose { channel_id }) => { + info!("Received channel close: channel_id={}", channel_id); + + // Mark channel as closed (drop lock before await) + { + let mut channels = self.active_channels.lock().unwrap(); + if let Some(state) = channels.get_mut(&channel_id) { + state.is_open = false; + } + } + + actor_handle + .call_function::<(String,), ()>( + "theater:simple/message-server-client.handle-channel-close".to_string(), + (channel_id.as_str().to_string(),), + ) + .await?; + } + ActorMessage::ChannelInitiated(ActorChannelInitiated { + target_id: _, + channel_id, + initial_msg: _, + }) => { + // Track the channel as open (from initiator side) + let mut channels = self.active_channels.lock().unwrap(); + channels.insert(channel_id.clone(), ChannelState { is_open: true }); + } + } + Ok(()) + } +} + +impl Handler for MessageServerHandler { + fn create_instance(&self) -> Box { + Box::new(self.clone()) + } + + fn name(&self) -> &str { + "message-server" + } + + fn imports(&self) -> Option { + Some("theater:simple/message-server-host".to_string()) + } + + fn exports(&self) -> Option { + Some("theater:simple/message-server-client".to_string()) + } + + fn setup_host_functions(&mut self, actor_component: &mut ActorComponent) -> Result<()> { + info!("Setting up message server host functions"); + + // Record setup start + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::HandlerSetupStart), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Starting message server host function setup".to_string()), + }); + + let mut interface = match actor_component + .linker + .instance("theater:simple/message-server-host") + { + Ok(interface) => { + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::LinkerInstanceSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some( + "Successfully created linker instance for message-server-host".to_string(), + ), + }); + interface + } + Err(e) => { + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::HandlerSetupError { + error: e.to_string(), + step: "linker_instance".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to create linker instance: {}", e)), + }); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/message-server-host: {}", + e + )); + } + }; + + // 1. send operation + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupStart { + function_name: "send".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Setting up 'send' function wrapper".to_string()), + }); + + let theater_tx = self.theater_tx.clone(); + + interface + .func_wrap_async( + "send", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (address, msg): (String, Vec)| + -> Box,)>> + Send> { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/send".to_string(), + data: EventData::Message(MessageEventData::SendMessageCall { + recipient: address.clone(), + message_type: "binary".to_string(), + data: msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Sending message to {}", address)), + }); + + info!("Sending message to actor: {}", address); + let actor_message = TheaterCommand::SendMessage { + actor_id: match TheaterId::parse(&address) { + Ok(id) => id, + Err(e) => { + let err_msg = format!("Failed to parse actor ID: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/send" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "send".to_string(), + recipient: Some(address.clone()), + message: err_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error sending message to {}: {}", + address, err_msg + )), + }); + return Box::new(async move { Ok((Err(err_msg),)) }); + } + }, + actor_message: ActorMessage::Send(ActorSend { data: msg.clone() }), + }; + let theater_tx = theater_tx.clone(); + let address_clone = address.clone(); + + Box::new(async move { + match theater_tx.send(actor_message).await { + Ok(_) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/send" + .to_string(), + data: EventData::Message(MessageEventData::SendMessageResult { + recipient: address_clone.clone(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully sent message to {}", + address_clone + )), + }); + Ok((Ok(()),)) + } + Err(e) => { + let err = e.to_string(); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/send" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "send".to_string(), + recipient: Some(address_clone.clone()), + message: err.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to send message to {}: {}", + address_clone, err + )), + }); + Ok((Err(err),)) + } + } + }) + }, + ) + .map_err(|e| { + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::HandlerSetupError { + error: e.to_string(), + step: "send_function_wrap".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to set up 'send' function wrapper: {}", + e + )), + }); + anyhow::anyhow!("Failed to wrap async send function: {}", e) + })?; + + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupSuccess { + function_name: "send".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Successfully set up 'send' function wrapper".to_string()), + }); + + // 2. request operation + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupStart { + function_name: "request".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Setting up 'request' function wrapper".to_string()), + }); + + let theater_tx = self.theater_tx.clone(); + + interface + .func_wrap_async( + "request", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (address, msg): (String, Vec)| + -> Box, String>,)>> + Send> { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/request".to_string(), + data: EventData::Message(MessageEventData::RequestMessageCall { + recipient: address.clone(), + message_type: "binary".to_string(), + data: msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Requesting message from {}", address)), + }); + + let theater_tx = theater_tx.clone(); + let address_clone = address.clone(); + + Box::new(async move { + let (response_tx, response_rx) = tokio::sync::oneshot::channel(); + let actor_message = TheaterCommand::SendMessage { + actor_id: match TheaterId::parse(&address) { + Ok(id) => id, + Err(e) => { + let err_msg = format!("Failed to parse actor ID: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/request" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "request".to_string(), + recipient: Some(address_clone.clone()), + message: err_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error requesting message from {}: {}", + address_clone, err_msg + )), + }); + return Ok((Err(err_msg),)); + } + }, + actor_message: ActorMessage::Request(ActorRequest { + data: msg, + response_tx, + }), + }; + + match theater_tx.send(actor_message).await { + Ok(_) => match response_rx.await { + Ok(response) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/request" + .to_string(), + data: EventData::Message( + MessageEventData::RequestMessageResult { + recipient: address_clone.clone(), + data: response.clone(), + success: true, + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully received response from {}", + address_clone + )), + }); + Ok((Ok(response),)) + } + Err(e) => { + let err = e.to_string(); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/request" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "request".to_string(), + recipient: Some(address_clone.clone()), + message: err.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to receive response from {}: {}", + address_clone, err + )), + }); + Ok((Err(err),)) + } + }, + Err(e) => { + let err = e.to_string(); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/request" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "request".to_string(), + recipient: Some(address_clone.clone()), + message: err.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to send request to {}: {}", + address_clone, err + )), + }); + Ok((Err(err),)) + } + } + }) + }, + ) + .map_err(|e| { + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::HandlerSetupError { + error: e.to_string(), + step: "request_function_wrap".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to set up 'request' function wrapper: {}", + e + )), + }); + anyhow::anyhow!("Failed to wrap async request function: {}", e) + })?; + + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupSuccess { + function_name: "request".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Successfully set up 'request' function wrapper".to_string()), + }); + + // 3. list-outstanding-requests operation + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupStart { + function_name: "list-outstanding-requests".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some( + "Setting up 'list-outstanding-requests' function wrapper".to_string(), + ), + }); + + let outstanding_requests = self.outstanding_requests.clone(); + + interface + .func_wrap_async( + "list-outstanding-requests", + move |mut ctx: StoreContextMut<'_, ActorStore>, + _: ()| + -> Box,)>> + Send> { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/list-outstanding-requests" + .to_string(), + data: EventData::Message(MessageEventData::ListOutstandingRequestsCall {}), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Listing outstanding requests".to_string()), + }); + + let outstanding_clone = outstanding_requests.clone(); + Box::new(async move { + let requests = outstanding_clone.lock().unwrap(); + let ids: Vec = requests.keys().cloned().collect(); + + ctx.data_mut().record_event(ChainEventData { + event_type: + "theater:simple/message-server-host/list-outstanding-requests" + .to_string(), + data: EventData::Message( + MessageEventData::ListOutstandingRequestsResult { + request_count: ids.len(), + request_ids: ids.clone(), + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Found {} outstanding requests", ids.len())), + }); + + Ok((ids,)) + }) + }, + ) + .map_err(|e| { + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::HandlerSetupError { + error: e.to_string(), + step: "list_outstanding_requests_function_wrap".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to set up 'list-outstanding-requests' function wrapper: {}", + e + )), + }); + anyhow::anyhow!( + "Failed to wrap async list-outstanding-requests function: {}", + e + ) + })?; + + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupSuccess { + function_name: "list-outstanding-requests".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some( + "Successfully set up 'list-outstanding-requests' function wrapper".to_string(), + ), + }); + + // 4. respond-to-request operation + let outstanding_requests = self.outstanding_requests.clone(); + + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupStart { + function_name: "respond-to-request".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Setting up 'respond-to-request' function wrapper".to_string()), + }); + + interface + .func_wrap_async( + "respond-to-request", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (request_id, response_data): (String, Vec)| + -> Box,)>> + Send> { + let request_id_clone = request_id.clone(); + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/respond-to-request" + .to_string(), + data: EventData::Message(MessageEventData::RespondToRequestCall { + request_id: request_id.clone(), + response_size: response_data.len(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Responding to request {}", request_id)), + }); + + let outstanding_clone = outstanding_requests.clone(); + Box::new(async move { + let mut requests = outstanding_clone.lock().unwrap(); + if let Some(sender) = requests.remove(&request_id) { + match sender.send(response_data) { + Ok(_) => { + ctx.data_mut().record_event(ChainEventData { + event_type: + "theater:simple/message-server-host/respond-to-request" + .to_string(), + data: EventData::Message( + MessageEventData::RespondToRequestResult { + request_id: request_id_clone.clone(), + success: true, + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully responded to request {}", + request_id_clone + )), + }); + Ok((Ok(()),)) + } + Err(e) => { + let err_msg = format!("Failed to send response: {:?}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: + "theater:simple/message-server-host/respond-to-request" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "respond-to-request".to_string(), + recipient: None, + message: err_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error responding to request {}: {}", + request_id_clone, err_msg + )), + }); + Ok((Err(err_msg),)) + } + } + } else { + let err_msg = format!("Request ID not found: {}", request_id); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/respond-to-request" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "respond-to-request".to_string(), + recipient: None, + message: err_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Request {} not found", + request_id_clone + )), + }); + Ok((Err(err_msg),)) + } + }) + }, + ) + .map_err(|e| { + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::HandlerSetupError { + error: e.to_string(), + step: "respond_to_request_function_wrap".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to set up 'respond-to-request' function wrapper: {}", + e + )), + }); + anyhow::anyhow!("Failed to wrap async respond-to-request function: {}", e) + })?; + + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupSuccess { + function_name: "respond-to-request".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some( + "Successfully set up 'respond-to-request' function wrapper".to_string(), + ), + }); + + // 5. cancel-request operation + let outstanding_requests = self.outstanding_requests.clone(); + + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupStart { + function_name: "cancel-request".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Setting up 'cancel-request' function wrapper".to_string()), + }); + + interface + .func_wrap_async( + "cancel-request", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (request_id,): (String,)| + -> Box,)>> + Send> { + let request_id_clone = request_id.clone(); + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/cancel-request" + .to_string(), + data: EventData::Message(MessageEventData::CancelRequestCall { + request_id: request_id.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Canceling request {}", request_id)), + }); + + let outstanding_clone = outstanding_requests.clone(); + Box::new(async move { + let mut requests = outstanding_clone.lock().unwrap(); + if requests.remove(&request_id).is_some() { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/cancel-request" + .to_string(), + data: EventData::Message(MessageEventData::CancelRequestResult { + request_id: request_id_clone.clone(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully canceled request {}", + request_id_clone + )), + }); + Ok((Ok(()),)) + } else { + let err_msg = format!("Request ID not found: {}", request_id); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/cancel-request" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "cancel-request".to_string(), + recipient: None, + message: err_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Request {} not found", + request_id_clone + )), + }); + Ok((Err(err_msg),)) + } + }) + }, + ) + .map_err(|e| { + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::HandlerSetupError { + error: e.to_string(), + step: "cancel_request_function_wrap".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to set up 'cancel-request' function wrapper: {}", + e + )), + }); + anyhow::anyhow!("Failed to wrap async cancel-request function: {}", e) + })?; + + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupSuccess { + function_name: "cancel-request".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Successfully set up 'cancel-request' function wrapper".to_string()), + }); + + // 6. open-channel operation + let theater_tx = self.theater_tx.clone(); + let mailbox_tx = self.mailbox_tx.clone(); + + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupStart { + function_name: "open-channel".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Setting up 'open-channel' function wrapper".to_string()), + }); + + interface + .func_wrap_async( + "open-channel", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (address, initial_msg): (String, Vec)| + -> Box,)>> + Send> { + let current_actor_id = ctx.data().id.clone(); + let address_clone = address.clone(); + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/open-channel".to_string(), + data: EventData::Message(MessageEventData::OpenChannelCall { + recipient: address.clone(), + message_type: "binary".to_string(), + size: initial_msg.len(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Opening channel to {}", address)), + }); + + let target_id = match TheaterId::parse(&address) { + Ok(id) => ChannelParticipant::Actor(id), + Err(e) => { + let err_msg = format!("Failed to parse actor ID: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/open-channel" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "open-channel".to_string(), + recipient: Some(address_clone.clone()), + message: err_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error opening channel to {}: {}", + address_clone, err_msg + )), + }); + return Box::new(async move { Ok((Err(err_msg),)) }); + } + }; + + let channel_id = ChannelId::new( + &ChannelParticipant::Actor(current_actor_id.clone()), + &target_id, + ); + let channel_id_str = channel_id.as_str().to_string(); + + let (response_tx, response_rx) = tokio::sync::oneshot::channel(); + + let command = TheaterCommand::ChannelOpen { + initiator_id: ChannelParticipant::Actor(current_actor_id.clone()), + target_id: target_id.clone(), + channel_id: channel_id.clone(), + initial_message: initial_msg.clone(), + response_tx, + }; + + let theater_tx = theater_tx.clone(); + let channel_id_clone = channel_id_str.clone(); + let mailbox_tx = mailbox_tx.clone(); + + Box::new(async move { + match theater_tx.send(command).await { + Ok(_) => match response_rx.await { + Ok(result) => match result { + Ok(accepted) => { + ctx.data_mut().record_event(ChainEventData { + event_type: + "theater:simple/message-server-host/open-channel" + .to_string(), + data: EventData::Message( + MessageEventData::OpenChannelResult { + recipient: address_clone.clone(), + channel_id: channel_id_clone.clone(), + accepted, + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Channel {} to {} {}", + channel_id_clone, + address_clone, + if accepted { "accepted" } else { "rejected" } + )), + }); + + if accepted { + tokio::spawn(async move { + if let Err(e) = mailbox_tx + .send(ActorMessage::ChannelInitiated( + ActorChannelInitiated { + target_id: target_id.clone(), + channel_id: channel_id.clone(), + initial_msg: initial_msg.clone(), + }, + )) + .await + { + error!( + "Failed to send channel initiated message: {}", + e + ); + } + }); + Ok((Ok(channel_id_clone),)) + } else { + Ok((Err( + "Channel request rejected by target actor" + .to_string(), + ),)) + } + } + Err(e) => { + let err_msg = format!("Error opening channel: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: + "theater:simple/message-server-host/open-channel" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "open-channel".to_string(), + recipient: Some(address_clone.clone()), + message: err_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error opening channel to {}: {}", + address_clone, err_msg + )), + }); + Ok((Err(err_msg),)) + } + }, + Err(e) => { + let err_msg = format!("Failed to receive channel open response: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/open-channel" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "open-channel".to_string(), + recipient: Some(address_clone.clone()), + message: err_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error opening channel to {}: {}", + address_clone, err_msg + )), + }); + Ok((Err(err_msg),)) + } + }, + Err(e) => { + let err_msg = format!("Failed to send channel open command: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/open-channel" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "open-channel".to_string(), + recipient: Some(address_clone.clone()), + message: err_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error opening channel to {}: {}", + address_clone, err_msg + )), + }); + Ok((Err(err_msg),)) + } + } + }) + }, + ) + .map_err(|e| { + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::HandlerSetupError { + error: e.to_string(), + step: "open_channel_function_wrap".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to set up 'open-channel' function wrapper: {}", + e + )), + }); + anyhow::anyhow!("Failed to wrap async open-channel function: {}", e) + })?; + + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupSuccess { + function_name: "open-channel".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Successfully set up 'open-channel' function wrapper".to_string()), + }); + + // 7. send-on-channel operation + let theater_tx = self.theater_tx.clone(); + + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupStart { + function_name: "send-on-channel".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Setting up 'send-on-channel' function wrapper".to_string()), + }); + + interface + .func_wrap_async( + "send-on-channel", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (channel_id_str, msg): (String, Vec)| + -> Box,)>> + Send> { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/send-on-channel" + .to_string(), + data: EventData::Message(MessageEventData::ChannelMessageCall { + channel_id: channel_id_str.clone(), + msg: msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Sending message on channel {}", + channel_id_str + )), + }); + + let channel_id = match ChannelId::parse(&channel_id_str) { + Ok(id) => id, + Err(e) => { + let err_msg = format!("Failed to parse channel ID: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/send-on-channel" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "send-on-channel".to_string(), + recipient: None, + message: err_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error sending on channel {}: {}", + channel_id_str, err_msg + )), + }); + return Box::new(async move { Ok((Err(err_msg),)) }); + } + }; + + let command = TheaterCommand::ChannelMessage { + channel_id: channel_id.clone(), + message: msg.clone(), + }; + + let theater_tx = theater_tx.clone(); + let channel_id_clone = channel_id_str.clone(); + + Box::new(async move { + match theater_tx.send(command).await { + Ok(_) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/send-on-channel" + .to_string(), + data: EventData::Message( + MessageEventData::ChannelMessageResult { + channel_id: channel_id_clone.clone(), + success: true, + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully sent message on channel {}", + channel_id_clone + )), + }); + Ok((Ok(()),)) + } + Err(e) => { + let err = e.to_string(); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/send-on-channel" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "send-on-channel".to_string(), + recipient: None, + message: err.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to send message on channel {}: {}", + channel_id_clone, err + )), + }); + Ok((Err(err),)) + } + } + }) + }, + ) + .map_err(|e| { + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::HandlerSetupError { + error: e.to_string(), + step: "send_on_channel_function_wrap".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to set up 'send-on-channel' function wrapper: {}", + e + )), + }); + anyhow::anyhow!("Failed to wrap async send-on-channel function: {}", e) + })?; + + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupSuccess { + function_name: "send-on-channel".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some( + "Successfully set up 'send-on-channel' function wrapper".to_string(), + ), + }); + + // 8. close-channel operation + let theater_tx = self.theater_tx.clone(); + + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupStart { + function_name: "close-channel".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Setting up 'close-channel' function wrapper".to_string()), + }); + + interface + .func_wrap_async( + "close-channel", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (channel_id_str,): (String,)| + -> Box,)>> + Send> { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/close-channel".to_string(), + data: EventData::Message(MessageEventData::CloseChannelCall { + channel_id: channel_id_str.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Closing channel {}", channel_id_str)), + }); + + let channel_id = match ChannelId::parse(&channel_id_str) { + Ok(id) => id, + Err(e) => { + let err_msg = format!("Failed to parse channel ID: {}", e); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/close-channel" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "close-channel".to_string(), + recipient: None, + message: err_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error closing channel {}: {}", + channel_id_str, err_msg + )), + }); + return Box::new(async move { Ok((Err(err_msg),)) }); + } + }; + + let command = TheaterCommand::ChannelClose { + channel_id: channel_id.clone(), + }; + + let theater_tx = theater_tx.clone(); + let channel_id_clone = channel_id_str.clone(); + + Box::new(async move { + match theater_tx.send(command).await { + Ok(_) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/close-channel" + .to_string(), + data: EventData::Message( + MessageEventData::CloseChannelResult { + channel_id: channel_id_clone.clone(), + success: true, + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully closed channel {}", + channel_id_clone + )), + }); + Ok((Ok(()),)) + } + Err(e) => { + let err = e.to_string(); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/message-server-host/close-channel" + .to_string(), + data: EventData::Message(MessageEventData::Error { + operation: "close-channel".to_string(), + recipient: None, + message: err.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to close channel {}: {}", + channel_id_clone, err + )), + }); + Ok((Err(err),)) + } + } + }) + }, + ) + .map_err(|e| { + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::HandlerSetupError { + error: e.to_string(), + step: "close_channel_function_wrap".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to set up 'close-channel' function wrapper: {}", + e + )), + }); + anyhow::anyhow!("Failed to wrap async close-channel function: {}", e) + })?; + + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::FunctionSetupSuccess { + function_name: "close-channel".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Successfully set up 'close-channel' function wrapper".to_string()), + }); + + // Record overall setup completion + actor_component.actor_store.record_event(ChainEventData { + event_type: "message-server-setup".to_string(), + data: EventData::Message(MessageEventData::HandlerSetupSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some( + "Message server host functions setup completed successfully".to_string(), + ), + }); + + info!("Message server host functions added"); + + Ok(()) + } + + fn add_export_functions(&self, actor_instance: &mut ActorInstance) -> Result<()> { + info!("Adding export functions for message server"); + + // 1. handle-send + actor_instance + .register_function_no_result::<(Vec,)>( + "theater:simple/message-server-client", + "handle-send", + ) + .map_err(|e| anyhow::anyhow!("Failed to register handle-send function: {}", e))?; + + // 2. handle-request + actor_instance + .register_function::<(String, Vec), (Option>,)>( + "theater:simple/message-server-client", + "handle-request", + ) + .map_err(|e| anyhow::anyhow!("Failed to register handle-request function: {}", e))?; + + // 3. handle-channel-open + actor_instance + .register_function::<(String, Vec), (ChannelAccept,)>( + "theater:simple/message-server-client", + "handle-channel-open", + ) + .map_err(|e| { + anyhow::anyhow!("Failed to register handle-channel-open function: {}", e) + })?; + + // 4. handle-channel-message + actor_instance + .register_function_no_result::<(String, Vec)>( + "theater:simple/message-server-client", + "handle-channel-message", + ) + .map_err(|e| { + anyhow::anyhow!("Failed to register handle-channel-message function: {}", e) + })?; + + // 5. handle-channel-close + actor_instance + .register_function_no_result::<(String,)>( + "theater:simple/message-server-client", + "handle-channel-close", + ) + .map_err(|e| { + anyhow::anyhow!("Failed to register handle-channel-close function: {}", e) + })?; + + info!("Added all export functions for message server"); + Ok(()) + } + + fn start( + &mut self, + actor_handle: ActorHandle, + mut shutdown_receiver: ShutdownReceiver, + ) -> Pin> + Send>> { + info!("Starting message server"); + + // Take the receiver out of the Option + let mailbox_rx_opt = self.mailbox_rx.lock().unwrap().take(); + + // Clone state for the async block + let mut handler_clone = self.clone(); + + Box::pin(async move { + // If we don't have a receiver (cloned instance), just return + let Some(mut mailbox_rx) = mailbox_rx_opt else { + info!("Message server has no receiver (cloned instance), not starting"); + return Ok(()); + }; + + loop { + tokio::select! { + _ = &mut shutdown_receiver.receiver => { + info!("Message server received shutdown signal"); + debug!("Message server shutting down"); + break; + } + msg = mailbox_rx.recv() => { + match msg { + Some(message) => { + if let Err(e) = handler_clone.process_message(message, actor_handle.clone()).await { + error!("Error processing message: {}", e); + } + } + None => { + info!("Message channel closed, shutting down"); + break; + } + } + } + } + } + info!("Message server shutdown complete"); + Ok(()) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_message_server_handler_creation() { + let (mailbox_tx, mailbox_rx) = tokio::sync::mpsc::channel(100); + let (theater_tx, _theater_rx) = tokio::sync::mpsc::channel(100); + let handler = MessageServerHandler::new(mailbox_tx, mailbox_rx, theater_tx, None); + + assert_eq!(handler.name(), "message-server"); + assert_eq!( + handler.imports(), + Some("theater:simple/message-server-host".to_string()) + ); + assert_eq!( + handler.exports(), + Some("theater:simple/message-server-client".to_string()) + ); + } + + #[test] + fn test_message_server_handler_clone() { + let (mailbox_tx, mailbox_rx) = tokio::sync::mpsc::channel(100); + let (theater_tx, _theater_rx) = tokio::sync::mpsc::channel(100); + let handler = MessageServerHandler::new(mailbox_tx, mailbox_rx, theater_tx, None); + + let cloned = handler.create_instance(); + assert_eq!(cloned.name(), "message-server"); + } +} diff --git a/crates/theater-handler-process/Cargo.toml b/crates/theater-handler-process/Cargo.toml new file mode 100644 index 00000000..c9bca378 --- /dev/null +++ b/crates/theater-handler-process/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "theater-handler-process" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +rust-version.workspace = true + +[dependencies] +theater = { workspace = true } +anyhow.workspace = true +tracing.workspace = true +tokio = { workspace = true, features = ["process", "io-util", "sync", "time"] } +wasmtime = "31.0" +chrono = "0.4" +thiserror = "2.0" +serde = { workspace = true } +serde_json.workspace = true diff --git a/crates/theater-handler-process/README.md b/crates/theater-handler-process/README.md new file mode 100644 index 00000000..b81780aa --- /dev/null +++ b/crates/theater-handler-process/README.md @@ -0,0 +1,170 @@ +# Theater Process Handler + +OS process spawning and management handler for the Theater WebAssembly runtime. + +## Overview + +The Process Handler provides comprehensive OS process spawning and management capabilities to WebAssembly actors in the Theater system. It enables actors to spawn external processes, manage their I/O streams, monitor their lifecycle, and control their execution with permission-based access control. + +## Features + +- **Process Spawning**: Spawn OS processes with full configuration control +- **I/O Management**: Async stdin/stdout/stderr handling with multiple processing modes +- **Multiple Output Modes**: + - Raw: Direct byte stream + - Line-by-Line: Buffered line reading + - JSON: Parse and validate JSON objects + - Chunked: Fixed-size chunks +- **Process Lifecycle**: Monitor process state, exit codes, and execution time +- **Execution Timeouts**: Automatic process termination after specified duration +- **Permission Control**: Configurable process spawn permissions and limits +- **Complete Auditability**: All operations recorded in event chains + +## Operations + +### Process Management + +- `os-spawn` - Spawn a new OS process with full configuration +- `os-status` - Get current process status (running, exit code, start time) +- `os-kill` - Terminate a running process +- `os-signal` - Send signals to processes (platform-specific) + +### I/O Operations + +- `os-write-stdin` - Write data to process stdin + +### Export Functions (Callbacks) + +Actors must implement these functions to receive process events: + +- `handle-stdout` - Receive stdout data from processes +- `handle-stderr` - Receive stderr data from processes +- `handle-exit` - Receive process exit notifications + +## Configuration + +```rust +use theater_handler_process::ProcessHandler; +use theater::config::actor_manifest::ProcessHostConfig; +use theater::actor::handle::ActorHandle; + +let config = ProcessHostConfig { + max_processes: 10, // Maximum concurrent processes + max_output_buffer: 8192, // Maximum output buffer size + allowed_programs: None, // Whitelist of allowed programs (None = all) + allowed_paths: None, // Whitelist of allowed working directories +}; + +let (operation_tx, _) = tokio::sync::mpsc::channel(100); +let (info_tx, _) = tokio::sync::mpsc::channel(100); +let (control_tx, _) = tokio::sync::mpsc::channel(100); +let actor_handle = ActorHandle::new(operation_tx, info_tx, control_tx); + +let handler = ProcessHandler::new(config, actor_handle, None); +``` + +## Process Configuration + +When spawning a process, you can configure: + +```rust +ProcessConfig { + program: String, // Executable path + args: Vec, // Command line arguments + cwd: Option, // Working directory + env: Vec<(String, String)>, // Environment variables + buffer_size: u32, // I/O buffer size + stdout_mode: OutputMode, // How to process stdout + stderr_mode: OutputMode, // How to process stderr + chunk_size: Option, // Chunk size for chunked mode + execution_timeout: Option, // Timeout in seconds +} +``` + +## Output Modes + +### Raw Mode +Direct byte stream - data is sent as soon as it's read from the process. + +### Line-by-Line Mode +Data is buffered and sent line by line (newline-delimited). Useful for processing line-oriented output. + +### JSON Mode +Expects newline-delimited JSON objects. Each line is validated as JSON before being sent to the actor. + +### Chunked Mode +Fixed-size chunks. Useful for binary data or when you need predictable chunk sizes. + +## Security & Permissions + +The Process Handler integrates with Theater's permission system: + +- **Process Limits**: Limit the number of concurrent processes +- **Program Whitelist**: Restrict which programs can be executed +- **Path Whitelist**: Restrict working directories +- **Complete Audit Trail**: All spawn attempts, I/O operations, and terminations are recorded + +## Event Recording + +Every operation records detailed events: + +- **Setup Events**: Handler initialization +- **Spawn Events**: Process spawn attempts and results +- **I/O Events**: stdin writes, stdout/stderr reads +- **Lifecycle Events**: Process state changes and exits +- **Error Events**: Detailed error information with operation context +- **Permission Events**: Permission checks and denials + +## Architecture + +### Process Lifecycle + +1. **Spawn**: Process is started with tokio::process::Command +2. **I/O Setup**: Stdin/stdout/stderr pipes are created and monitored +3. **Async I/O Handling**: Separate tasks read stdout/stderr and send to actor +4. **Timeout Monitoring**: Optional timeout task kills process if it exceeds duration +5. **Exit Monitoring**: Task waits for process exit and notifies actor +6. **Cleanup**: Resources are released when process terminates + +### Thread Safety + +- Uses `Arc>` for process management +- Careful lock management to avoid holding locks across await points +- All async operations are Send + 'static safe + +## Example Usage in Actor Manifests + +```toml +[[handlers]] +type = "process" +max_processes = 5 +max_output_buffer = 4096 +``` + +## Development + +Run tests: +```bash +cargo test -p theater-handler-process +``` + +Build: +```bash +cargo build -p theater-handler-process +``` + +## Migration Status + +This handler was migrated from the core `theater` crate (`src/host/process.rs`) to provide: + +- ✅ Better modularity and separation of concerns +- ✅ Independent testing and development +- ✅ Clearer architecture and boundaries +- ✅ Simplified dependencies + +**Original**: 1408 lines in `theater/src/host/process.rs` +**Migrated**: ~990 lines in standalone crate + +## License + +See the LICENSE file in the repository root. diff --git a/crates/theater-handler-process/src/lib.rs b/crates/theater-handler-process/src/lib.rs new file mode 100644 index 00000000..38b444d9 --- /dev/null +++ b/crates/theater-handler-process/src/lib.rs @@ -0,0 +1,999 @@ +//! # Process Handler +//! +//! Provides OS process spawning and management capabilities to WebAssembly actors in the Theater system. +//! This handler allows actors to spawn processes, manage their I/O, and monitor their lifecycle with +//! permission-based access control. +//! +//! ## Features +//! +//! - Process spawning with full configuration +//! - Process I/O management (stdin/stdout/stderr) +//! - Multiple output modes (raw, line-by-line, JSON, chunked) +//! - Process lifecycle monitoring +//! - Execution timeouts +//! - Permission-based access control +//! - Complete event chain recording for auditability +//! +//! ## Operations +//! +//! - `os-spawn` - Spawn a new OS process +//! - `os-write-stdin` - Write data to process stdin +//! - `os-status` - Get process status +//! - `os-kill` - Kill a process +//! - `os-signal` - Send signal to a process +//! +//! ## Usage +//! +//! ```rust +//! use theater_handler_process::ProcessHandler; +//! use theater::config::actor_manifest::ProcessHostConfig; +//! use theater::actor::handle::ActorHandle; +//! +//! # fn example() { +//! let config = ProcessHostConfig { +//! max_processes: 10, +//! max_output_buffer: 1024, +//! allowed_programs: None, +//! allowed_paths: None, +//! }; +//! let (operation_tx, _) = tokio::sync::mpsc::channel(100); +//! let (info_tx, _) = tokio::sync::mpsc::channel(100); +//! let (control_tx, _) = tokio::sync::mpsc::channel(100); +//! let actor_handle = ActorHandle::new(operation_tx, info_tx, control_tx); +//! let handler = ProcessHandler::new(config, actor_handle, None); +//! # } +//! ``` + +use std::collections::HashMap; +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime}; +use thiserror::Error; +use tracing::{debug, error, info}; +use wasmtime::component::{ComponentType, Lift, Lower}; +use wasmtime::StoreContextMut; + +use serde::{Deserialize, Serialize}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::process::{Child, Command}; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; + +use theater::actor::handle::ActorHandle; +use theater::actor::store::ActorStore; +use theater::config::actor_manifest::ProcessHostConfig; +use theater::config::enforcement::PermissionChecker; +use theater::events::process::ProcessEventData; +use theater::events::{ChainEventData, EventData}; +use theater::handler::Handler; +use theater::shutdown::ShutdownReceiver; +use theater::wasm::{ActorComponent, ActorInstance}; + +/// Errors that can occur in the ProcessHandler +#[derive(Error, Debug)] +pub enum ProcessError { + #[error("Process error: {0}")] + ProcessError(String), + + #[error("Process output error: {0}")] + OutputError(String), + + #[error("Process not found: {0}")] + ProcessNotFound(u64), + + #[error("Invalid configuration: {0}")] + InvalidConfig(String), + + #[error("Path not allowed: {0}")] + PathNotAllowed(String), + + #[error("Program not allowed: {0}")] + ProgramNotAllowed(String), + + #[error("Too many processes")] + TooManyProcesses, + + #[error("OS error: {0}")] + OsError(#[from] std::io::Error), + + #[error("Serialization error: {0}")] + SerializationError(#[from] serde_json::Error), +} + +/// Output processing mode for process stdout/stderr +#[derive(Debug, Clone, Copy, PartialEq, ComponentType, Lift, Lower, Deserialize, Serialize)] +#[component(variant)] +pub enum OutputMode { + #[component(name = "raw")] + Raw, + #[component(name = "line-by-line")] + LineByLine, + #[component(name = "json")] + Json, + #[component(name = "chunked")] + Chunked, +} + +/// Configuration for a process +#[derive(Debug, Clone, Deserialize, Serialize, ComponentType, Lift, Lower)] +#[component(record)] +pub struct ProcessConfig { + /// Executable path + pub program: String, + /// Command line arguments + pub args: Vec, + /// Working directory + pub cwd: Option, + /// Environment variables + pub env: Vec<(String, String)>, + /// Buffer size for stdout/stderr + #[component(name = "buffer-size")] + pub buffer_size: u32, + /// How to process stdout + #[component(name = "stdout-mode")] + pub stdout_mode: OutputMode, + /// How to process stderr + #[component(name = "stderr-mode")] + pub stderr_mode: OutputMode, + /// Chunk size for chunked mode + #[component(name = "chunk-size")] + pub chunk_size: Option, + /// Execution timeout in seconds (None = no timeout) + #[component(name = "execution-timeout")] + pub execution_timeout: Option, +} + +/// Status of a running process +#[derive(Debug, Clone, ComponentType, Lift, Lower)] +#[component(record)] +pub struct ProcessStatus { + /// Process ID (within Theater) + pub pid: u64, + /// Whether the process is running + pub running: bool, + /// Exit code if not running + #[component(name = "exit-code")] + pub exit_code: Option, + /// Start time in milliseconds since epoch + #[component(name = "start-time")] + pub start_time: u64, +} + +/// Represents an OS process managed by Theater +#[allow(dead_code)] +struct ManagedProcess { + /// Unique ID for this process (within Theater) + id: u64, + /// Child process handle + child: Option, + /// OS process ID + os_pid: Option, + /// Process configuration + config: ProcessConfig, + /// When the process was started + start_time: SystemTime, + /// Handle to the stdin writer task + stdin_writer: Option>, + /// Channel to send data to the stdin writer + stdin_tx: Option>>, + /// Handle to the stdout reader task + stdout_reader: Option>, + /// Handle to the stderr reader task + stderr_reader: Option>, + /// Last known exit code + exit_code: Option, + /// Timeout monitoring task + timeout_handle: Option>, + /// Flag to indicate if process was terminated due to timeout + timeout_terminated: bool, +} + +/// Handler for providing OS process spawning and management to WebAssembly actors +#[derive(Clone)] +pub struct ProcessHandler { + /// Configuration for the process handler + config: ProcessHostConfig, + /// Map of process IDs to managed processes + processes: Arc>>, + /// Next process ID to assign + next_process_id: Arc>, + /// Actor handle for sending events + actor_handle: ActorHandle, + /// Permission configuration + permissions: Option, +} + +impl ProcessHandler { + /// Create a new ProcessHandler with the given configuration + pub fn new( + config: ProcessHostConfig, + actor_handle: ActorHandle, + permissions: Option, + ) -> Self { + Self { + config, + processes: Arc::new(Mutex::new(HashMap::new())), + next_process_id: Arc::new(Mutex::new(1)), + actor_handle, + permissions, + } + } + + /// Process output from a child process + async fn process_output( + mut reader: R, + mode: OutputMode, + buffer_size: usize, + process_id: u64, + _actor_id: theater::id::TheaterId, + _theater_tx: tokio::sync::mpsc::Sender, + actor_handle: ActorHandle, + handler: String, + ) where + R: AsyncReadExt + Unpin + Send + 'static, + { + match mode { + OutputMode::Raw => { + let mut buffer = vec![0; buffer_size]; + loop { + match reader.read(&mut buffer).await { + Ok(n) if n > 0 => { + let data = buffer[0..n].to_vec(); + let _ = actor_handle + .call_function::<(u64, Vec), ()>(handler.clone(), (process_id, data)) + .await; + } + Ok(_) => break, + Err(e) => { + error!("Error reading process output: {}", e); + break; + } + } + } + } + OutputMode::LineByLine => { + let mut line = vec![]; + let mut buffer = vec![0; 1]; + + loop { + match reader.read(&mut buffer).await { + Ok(n) if n > 0 => { + if buffer[0] == b'\n' { + if !line.is_empty() { + let data = line.clone(); + let _ = actor_handle + .call_function::<(u64, Vec), ()>(handler.clone(), (process_id, data)) + .await; + line.clear(); + } + } else { + line.push(buffer[0]); + if line.len() >= buffer_size { + let data = line.clone(); + let _ = actor_handle + .call_function::<(u64, Vec), ()>(handler.clone(), (process_id, data)) + .await; + line.clear(); + } + } + } + Ok(_) => { + if !line.is_empty() { + let data = line.clone(); + let _ = actor_handle + .call_function::<(u64, Vec), ()>(handler.clone(), (process_id, data)) + .await; + } + break; + } + Err(e) => { + error!("Error reading process output: {}", e); + break; + } + } + } + } + OutputMode::Json => { + let mut buffer = String::new(); + let mut temp_buffer = vec![0; 1024]; + + loop { + match reader.read(&mut temp_buffer).await { + Ok(n) if n > 0 => { + let chunk = String::from_utf8_lossy(&temp_buffer[0..n]); + buffer.push_str(&chunk); + + while let Some(pos) = buffer.find('\n') { + let line = buffer[0..pos].trim().to_string(); + let remaining = buffer[pos + 1..].to_string(); + buffer = remaining; + + if !line.is_empty() { + if serde_json::from_str::(&line).is_ok() { + let data = line.as_bytes().to_vec(); + let _ = actor_handle + .call_function::<(u64, Vec), ()>(handler.clone(), (process_id, data)) + .await; + } + } + } + + if buffer.len() > buffer_size { + let data = buffer.as_bytes().to_vec(); + let _ = actor_handle + .call_function::<(u64, Vec), ()>(handler.clone(), (process_id, data)) + .await; + buffer.clear(); + } + } + Ok(_) => { + if !buffer.is_empty() { + let data = buffer.as_bytes().to_vec(); + let _ = actor_handle + .call_function::<(u64, Vec), ()>(handler.clone(), (process_id, data)) + .await; + } + break; + } + Err(e) => { + error!("Error reading process output: {}", e); + break; + } + } + } + } + OutputMode::Chunked => { + let chunk_size = buffer_size; + let mut buffer = vec![0; chunk_size]; + + loop { + match reader.read_exact(&mut buffer).await { + Ok(_) => { + let data = buffer.clone(); + let _ = actor_handle + .call_function::<(u64, Vec), ()>(handler.clone(), (process_id, data)) + .await; + } + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => { + break; + } + Err(e) => { + error!("Error reading process output: {}", e); + break; + } + } + } + } + } + } + + /// Kill a process directly + async fn kill_process_directly( + processes: Arc>>, + process_id: u64, + ) -> Result<(), ProcessError> { + // Take the child out of the process struct while holding the lock + let mut child_opt = { + let mut processes_lock = processes.lock().unwrap(); + if let Some(process) = processes_lock.get_mut(&process_id) { + process.child.take() + } else { + return Err(ProcessError::ProcessNotFound(process_id)); + } + }; + + // Kill the child without holding the lock + if let Some(ref mut child) = child_opt { + child.kill().await?; + } + + Ok(()) + } +} + +impl Handler for ProcessHandler { + fn create_instance(&self) -> Box { + Box::new(self.clone()) + } + + fn start( + &mut self, + _actor_handle: ActorHandle, + shutdown_receiver: ShutdownReceiver, + ) -> Pin> + Send>> { + info!("Starting process handler"); + + Box::pin(async move { + shutdown_receiver.wait_for_shutdown().await; + info!("Process handler received shutdown signal"); + Ok(()) + }) + } + + fn setup_host_functions( + &mut self, + actor_component: &mut ActorComponent, + ) -> anyhow::Result<()> { + // Record setup start + actor_component.actor_store.record_event(ChainEventData { + event_type: "process-setup".to_string(), + data: EventData::Process(ProcessEventData::HandlerSetupStart), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Starting process host function setup".to_string()), + }); + + info!("Setting up host functions for process handling"); + + let mut interface = match actor_component.linker.instance("theater:simple/process") { + Ok(interface) => { + actor_component.actor_store.record_event(ChainEventData { + event_type: "process-setup".to_string(), + data: EventData::Process(ProcessEventData::LinkerInstanceSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Successfully created linker instance".to_string()), + }); + interface + } + Err(e) => { + actor_component.actor_store.record_event(ChainEventData { + event_type: "process-setup".to_string(), + data: EventData::Process(ProcessEventData::HandlerSetupError { + error: e.to_string(), + step: "linker_instance".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to create linker instance: {}", e)), + }); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/process: {}", + e + )); + } + }; + + // Setup: os-spawn - Spawn a new OS process + let processes = self.processes.clone(); + let next_process_id = self.next_process_id.clone(); + let config = self.config.clone(); + let actor_handle = self.actor_handle.clone(); + let permissions = self.permissions.clone(); + + interface.func_wrap_async( + "os-spawn", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (process_config,): (ProcessConfig,)| + -> Box,)>> + Send> { + let processes = processes.clone(); + let next_process_id = next_process_id.clone(); + let _config = config.clone(); + let actor_handle = actor_handle.clone(); + let permissions = permissions.clone(); + + Box::new(async move { + let stdout_mode = process_config.stdout_mode; + let stderr_mode = process_config.stderr_mode; + let program = process_config.program.clone(); + let args = process_config.args.clone(); + let cwd = process_config.cwd.clone(); + + // Permission check + let current_process_count = { + let processes_lock = processes.lock().unwrap(); + processes_lock.len() + }; + + if let Err(e) = PermissionChecker::check_process_operation( + &permissions, + &program, + current_process_count, + ) { + ctx.data_mut().record_event(ChainEventData { + event_type: "process/permission-denied".to_string(), + data: EventData::Process(ProcessEventData::PermissionDenied { + operation: "spawn".to_string(), + program: program.clone(), + reason: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Permission denied for process spawn: {}", e)), + }); + + return Ok((Err(format!("Permission denied: {}", e)),)); + } + + // Get new process ID + let process_id = { + let mut id_lock = next_process_id.lock().unwrap(); + let id = *id_lock; + *id_lock += 1; + id + }; + + // Record spawn attempt + ctx.data_mut().record_event(ChainEventData { + event_type: "process/spawn".to_string(), + data: EventData::Process(ProcessEventData::ProcessSpawn { + process_id, + program: program.clone(), + args: args.clone(), + os_pid: None, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Attempting to spawn process: {}", program)), + }); + + // Build command + let mut command = Command::new(&program); + command.args(&args); + command.stdin(std::process::Stdio::piped()); + command.stdout(std::process::Stdio::piped()); + command.stderr(std::process::Stdio::piped()); + + if let Some(cwd_path) = cwd { + command.current_dir(&cwd_path); + } + + for (key, value) in &process_config.env { + command.env(key, value); + } + + // Spawn process + match command.spawn() { + Ok(mut child) => { + let os_pid = child.id(); + let start_time = SystemTime::now(); + + // Set up stdin writer + let (stdin_tx, mut stdin_rx) = mpsc::channel::>(100); + let stdin_writer = if let Some(mut stdin) = child.stdin.take() { + Some(tokio::spawn(async move { + while let Some(data) = stdin_rx.recv().await { + if let Err(e) = stdin.write_all(&data).await { + error!("Error writing to stdin: {}", e); + break; + } + } + })) + } else { + None + }; + + // Set up stdout reader + let stdout_reader = if let Some(stdout) = child.stdout.take() { + let actor_id = ctx.data().id; + let theater_tx = ctx.data().theater_tx.clone(); + let actor_handle_clone = actor_handle.clone(); + Some(tokio::spawn(async move { + Self::process_output( + stdout, + stdout_mode, + process_config.buffer_size as usize, + process_id, + actor_id, + theater_tx, + actor_handle_clone, + "theater:simple/process-handlers/handle-stdout".to_string(), + ) + .await; + })) + } else { + None + }; + + // Set up stderr reader + let stderr_reader = if let Some(stderr) = child.stderr.take() { + let actor_id = ctx.data().id; + let theater_tx = ctx.data().theater_tx.clone(); + let actor_handle_clone = actor_handle.clone(); + Some(tokio::spawn(async move { + Self::process_output( + stderr, + stderr_mode, + process_config.buffer_size as usize, + process_id, + actor_id, + theater_tx, + actor_handle_clone, + "theater:simple/process-handlers/handle-stderr".to_string(), + ) + .await; + })) + } else { + None + }; + + // Set up timeout monitoring + let timeout_handle = if let Some(timeout_secs) = process_config.execution_timeout { + let processes_clone = processes.clone(); + let pid = process_id; + Some(tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(timeout_secs)).await; + let _ = Self::kill_process_directly(processes_clone, pid).await; + })) + } else { + None + }; + + // Store managed process + let managed_process = ManagedProcess { + id: process_id, + child: Some(child), + os_pid, + config: process_config.clone(), + start_time, + stdin_writer, + stdin_tx: Some(stdin_tx), + stdout_reader, + stderr_reader, + exit_code: None, + timeout_handle, + timeout_terminated: false, + }; + + { + let mut processes_lock = processes.lock().unwrap(); + processes_lock.insert(process_id, managed_process); + } + + // Monitor process exit + let processes_monitor = processes.clone(); + let actor_handle_exit = actor_handle.clone(); + tokio::spawn(async move { + // Take the child out of the process struct + let mut child_opt = { + let mut processes_lock = processes_monitor.lock().unwrap(); + if let Some(process) = processes_lock.get_mut(&process_id) { + process.child.take() + } else { + None + } + }; + + // Wait for process to exit without holding lock + if let Some(ref mut child) = child_opt { + if let Ok(status) = child.wait().await { + if let Some(code) = status.code() { + // Record exit code in a separate block to ensure lock is dropped + { + let mut processes_lock = processes_monitor.lock().unwrap(); + if let Some(process) = processes_lock.get_mut(&process_id) { + process.exit_code = Some(code); + } + } // Lock is dropped here + + // Now safe to await + let _ = actor_handle_exit + .call_function::<(u64, i32), ()>( + "theater:simple/process-handlers/handle-exit".to_string(), + (process_id, code), + ) + .await; + } + } + } + }); + + ctx.data_mut().record_event(ChainEventData { + event_type: "process/spawn-success".to_string(), + data: EventData::Process(ProcessEventData::ProcessSpawn { + process_id, + program: program.clone(), + args: args.clone(), + os_pid, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully spawned process {} with OS PID {:?}", + process_id, os_pid + )), + }); + + Ok((Ok(process_id),)) + } + Err(e) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "process/spawn-error".to_string(), + data: EventData::Process(ProcessEventData::Error { + process_id: None, + operation: "spawn".to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to spawn process: {}", e)), + }); + + Ok((Err(format!("Failed to spawn process: {}", e)),)) + } + } + }) + }, + )?; + + // Setup: os-write-stdin - Write to process stdin + let processes = self.processes.clone(); + interface.func_wrap_async( + "os-write-stdin", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (process_id, data): (u64, Vec)| + -> Box,)>> + Send> { + let processes = processes.clone(); + + Box::new(async move { + // Clone the stdin sender to avoid holding the lock across await + let stdin_tx_opt = { + let processes_lock = processes.lock().unwrap(); + if let Some(process) = processes_lock.get(&process_id) { + process.stdin_tx.clone() + } else { + None + } + }; + + let result = if let Some(stdin_tx) = stdin_tx_opt { + stdin_tx.send(data.clone()).await + .map_err(|e| format!("Failed to send to stdin: {}", e)) + } else { + Err(format!("Process {} not found or has no stdin", process_id)) + }; + + match result { + Ok(_) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "process/write-stdin".to_string(), + data: EventData::Process(ProcessEventData::StdinWrite { + process_id, + bytes_written: data.len() as u32, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Wrote {} bytes to process {} stdin", data.len(), process_id)), + }); + Ok((Ok(()),)) + } + Err(e) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "process/write-stdin-error".to_string(), + data: EventData::Process(ProcessEventData::Error { + process_id: Some(process_id), + operation: "write-stdin".to_string(), + message: e.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Error writing to stdin: {}", e)), + }); + Ok((Err(e),)) + } + } + }) + }, + )?; + + // Setup: os-status - Get process status + let processes = self.processes.clone(); + interface.func_wrap_async( + "os-status", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (process_id,): (u64,)| + -> Box,)>> + Send> { + let processes = processes.clone(); + + Box::new(async move { + let result = { + let processes_lock = processes.lock().unwrap(); + if let Some(process) = processes_lock.get(&process_id) { + let running = process.child.is_some(); + let start_time = process.start_time + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64; + + Ok(ProcessStatus { + pid: process_id, + running, + exit_code: process.exit_code, + start_time, + }) + } else { + Err(format!("Process {} not found", process_id)) + } + }; + + match &result { + Ok(_status) => { + // Status check successful - event recorded via result + } + Err(e) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "process/status-error".to_string(), + data: EventData::Process(ProcessEventData::Error { + process_id: Some(process_id), + operation: "status".to_string(), + message: e.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Error checking status: {}", e)), + }); + } + } + + Ok((result,)) + }) + }, + )?; + + // Setup: os-kill - Kill a process + let processes = self.processes.clone(); + interface.func_wrap_async( + "os-kill", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (process_id,): (u64,)| + -> Box,)>> + Send> { + let processes = processes.clone(); + + Box::new(async move { + let result = Self::kill_process_directly(processes, process_id) + .await + .map_err(|e| e.to_string()); + + match &result { + Ok(_) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "process/kill".to_string(), + data: EventData::Process(ProcessEventData::KillRequest { process_id }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Killed process {}", process_id)), + }); + } + Err(e) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "process/kill-error".to_string(), + data: EventData::Process(ProcessEventData::Error { + process_id: Some(process_id), + operation: "kill".to_string(), + message: e.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Error killing process: {}", e)), + }); + } + } + + Ok((result,)) + }) + }, + )?; + + // Setup: os-signal - Send signal to process (Unix only, stub for cross-platform) + let processes = self.processes.clone(); + interface.func_wrap_async( + "os-signal", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (process_id, _signal): (u64, i32)| + -> Box,)>> + Send> { + let _processes = processes.clone(); + + Box::new(async move { + // Signal sending is platform-specific and not implemented in this version + ctx.data_mut().record_event(ChainEventData { + event_type: "process/signal-not-implemented".to_string(), + data: EventData::Process(ProcessEventData::Error { + process_id: None, + operation: "signal".to_string(), + message: "Signal sending not implemented".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Signal sending not implemented for process {}", process_id)), + }); + + Ok((Err("Signal sending not implemented".to_string()),)) + }) + }, + )?; + + // Record overall setup completion + actor_component.actor_store.record_event(ChainEventData { + event_type: "process-setup".to_string(), + data: EventData::Process(ProcessEventData::HandlerSetupSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Process host functions setup completed successfully".to_string()), + }); + + info!("Process host functions set up successfully"); + + Ok(()) + } + + fn add_export_functions( + &self, + actor_instance: &mut ActorInstance, + ) -> anyhow::Result<()> { + info!("Adding export functions for process handling"); + + // Register handle-stdout + actor_instance.register_function_no_result::<(u64, Vec)>( + "theater:simple/process-handlers", + "handle-stdout", + )?; + + // Register handle-stderr + actor_instance.register_function_no_result::<(u64, Vec)>( + "theater:simple/process-handlers", + "handle-stderr", + )?; + + // Register handle-exit + actor_instance.register_function_no_result::<(u64, i32)>( + "theater:simple/process-handlers", + "handle-exit", + )?; + + info!("Successfully registered all process handler export functions"); + + Ok(()) + } + + fn name(&self) -> &str { + "process" + } + + fn imports(&self) -> Option { + Some("theater:simple/process".to_string()) + } + + fn exports(&self) -> Option { + Some("theater:simple/process-handlers".to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_test_handle() -> ActorHandle { + let (operation_tx, _) = tokio::sync::mpsc::channel(100); + let (info_tx, _) = tokio::sync::mpsc::channel(100); + let (control_tx, _) = tokio::sync::mpsc::channel(100); + ActorHandle::new(operation_tx, info_tx, control_tx) + } + + #[test] + fn test_process_handler_creation() { + let config = ProcessHostConfig { + max_processes: 10, + max_output_buffer: 1024, + allowed_programs: None, + allowed_paths: None, + }; + let actor_handle = create_test_handle(); + + let handler = ProcessHandler::new(config, actor_handle, None); + + assert_eq!(handler.name(), "process"); + assert_eq!(handler.imports(), Some("theater:simple/process".to_string())); + assert_eq!(handler.exports(), Some("theater:simple/process-handlers".to_string())); + } + + #[test] + fn test_process_handler_clone() { + let config = ProcessHostConfig { + max_processes: 10, + max_output_buffer: 1024, + allowed_programs: None, + allowed_paths: None, + }; + let actor_handle = create_test_handle(); + + let handler = ProcessHandler::new(config, actor_handle, None); + let cloned = handler.create_instance(); + + assert_eq!(cloned.name(), "process"); + } + + #[test] + fn test_output_mode_serialization() { + let mode = OutputMode::LineByLine; + let json = serde_json::to_string(&mode).unwrap(); + let deserialized: OutputMode = serde_json::from_str(&json).unwrap(); + assert_eq!(mode, deserialized); + } +} diff --git a/crates/theater-handler-random/Cargo.toml b/crates/theater-handler-random/Cargo.toml new file mode 100644 index 00000000..397f1080 --- /dev/null +++ b/crates/theater-handler-random/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "theater-handler-random" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +rust-version.workspace = true + +[dependencies] +# Core theater dependencies +theater = { path = "../theater" } + +# WebAssembly runtime +wasmtime = { version = "31.0", features = ["component-model", "async"] } + +# Async runtime +tokio = { version = "1.0", features = ["full"] } + +# Error handling +anyhow = "1.0" +thiserror = "1.0" + +# Logging +tracing = "0.1" + +# Serialization +serde = { version = "1.0", features = ["derive"] } + +# Random number generation +rand = "0.8.5" +rand_chacha = "0.3" + +# Utilities +chrono = "0.4" +uuid = { version = "1.6", features = ["v4", "serde"] } + +[dev-dependencies] +test-log = "0.2" +pretty_assertions = "1.4" +tempfile = "3.8.1" diff --git a/crates/theater-handler-random/README.md b/crates/theater-handler-random/README.md new file mode 100644 index 00000000..07e898f7 --- /dev/null +++ b/crates/theater-handler-random/README.md @@ -0,0 +1,94 @@ +# Theater Handler: Random + +A random number generation handler for the Theater WebAssembly actor runtime. + +## Overview + +This handler provides secure random number generation capabilities to WebAssembly actors. It supports: + +- **Random bytes generation** - Generate cryptographically secure random bytes +- **Random integers in ranges** - Generate random numbers within specified bounds +- **Random floats** - Generate random floating-point numbers between 0.0 and 1.0 +- **UUID generation** - Generate v4 UUIDs + +## Features + +- **Reproducible randomness** - Optional seeding for deterministic behavior +- **Permission controls** - Fine-grained access control via permissions +- **Chain integration** - All random operations are logged to the actor's chain for debugging +- **Resource limits** - Configurable limits on byte generation and integer ranges + +## Usage + +### Adding to a Theater Runtime + +```rust +use theater_handler_random::RandomHandler; +use theater::config::actor_manifest::RandomHandlerConfig; +use theater::handler::Handler; + +// Create configuration +let config = RandomHandlerConfig { + seed: None, // Use OS entropy (or Some(12345) for deterministic) + max_bytes: 1024 * 1024, // Maximum bytes per request + max_int: u64::MAX, // Maximum integer value + allow_crypto_secure: true, // Allow cryptographic operations +}; + +// Create the handler +let random_handler = RandomHandler::new(config, None); + +// Register with the Theater runtime +registry.register(random_handler); +``` + +### Configuration Options + +- **`seed`**: Optional u64 seed for reproducible random sequences +- **`max_bytes`**: Maximum number of bytes that can be requested in a single call +- **`max_int`**: Maximum integer value that can be generated +- **`allow_crypto_secure`**: Whether to allow cryptographic-quality randomness + +### Permissions + +You can restrict random operations using permissions: + +```rust +use theater::config::permissions::RandomPermissions; + +let permissions = RandomPermissions { + allow_random_bytes: true, + max_bytes_per_request: 1024, + allow_random_range: true, + allow_random_float: true, + allow_uuid_generation: true, +}; + +let handler = RandomHandler::new(config, Some(permissions)); +``` + +## WIT Interface + +This handler implements the `theater:simple/random` interface: + +```wit +interface random { + random-bytes: func(size: u32) -> result, string> + random-range: func(min: u64, max: u64) -> result + random-float: func() -> result + generate-uuid: func() -> result +} +``` + +## Migration from Built-in Handler + +This handler was migrated from the core Theater runtime to enable: + +1. **Modularity** - Use only the handlers you need +2. **Independent versioning** - Update handlers without updating the runtime +3. **Easier maintenance** - Clear separation of concerns +4. **Third-party handlers** - Pattern for building custom handlers + +## License + +Apache-2.0 diff --git a/crates/theater-handler-random/src/lib.rs b/crates/theater-handler-random/src/lib.rs new file mode 100644 index 00000000..02de9fdc --- /dev/null +++ b/crates/theater-handler-random/src/lib.rs @@ -0,0 +1,510 @@ +//! # Random Number Generator Handler +//! +//! Provides random number generation capabilities to WebAssembly actors in the Theater system. +//! This handler allows actors to generate random bytes, integers within ranges, and floating-point +//! numbers while maintaining security boundaries and resource limits. + +use rand::prelude::*; +use rand_chacha::ChaCha20Rng; +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use tracing::info; +use wasmtime::StoreContextMut; + +use theater::actor::handle::ActorHandle; +use theater::actor::store::ActorStore; +use theater::config::actor_manifest::RandomHandlerConfig; +use theater::config::enforcement::PermissionChecker; +use theater::config::permissions::RandomPermissions; +use theater::events::{random::RandomEventData, ChainEventData, EventData}; +use theater::handler::Handler; +use theater::shutdown::ShutdownReceiver; +use theater::wasm::{ActorComponent, ActorInstance}; + +/// Host for providing random number generation capabilities to WebAssembly actors +pub struct RandomHandler { + config: RandomHandlerConfig, + rng: Arc>, + permissions: Option, +} + +/// Error types for random operations +#[derive(Debug, thiserror::Error)] +pub enum RandomError { + #[error("Random generation error: {0}")] + GenerationError(String), + + #[error("Invalid range: min ({0}) >= max ({1})")] + InvalidRange(u64, u64), + + #[error("Requested bytes ({0}) exceeds maximum allowed ({1})")] + TooManyBytes(usize, usize), + + #[error("Requested max value ({0}) exceeds configured maximum ({1})")] + ValueTooLarge(u64, u64), +} + +impl RandomHandler { + pub fn new(config: RandomHandlerConfig, permissions: Option) -> Self { + let rng = if let Some(seed) = config.seed { + info!("Initializing random handler with seed: {}", seed); + Arc::new(Mutex::new(ChaCha20Rng::seed_from_u64(seed))) + } else { + info!("Initializing random handler with entropy from OS"); + Arc::new(Mutex::new(ChaCha20Rng::from_entropy())) + }; + + Self { + config, + rng, + permissions, + } + } +} + +impl Handler for RandomHandler { + fn create_instance(&self) -> Box { + Box::new(Self::new(self.config.clone(), self.permissions.clone())) + } + + fn start( + &mut self, + _actor_handle: ActorHandle, + shutdown_receiver: ShutdownReceiver, + ) -> Pin> + Send>> { + info!("Starting random number generator handler"); + + Box::pin(async { + // Random handler doesn't need a background task, but we should wait for shutdown + shutdown_receiver.wait_for_shutdown().await; + info!("Random handler received shutdown signal"); + info!("Random handler shut down"); + Ok(()) + }) + } + + fn setup_host_functions( + &mut self, + actor_component: &mut ActorComponent, + ) -> anyhow::Result<()> { + // Clone what we need for the closures + let rng1 = Arc::clone(&self.rng); + let config1 = self.config.clone(); + let permissions1 = self.permissions.clone(); + + let rng2 = Arc::clone(&self.rng); + let config2 = self.config.clone(); + + let rng3 = Arc::clone(&self.rng); + let rng4 = Arc::clone(&self.rng); + + // Record setup start + actor_component.actor_store.record_event(ChainEventData { + event_type: "random-setup".to_string(), + data: EventData::Random(RandomEventData::HandlerSetupStart), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Starting random host function setup".to_string()), + }); + + info!("Setting up random number generator host functions"); + + let mut interface = match actor_component.linker.instance("theater:simple/random") { + Ok(interface) => { + // Record successful linker instance creation + actor_component.actor_store.record_event(ChainEventData { + event_type: "random-setup".to_string(), + data: EventData::Random(RandomEventData::LinkerInstanceSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Successfully created linker instance".to_string()), + }); + interface + } + Err(e) => { + // Record the specific error where it happens + actor_component.actor_store.record_event(ChainEventData { + event_type: "random-setup".to_string(), + data: EventData::Random(RandomEventData::HandlerSetupError { + error: e.to_string(), + step: "linker_instance".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to create linker instance: {}", e)), + }); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/random: {}", + e + )); + } + }; + + // Generate random bytes + interface.func_wrap_async( + "random-bytes", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (size,): (u32,)| -> Box, String>,)>> + Send> { + let rng = Arc::clone(&rng1); + let _config = config1.clone(); + let permissions = permissions1.clone(); + + Box::new(async move { + let size = size as usize; + + // Record the random bytes call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/random-host/random-bytes".to_string(), + data: EventData::Random(RandomEventData::RandomBytesCall { + requested_size: size, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Generating {} random bytes", size)), + }); + + // PERMISSION CHECK BEFORE OPERATION + if let Err(e) = PermissionChecker::check_random_operation( + &permissions, + "random-bytes", + Some(size), + None, + ) { + // Record permission denied event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/random-host/permission-denied".to_string(), + data: EventData::Random(RandomEventData::PermissionDenied { + operation: "random-bytes".to_string(), + reason: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Permission denied for random bytes generation: {}", e)), + }); + + return Ok((Err(format!("Permission denied: {}", e)),)); + } + + let mut bytes = vec![0u8; size]; + match rng.lock() { + Ok(mut generator) => { + generator.fill_bytes(&mut bytes); + + // Record successful result + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/random-host/random-bytes".to_string(), + data: EventData::Random(RandomEventData::RandomBytesResult { + generated_size: size, + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Successfully generated {} random bytes", size)), + }); + + Ok((Ok(bytes),)) + } + Err(e) => { + let error_msg = format!("Failed to acquire RNG lock: {}", e); + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/random-host/random-bytes".to_string(), + data: EventData::Random(RandomEventData::Error { + operation: "random-bytes".to_string(), + message: error_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Error generating random bytes: {}", error_msg)), + }); + + Ok((Err(error_msg),)) + } + } + }) + }, + )?; + + // Generate random integer in range + interface.func_wrap_async( + "random-range", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (min, max): (u64, u64)| + -> Box,)>> + Send> { + let rng = Arc::clone(&rng2); + let config = config2.clone(); + + Box::new(async move { + // Record the random range call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/random-host/random-range".to_string(), + data: EventData::Random(RandomEventData::RandomRangeCall { min, max }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Generating random number in range {} to {}", + min, max + )), + }); + + if min >= max { + let error_msg = format!("Invalid range: min ({}) >= max ({})", min, max); + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/random-host/random-range".to_string(), + data: EventData::Random(RandomEventData::Error { + operation: "random-range".to_string(), + message: error_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error generating random range: {}", + error_msg + )), + }); + + return Ok((Err(error_msg),)); + } + + if max > config.max_int { + let error_msg = format!( + "Requested max value ({}) exceeds configured maximum ({})", + max, config.max_int + ); + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/random-host/random-range".to_string(), + data: EventData::Random(RandomEventData::Error { + operation: "random-range".to_string(), + message: error_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error generating random range: {}", + error_msg + )), + }); + + return Ok((Err(error_msg),)); + } + + match rng.lock() { + Ok(mut generator) => { + let value = generator.gen_range(min..max); + + // Record successful result + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/random-host/random-range".to_string(), + data: EventData::Random(RandomEventData::RandomRangeResult { + min, + max, + value, + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully generated random number {} in range {} to {}", + value, min, max + )), + }); + + Ok((Ok(value),)) + } + Err(e) => { + let error_msg = format!("Failed to acquire RNG lock: {}", e); + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/random-host/random-range".to_string(), + data: EventData::Random(RandomEventData::Error { + operation: "random-range".to_string(), + message: error_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error generating random range: {}", + error_msg + )), + }); + + Ok((Err(error_msg),)) + } + } + }) + }, + )?; + + // Generate random float between 0.0 and 1.0 + interface.func_wrap_async( + "random-float", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (): ()| + -> Box,)>> + Send> { + let rng = Arc::clone(&rng3); + + Box::new(async move { + // Record the random float call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/random-host/random-float".to_string(), + data: EventData::Random(RandomEventData::RandomFloatCall), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some( + "Generating random float between 0.0 and 1.0".to_string(), + ), + }); + + match rng.lock() { + Ok(mut generator) => { + let value: f64 = generator.gen(); + + // Record successful result + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/random-host/random-float".to_string(), + data: EventData::Random(RandomEventData::RandomFloatResult { + value, + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully generated random float: {}", + value + )), + }); + + Ok((Ok(value),)) + } + Err(e) => { + let error_msg = format!("Failed to acquire RNG lock: {}", e); + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/random-host/random-float".to_string(), + data: EventData::Random(RandomEventData::Error { + operation: "random-float".to_string(), + message: error_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error generating random float: {}", + error_msg + )), + }); + + Ok((Err(error_msg),)) + } + } + }) + }, + )?; + + interface.func_wrap_async( + "generate-uuid", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (): ()| -> Box,)>> + Send> { + let rng = Arc::clone(&rng4); + + Box::new(async move { + // Record the UUID generation call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/random-host/generate-uuid".to_string(), + data: EventData::Random(RandomEventData::GenerateUuidCall), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Generating random UUID".to_string()), + }); + + match rng.lock() { + Ok(_generator) => { + let uuid = uuid::Uuid::new_v4(); + let uuid_str = uuid.to_string(); + + // Record successful result + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/random-host/generate-uuid".to_string(), + data: EventData::Random(RandomEventData::GenerateUuidResult { + uuid: uuid_str.clone(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Successfully generated UUID: {}", uuid_str)), + }); + + Ok((Ok(uuid_str),)) + } + Err(e) => { + let error_msg = format!("Failed to acquire RNG lock: {}", e); + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/random-host/generate-uuid".to_string(), + data: EventData::Random(RandomEventData::Error { + operation: "generate-uuid".to_string(), + message: error_msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Error generating UUID: {}", error_msg)), + }); + + Ok((Err(error_msg),)) + } + } + }) + }, + )?; + + // Record overall setup completion + actor_component.actor_store.record_event(ChainEventData { + event_type: "random-setup".to_string(), + data: EventData::Random(RandomEventData::HandlerSetupSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Random host functions setup completed successfully".to_string()), + }); + + info!("Random number generator host functions setup complete"); + Ok(()) + } + + fn add_export_functions( + &self, + _actor_instance: &mut ActorInstance, + ) -> anyhow::Result<()> { + // Random handler doesn't export functions to actors, only provides host functions + Ok(()) + } + + fn name(&self) -> &str { + "random" + } + + fn imports(&self) -> Option { + Some("theater:simple/random".to_string()) + } + + fn exports(&self) -> Option { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use theater::config::actor_manifest::RandomHandlerConfig; + + #[test] + fn test_random_handler_config_defaults() { + let config = RandomHandlerConfig { + seed: None, + max_bytes: 1024, + max_int: 1000, + allow_crypto_secure: false, + }; + + let handler = RandomHandler::new(config.clone(), None); + assert_eq!(handler.config.max_bytes, 1024); + assert_eq!(handler.config.max_int, 1000); + assert_eq!(handler.config.allow_crypto_secure, false); + assert!(handler.config.seed.is_none()); + } + + #[test] + fn test_random_handler_with_seed() { + let config = RandomHandlerConfig { + seed: Some(12345), + max_bytes: 1024 * 1024, + max_int: u64::MAX, + allow_crypto_secure: false, + }; + + let handler = RandomHandler::new(config, None); + assert_eq!(handler.config.seed, Some(12345)); + } +} diff --git a/crates/theater-handler-runtime/Cargo.toml b/crates/theater-handler-runtime/Cargo.toml new file mode 100644 index 00000000..2b650636 --- /dev/null +++ b/crates/theater-handler-runtime/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "theater-handler-runtime" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +rust-version.workspace = true + +[dependencies] +# Core theater dependencies +theater = { path = "../theater" } + +# WebAssembly runtime +wasmtime = { version = "31.0", features = ["component-model", "async"] } + +# Async runtime +tokio = { version = "1.0", features = ["full"] } + +# Error handling +anyhow = "1.0" +thiserror = "1.0" + +# Logging +tracing = "0.1" + +# Serialization +serde = { version = "1.0", features = ["derive"] } + +# Utilities +chrono = "0.4" + +[dev-dependencies] +test-log = "0.2" +pretty_assertions = "1.4" +tempfile = "3.8.1" diff --git a/crates/theater-handler-runtime/README.md b/crates/theater-handler-runtime/README.md new file mode 100644 index 00000000..bfa0e7cb --- /dev/null +++ b/crates/theater-handler-runtime/README.md @@ -0,0 +1,68 @@ +# Theater Runtime Handler + +Provides runtime information and control capabilities to WebAssembly actors in the Theater system. + +## Features + +This handler allows actors to: +- **Log messages** - Output log messages from within the actor +- **Get state** - Retrieve the current state information +- **Request shutdown** - Gracefully shutdown the actor with optional data + +## Usage + +Add this handler when creating your Theater runtime: + +```rust +use theater_handler_runtime::RuntimeHandler; +use theater::config::actor_manifest::RuntimeHostConfig; + +// Create the handler with theater command channel +let runtime_handler = RuntimeHandler::new( + RuntimeHostConfig {}, + theater_tx.clone(), + None, // Optional permissions +); + +// Register with your handler registry +registry.register(runtime_handler); +``` + +## WIT Interface + +This handler implements the `theater:simple/runtime` interface: + +```wit +interface runtime { + // Log a message from the actor + log: func(message: string) + + // Get the current state + get-state: func() -> list + + // Request shutdown with optional data + shutdown: func(data: option>) -> result<_, string> +} +``` + +## Configuration + +The runtime handler accepts: +- `RuntimeHostConfig` - Currently has no configuration options +- `theater_tx` - Channel for sending commands to the Theater runtime +- `RuntimePermissions` - Optional permission constraints (currently unused) + +## Implementation Notes + +- The `log` function is synchronous and outputs to the tracing logger +- The `get-state` function returns the last event data from the actor store +- The `shutdown` function is async and sends a shutdown command to the theater runtime +- All operations are recorded as events in the actor's chain + +## Events + +The handler records the following event types: +- `runtime-setup` - Handler initialization +- `theater:simple/runtime/log` - Log operations +- `theater:simple/runtime/get-state` - State retrieval +- `theater:simple/runtime/shutdown` - Shutdown requests diff --git a/crates/theater-handler-runtime/src/lib.rs b/crates/theater-handler-runtime/src/lib.rs new file mode 100644 index 00000000..d95770db --- /dev/null +++ b/crates/theater-handler-runtime/src/lib.rs @@ -0,0 +1,314 @@ +//! # Runtime Handler +//! +//! Provides runtime information and control capabilities to WebAssembly actors in the Theater system. +//! This handler allows actors to log messages, get state information, and request shutdown. + +use std::future::Future; +use std::pin::Pin; +use tracing::{error, info}; +use wasmtime::StoreContextMut; + +use theater::actor::handle::ActorHandle; +use theater::actor::store::ActorStore; +use theater::config::actor_manifest::RuntimeHostConfig; +use theater::config::permissions::RuntimePermissions; +use theater::events::{runtime::RuntimeEventData, ChainEventData, EventData}; +use theater::handler::Handler; +use theater::messages::TheaterCommand; +use theater::shutdown::ShutdownReceiver; +use theater::wasm::{ActorComponent, ActorInstance}; +use tokio::sync::mpsc::Sender; + +/// Handler for providing runtime information and control to WebAssembly actors +#[derive(Clone)] +pub struct RuntimeHandler { + config: RuntimeHostConfig, + theater_tx: Sender, + permissions: Option, +} + +impl RuntimeHandler { + pub fn new( + config: RuntimeHostConfig, + theater_tx: Sender, + permissions: Option, + ) -> Self { + Self { + config, + theater_tx, + permissions, + } + } +} + +impl Handler for RuntimeHandler { + fn create_instance(&self) -> Box { + Box::new(self.clone()) + } + + fn start( + &mut self, + _actor_handle: ActorHandle, + shutdown_receiver: ShutdownReceiver, + ) -> Pin> + Send>> { + info!("Starting runtime handler"); + + Box::pin(async { + // Runtime handler doesn't need a background task, but we should wait for shutdown + shutdown_receiver.wait_for_shutdown().await; + info!("Runtime handler received shutdown signal"); + info!("Runtime handler shut down"); + Ok(()) + }) + } + + fn setup_host_functions( + &mut self, + actor_component: &mut ActorComponent, + ) -> anyhow::Result<()> { + info!("Setting up runtime host functions"); + + let name1 = actor_component.name.clone(); + let name2 = actor_component.name.clone(); + let theater_tx = self.theater_tx.clone(); + + let mut interface = match actor_component.linker.instance("theater:simple/runtime") { + Ok(interface) => { + // Record successful linker instance creation + actor_component.actor_store.record_event(ChainEventData { + event_type: "runtime-setup".to_string(), + data: EventData::Runtime(RuntimeEventData::LinkerInstanceSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Successfully created linker instance".to_string()), + }); + interface + } + Err(e) => { + // Record the specific error where it happens + actor_component.actor_store.record_event(ChainEventData { + event_type: "runtime-setup".to_string(), + data: EventData::Runtime(RuntimeEventData::HandlerSetupError { + error: e.to_string(), + step: "linker_instance".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to create linker instance: {}", e)), + }); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/runtime: {}", + e + )); + } + }; + + // Log function + interface + .func_wrap( + "log", + move |mut ctx: StoreContextMut<'_, ActorStore>, (msg,): (String,)| { + let id = ctx.data().id.clone(); + + // Record log call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/runtime/log".to_string(), + data: EventData::Runtime(RuntimeEventData::Log { + level: "info".to_string(), + message: msg.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Actor log: {}", msg)), + }); + + info!("[ACTOR] [{}] [{}] {}", id, name1, msg); + Ok(()) + }, + ) + .map_err(|e| { + // Record function setup error + actor_component.actor_store.record_event(ChainEventData { + event_type: "runtime-setup".to_string(), + data: EventData::Runtime(RuntimeEventData::HandlerSetupError { + error: e.to_string(), + step: "log_function_wrap".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to wrap log function: {}", e)), + }); + anyhow::anyhow!("Failed to wrap log function: {}", e) + })?; + + // Get state function + interface + .func_wrap( + "get-state", + move |mut ctx: StoreContextMut<'_, ActorStore>, ()| -> anyhow::Result<(Vec,)> { + // Record state request call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/runtime/get-state".to_string(), + data: EventData::Runtime(RuntimeEventData::StateChangeCall { + old_state: "unknown".to_string(), + new_state: "requested".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Get state request".to_string()), + }); + + // Return current state + let state = ctx + .data() + .get_last_event() + .map(|e| e.data.clone()) + .unwrap_or_default(); + + // Record state request result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/runtime/get-state".to_string(), + data: EventData::Runtime(RuntimeEventData::StateChangeResult { + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("State retrieved: {} bytes", state.len())), + }); + + Ok((state,)) + }, + ) + .map_err(|e| { + // Record function setup error + actor_component.actor_store.record_event(ChainEventData { + event_type: "runtime-setup".to_string(), + data: EventData::Runtime(RuntimeEventData::HandlerSetupError { + error: e.to_string(), + step: "get_state_function_wrap".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to wrap get-state function: {}", e)), + }); + anyhow::anyhow!("Failed to wrap get-state function: {}", e) + })?; + + // Shutdown function + interface + .func_wrap_async( + "shutdown", + move |mut ctx: StoreContextMut<'_, ActorStore>, (data,): (Option>,)| + -> Box,)>> + Send> { + // Record shutdown call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/runtime/shutdown".to_string(), + data: EventData::Runtime(RuntimeEventData::ShutdownCall { + data: data.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Actor shutdown with data: {:?}", data)), + }); + + info!( + "[ACTOR] [{}] [{}] Shutdown requested: {:?}", + ctx.data().id, + name2, + data + ); + let theater_tx = theater_tx.clone(); + + Box::new(async move { + match theater_tx + .send(TheaterCommand::ShuttingDown { + actor_id: ctx.data().id.clone(), + data, + }) + .await + { + Ok(_) => { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/runtime/shutdown".to_string(), + data: EventData::Runtime(RuntimeEventData::ShutdownRequested { + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Shutdown command sent successfully".to_string()), + }); + Ok((Ok(()),)) + } + Err(e) => { + let err = e.to_string(); + // Record failed shutdown result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/runtime/shutdown".to_string(), + data: EventData::Runtime(RuntimeEventData::ShutdownRequested { + success: false, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to send shutdown command: {}", + err + )), + }); + Ok((Err(err),)) + } + } + }) + }, + ) + .map_err(|e| { + // Record function setup error + actor_component.actor_store.record_event(ChainEventData { + event_type: "runtime-setup".to_string(), + data: EventData::Runtime(RuntimeEventData::HandlerSetupError { + error: e.to_string(), + step: "shutdown_function_wrap".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to wrap shutdown function: {}", e)), + }); + anyhow::anyhow!("Failed to wrap shutdown function: {}", e) + })?; + + // Record overall setup completion + actor_component.actor_store.record_event(ChainEventData { + event_type: "runtime-setup".to_string(), + data: EventData::Runtime(RuntimeEventData::HandlerSetupSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Runtime host functions setup completed successfully".to_string()), + }); + + Ok(()) + } + + fn add_export_functions( + &self, + actor_instance: &mut ActorInstance, + ) -> anyhow::Result<()> { + actor_instance.register_function_no_result::<(String,)>("theater:simple/actor", "init") + } + + fn name(&self) -> &str { + "runtime" + } + + fn imports(&self) -> Option { + Some("theater:simple/runtime".to_string()) + } + + fn exports(&self) -> Option { + Some("theater:simple/actor".to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use theater::config::actor_manifest::RuntimeHostConfig; + use tokio::sync::mpsc; + + #[test] + fn test_runtime_handler_creation() { + let config = RuntimeHostConfig {}; + let (tx, _rx) = mpsc::channel(100); + + let handler = RuntimeHandler::new(config, tx, None); + assert_eq!(handler.name(), "runtime"); + assert_eq!(handler.imports(), Some("theater:simple/runtime".to_string())); + assert_eq!(handler.exports(), Some("theater:simple/actor".to_string())); + } +} diff --git a/crates/theater-handler-store/Cargo.toml b/crates/theater-handler-store/Cargo.toml new file mode 100644 index 00000000..c96cd9ae --- /dev/null +++ b/crates/theater-handler-store/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "theater-handler-store" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +rust-version.workspace = true + +description = "Content storage handler for Theater WebAssembly runtime" + +[dependencies] +theater = { workspace = true } +anyhow.workspace = true +tracing.workspace = true +tokio.workspace = true +wasmtime = "31.0" +chrono = "0.4" +thiserror = "2.0" +serde = { workspace = true } +serde_json.workspace = true diff --git a/crates/theater-handler-store/README.md b/crates/theater-handler-store/README.md new file mode 100644 index 00000000..4d6fb79e --- /dev/null +++ b/crates/theater-handler-store/README.md @@ -0,0 +1,119 @@ +# Theater Store Handler + +Content storage handler for the Theater WebAssembly runtime. + +## Overview + +The Store Handler provides content-addressed storage capabilities to WebAssembly actors in the Theater system. It enables actors to store, retrieve, and manage content with labels in a secure and auditable manner. + +## Features + +- **Content-Addressed Storage**: All content is stored using SHA1 hashing for integrity +- **Label Management**: Organize content with human-readable labels +- **Full Auditability**: All operations are recorded in the event chain +- **Permission Control**: Configurable permissions for store access +- **13 Storage Operations**: Complete API for content management + +## Operations + +### Core Operations + +- `new()` - Create a new content store instance +- `store(content)` - Store content and get a content reference +- `get(content_ref)` - Retrieve content by reference +- `exists(content_ref)` - Check if content exists + +### Label Operations + +- `label(label, content_ref)` - Add a label to existing content +- `get-by-label(label)` - Get content reference by label +- `remove-label(label)` - Remove a label +- `store-at-label(label, content)` - Store content and immediately label it +- `replace-content-at-label(label, content)` - Replace content at a label +- `replace-at-label(label, content_ref)` - Replace reference at a label + +### Utility Operations + +- `list-all-content()` - List all content references +- `calculate-total-size()` - Calculate total size of all stored content +- `list-labels()` - List all labels + +## Usage + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +theater-handler-store = "0.2" +``` + +### Basic Example + +```rust +use theater_handler_store::StoreHandler; +use theater::config::actor_manifest::StoreHandlerConfig; +use theater::handler::Handler; + +// Create the handler +let config = StoreHandlerConfig {}; +let handler = StoreHandler::new(config, None); + +// The handler can now be registered with the Theater runtime +``` + +### In Actor Manifests + +```toml +[[handlers]] +type = "store" +``` + +## Architecture + +The Store Handler implements the Theater `Handler` trait and provides: + +- Synchronous setup of host functions +- Async operations for all storage operations +- Complete event chain recording for auditability +- Integration with the Theater content storage system + +All storage operations are recorded in the actor's event chain, providing a complete audit trail of all content operations. + +## Event Recording + +Every operation records events including: + +- **Setup Events**: Handler initialization and configuration +- **Operation Events**: Each store/retrieve/label operation +- **Result Events**: Success/failure of operations with details +- **Error Events**: Detailed error information for debugging + +## Security + +The Store Handler integrates with Theater's permission system: + +- Content is isolated per store instance +- All operations are auditable through event chains +- No direct filesystem access from actors +- Content-addressed storage prevents tampering + +## Migration from Core Theater + +This handler was migrated from the core `theater` crate (`src/host/store.rs`) to provide: + +- ✅ Better modularity +- ✅ Independent testing +- ✅ Clearer architecture +- ✅ Simplified dependencies + +## Development + +Run tests: + +```bash +cargo test -p theater-handler-store +``` + +## License + +See the LICENSE file in the repository root. diff --git a/crates/theater-handler-store/src/lib.rs b/crates/theater-handler-store/src/lib.rs new file mode 100644 index 00000000..7536f0ba --- /dev/null +++ b/crates/theater-handler-store/src/lib.rs @@ -0,0 +1,963 @@ +//! # Store Handler +//! +//! Provides content storage capabilities to WebAssembly actors in the Theater system. +//! This handler allows actors to store, retrieve, and manage content with labels in a +//! content-addressed storage system. +//! +//! ## Features +//! +//! - Content-addressed storage with SHA1 hashing +//! - Label-based content management +//! - Store, retrieve, list, and delete operations +//! - Permission-based access control +//! - Complete event chain recording for auditability +//! +//! ## Usage +//! +//! ```rust +//! use theater_handler_store::StoreHandler; +//! use theater::config::actor_manifest::StoreHandlerConfig; +//! +//! let config = StoreHandlerConfig {}; +//! let handler = StoreHandler::new(config, None); +//! ``` + +use std::future::Future; +use std::pin::Pin; +use thiserror::Error; +use tracing::{debug, error, info}; +use wasmtime::StoreContextMut; + +use theater::actor::handle::ActorHandle; +use theater::actor::store::ActorStore; +use theater::actor::types::ActorError; +use theater::config::actor_manifest::StoreHandlerConfig; +use theater::config::permissions::StorePermissions; +use theater::events::store::StoreEventData; +use theater::events::{ChainEventData, EventData}; +use theater::handler::Handler; +use theater::shutdown::ShutdownReceiver; +use theater::store::{ContentRef, ContentStore, Label}; +use theater::wasm::{ActorComponent, ActorInstance}; + +/// Errors that can occur during store operations +#[derive(Error, Debug)] +pub enum StoreError { + #[error("Store error: {0}")] + StoreError(String), + + #[error("Actor error: {0}")] + ActorError(#[from] ActorError), + + #[error("Serialization error: {0}")] + SerializationError(#[from] serde_json::Error), +} + +/// Handler for providing content storage access to WebAssembly actors +#[derive(Clone)] +pub struct StoreHandler { + #[allow(dead_code)] + permissions: Option, +} + +impl StoreHandler { + /// Create a new store handler with the given configuration and permissions + pub fn new( + _config: StoreHandlerConfig, + permissions: Option, + ) -> Self { + Self { permissions } + } +} + +impl Handler for StoreHandler { + fn create_instance(&self) -> Box { + Box::new(self.clone()) + } + + fn start( + &mut self, + _actor_handle: ActorHandle, + shutdown_receiver: ShutdownReceiver, + ) -> Pin> + Send>> { + info!("Store handler starting..."); + + Box::pin(async move { + shutdown_receiver.wait_for_shutdown().await; + info!("Store handler received shutdown signal"); + Ok(()) + }) + } + + fn setup_host_functions( + &mut self, + actor_component: &mut ActorComponent, + ) -> anyhow::Result<()> { + // Record setup start + actor_component.actor_store.record_event(ChainEventData { + event_type: "store-setup".to_string(), + data: EventData::Store(StoreEventData::HandlerSetupStart), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Starting store host function setup".to_string()), + }); + + info!("Setting up store host functions"); + + let mut interface = match actor_component.linker.instance("theater:simple/store") { + Ok(interface) => { + // Record successful linker instance creation + actor_component.actor_store.record_event(ChainEventData { + event_type: "store-setup".to_string(), + data: EventData::Store(StoreEventData::LinkerInstanceSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Successfully created linker instance".to_string()), + }); + interface + } + Err(e) => { + // Record the specific error where it happens + actor_component.actor_store.record_event(ChainEventData { + event_type: "store-setup".to_string(), + data: EventData::Store(StoreEventData::HandlerSetupError { + error: e.to_string(), + step: "linker_instance".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to create linker instance: {}", e)), + }); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/store: {}", + e + )); + } + }; + + // Setup: new() - Create a new content store + interface.func_wrap( + "new", + move |mut ctx: StoreContextMut<'_, ActorStore>, (): ()| -> Result<(Result,), anyhow::Error> { + // Record store call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/new".to_string(), + data: EventData::Store(StoreEventData::NewStoreCall {}), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Creating new content store".to_string()), + }); + + let store = ContentStore::new(); + + // Record store result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/new".to_string(), + data: EventData::Store(StoreEventData::NewStoreResult { + store_id: store.id().to_string(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("New content store created with ID: {}", store.id())), + }); + + Ok((Ok(store.id().to_string()),)) + }, + )?; + + // Setup: store() - Store content + interface.func_wrap_async( + "store", + move |mut ctx: StoreContextMut<'_, ActorStore>, (store_id, content): (String, Vec)| + -> Box,), anyhow::Error>> + Send> { + // Record store call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/store".to_string(), + data: EventData::Store(StoreEventData::StoreCall { + store_id: store_id.clone(), + content: content.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Storing {} bytes of content", content.len())), + }); + + let store = ContentStore::from_id(&store_id); + + Box::new(async move { + // Perform the actual store operation (async) + let content_ref = store.store(content).await; + debug!("Content stored successfully: {}", content_ref.hash()); + + // Record store result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/store".to_string(), + data: EventData::Store(StoreEventData::StoreResult { + store_id: store_id.clone(), + content_ref: content_ref.clone(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Content stored successfully with hash: {}", content_ref.hash())), + }); + + Ok((Ok(ContentRef::from(content_ref)),)) + }) + }, + )?; + + // Setup: get() - Retrieve content + interface.func_wrap_async( + "get", + move |mut ctx: StoreContextMut<'_, ActorStore>, (store_id, content_ref): (String, ContentRef)| + -> Box, String>,), anyhow::Error>> + Send> { + // Record get call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/get".to_string(), + data: EventData::Store(StoreEventData::GetCall { + store_id: store_id.clone(), + content_ref: content_ref.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Getting content with hash: {}", content_ref.hash())), + }); + + let store = ContentStore::from_id(&store_id); + + Box::new(async move { + // Perform the operation + match store.get(&content_ref).await { + Ok(content) => { + debug!("Content retrieved successfully"); + + // Record get result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/get".to_string(), + data: EventData::Store(StoreEventData::GetResult { + store_id: store_id.clone(), + content_ref: content_ref.clone(), + content: Some(content.clone()), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Retrieved {} bytes of content with hash: {}", content.len(), content_ref.hash())), + }); + + Ok((Ok(content),)) + }, + Err(e) => { + error!("Error retrieving content: {}", e); + + // Record get error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/get".to_string(), + data: EventData::Store(StoreEventData::Error { + operation: "get".to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Error retrieving content with hash {}: {}", content_ref.hash(), e)), + }); + + Ok((Err(e.to_string()),)) + } + } + }) + }, + )?; + + // Setup: exists() - Check if content exists + interface.func_wrap_async( + "exists", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (store_id, content_ref): (String, ContentRef)| + -> Box,), anyhow::Error>> + Send> { + // Record exists call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/exists".to_string(), + data: EventData::Store(StoreEventData::ExistsCall { + store_id: store_id.clone(), + content_ref: content_ref.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Checking if content with hash {} exists", + content_ref.hash() + )), + }); + + let store = ContentStore::from_id(&store_id); + + Box::new(async move { + // Perform the operation + let exists = store.exists(&content_ref).await; + debug!("Content existence checked successfully"); + + // Record exists result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/exists".to_string(), + data: EventData::Store(StoreEventData::ExistsResult { + store_id: store_id.clone(), + content_ref: content_ref.clone(), + exists, + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Content with hash {} exists: {}", + content_ref.hash(), + exists + )), + }); + + Ok((Ok(exists),)) + }) + }, + )?; + + // Setup: label() - Add a label to content + interface.func_wrap_async( + "label", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (store_id, label_string, content_ref): (String, String, ContentRef)| + -> Box,), anyhow::Error>> + Send> { + // Record label call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/label".to_string(), + data: EventData::Store(StoreEventData::LabelCall { + store_id: store_id.clone(), + label: label_string.clone(), + content_ref: content_ref.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Labeling content with hash {} as '{}'", + content_ref.hash(), + label_string + )), + }); + + let store = ContentStore::from_id(&store_id); + let label = Label::new(label_string.clone()); + let label_clone = label.clone(); + + Box::new(async move { + // Perform the operation + match store.label(&label, &content_ref).await { + Ok(_) => { + debug!("Content labeled successfully"); + + // Record label result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/label".to_string(), + data: EventData::Store(StoreEventData::LabelResult { + store_id: store_id.clone(), + label: label_string.clone(), + content_ref: content_ref.clone(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully labeled content with hash {} as '{}'", + content_ref.hash(), + label_clone + )), + }); + + Ok((Ok(()),)) + } + Err(e) => { + error!("Error labeling content: {}", e); + + // Record label error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/label".to_string(), + data: EventData::Store(StoreEventData::Error { + operation: "label".to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error labeling content with hash {} as '{}': {}", + content_ref.hash(), + label_clone, + e + )), + }); + + Ok((Err(e.to_string()),)) + } + } + }) + }, + )?; + + // Setup: get-by-label() - Get content reference by label + interface.func_wrap_async( + "get-by-label", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (store_id, label_string): (String, String)| + -> Box, String>,), anyhow::Error>> + Send> { + // Record get-by-label call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/get-by-label".to_string(), + data: EventData::Store(StoreEventData::GetByLabelCall { + store_id: store_id.clone(), + label: label_string.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Getting content reference by label: {}", + label_string + )), + }); + + let store = ContentStore::from_id(&store_id); + let label = Label::new(label_string.clone()); + let label_clone = label.clone(); + + Box::new(async move { + // Perform the operation + match store.get_by_label(&label).await { + Ok(content_ref_opt) => { + debug!("Content reference by label retrieved successfully"); + + // Record get-by-label result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/get-by-label".to_string(), + data: EventData::Store(StoreEventData::GetByLabelResult { + store_id: store_id.clone(), + label: label_clone.name().to_string(), + content_ref: content_ref_opt.clone(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully retrieved content reference {:?} for label '{}'", + content_ref_opt, label_clone + )), + }); + + Ok((Ok(content_ref_opt),)) + } + Err(e) => { + error!("Error retrieving content reference by label: {}", e); + + // Record get-by-label error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/get-by-label".to_string(), + data: EventData::Store(StoreEventData::Error { + operation: "get-by-label".to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error retrieving content reference for label '{}': {}", + label_clone, e + )), + }); + + Ok((Err(e.to_string()),)) + } + } + }) + }, + )?; + + // Setup: remove-label() - Remove a label + interface.func_wrap_async( + "remove-label", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (store_id, label_string): (String, String)| + -> Box,), anyhow::Error>> + Send> { + // Record remove-label call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/remove-label".to_string(), + data: EventData::Store(StoreEventData::RemoveLabelCall { + store_id: store_id.clone(), + label: label_string.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Removing label: {}", label_string)), + }); + + let store = ContentStore::from_id(&store_id); + let label = Label::new(label_string.clone()); + let label_clone = label.clone(); + + Box::new(async move { + // Perform the operation + match store.remove_label(&label).await { + Ok(_) => { + debug!("Label removed successfully"); + + // Record remove-label result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/remove-label".to_string(), + data: EventData::Store(StoreEventData::RemoveLabelResult { + store_id: store_id.clone(), + label: label_string.clone(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully removed label '{}'", + label_clone + )), + }); + + Ok((Ok(()),)) + } + Err(e) => { + error!("Error removing label: {}", e); + + // Record remove-label error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/remove-label".to_string(), + data: EventData::Store(StoreEventData::Error { + operation: "remove-label".to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error removing label '{}': {}", + label_clone, e + )), + }); + + Ok((Err(e.to_string()),)) + } + } + }) + }, + )?; + + // Setup: store-at-label() - Store content and label it + interface.func_wrap_async( + "store-at-label", + move |mut ctx: StoreContextMut<'_, ActorStore>, (store_id, label_string, content): (String, String, Vec)| + -> Box,), anyhow::Error>> + Send> { + // Record store-at-label call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/store-at-label".to_string(), + data: EventData::Store(StoreEventData::StoreAtLabelCall { + store_id: store_id.clone(), + label: label_string.clone(), + content: content.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Storing {} bytes of content at label: {}", content.len(), label_string)), + }); + + let store = ContentStore::from_id(&store_id); + let label = Label::new(label_string.clone()); + let label_clone = label.clone(); + + Box::new(async move { + // Perform the operation + match store.store_at_label(&label, content).await { + Ok(content_ref) => { + debug!("Content stored at label successfully"); + let content_ref_wit = ContentRef::from(content_ref.clone()); + + // Record store-at-label result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/store-at-label".to_string(), + data: EventData::Store(StoreEventData::StoreAtLabelResult { + store_id: store_id.clone(), + label: label_string.clone(), + content_ref: content_ref.clone(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Successfully stored content with hash {} at label '{}'", content_ref.hash(), label_clone)), + }); + + Ok((Ok(content_ref_wit),)) + }, + Err(e) => { + error!("Error storing content at label [{}]: {}", label, e); + + // Record store-at-label error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/store-at-label".to_string(), + data: EventData::Store(StoreEventData::Error { + operation: "store-at-label".to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Error storing content at label '{}': {}", label_clone, e)), + }); + + Ok((Err(e.to_string()),)) + } + } + }) + }, + )?; + + // Setup: replace-content-at-label() - Replace content at a label + interface.func_wrap_async( + "replace-content-at-label", + move |mut ctx: StoreContextMut<'_, ActorStore>, (store_id, label_string, content): (String, String, Vec)| + -> Box,), anyhow::Error>> + Send> { + // Record replace-content-at-label call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/replace-content-at-label".to_string(), + data: EventData::Store(StoreEventData::ReplaceContentAtLabelCall { + store_id: store_id.clone(), + label: label_string.clone(), + content: content.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Replacing content at label {} with {} bytes of new content", label_string, content.len())), + }); + + let store = ContentStore::from_id(&store_id); + let label = Label::new(label_string.clone()); + let label_clone = label.clone(); + + Box::new(async move { + // Perform the operation + match store.replace_content_at_label(&label, content).await { + Ok(content_ref) => { + debug!("Content at label replaced successfully"); + let content_ref_wit = ContentRef::from(content_ref.clone()); + + // Record replace-content-at-label result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/replace-content-at-label".to_string(), + data: EventData::Store(StoreEventData::ReplaceContentAtLabelResult { + store_id: store_id.clone(), + label: label_string.clone(), + content_ref: content_ref.clone(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Successfully replaced content at label '{}' with new content (hash: {})", label_clone, content_ref.hash())), + }); + + Ok((Ok(content_ref_wit),)) + }, + Err(e) => { + error!("Error replacing content at label: {}", e); + + // Record replace-content-at-label error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/replace-content-at-label".to_string(), + data: EventData::Store(StoreEventData::Error { + operation: "replace-content-at-label".to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Error replacing content at label '{}': {}", label_clone, e)), + }); + + Ok((Err(e.to_string()),)) + } + } + }) + }, + )?; + + // Setup: replace-at-label() - Replace content reference at a label + interface.func_wrap_async( + "replace-at-label", + move |mut ctx: StoreContextMut<'_, ActorStore>, (store_id, label_string, content_ref): (String, String, ContentRef)| + -> Box,), anyhow::Error>> + Send> { + // Record replace-at-label call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/replace-at-label".to_string(), + data: EventData::Store(StoreEventData::ReplaceAtLabelCall { + store_id: store_id.clone(), + label: label_string.clone(), + content_ref: content_ref.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Replacing content at label {} with content reference: {}", label_string, content_ref.hash())), + }); + + let store = ContentStore::from_id(&store_id); + let label = Label::new(label_string.clone()); + let label_clone = label.clone(); + + Box::new(async move { + // Perform the operation + match store.replace_at_label(&label, &content_ref).await { + Ok(_) => { + debug!("Content at label replaced with reference successfully"); + + // Record replace-at-label result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/replace-at-label".to_string(), + data: EventData::Store(StoreEventData::ReplaceAtLabelResult { + store_id: store_id.clone(), + label: label_string.clone(), + content_ref: content_ref.clone(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Successfully replaced content at label '{}' with content reference (hash: {})", label_clone, content_ref.hash())), + }); + + Ok((Ok(()),)) + }, + Err(e) => { + error!("Error replacing content at label with reference: {}", e); + + // Record replace-at-label error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/replace-at-label".to_string(), + data: EventData::Store(StoreEventData::Error { + operation: "replace-at-label".to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Error replacing content at label '{}' with reference (hash: {}): {}", label_clone, content_ref.hash(), e)), + }); + + Ok((Err(e.to_string()),)) + } + } + }) + }, + )?; + + // Setup: list-all-content() - List all content references + interface.func_wrap_async( + "list-all-content", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (store_id,): (String,)| + -> Box, String>,), anyhow::Error>> + Send> { + // Record list-all-content call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/list-all-content".to_string(), + data: EventData::Store(StoreEventData::ListAllContentCall { + store_id: store_id.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Listing all content references".to_string()), + }); + + let store = ContentStore::from_id(&store_id); + + Box::new(async move { + // Perform the operation + match store.list_all_content().await { + Ok(content_refs) => { + debug!("All content references listed successfully"); + + // Record list-all-content result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/list-all-content".to_string(), + data: EventData::Store(StoreEventData::ListAllContentResult { + store_id: store_id.clone(), + content_refs: content_refs.clone(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully listed {} content references", + content_refs.len() + )), + }); + + Ok((Ok(content_refs),)) + } + Err(e) => { + error!("Error listing all content references: {}", e); + + // Record list-all-content error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/list-all-content".to_string(), + data: EventData::Store(StoreEventData::Error { + operation: "list-all-content".to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error listing all content references: {}", + e + )), + }); + + Ok((Err(e.to_string()),)) + } + } + }) + }, + )?; + + // Setup: calculate-total-size() - Calculate total size of all content + interface.func_wrap_async( + "calculate-total-size", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (store_id,): (String,)| + -> Box,), anyhow::Error>> + Send> { + // Record calculate-total-size call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/calculate-total-size".to_string(), + data: EventData::Store(StoreEventData::CalculateTotalSizeCall { + store_id: store_id.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Calculating total size of all content".to_string()), + }); + + let store = ContentStore::from_id(&store_id); + + Box::new(async move { + // Perform the operation + match store.calculate_total_size().await { + Ok(total_size) => { + debug!("Total size calculated successfully"); + + // Record calculate-total-size result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/calculate-total-size".to_string(), + data: EventData::Store(StoreEventData::CalculateTotalSizeResult { + store_id: store_id.clone(), + size: total_size, + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully calculated total content size: {} bytes", + total_size + )), + }); + + Ok((Ok(total_size),)) + } + Err(e) => { + error!("Error calculating total content size: {}", e); + + // Record calculate-total-size error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/calculate-total-size".to_string(), + data: EventData::Store(StoreEventData::Error { + operation: "calculate-total-size".to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Error calculating total content size: {}", + e + )), + }); + + Ok((Err(e.to_string()),)) + } + } + }) + }, + )?; + + // Setup: list-labels() - List all labels + interface.func_wrap_async( + "list-labels", + move |mut ctx: StoreContextMut<'_, ActorStore>, (store_id,): (String,)| + -> Box, String>,), anyhow::Error>> + Send> { + // Record list labels call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/list-labels".to_string(), + data: EventData::Store(StoreEventData::ListLabelsCall { store_id: store_id.clone() }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Listing all labels".to_string()), + }); + + let store = ContentStore::from_id(&store_id); + + Box::new(async move { + // Perform the operation + match store.list_labels().await { + Ok(labels) => { + debug!("Labels listed successfully"); + + // Record list labels result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/list-labels".to_string(), + data: EventData::Store(StoreEventData::ListLabelsResult { + store_id: store_id.clone(), + labels: labels.clone(), + success: true, + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Successfully listed {} labels", labels.len())), + }); + + Ok((Ok(labels),)) + }, + Err(e) => { + error!("Error listing labels: {}", e); + + // Record list labels error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/store/list-labels".to_string(), + data: EventData::Store(StoreEventData::Error { + operation: "list-labels".to_string(), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Error listing labels: {}", e)), + }); + + Ok((Err(e.to_string()),)) + } + } + }) + }, + )?; + + // Record overall setup completion + actor_component.actor_store.record_event(ChainEventData { + event_type: "store-setup".to_string(), + data: EventData::Store(StoreEventData::HandlerSetupSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Store host functions setup completed successfully".to_string()), + }); + + info!("Store host functions set up successfully"); + + Ok(()) + } + + fn add_export_functions( + &self, + _actor_instance: &mut ActorInstance, + ) -> anyhow::Result<()> { + info!("No export functions needed for store handler"); + Ok(()) + } + + fn name(&self) -> &str { + "store" + } + + fn imports(&self) -> Option { + Some("theater:simple/store".to_string()) + } + + fn exports(&self) -> Option { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_store_handler_creation() { + let config = StoreHandlerConfig {}; + let handler = StoreHandler::new(config, None); + + assert_eq!(handler.name(), "store"); + assert_eq!(handler.imports(), Some("theater:simple/store".to_string())); + assert_eq!(handler.exports(), None); + } + + #[test] + fn test_store_handler_clone() { + let config = StoreHandlerConfig {}; + let handler = StoreHandler::new(config, None); + let cloned = handler.create_instance(); + + assert_eq!(cloned.name(), "store"); + } +} diff --git a/crates/theater-handler-supervisor/Cargo.toml b/crates/theater-handler-supervisor/Cargo.toml new file mode 100644 index 00000000..b87ee7ae --- /dev/null +++ b/crates/theater-handler-supervisor/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "theater-handler-supervisor" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +rust-version.workspace = true + +[dependencies] +theater = { workspace = true } +anyhow.workspace = true +tracing.workspace = true +tokio = { workspace = true, features = ["sync", "time"] } +wasmtime = "31.0" +chrono = "0.4" +thiserror = "2.0" +serde = { workspace = true } +serde_json.workspace = true diff --git a/crates/theater-handler-supervisor/README.md b/crates/theater-handler-supervisor/README.md new file mode 100644 index 00000000..db47d9c7 --- /dev/null +++ b/crates/theater-handler-supervisor/README.md @@ -0,0 +1,213 @@ +# Theater Supervisor Handler + +Child actor supervision and management handler for the Theater WebAssembly runtime. + +## Overview + +The Supervisor Handler enables parent actors to spawn, manage, and monitor child actors in the Theater system. It provides comprehensive child lifecycle management with automatic notification of child errors, exits, and external stops. + +## Features + +- **Child Actor Spawning**: Create new child actors from manifests +- **State Management**: Resume child actors from saved state +- **Lifecycle Monitoring**: Automatic notifications for child events +- **Child Control**: Restart and stop child actors +- **Introspection**: Query child state and event chains +- **Complete Auditability**: All operations recorded in event chains + +## Operations + +### Child Creation + +- `spawn` - Create a new child actor from a manifest file +- `resume` - Resume a child actor from saved state + +### Child Management + +- `list-children` - Get IDs of all child actors +- `restart-child` - Restart a specific child actor +- `stop-child` - Stop a specific child actor + +### Child Introspection + +- `get-child-state` - Retrieve the current state of a child actor +- `get-child-events` - Get the complete event chain for a child actor + +### Export Functions (Callbacks) + +Supervisor actors must implement these functions to receive notifications: + +- `handle-child-error` - Called when a child actor encounters an error +- `handle-child-exit` - Called when a child actor exits successfully +- `handle-child-external-stop` - Called when a child actor is stopped externally + +## Configuration + +```rust +use theater_handler_supervisor::SupervisorHandler; +use theater::config::actor_manifest::SupervisorHostConfig; +use theater::config::permissions::SupervisorPermissions; + +let config = SupervisorHostConfig {}; +let permissions = Some(SupervisorPermissions::default()); +let handler = SupervisorHandler::new(config, permissions); +``` + +## Child Actor Lifecycle + +### 1. Spawning a Child + +```wit +// In actor code +let child_id = spawn("path/to/child/manifest.toml", some(init_bytes)); +``` + +This: +- Loads the child's manifest +- Initializes the child with optional init bytes +- Registers the child with the supervisor +- Returns the child's unique ID + +### 2. Monitoring Children + +The supervisor automatically receives notifications when: +- A child encounters an error → `handle-child-error` is called +- A child exits successfully → `handle-child-exit` is called +- A child is stopped externally → `handle-child-external-stop` is called + +### 3. Restarting Children + +```wit +// Restart a child that has stopped +let result = restart-child(child_id); +``` + +This: +- Recreates the child actor from its original manifest +- Starts with fresh state (not the old state) +- Reregisters with the supervisor + +### 4. Stopping Children + +```wit +// Gracefully stop a child +let result = stop-child(child_id); +``` + +This: +- Sends shutdown signal to child +- Waits for child to complete +- Calls `handle-child-exit` when done + +## Resuming from State + +Unlike `spawn`, which creates a fresh actor, `resume` recreates an actor from saved state: + +```wit +// Resume a child from saved state +let saved_state = get-child-state(old_child_id); +let new_child_id = resume("path/to/manifest.toml", saved_state); +``` + +This enables: +- Persisting actor state across restarts +- Checkpointing long-running computations +- Fault tolerance and recovery + +## Getting Child Information + +### Query Current State + +```wit +let state_bytes = get-child-state(child_id); +match state_bytes { + some(bytes) => // Child has state + none => // Child has no state or doesn't exist +} +``` + +### Query Event History + +```wit +let events = get-child-events(child_id); +// Returns the complete event chain for introspection +``` + +## Error Handling + +All operations return `result` for proper error handling: + +```wit +match spawn("manifest.toml", none) { + ok(child_id) => // Success + err(message) => // Handle error +} +``` + +Common errors: +- Manifest file not found +- Invalid manifest format +- Child ID not found +- Permission denied + +## Event Recording + +Every operation records detailed events: + +- **Spawn Events**: Child creation attempts and results +- **Resume Events**: Child restoration from state +- **Lifecycle Events**: Stops, restarts, errors, exits +- **Query Events**: State and event chain requests +- **Error Events**: Detailed error information + +## Architecture + +### Parent-Child Communication + +The supervisor uses channels to receive notifications from children: + +1. When spawning/resuming, the supervisor's channel sender is registered with the child +2. When child events occur (error, exit, external stop), they're sent to this channel +3. The supervisor's background task receives these events +4. The supervisor calls the appropriate handler function on the parent actor + +### Thread Safety + +- Uses `Arc>>` for channel management +- Only the original (not cloned) handler instance runs the background task +- All operations are Send + Sync safe + +## Example Usage in Actor Manifests + +```toml +[[handlers]] +type = "supervisor" +``` + +## Development + +Run tests: +```bash +cargo test -p theater-handler-supervisor +``` + +Build: +```bash +cargo build -p theater-handler-supervisor +``` + +## Migration Status + +This handler was migrated from the core `theater` crate (`src/host/supervisor.rs`) to provide: + +- ✅ Better modularity and separation of concerns +- ✅ Independent testing and development +- ✅ Clearer architecture and boundaries +- ✅ Simplified dependencies + +**Original**: 1079 lines in `theater/src/host/supervisor.rs` +**Migrated**: ~1230 lines in standalone crate (includes comprehensive docs) + +## License + +See the LICENSE file in the repository root. diff --git a/crates/theater-handler-supervisor/src/lib.rs b/crates/theater-handler-supervisor/src/lib.rs new file mode 100644 index 00000000..6bb3845e --- /dev/null +++ b/crates/theater-handler-supervisor/src/lib.rs @@ -0,0 +1,1226 @@ +//! Theater Supervisor Handler +//! +//! Provides supervisor capabilities for spawning and managing child actors. + +use theater::actor::handle::ActorHandle; +use theater::actor::store::ActorStore; +use theater::actor::types::{ActorError, WitActorError}; +use theater::config::actor_manifest::SupervisorHostConfig; +use theater::config::permissions::SupervisorPermissions; +use theater::events::supervisor::SupervisorEventData; +use theater::events::{ChainEventData, EventData}; +use theater::handler::Handler; +use theater::messages::{ActorResult, TheaterCommand}; +use theater::shutdown::ShutdownReceiver; +use theater::wasm::{ActorComponent, ActorInstance}; +use theater::ChainEvent; + +use anyhow::Result; +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use thiserror::Error; +use tokio::sync::oneshot; +use tracing::{error, info}; +use wasmtime::StoreContextMut; + +/// Errors that can occur during supervisor operations +#[derive(Error, Debug)] +pub enum SupervisorError { + #[error("Handler error: {0}")] + HandlerError(String), + + #[error("Actor error: {0}")] + ActorError(#[from] ActorError), + + #[error("Serialization error: {0}")] + SerializationError(#[from] serde_json::Error), +} + + +/// The SupervisorHandler provides child actor management capabilities. +/// +/// This handler enables actors to: +/// - Spawn new child actors +/// - Resume child actors from saved state +/// - List, restart, and stop children +/// - Get child state and event chains +/// - Receive notifications when children error, exit, or are stopped +#[derive(Clone)] +pub struct SupervisorHandler { + channel_tx: tokio::sync::mpsc::Sender, + channel_rx: Arc>>>, + #[allow(dead_code)] + permissions: Option, +} + +impl SupervisorHandler { + /// Create a new SupervisorHandler + /// + /// # Arguments + /// * `config` - Configuration for the supervisor handler + /// * `permissions` - Optional permission restrictions + /// + /// # Returns + /// The SupervisorHandler (receiver is stored internally) + pub fn new( + _config: SupervisorHostConfig, + permissions: Option, + ) -> Self { + let (channel_tx, channel_rx) = tokio::sync::mpsc::channel(100); + Self { + channel_tx, + channel_rx: Arc::new(Mutex::new(Some(channel_rx))), + permissions, + } + } + + /// Get a clone of the supervisor channel sender + /// + /// This is used by parent actors when spawning children so the supervisor + /// can receive notifications about child lifecycle events. + pub fn get_sender(&self) -> tokio::sync::mpsc::Sender { + self.channel_tx.clone() + } + + /// Process child actor results received via the channel + /// + /// This should be called in a loop to handle child lifecycle events. + async fn process_child_result( + actor_handle: &ActorHandle, + actor_result: ActorResult, + ) -> Result<()> { + info!("Processing child result"); + + match actor_result { + ActorResult::Error(child_error) => { + actor_handle + .call_function::<(String, WitActorError), ()>( + "theater:simple/supervisor-handlers.handle-child-error".to_string(), + (child_error.actor_id.to_string(), child_error.error.into()), + ) + .await?; + } + ActorResult::Success(child_result) => { + info!("Child result: {:?}", child_result); + actor_handle + .call_function::<(String, Option>), ()>( + "theater:simple/supervisor-handlers.handle-child-exit".to_string(), + ( + child_result.actor_id.to_string(), + child_result.result.into(), + ), + ) + .await?; + } + ActorResult::ExternalStop(stop_data) => { + info!("External stop received for actor: {}", stop_data.actor_id); + actor_handle + .call_function::<(String,), ()>( + "theater:simple/supervisor-handlers.handle-child-external-stop" + .to_string(), + (stop_data.actor_id.to_string(),), + ) + .await?; + } + } + + Ok(()) + } +} + +impl Handler for SupervisorHandler { + fn create_instance(&self) -> Box { + Box::new(self.clone()) + } + + fn name(&self) -> &str { + "supervisor" + } + + fn imports(&self) -> Option { + Some("theater:simple/supervisor".to_string()) + } + + fn exports(&self) -> Option { + Some("theater:simple/supervisor-handlers".to_string()) + } + + fn setup_host_functions(&mut self, actor_component: &mut ActorComponent) -> Result<()> { + // Record setup start + actor_component.actor_store.record_event(ChainEventData { + event_type: "supervisor-setup".to_string(), + data: EventData::Supervisor(SupervisorEventData::HandlerSetupStart), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Starting supervisor host function setup".to_string()), + }); + + info!("Setting up host functions for supervisor"); + + let mut interface = match actor_component.linker.instance("theater:simple/supervisor") { + Ok(interface) => { + // Record successful linker instance creation + actor_component.actor_store.record_event(ChainEventData { + event_type: "supervisor-setup".to_string(), + data: EventData::Supervisor(SupervisorEventData::LinkerInstanceSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Successfully created linker instance".to_string()), + }); + interface + } + Err(e) => { + // Record the specific error where it happens + actor_component.actor_store.record_event(ChainEventData { + event_type: "supervisor-setup".to_string(), + data: EventData::Supervisor(SupervisorEventData::HandlerSetupError { + error: e.to_string(), + step: "linker_instance".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to create linker instance: {}", e)), + }); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/supervisor: {}", + e + )); + } + }; + + let supervisor_tx = self.channel_tx.clone(); + + // spawn implementation + info!("Registering spawn function"); + let _ = interface + .func_wrap_async( + "spawn", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (manifest, init_bytes): (String, Option>)| + -> Box,)>> + Send> { + // Record spawn child call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/spawn".to_string(), + data: EventData::Supervisor(SupervisorEventData::SpawnChildCall { + manifest_path: manifest.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Spawning child from manifest: {}", manifest)), + }); + + let store = ctx.data_mut(); + let theater_tx = store.theater_tx.clone(); + let parent_id = store.id.clone(); + let supervisor_tx = supervisor_tx.clone(); + + Box::new(async move { + let (response_tx, response_rx) = oneshot::channel(); + match theater_tx + .send(TheaterCommand::SpawnActor { + manifest_path: manifest, + init_bytes, + response_tx, + parent_id: Some(parent_id), + supervisor_tx: Some(supervisor_tx), + subscription_tx: None, + }) + .await + { + Ok(_) => match response_rx.await { + Ok(Ok(actor_id)) => { + let actor_id_str = actor_id.to_string(); + + // Record spawn child result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/spawn".to_string(), + data: EventData::Supervisor( + SupervisorEventData::SpawnChildResult { + child_id: actor_id_str.clone(), + success: true, + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully spawned child with ID: {}", + actor_id_str + )), + }); + + Ok((Ok(actor_id_str),)) + } + Ok(Err(e)) => { + // Record spawn child error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/spawn".to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "spawn".to_string(), + child_id: None, + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to spawn child: {}", e)), + }); + + Ok((Err(e.to_string()),)) + } + Err(e) => { + // Record spawn child error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/spawn".to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "spawn".to_string(), + child_id: None, + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to receive spawn response: {}", + e + )), + }); + + Ok((Err(format!("Failed to receive response: {}", e)),)) + } + }, + Err(e) => { + // Record spawn child error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/spawn".to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "spawn".to_string(), + child_id: None, + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to send spawn command: {}", + e + )), + }); + + Ok((Err(format!("Failed to send spawn command: {}", e)),)) + } + } + }) + }, + ) + .expect("Failed to wrap spawn function"); + + let supervisor_tx = self.channel_tx.clone(); + + // resume implementation + info!("Registering resume function"); + let _ = interface + .func_wrap_async( + "resume", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (manifest, state_bytes): (String, Option>)| + -> Box,)>> + Send> { + // Record resume child call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/resume".to_string(), + data: EventData::Supervisor(SupervisorEventData::ResumeChildCall { + manifest_path: manifest.clone(), + initial_state: state_bytes.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Resuming child from manifest: {}", manifest)), + }); + + let store = ctx.data_mut(); + let theater_tx = store.theater_tx.clone(); + let parent_id = store.id.clone(); + let supervisor_tx = supervisor_tx.clone(); + + Box::new(async move { + let (response_tx, response_rx) = oneshot::channel(); + match theater_tx + .send(TheaterCommand::ResumeActor { + manifest_path: manifest, + state_bytes, + response_tx, + parent_id: Some(parent_id), + supervisor_tx: Some(supervisor_tx), + subscription_tx: None, + }) + .await + { + Ok(_) => match response_rx.await { + Ok(Ok(actor_id)) => { + let actor_id_str = actor_id.to_string(); + + // Record resume child result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/resume".to_string(), + data: EventData::Supervisor( + SupervisorEventData::ResumeChildResult { + child_id: actor_id_str.clone(), + success: true, + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully resumed child with ID: {}", + actor_id_str + )), + }); + + Ok((Ok(actor_id_str),)) + } + Ok(Err(e)) => { + // Record resume child error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/resume".to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "resume".to_string(), + child_id: None, + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to resume child: {}", e)), + }); + + Ok((Err(e.to_string()),)) + } + Err(e) => { + // Record resume child error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/resume".to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "resume".to_string(), + child_id: None, + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to receive resume response: {}", + e + )), + }); + + Ok((Err(format!("Failed to receive response: {}", e)),)) + } + }, + Err(e) => { + // Record resume child error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/resume".to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "resume".to_string(), + child_id: None, + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to send resume command: {}", + e + )), + }); + + Ok((Err(format!("Failed to send resume command: {}", e)),)) + } + } + }) + }, + ) + .expect("Failed to wrap resume function"); + + // list-children implementation + info!("Registering list-children function"); + let _ = interface + .func_wrap_async( + "list-children", + move |mut ctx: StoreContextMut<'_, ActorStore>, + ()| + -> Box,)>> + Send> { + // Record list children call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/list-children".to_string(), + data: EventData::Supervisor(SupervisorEventData::ListChildrenCall {}), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Listing children".to_string()), + }); + + let store = ctx.data_mut(); + let theater_tx = store.theater_tx.clone(); + let parent_id = store.id.clone(); + + Box::new(async move { + let (response_tx, response_rx) = oneshot::channel(); + match theater_tx + .send(TheaterCommand::ListChildren { + parent_id, + response_tx, + }) + .await + { + Ok(_) => match response_rx.await { + Ok(children) => { + let children_str: Vec = + children.into_iter().map(|id| id.to_string()).collect(); + + // Record list children result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/list-children" + .to_string(), + data: EventData::Supervisor( + SupervisorEventData::ListChildrenResult { + children_count: children_str.len(), + success: true, + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Found {} children", + children_str.len() + )), + }); + + Ok((children_str,)) + } + Err(e) => { + // Record list children error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/list-children" + .to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "list-children".to_string(), + child_id: None, + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to receive children list: {}", + e + )), + }); + + Err(anyhow::anyhow!("Failed to receive children list: {}", e)) + } + }, + Err(e) => { + // Record list children error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/list-children" + .to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "list-children".to_string(), + child_id: None, + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to send list children command: {}", + e + )), + }); + + Err(anyhow::anyhow!( + "Failed to send list children command: {}", + e + )) + } + } + }) + }, + ) + .expect("Failed to wrap list-children function"); + + // restart-child implementation + info!("Registering restart-child function"); + let _ = interface + .func_wrap_async( + "restart-child", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (child_id,): (String,)| + -> Box,)>> + Send> { + // Record restart child call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/restart-child".to_string(), + data: EventData::Supervisor(SupervisorEventData::RestartChildCall { + child_id: child_id.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Restarting child: {}", child_id)), + }); + + let store = ctx.data_mut(); + let theater_tx = store.theater_tx.clone(); + let child_id_clone = child_id.clone(); + + Box::new(async move { + let (response_tx, response_rx) = oneshot::channel(); + match theater_tx + .send(TheaterCommand::RestartActor { + actor_id: match child_id.parse() { + Ok(id) => id, + Err(e) => { + // Record error event + ctx.data_mut().record_event(ChainEventData { + event_type: + "theater:simple/supervisor/restart-child" + .to_string(), + data: EventData::Supervisor( + SupervisorEventData::Error { + operation: "restart-child".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to parse child ID: {}", + e + )), + }); + + return Ok((Err(format!("Invalid child ID: {}", e)),)); + } + }, + response_tx, + }) + .await + { + Ok(_) => match response_rx.await { + Ok(Ok(())) => { + // Record restart child result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/restart-child" + .to_string(), + data: EventData::Supervisor( + SupervisorEventData::RestartChildResult { + child_id: child_id_clone.clone(), + success: true, + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully restarted child: {}", + child_id_clone + )), + }); + + Ok((Ok(()),)) + } + Ok(Err(e)) => { + // Record restart child error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/restart-child" + .to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "restart-child".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to restart child: {}", + e + )), + }); + + Ok((Err(e.to_string()),)) + } + Err(e) => { + // Record restart child error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/restart-child" + .to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "restart-child".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to receive restart response: {}", + e + )), + }); + + Ok((Err(format!("Failed to receive restart response: {}", e)),)) + } + }, + Err(e) => { + // Record restart child error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/restart-child" + .to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "restart-child".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to send restart command: {}", + e + )), + }); + + Ok((Err(format!("Failed to send restart command: {}", e)),)) + } + } + }) + }, + ) + .expect("Failed to wrap restart-child function"); + + // stop-child implementation + info!("Registering stop-child function"); + let _ = interface + .func_wrap_async( + "stop-child", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (child_id,): (String,)| + -> Box,)>> + Send> { + // Record stop child call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/stop-child".to_string(), + data: EventData::Supervisor(SupervisorEventData::StopChildCall { + child_id: child_id.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Stopping child: {}", child_id)), + }); + + let store = ctx.data_mut(); + let theater_tx = store.theater_tx.clone(); + let child_id_clone = child_id.clone(); + + Box::new(async move { + let (response_tx, response_rx) = oneshot::channel(); + match theater_tx + .send(TheaterCommand::StopActor { + actor_id: match child_id.parse() { + Ok(id) => id, + Err(e) => { + // Record error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/stop-child" + .to_string(), + data: EventData::Supervisor( + SupervisorEventData::Error { + operation: "stop-child".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to parse child ID: {}", + e + )), + }); + + return Ok((Err(format!("Invalid child ID: {}", e)),)); + } + }, + response_tx, + }) + .await + { + Ok(_) => match response_rx.await { + Ok(Ok(())) => { + // Record stop child result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/stop-child" + .to_string(), + data: EventData::Supervisor( + SupervisorEventData::StopChildResult { + child_id: child_id_clone.clone(), + success: true, + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully stopped child: {}", + child_id_clone + )), + }); + + Ok((Ok(()),)) + } + Ok(Err(e)) => { + // Record stop child error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/stop-child" + .to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "stop-child".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to stop child: {}", e)), + }); + + Ok((Err(e.to_string()),)) + } + Err(e) => { + // Record stop child error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/stop-child" + .to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "stop-child".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to receive stop response: {}", + e + )), + }); + + Ok((Err(format!("Failed to receive stop response: {}", e)),)) + } + }, + Err(e) => { + // Record stop child error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/stop-child".to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "stop-child".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to send stop command: {}", + e + )), + }); + + Ok((Err(format!("Failed to send stop command: {}", e)),)) + } + } + }) + }, + ) + .expect("Failed to wrap stop-child function"); + + // get-child-state implementation + info!("Registering get-child-state function"); + let _ = interface + .func_wrap_async( + "get-child-state", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (child_id,): (String,)| + -> Box< + dyn Future>, String>,)>> + Send, + > { + // Record get child state call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/get-child-state".to_string(), + data: EventData::Supervisor(SupervisorEventData::GetChildStateCall { + child_id: child_id.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Getting state for child: {}", child_id)), + }); + + let store = ctx.data_mut(); + let theater_tx = store.theater_tx.clone(); + let child_id_clone = child_id.clone(); + + Box::new(async move { + let (response_tx, response_rx) = oneshot::channel(); + match theater_tx + .send(TheaterCommand::GetActorState { + actor_id: match child_id.parse() { + Ok(id) => id, + Err(e) => { + // Record error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/get-child-state" + .to_string(), + data: EventData::Supervisor( + SupervisorEventData::Error { + operation: "get-child-state".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to parse child ID: {}", + e + )), + }); + + return Ok((Err(format!("Invalid child ID: {}", e)),)); + } + }, + response_tx, + }) + .await + { + Ok(_) => match response_rx.await { + Ok(Ok(state)) => { + // Record get child state result event + let state_size = state.as_ref().map_or(0, |s| s.len()); + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/get-child-state" + .to_string(), + data: EventData::Supervisor( + SupervisorEventData::GetChildStateResult { + child_id: child_id_clone.clone(), + state_size, + success: true, + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully retrieved state for child {}: {} bytes", + child_id_clone, state_size + )), + }); + + Ok((Ok(state),)) + } + Ok(Err(e)) => { + // Record get child state error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/get-child-state" + .to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "get-child-state".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to get child state: {}", + e + )), + }); + + Ok((Err(e.to_string()),)) + } + Err(e) => { + // Record get child state error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/get-child-state" + .to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "get-child-state".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to receive state: {}", + e + )), + }); + + Ok((Err(format!("Failed to receive state: {}", e)),)) + } + }, + Err(e) => { + // Record get child state error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/get-child-state" + .to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "get-child-state".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to send state request: {}", + e + )), + }); + + Ok((Err(format!("Failed to send state request: {}", e)),)) + } + } + }) + }, + ) + .expect("Failed to wrap get-child-state function"); + + // get-child-events implementation + info!("Registering get-child-events function"); + let _ = interface + .func_wrap_async( + "get-child-events", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (child_id,): (String,)| + -> Box< + dyn Future, String>,)>> + Send, + > { + // Record get child events call event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/get-child-events".to_string(), + data: EventData::Supervisor(SupervisorEventData::GetChildEventsCall { + child_id: child_id.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Getting events for child: {}", child_id)), + }); + + let store = ctx.data_mut(); + let theater_tx = store.theater_tx.clone(); + let child_id_clone = child_id.clone(); + + Box::new(async move { + let (response_tx, response_rx) = oneshot::channel(); + match theater_tx + .send(TheaterCommand::GetActorEvents { + actor_id: match child_id.parse() { + Ok(id) => id, + Err(e) => { + // Record error event + ctx.data_mut().record_event(ChainEventData { + event_type: + "theater:simple/supervisor/get-child-events" + .to_string(), + data: EventData::Supervisor( + SupervisorEventData::Error { + operation: "get-child-events".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to parse child ID: {}", + e + )), + }); + + return Ok((Err(format!("Invalid child ID: {}", e)),)); + } + }, + response_tx, + }) + .await + { + Ok(_) => match response_rx.await { + Ok(Ok(events)) => { + // Record get child events result event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/get-child-events" + .to_string(), + data: EventData::Supervisor( + SupervisorEventData::GetChildEventsResult { + child_id: child_id_clone.clone(), + events_count: events.len(), + success: true, + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Successfully retrieved {} events for child {}", + events.len(), + child_id_clone + )), + }); + + Ok((Ok(events),)) + } + Ok(Err(e)) => { + // Record get child events error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/get-child-events" + .to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "get-child-events".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to get child events: {}", + e + )), + }); + + Ok((Err(e.to_string()),)) + } + Err(e) => { + // Record get child events error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/get-child-events" + .to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "get-child-events".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to receive events: {}", + e + )), + }); + + Ok((Err(format!("Failed to receive events: {}", e)),)) + } + }, + Err(e) => { + // Record get child events error event + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/supervisor/get-child-events" + .to_string(), + data: EventData::Supervisor(SupervisorEventData::Error { + operation: "get-child-events".to_string(), + child_id: Some(child_id_clone.clone()), + message: e.to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!( + "Failed to send events request: {}", + e + )), + }); + + Ok((Err(format!("Failed to send events request: {}", e)),)) + } + } + }) + }, + ) + .expect("Failed to wrap get-child-events function"); + + // Record overall setup completion + actor_component.actor_store.record_event(ChainEventData { + event_type: "supervisor-setup".to_string(), + data: EventData::Supervisor(SupervisorEventData::HandlerSetupSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Supervisor host functions setup completed successfully".to_string()), + }); + + info!("Supervisor host functions added"); + + Ok(()) + } + + fn add_export_functions(&self, actor_instance: &mut ActorInstance) -> Result<()> { + info!("Adding export functions for supervisor"); + + // Register handle-child-error callback + match actor_instance.register_function_no_result::<(String, WitActorError)>( + "theater:simple/supervisor-handlers", + "handle-child-error", + ) { + Ok(_) => { + info!("Successfully registered handle-child-error function"); + } + Err(e) => { + error!("Failed to register handle-child-error function: {}", e); + return Err(anyhow::anyhow!( + "Failed to register handle-child-error function: {}", + e + )); + } + } + + // Register handle-child-exit callback + match actor_instance.register_function_no_result::<(String, Option>)>( + "theater:simple/supervisor-handlers", + "handle-child-exit", + ) { + Ok(_) => { + info!("Successfully registered handle-child-exit function"); + } + Err(e) => { + error!("Failed to register handle-child-exit function: {}", e); + return Err(anyhow::anyhow!( + "Failed to register handle-child-exit function: {}", + e + )); + } + } + + // Register handle-child-external-stop callback + match actor_instance.register_function_no_result::<(String,)>( + "theater:simple/supervisor-handlers", + "handle-child-external-stop", + ) { + Ok(_) => { + info!("Successfully registered handle-child-external-stop function"); + } + Err(e) => { + error!( + "Failed to register handle-child-external-stop function: {}", + e + ); + return Err(anyhow::anyhow!( + "Failed to register handle-child-external-stop function: {}", + e + )); + } + } + + info!("Added all export functions for supervisor"); + Ok(()) + } + + fn start( + &mut self, + actor_handle: ActorHandle, + mut shutdown_receiver: ShutdownReceiver, + ) -> Pin> + Send>> { + info!("Starting supervisor handler"); + + // Take the receiver out of the Arc>> + let channel_rx_opt = self.channel_rx.lock().unwrap().take(); + + Box::pin(async move { + // If we don't have a receiver (e.g., this is a cloned instance), just return Ok + let Some(mut channel_rx) = channel_rx_opt else { + info!("Supervisor handler has no receiver (cloned instance), not starting"); + return Ok(()); + }; + + loop { + tokio::select! { + Some(child_result) = channel_rx.recv() => { + if let Err(e) = Self::process_child_result(&actor_handle, child_result).await { + error!("Error processing child result: {}", e); + } + } + _ = &mut shutdown_receiver.receiver => { + info!("Shutdown signal received"); + break; + } + } + } + info!("Supervisor handler shut down complete"); + Ok(()) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use theater::config::actor_manifest::SupervisorHostConfig; + + #[test] + fn test_supervisor_handler_creation() { + let config = SupervisorHostConfig {}; + let handler = SupervisorHandler::new(config, None); + assert_eq!(handler.name(), "supervisor"); + assert_eq!( + handler.imports(), + Some("theater:simple/supervisor".to_string()) + ); + assert_eq!( + handler.exports(), + Some("theater:simple/supervisor-handlers".to_string()) + ); + } + + #[test] + fn test_supervisor_handler_clone() { + let config = SupervisorHostConfig {}; + let handler = SupervisorHandler::new(config, None); + let cloned = handler.create_instance(); + assert_eq!(cloned.name(), "supervisor"); + } +} diff --git a/crates/theater-handler-timing/Cargo.toml b/crates/theater-handler-timing/Cargo.toml new file mode 100644 index 00000000..12647085 --- /dev/null +++ b/crates/theater-handler-timing/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "theater-handler-timing" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +rust-version.workspace = true + +[dependencies] +# Core theater dependencies +theater = { path = "../theater" } + +# Async runtime +tokio = { workspace = true } +futures = "0.3" + +# Error handling +anyhow = { workspace = true } +thiserror = "1.0" + +# Time handling +chrono = { version = "0.4", features = ["serde"] } + +# WASM runtime +wasmtime = { version = "31.0", features = ["component-model", "async"] } + +# Logging +tracing = { workspace = true } diff --git a/crates/theater-handler-timing/src/lib.rs b/crates/theater-handler-timing/src/lib.rs new file mode 100644 index 00000000..c924eb51 --- /dev/null +++ b/crates/theater-handler-timing/src/lib.rs @@ -0,0 +1,366 @@ +//! # Timing Handler +//! +//! Provides timing capabilities to WebAssembly actors in the Theater system. +//! This handler allows actors to get the current time, sleep for durations, +//! and wait until specific deadlines. + +use anyhow::Result; +use chrono::Utc; +use std::future::Future; +use std::pin::Pin; +use thiserror::Error; +use tokio::time::{sleep, Duration}; +use tracing::{info, error}; +use wasmtime::StoreContextMut; + +use theater::actor::handle::ActorHandle; +use theater::actor::store::ActorStore; +use theater::actor::types::ActorError; +use theater::config::actor_manifest::TimingHostConfig; +use theater::config::enforcement::PermissionChecker; +use theater::config::permissions::TimingPermissions; +use theater::events::timing::TimingEventData; +use theater::events::{ChainEventData, EventData}; +use theater::handler::Handler; +use theater::shutdown::ShutdownReceiver; +use theater::wasm::{ActorComponent, ActorInstance}; + +#[derive(Clone)] +pub struct TimingHandler { + #[allow(dead_code)] + config: TimingHostConfig, + permissions: Option, +} + +#[derive(Error, Debug)] +pub enum TimingError { + #[error("Timing error: {0}")] + TimingError(String), + + #[error("Duration too long: {duration} ms exceeds maximum of {max} ms")] + DurationTooLong { duration: u64, max: u64 }, + + #[error("Duration too short: {duration} ms is below minimum of {min} ms")] + DurationTooShort { duration: u64, min: u64 }, + + #[error("Invalid deadline: {timestamp} is in the past")] + InvalidDeadline { timestamp: u64 }, + + #[error("Actor error: {0}")] + ActorError(#[from] ActorError), +} + +impl TimingHandler { + pub fn new(config: TimingHostConfig, permissions: Option) -> Self { + Self { + config, + permissions, + } + } + + async fn setup_host_functions_impl(&mut self, actor_component: &mut ActorComponent) -> Result<()> { + // Record setup start + actor_component.actor_store.record_event(ChainEventData { + event_type: "timing-setup".to_string(), + data: EventData::Timing(TimingEventData::HandlerSetupStart), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Starting timing host function setup".to_string()), + }); + + info!("Setting up timing host functions"); + + let mut interface = match actor_component.linker.instance("theater:simple/timing") { + Ok(interface) => { + actor_component.actor_store.record_event(ChainEventData { + event_type: "timing-setup".to_string(), + data: EventData::Timing(TimingEventData::LinkerInstanceSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Successfully created linker instance".to_string()), + }); + interface + } + Err(e) => { + actor_component.actor_store.record_event(ChainEventData { + event_type: "timing-setup".to_string(), + data: EventData::Timing(TimingEventData::HandlerSetupError { + error: e.to_string(), + step: "linker_instance".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to create linker instance: {}", e)), + }); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/timing: {}", + e + )); + } + }; + + let permissions = self.permissions.clone(); + + // Implementation of the now() function + let _ = interface + .func_wrap( + "now", + move |mut ctx: StoreContextMut<'_, ActorStore>, ()| -> Result<(u64,)> { + let now = Utc::now().timestamp_millis() as u64; + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/timing/now".to_string(), + data: EventData::Timing(TimingEventData::NowCall {}), + timestamp: now, + description: Some("Getting current timestamp".to_string()), + }); + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/timing/now".to_string(), + data: EventData::Timing(TimingEventData::NowResult { timestamp: now }), + timestamp: now, + description: Some(format!("Current timestamp: {}", now)), + }); + + Ok((now,)) + }, + ) + .map_err(|e| { + actor_component.actor_store.record_event(ChainEventData { + event_type: "timing-setup".to_string(), + data: EventData::Timing(TimingEventData::HandlerSetupError { + error: e.to_string(), + step: "now_function_wrap".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to wrap now function: {}", e)), + }); + anyhow::anyhow!("Failed to wrap now function: {}", e) + })?; + + // Implementation of the sleep() function + let permissions_clone = permissions.clone(); + let _ = interface + .func_wrap_async( + "sleep", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (duration,): (u64,)| + -> Box,)>> + Send> { + let now = Utc::now().timestamp_millis() as u64; + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/timing/sleep".to_string(), + data: EventData::Timing(TimingEventData::SleepCall { duration }), + timestamp: now, + description: Some(format!("Sleeping for {} ms", duration)), + }); + + if let Err(e) = PermissionChecker::check_timing_operation( + &permissions_clone, + "sleep", + duration, + ) { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/timing/permission-denied".to_string(), + data: EventData::Timing(TimingEventData::PermissionDenied { + operation: "sleep".to_string(), + reason: e.to_string(), + }), + timestamp: now, + description: Some(format!("Permission denied for sleep operation: {}", e)), + }); + + return Box::new(futures::future::ready(Ok((Err(format!("Permission denied: {}", e)),)))); + } + + let duration_clone = duration; + + Box::new(async move { + if duration_clone > 0 { + sleep(Duration::from_millis(duration_clone)).await; + } + + let end_time = Utc::now().timestamp_millis() as u64; + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/timing/sleep".to_string(), + data: EventData::Timing(TimingEventData::SleepResult { + duration: duration_clone, + success: true, + }), + timestamp: end_time, + description: Some(format!("Successfully slept for {} ms", duration_clone)), + }); + + Ok((Ok(()),)) + }) + }, + ) + .map_err(|e| { + actor_component.actor_store.record_event(ChainEventData { + event_type: "timing-setup".to_string(), + data: EventData::Timing(TimingEventData::HandlerSetupError { + error: e.to_string(), + step: "sleep_function_wrap".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to wrap sleep function: {}", e)), + }); + anyhow::anyhow!("Failed to wrap sleep function: {}", e) + })?; + + // Implementation of the deadline() function + let permissions_clone2 = permissions.clone(); + let _ = interface + .func_wrap_async( + "deadline", + move |mut ctx: StoreContextMut<'_, ActorStore>, + (timestamp,): (u64,)| + -> Box,)>> + Send> { + let now = Utc::now().timestamp_millis() as u64; + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/timing/deadline".to_string(), + data: EventData::Timing(TimingEventData::DeadlineCall { timestamp }), + timestamp: now, + description: Some(format!("Waiting until timestamp: {}", timestamp)), + }); + + if timestamp <= now { + let success_msg = "Deadline already passed, continuing immediately"; + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/timing/deadline".to_string(), + data: EventData::Timing(TimingEventData::DeadlineResult { + timestamp, + success: true, + }), + timestamp: now, + description: Some(success_msg.to_string()), + }); + + return Box::new(futures::future::ready(Ok((Ok(()),)))); + } + + let duration = timestamp - now; + + if let Err(e) = PermissionChecker::check_timing_operation( + &permissions_clone2, + "deadline", + duration, + ) { + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/timing/permission-denied".to_string(), + data: EventData::Timing(TimingEventData::PermissionDenied { + operation: "deadline".to_string(), + reason: e.to_string(), + }), + timestamp: now, + description: Some(format!("Permission denied for deadline operation: {}", e)), + }); + + return Box::new(futures::future::ready(Ok((Err(format!("Permission denied: {}", e)),)))); + } + + let timestamp_clone = timestamp; + + Box::new(async move { + sleep(Duration::from_millis(duration)).await; + + let end_time = Utc::now().timestamp_millis() as u64; + let reached_deadline = end_time >= timestamp_clone; + + ctx.data_mut().record_event(ChainEventData { + event_type: "theater:simple/timing/deadline".to_string(), + data: EventData::Timing(TimingEventData::DeadlineResult { + timestamp: timestamp_clone, + success: reached_deadline, + }), + timestamp: end_time, + description: Some(format!( + "Deadline wait completed at {}. Target was {}", + end_time, + timestamp_clone + )), + }); + + Ok((Ok(()),)) + }) + }, + ) + .map_err(|e| { + actor_component.actor_store.record_event(ChainEventData { + event_type: "timing-setup".to_string(), + data: EventData::Timing(TimingEventData::HandlerSetupError { + error: e.to_string(), + step: "deadline_function_wrap".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(format!("Failed to wrap deadline function: {}", e)), + }); + anyhow::anyhow!("Failed to wrap deadline function: {}", e) + })?; + + actor_component.actor_store.record_event(ChainEventData { + event_type: "timing-setup".to_string(), + data: EventData::Timing(TimingEventData::HandlerSetupSuccess), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some("Timing host functions setup completed successfully".to_string()), + }); + + info!("Timing host functions added successfully"); + Ok(()) + } + + async fn add_export_functions_impl(&self, _actor_instance: &mut ActorInstance) -> Result<()> { + info!("No export functions needed for timing handler"); + Ok(()) + } + + async fn start_impl( + &self, + _actor_handle: ActorHandle, + _shutdown_receiver: ShutdownReceiver, + ) -> Result<()> { + info!("Starting timing handler"); + Ok(()) + } +} + +impl Handler for TimingHandler { + fn create_instance(&self) -> Box { + Box::new(self.clone()) + } + + fn start( + &mut self, + actor_handle: ActorHandle, + shutdown_receiver: ShutdownReceiver, + ) -> Pin> + Send>> { + let handler = self.clone(); + Box::pin(async move { handler.start_impl(actor_handle, shutdown_receiver).await }) + } + + fn setup_host_functions( + &mut self, + actor_component: &mut ActorComponent, + ) -> Pin> + Send + '_>> { + Box::pin(self.setup_host_functions_impl(actor_component)) + } + + fn add_export_functions( + &self, + actor_instance: &mut ActorInstance, + ) -> Pin> + Send + '_>> { + Box::pin(self.add_export_functions_impl(actor_instance)) + } + + fn name(&self) -> &str { + "timing" + } + + fn imports(&self) -> Option { + Some("theater:simple/timing".to_string()) + } + + fn exports(&self) -> Option { + None + } +} diff --git a/crates/theater-server/src/fragmenting_codec.rs b/crates/theater-server/src/fragmenting_codec.rs index aaa7aa8f..b85c4a9d 100644 --- a/crates/theater-server/src/fragmenting_codec.rs +++ b/crates/theater-server/src/fragmenting_codec.rs @@ -5,15 +5,15 @@ use bytes::{Bytes, BytesMut}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::io; -use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use tokio_util::codec::{Decoder, Encoder, LengthDelimitedCodec}; use tracing::{debug, warn}; /// Maximum size for a single fragment data (12MB) /// This leaves room for JSON serialization overhead while staying well under the 32MB frame limit -const MAX_FRAGMENT_DATA_SIZE: usize = 8 * 1024 * 1024; // Reduced to 8MB to account for base64 + JSON overhead +const MAX_FRAGMENT_DATA_SIZE: usize = 8 * 1024 * 1024; // Reduced to 8MB to account for base64 + JSON overhead /// How long to keep partial messages before timing out (30 seconds) const FRAGMENT_TIMEOUT: Duration = Duration::from_secs(30); @@ -133,7 +133,7 @@ impl SharedState { let mut partial_messages = self.partial_messages.lock().unwrap(); let before_count = partial_messages.len(); - + partial_messages.retain(|message_id, partial| { if partial.is_expired() { warn!("Cleaning up expired partial message {}", message_id); @@ -225,7 +225,7 @@ impl Clone for FragmentingCodec { fn clone(&self) -> Self { let mut inner = LengthDelimitedCodec::new(); inner.set_max_frame_length(32 * 1024 * 1024); // 32MB max frame - CRITICAL! - + Self { inner, shared_state: Arc::clone(&self.shared_state), @@ -339,7 +339,7 @@ impl Decoder for FragmentingCodec { // Remove from partial messages and reassemble let partial = partial_messages.remove(&message_id).unwrap(); drop(partial_messages); // Release the lock - + let complete_data = partial.reassemble()?; Ok(Some(Bytes::from(complete_data))) } else { diff --git a/crates/theater/Cargo.toml b/crates/theater/Cargo.toml index 56c46182..346cf92a 100644 --- a/crates/theater/Cargo.toml +++ b/crates/theater/Cargo.toml @@ -21,6 +21,8 @@ serde_json.workspace = true tracing.workspace = true uuid.workspace = true +theater-chain = { path = "../theater-chain" } + # WebAssembly runtime wasmtime = { version = "31.0", features = ["component-model", "async"] } wit-bindgen = "0.36.0" diff --git a/crates/theater/MIGRATION_GUIDE.md b/crates/theater/MIGRATION_GUIDE.md new file mode 100644 index 00000000..bc914f04 --- /dev/null +++ b/crates/theater/MIGRATION_GUIDE.md @@ -0,0 +1,569 @@ +# Step-by-Step Migration Guide + +This guide shows how to migrate from the current implementation to the explicit state machine **incrementally**, without breaking existing functionality. + +## Phase 1: Define Types (No Behavior Changes) + +### Step 1.1: Add the state enum alongside existing code + +```rust +// In actor/runtime.rs - ADD this, don't replace anything yet + +/// New explicit state enum - will gradually migrate to this +#[allow(dead_code)] // Remove this as we migrate +enum ActorState { + Starting { + setup_task: JoinHandle>, + status_rx: Receiver, + current_status: String, + pending_shutdown: Option>>, + }, + Idle { + resources: ActorResources, + }, + Processing { + resources: ActorResources, + current_operation: JoinHandle, ActorError>>, + operation_name: String, + pending_shutdown: Option>>, + }, + Paused { + resources: ActorResources, + }, + ShuttingDown, +} + +struct ActorResources { + instance: Arc>, + metrics: Arc>, + handler_tasks: Vec>, + shutdown_controller: ShutdownController, +} + +struct SetupComplete { + instance: ActorInstance, + handler_tasks: Vec>, + metrics: MetricsCollector, +} + +enum StateTransition { + Continue(ActorState), + Shutdown, + Error(ActorError), +} +``` + +**Test:** Code still compiles ✅ + +### Step 1.2: Create conversion helpers + +```rust +impl ActorRuntime { + /// Helper to convert from old-style state to new enum + #[allow(dead_code)] + fn to_state_enum( + actor_instance: &Option>>, + metrics: &Option>>, + handler_tasks: &[JoinHandle<()>], + current_operation: &Option>, + paused: bool, + shutdown_requested: bool, + ) -> Option { + if shutdown_requested { + return Some(ActorState::ShuttingDown); + } + + if let (Some(instance), Some(metrics)) = (actor_instance, metrics) { + let resources = ActorResources { + instance: instance.clone(), + metrics: metrics.clone(), + handler_tasks: handler_tasks.to_vec(), + shutdown_controller: ShutdownController::new(), + }; + + if paused { + return Some(ActorState::Paused { resources }); + } else if current_operation.is_some() { + // We'd need more info for Processing, so return None for now + return None; + } else { + return Some(ActorState::Idle { resources }); + } + } + + None // Still starting + } +} +``` + +**Test:** Code still compiles, no behavior changes ✅ + +## Phase 2: Extract One State Handler (First Real Change) + +### Step 2.1: Extract the Paused state handler + +This is the **simplest state** - good place to start! + +```rust +impl ActorRuntime { + /// New handler for Paused state + async fn handle_paused_state_new( + &mut self, + resources: ActorResources, + // Keep receiving from original channels + info_rx: &mut Receiver, + control_rx: &mut Receiver, + parent_shutdown_receiver: &mut ShutdownReceiver, + operation_rx: &mut Receiver, + ) -> StateTransition { + tokio::select! { + Some(_op) = operation_rx.recv() => { + // This shouldn't happen, but if it does, ignore it + error!("Operation received while paused"); + StateTransition::Continue(ActorState::Paused { resources }) + } + + Some(info) = info_rx.recv() => { + self.handle_info_paused(&resources, info).await; + StateTransition::Continue(ActorState::Paused { resources }) + } + + Some(control) = control_rx.recv() => { + match control { + ActorControl::Shutdown { response_tx } => { + info!("Shutdown requested while paused"); + let _ = response_tx.send(Ok(())); + StateTransition::Shutdown + } + ActorControl::Terminate { response_tx } => { + info!("Terminate requested while paused"); + let _ = response_tx.send(Ok(())); + StateTransition::Shutdown + } + ActorControl::Pause { response_tx } => { + let _ = response_tx.send(Ok(())); + StateTransition::Continue(ActorState::Paused { resources }) + } + ActorControl::Resume { response_tx } => { + info!("Resuming actor"); + let _ = response_tx.send(Ok(())); + StateTransition::Continue(ActorState::Idle { resources }) + } + } + } + + shutdown_signal = &mut parent_shutdown_receiver.receiver => { + match shutdown_signal { + Ok(_) => StateTransition::Shutdown, + Err(e) => { + error!("Shutdown signal error: {:?}", e); + StateTransition::Shutdown + } + } + } + } + } + + async fn handle_info_paused(&self, resources: &ActorResources, info: ActorInfo) { + match info { + ActorInfo::GetStatus { response_tx } => { + let _ = response_tx.send(Ok("Paused".to_string())); + } + ActorInfo::GetState { response_tx } => { + let instance = resources.instance.read().await; + let state = instance.store.data().get_state(); + let _ = response_tx.send(Ok(state)); + } + ActorInfo::GetChain { response_tx } => { + let instance = resources.instance.read().await; + let chain = instance.store.data().get_chain(); + let _ = response_tx.send(Ok(chain)); + } + ActorInfo::GetMetrics { response_tx } => { + let metrics = resources.metrics.read().await; + let metrics_data = metrics.get_metrics().await; + let _ = response_tx.send(Ok(metrics_data)); + } + ActorInfo::SaveChain { response_tx } => { + let instance = resources.instance.read().await; + match instance.save_chain() { + Ok(_) => { let _ = response_tx.send(Ok(())); } + Err(e) => { let _ = response_tx.send(Err(ActorError::UnexpectedError(e.to_string()))); } + } + } + } + } +} +``` + +### Step 2.2: Integrate into existing code + +In your main `start()` loop, add this **at the very top** of the select block: + +```rust +loop { + // NEW: Check if we're in paused state and use new handler + if *paused.read().await && actor_instance.is_some() && current_operation.is_none() { + if let Some(resources) = Self::build_resources( + &actor_instance, + &metrics, + &handler_tasks, + &shutdown_controller, + ) { + info!("Using new paused state handler"); + match Self::handle_paused_state_new( + &mut self, + resources, + &mut info_rx, + &mut control_rx, + &mut parent_shutdown_receiver, + &mut operation_rx, + ).await { + StateTransition::Continue(ActorState::Idle { resources }) => { + // Unpack resources back into old variables + actor_instance = Some(resources.instance); + metrics = Some(resources.metrics); + handler_tasks = resources.handler_tasks; + *paused.write().await = false; + continue; + } + StateTransition::Continue(ActorState::Paused { .. }) => { + // Stay paused + continue; + } + StateTransition::Shutdown | StateTransition::Error(_) => { + break; + } + _ => unreachable!(), + } + } + } + + // EXISTING: Original select block + tokio::select! { + // ... all your existing code ... + } +} +``` + +**Test:** +- Run all existing tests ✅ +- Specifically test pausing/resuming ✅ +- Add new unit tests for `handle_paused_state_new` ✅ + +### Step 2.3: Remove old paused handling + +Once you've verified the new handler works, you can remove the old pause/resume logic from the main select block. + +## Phase 3: Extract Idle State + +This is the next most straightforward state. + +```rust +impl ActorRuntime { + async fn handle_idle_state_new( + &mut self, + resources: ActorResources, + info_rx: &mut Receiver, + control_rx: &mut Receiver, + operation_rx: &mut Receiver, + parent_shutdown_receiver: &mut ShutdownReceiver, + theater_tx: &Sender, + ) -> StateTransition { + tokio::select! { + Some(op) = operation_rx.recv() => { + match op { + ActorOperation::CallFunction { name, params, response_tx } => { + info!("Starting operation: {}", name); + + let operation_task = self.spawn_operation( + &resources, + name.clone(), + params, + response_tx, + theater_tx, + ); + + StateTransition::Continue(ActorState::Processing { + resources, + current_operation: operation_task, + operation_name: name, + pending_shutdown: None, + }) + } + ActorOperation::UpdateComponent { component_address: _, response_tx } => { + let _ = response_tx.send(Err(ActorError::UpdateComponentError( + "Not implemented".to_string() + ))); + StateTransition::Continue(ActorState::Idle { resources }) + } + } + } + + Some(info) = info_rx.recv() => { + self.handle_info_idle(&resources, info).await; + StateTransition::Continue(ActorState::Idle { resources }) + } + + Some(control) = control_rx.recv() => { + match control { + ActorControl::Shutdown { response_tx } => { + let _ = response_tx.send(Ok(())); + StateTransition::Shutdown + } + ActorControl::Terminate { response_tx } => { + let _ = response_tx.send(Ok(())); + StateTransition::Shutdown + } + ActorControl::Pause { response_tx } => { + let _ = response_tx.send(Ok(())); + StateTransition::Continue(ActorState::Paused { resources }) + } + ActorControl::Resume { response_tx } => { + let _ = response_tx.send(Err(ActorError::NotPaused)); + StateTransition::Continue(ActorState::Idle { resources }) + } + } + } + + shutdown_signal = &mut parent_shutdown_receiver.receiver => { + match shutdown_signal { + Ok(_) => StateTransition::Shutdown, + Err(e) => { + error!("Shutdown signal error: {:?}", e); + StateTransition::Shutdown + } + } + } + } + } +} +``` + +## Phase 4: Extract Processing State + +This is more complex because of operation tracking. + +```rust +async fn handle_processing_state_new( + &mut self, + resources: ActorResources, + mut current_operation: JoinHandle, ActorError>>, + operation_name: String, + pending_shutdown: Option>>, + info_rx: &mut Receiver, + control_rx: &mut Receiver, + parent_shutdown_receiver: &mut ShutdownReceiver, + operation_rx: &mut Receiver, +) -> StateTransition { + tokio::select! { + result = &mut current_operation => { + info!("Operation '{}' completed: {:?}", operation_name, result); + + if let Some(response_tx) = pending_shutdown { + let _ = response_tx.send(Ok(())); + return StateTransition::Shutdown; + } + + StateTransition::Continue(ActorState::Idle { resources }) + } + + Some(info) = info_rx.recv() => { + self.handle_info_processing(&resources, info).await; + StateTransition::Continue(ActorState::Processing { + resources, + current_operation, + operation_name, + pending_shutdown, + }) + } + + Some(control) = control_rx.recv() => { + match control { + ActorControl::Shutdown { response_tx } => { + info!("Shutdown requested - waiting for operation to complete"); + StateTransition::Continue(ActorState::Processing { + resources, + current_operation, + operation_name, + pending_shutdown: Some(response_tx), + }) + } + ActorControl::Terminate { response_tx } => { + info!("Terminate requested - aborting operation"); + current_operation.abort(); + let _ = response_tx.send(Ok(())); + StateTransition::Shutdown + } + ActorControl::Pause { response_tx } => { + let _ = response_tx.send(Err(ActorError::UnexpectedError( + "Cannot pause during operation".to_string() + ))); + StateTransition::Continue(ActorState::Processing { + resources, + current_operation, + operation_name, + pending_shutdown, + }) + } + ActorControl::Resume { response_tx } => { + let _ = response_tx.send(Err(ActorError::NotPaused)); + StateTransition::Continue(ActorState::Processing { + resources, + current_operation, + operation_name, + pending_shutdown, + }) + } + } + } + + shutdown_signal = &mut parent_shutdown_receiver.receiver => { + match shutdown_signal { + Ok(signal) => { + match signal.shutdown_type { + ShutdownType::Graceful => { + info!("Graceful shutdown - waiting for operation"); + StateTransition::Continue(ActorState::Processing { + resources, + current_operation, + operation_name, + pending_shutdown, + }) + } + ShutdownType::Force => { + current_operation.abort(); + StateTransition::Shutdown + } + } + } + Err(e) => { + error!("Shutdown signal error: {:?}", e); + StateTransition::Shutdown + } + } + } + + // Ignore new operations while processing + Some(_) = operation_rx.recv() => { + error!("Operation received while processing - this shouldn't happen"); + StateTransition::Continue(ActorState::Processing { + resources, + current_operation, + operation_name, + pending_shutdown, + }) + } + } +} +``` + +## Phase 5: Extract Starting State + +This is the most complex, save it for last. + +## Phase 6: Replace Main Loop + +Once all state handlers are extracted and tested, replace the main loop: + +```rust +pub async fn start(...) { + let mut state = ActorState::Starting { /* ... */ }; + + loop { + let transition = match state { + ActorState::Starting { .. } => self.handle_starting_state().await, + ActorState::Idle { .. } => self.handle_idle_state().await, + ActorState::Processing { .. } => self.handle_processing_state().await, + ActorState::Paused { .. } => self.handle_paused_state().await, + ActorState::ShuttingDown => break, + }; + + match transition { + StateTransition::Continue(new_state) => { + state = new_state; + } + StateTransition::Shutdown => { + self.transition_to_shutdown(state).await; + break; + } + StateTransition::Error(error) => { + self.notify_error(error).await; + self.transition_to_shutdown(state).await; + break; + } + } + } +} +``` + +## Testing Strategy + +For each phase: + +1. **Write tests first** for the new state handler +2. **Run existing integration tests** to ensure no regressions +3. **Add logging** to track state transitions +4. **Run in development** for a period before moving to next phase + +### Example Test + +```rust +#[tokio::test] +async fn test_pause_from_idle() { + let (mut runtime, channels) = create_test_runtime(); + + // Set up idle state + let resources = create_test_resources(); + runtime.state = ActorState::Idle { resources }; + + // Send pause command + let (response_tx, response_rx) = oneshot::channel(); + channels.control_tx.send(ActorControl::Pause { response_tx }).await.unwrap(); + + // Handle state + let transition = runtime.handle_idle_state_new(/* ... */).await; + + // Verify transition to paused + assert!(matches!(transition, StateTransition::Continue(ActorState::Paused { .. }))); + + // Verify response + assert!(response_rx.await.unwrap().is_ok()); +} +``` + +## Rollback Plan + +At any phase, if issues arise: + +1. Comment out the new handler integration +2. The old code path still works +3. Fix the issue +4. Re-enable the new handler + +This is the beauty of incremental migration! + +## Timeline Estimate + +- **Phase 1 (Types):** 1-2 hours +- **Phase 2 (Paused):** 4-6 hours (includes testing) +- **Phase 3 (Idle):** 4-6 hours +- **Phase 4 (Processing):** 6-8 hours (most complex) +- **Phase 5 (Starting):** 6-8 hours +- **Phase 6 (Replace main loop):** 2-4 hours +- **Total:** ~25-35 hours spread over 1-2 weeks + +But you get incremental benefits after each phase! + +## Success Metrics + +You'll know the refactoring is working when: + +- ✅ New code has fewer lines but is more readable +- ✅ State transitions are logged clearly +- ✅ Tests are easier to write +- ✅ Bugs in state handling decrease +- ✅ New developers can understand the code faster +- ✅ Adding new states is straightforward + +Good luck! 🚀 diff --git a/crates/theater/QUICK_WINS.md b/crates/theater/QUICK_WINS.md new file mode 100644 index 00000000..82aa62e5 --- /dev/null +++ b/crates/theater/QUICK_WINS.md @@ -0,0 +1,437 @@ +# Quick Wins: Immediate Improvements You Can Make Today + +These are small, low-risk refactorings you can do RIGHT NOW to start improving the codebase without committing to the full state machine refactor. + +## Quick Win #1: Extract Helper Methods (30 minutes) + +### Before: Inline Operation Execution +```rust +// In the giant select block +Some(op) = operation_rx.recv(), if actor_instance.is_some() && current_operation.is_none() => { + match op { + ActorOperation::CallFunction { name, params, response_tx } => { + let theater_tx = theater_tx.clone(); + let metrics = metrics.clone(); + let actor_instance = actor_instance.clone(); + + current_operation = Some(tokio::spawn(async move { + let mut actor_instance = actor_instance.write().await; + let metrics = metrics.write().await; + match Self::execute_call(&mut actor_instance, &name, params, &theater_tx, &metrics).await { + // 20+ lines of handling... + } + })); + } + } +} +``` + +### After: Extracted Method +```rust +// In the select block +Some(op) = operation_rx.recv(), if actor_instance.is_some() && current_operation.is_none() => { + current_operation = Some(self.spawn_operation_task(op, &actor_instance, &metrics)); +} + +// New helper method +fn spawn_operation_task( + &self, + op: ActorOperation, + actor_instance: &Arc>, + metrics: &Arc>, +) -> JoinHandle<()> { + match op { + ActorOperation::CallFunction { name, params, response_tx } => { + let theater_tx = self.theater_tx.clone(); + let metrics = metrics.clone(); + let actor_instance = actor_instance.clone(); + + tokio::spawn(async move { + let mut actor_instance = actor_instance.write().await; + let metrics = metrics.write().await; + match Self::execute_call(&mut actor_instance, &name, params, &theater_tx, &metrics).await { + Ok(result) => { + let _ = response_tx.send(Ok(result)); + } + Err(error) => { + let _ = theater_tx.send(TheaterCommand::ActorError { + actor_id: actor_instance.id().clone(), + error: error.clone(), + }).await; + let _ = response_tx.send(Err(error)); + } + } + }) + } + ActorOperation::UpdateComponent { component_address: _, response_tx } => { + tokio::spawn(async move { + let _ = response_tx.send(Err(ActorError::UpdateComponentError("Not implemented".to_string()))); + }) + } + } +} +``` + +**Impact:** Reduces select block by ~20 lines, improves readability + +--- + +## Quick Win #2: Use Enums for Status (15 minutes) + +### Before: String Status +```rust +let mut current_status = "Starting".to_string(); + +// Later... +current_status = "Ready".to_string(); +current_status = "Processing".to_string(); +current_status = "Shutting down".to_string(); +``` + +### After: Enum Status +```rust +#[derive(Debug, Clone, Copy)] +enum RuntimeStatus { + Starting, + SettingUpStore, + CreatingHandlers, + CreatingComponent, + SettingUpHostFunctions, + Instantiating, + Ready, + Processing, + Paused, + ShuttingDown, +} + +impl RuntimeStatus { + fn as_str(&self) -> &'static str { + match self { + Self::Starting => "Starting", + Self::SettingUpStore => "Setting up actor store", + Self::CreatingHandlers => "Creating handlers", + Self::CreatingComponent => "Creating component", + Self::SettingUpHostFunctions => "Setting up host functions", + Self::Instantiating => "Instantiating component", + Self::Ready => "Ready", + Self::Processing => "Processing", + Self::Paused => "Paused", + Self::ShuttingDown => "Shutting down", + } + } +} + +let mut current_status = RuntimeStatus::Starting; +``` + +**Impact:** Type safety, no typos, better autocomplete + +--- + +## Quick Win #3: Extract Status Determination (20 minutes) + +### Before: Scattered Status Logic +```rust +ActorInfo::GetStatus { response_tx } => { + let status = if shutdown_requested { + if setup_task.is_some() { + "Shutting down (during startup)".to_string() + } else if current_operation.is_some() { + "Shutting down (waiting for operation)".to_string() + } else { + "Shutting down".to_string() + } + } else if setup_task.is_some() { + current_status.clone() + } else if *paused.read().await { + "Paused".to_string() + } else if current_operation.is_some() { + "Processing".to_string() + } else { + "Idle".to_string() + }; + let _ = response_tx.send(Ok(status)); +} +``` + +### After: Extracted Method +```rust +ActorInfo::GetStatus { response_tx } => { + let status = self.current_status( + shutdown_requested, + &setup_task, + ¤t_operation, + ¤t_status, + &paused, + ).await; + let _ = response_tx.send(Ok(status.as_str().to_string())); +} + +async fn current_status( + &self, + shutdown_requested: bool, + setup_task: &Option>, + current_operation: &Option>, + startup_status: &RuntimeStatus, + paused: &Arc>, +) -> RuntimeStatus { + if shutdown_requested { + return RuntimeStatus::ShuttingDown; + } + + if setup_task.is_some() { + return *startup_status; + } + + if *paused.read().await { + return RuntimeStatus::Paused; + } + + if current_operation.is_some() { + return RuntimeStatus::Processing; + } + + RuntimeStatus::Ready +} +``` + +**Impact:** Logic is testable, reusable, clear + +--- + +## Quick Win #4: Named Constants for Magic Numbers (10 minutes) + +### Before +```rust +let (status_tx, status_rx) = mpsc::channel(10); +let (mailbox_tx, mailbox_rx) = mpsc::channel(100); +``` + +### After +```rust +const STATUS_CHANNEL_SIZE: usize = 10; +const MAILBOX_CHANNEL_SIZE: usize = 100; + +let (status_tx, status_rx) = mpsc::channel(STATUS_CHANNEL_SIZE); +let (mailbox_tx, mailbox_rx) = mpsc::channel(MAILBOX_CHANNEL_SIZE); +``` + +**Impact:** Self-documenting, easier to tune + +--- + +## Quick Win #5: Builder for SpawnActor (45 minutes) + +### Before: 6 Parameters +```rust +async fn spawn_actor( + &mut self, + manifest_path: String, + init_bytes: Option>, + parent_id: Option, + init: bool, + supervisor_tx: Option>, + subscription_tx: Option>>, +) -> Result +``` + +### After: Builder Pattern +```rust +pub struct SpawnActorRequest { + manifest_path: String, + init_bytes: Option>, + parent_id: Option, + init: bool, + supervisor_tx: Option>, + subscription_tx: Option>>, +} + +impl SpawnActorRequest { + pub fn new(manifest_path: impl Into) -> Self { + Self { + manifest_path: manifest_path.into(), + init_bytes: None, + parent_id: None, + init: true, + supervisor_tx: None, + subscription_tx: None, + } + } + + pub fn with_init_bytes(mut self, bytes: Vec) -> Self { + self.init_bytes = Some(bytes); + self + } + + pub fn with_parent(mut self, parent_id: TheaterId) -> Self { + self.parent_id = Some(parent_id); + self + } + + pub fn no_init(mut self) -> Self { + self.init = false; + self + } + + pub fn with_supervisor(mut self, tx: Sender) -> Self { + self.supervisor_tx = Some(tx); + self + } + + pub fn with_subscription(mut self, tx: Sender>) -> Self { + self.subscription_tx = Some(tx); + self + } +} + +async fn spawn_actor(&mut self, request: SpawnActorRequest) -> Result { + // Use request.manifest_path, request.init_bytes, etc. +} + +// Usage: +let actor_id = runtime.spawn_actor( + SpawnActorRequest::new("path/to/manifest.toml") + .with_parent(parent_id) + .with_supervisor(supervisor_tx) +).await?; +``` + +**Impact:** Self-documenting, flexible, easier to evolve + +--- + +## Quick Win #6: Extract Channel Handling (1 hour) + +The channel handling code is ~100 lines in the main select. Extract it: + +```rust +// In theater_runtime.rs +impl TheaterRuntime { + async fn handle_channel_command(&mut self, cmd: ChannelCommand) -> Result<()> { + match cmd { + ChannelCommand::Open { initiator_id, target_id, channel_id, initial_message, response_tx } => { + self.handle_channel_open(initiator_id, target_id, channel_id, initial_message, response_tx).await + } + ChannelCommand::Message { channel_id, message, sender_id } => { + self.handle_channel_message(channel_id, message, sender_id).await + } + ChannelCommand::Close { channel_id } => { + self.handle_channel_close(channel_id).await + } + } + } +} + +// In the main loop: +match cmd { + TheaterCommand::ChannelOpen { .. } => { + self.handle_channel_command(ChannelCommand::Open { .. }).await?; + } + // etc. +} +``` + +**Impact:** Main select block gets much smaller, channel logic is grouped + +--- + +## Quick Win #7: Consolidate Error Responses (30 minutes) + +### Before: Repeated Pattern +```rust +match result { + Ok(value) => { + if let Err(e) = response_tx.send(Ok(value)) { + error!("Failed to send response: {:?}", e); + } + } + Err(e) => { + error!("Operation failed: {}", e); + if let Err(send_err) = response_tx.send(Err(e)) { + error!("Failed to send error response: {:?}", send_err); + } + } +} +``` + +### After: Helper Method +```rust +fn respond(response_tx: oneshot::Sender>, result: Result, operation: &str) { + match &result { + Ok(_) => debug!("Operation '{}' succeeded", operation), + Err(e) => error!("Operation '{}' failed: {}", operation, e), + } + + if let Err(_) = response_tx.send(result) { + error!("Failed to send response for operation '{}'", operation); + } +} + +// Usage: +Self::respond(response_tx, result, "spawn_actor"); +``` + +**Impact:** DRY, consistent logging, less boilerplate + +--- + +## Quick Win #8: Add Tracing Spans (20 minutes) + +### Before: Individual debug/info calls +```rust +debug!("Starting operation: {}", name); +// ... operation code ... +debug!("Operation completed: {}", name); +``` + +### After: Tracing Spans +```rust +use tracing::{info_span, Instrument}; + +let span = info_span!("operation", name = %name); +async move { + // operation code +} +.instrument(span) +.await +``` + +**Impact:** Better structured logging, easier debugging with distributed tracing + +--- + +## Implementation Order + +Do these in order for maximum impact with minimum risk: + +1. **Quick Win #4** (10 min) - Named constants, zero risk +2. **Quick Win #2** (15 min) - Status enum, minimal risk +3. **Quick Win #7** (30 min) - Error response helper, safe +4. **Quick Win #3** (20 min) - Extract status determination, testable +5. **Quick Win #1** (30 min) - Extract operation spawning +6. **Quick Win #8** (20 min) - Add tracing spans +7. **Quick Win #5** (45 min) - Builder pattern (breaking change, needs more thought) +8. **Quick Win #6** (1 hour) - Extract channel handling + +**Total time for wins 1-6:** ~2.5 hours +**Total impact:** Significantly more readable code, foundation for bigger refactors + +## Measuring Success + +After implementing these quick wins: + +- [ ] Main `start()` method is 50+ lines shorter +- [ ] Main `run()` method in TheaterRuntime is 100+ lines shorter +- [ ] At least 3 new helper methods with unit tests +- [ ] No regressions in existing test suite +- [ ] Team agrees code is more readable + +## Next Steps + +Once you've done these quick wins, you'll have: +1. Cleaner code that's easier to work with +2. Better understanding of the codebase +3. Foundation for the bigger state machine refactor +4. Confidence that incremental improvements work + +Then you can decide: continue with quick wins, or start the state machine refactor from the migration guide! diff --git a/crates/theater/REFACTORING_ANALYSIS.md b/crates/theater/REFACTORING_ANALYSIS.md new file mode 100644 index 00000000..fe83afc9 --- /dev/null +++ b/crates/theater/REFACTORING_ANALYSIS.md @@ -0,0 +1,325 @@ +# Actor Runtime Refactoring: Before & After Analysis + +## The Problem with the Current Implementation + +### State Management Complexity +The current implementation manages state through **7+ mutable variables** with complex interactions: + +```rust +let mut actor_instance: Option>> = None; +let mut metrics: Option>> = None; +let mut handler_tasks: Vec> = Vec::new(); +let mut current_operation: Option> = None; +let mut shutdown_requested = false; +let mut shutdown_response_tx: Option> = None; +let mut current_status = "Starting".to_string(); +``` + +**Problems:** +- No clear indication of valid states +- Easy to have inconsistent state (e.g., `actor_instance` is Some but `metrics` is None) +- `Option` unwrapping everywhere creates boilerplate and panic risks +- Boolean flags (`shutdown_requested`) don't compose well + +### The Giant Select Loop +The current `start()` method has a ~400 line `tokio::select!` with 8+ branches, each handling multiple states implicitly: + +```rust +Some(op) = operation_rx.recv(), if actor_instance.is_some() && current_operation.is_none() && !*paused.read().await => { + // Can only receive operations if: + // - Setup is complete (actor_instance exists) + // - No operation is running + // - Not paused + // This logic is buried in the guard! +} +``` + +**Problems:** +- State transition logic is scattered across branches +- Guards on select branches hide important logic +- Hard to see which messages are valid in which states +- Testing individual states is nearly impossible + +### Unclear State Transitions +Try answering these questions from the current code: +- Can I pause during startup? (No, but you have to read the control handler to know) +- What happens if shutdown is requested during an operation? (Waits for completion, but this is implicit) +- Can info requests work during startup? (Some can, some can't - depends on the request) + +The answers exist in the code, but they're **not obvious**. + +## The Solution: Explicit State Machine + +### State Definition +```rust +enum ActorState { + Starting { + setup_task: JoinHandle>, + status_rx: Receiver, + current_status: String, + pending_shutdown: Option>>, + }, + Idle { + resources: ActorResources, + }, + Processing { + resources: ActorResources, + current_operation: JoinHandle, ActorError>>, + operation_name: String, + pending_shutdown: Option>>, + }, + Paused { + resources: ActorResources, + }, + ShuttingDown, +} +``` + +### Benefits + +#### 1. **Impossible States Are Unrepresentable** +You can't have `actor_instance = None` while in the `Processing` state, because `Processing` contains `resources: ActorResources` which includes the instance. + +The compiler enforces correctness! + +#### 2. **Clear State Transitions** +```rust +loop { + let next_state = match &mut self.state { + ActorState::Starting { .. } => self.handle_starting_state().await, + ActorState::Idle { .. } => self.handle_idle_state().await, + ActorState::Processing { .. } => self.handle_processing_state().await, + ActorState::Paused { .. } => self.handle_paused_state().await, + ActorState::ShuttingDown => break, + }; + + match next_state { + StateTransition::Continue(new_state) => self.state = new_state, + StateTransition::Shutdown => { + self.transition_to_shutdown().await; + break; + } + StateTransition::Error(error) => { + self.notify_error(error).await; + self.transition_to_shutdown().await; + break; + } + } +} +``` + +**Every state transition is explicit and visible!** + +#### 3. **Each State Handler is Focused** +Instead of one giant select handling all states: + +```rust +async fn handle_idle_state(&mut self) -> StateTransition { + let resources = /* extract from state */; + + tokio::select! { + Some(op) = self.operation_rx.recv() => { + // Start operation + StateTransition::Continue(ActorState::Processing { ... }) + } + Some(control) = self.control_rx.recv() => { + match control { + ActorControl::Pause { response_tx } => { + StateTransition::Continue(ActorState::Paused { resources }) + } + // ... + } + } + // ... + } +} +``` + +**Much easier to understand!** Each handler only deals with messages relevant to that state. + +#### 4. **Easier Testing** +You can test individual state handlers: + +```rust +#[tokio::test] +async fn test_pause_during_idle() { + let mut machine = create_test_machine(ActorState::Idle { ... }); + + send_control_message(&machine, ActorControl::Pause); + + let transition = machine.handle_idle_state().await; + + assert!(matches!(transition, StateTransition::Continue(ActorState::Paused { .. }))); +} +``` + +#### 5. **Better Error Handling** +Errors are handled at the state machine level: + +```rust +StateTransition::Error(error) => { + self.notify_error(error).await; + self.transition_to_shutdown().await; + break; +} +``` + +No more scattered error handling! + +## Side-by-Side Comparison + +### Handling Shutdown During Operation + +**Before (implicit):** +```rust +ActorControl::Shutdown { response_tx } => { + if setup_task.is_some() { + shutdown_requested = true; + shutdown_response_tx = Some(response_tx); + } else if current_operation.is_some() { + shutdown_requested = true; + shutdown_response_tx = Some(response_tx); + } else { + let _ = response_tx.send(Ok(())); + break; + } +} +``` + +**After (explicit):** +```rust +// In handle_processing_state() +ActorControl::Shutdown { response_tx } => { + info!("Shutdown requested during operation - will complete after operation"); + StateTransition::Continue(ActorState::Processing { + resources, + current_operation, + operation_name, + pending_shutdown: Some(response_tx), // Clear intent! + }) +} +``` + +### Handling Operation Completion + +**Before:** +```rust +_ = async { + match current_operation.as_mut() { + Some(task) => task.await, + None => std::future::pending().await, + } +} => { + info!("Operation completed"); + current_operation = None; + + // Check if shutdown was requested and no more operations are running + if shutdown_requested { + if let Some(response_tx) = shutdown_response_tx.take() { + let _ = response_tx.send(Ok(())); + } + break; + } +} +``` + +**After:** +```rust +// In handle_processing_state() +result = current_operation => { + info!("Operation '{}' completed", operation_name); + + // If shutdown was pending, do it now + if let Some(response_tx) = pending_shutdown { + let _ = response_tx.send(Ok(())); + return StateTransition::Shutdown; + } + + StateTransition::Continue(ActorState::Idle { resources }) +} +``` + +## Metrics + +### Lines of Code +- **Current `start()` method:** ~400 lines +- **Refactored:** + - `run()` loop: ~40 lines + - `handle_starting_state()`: ~80 lines + - `handle_idle_state()`: ~60 lines + - `handle_processing_state()`: ~70 lines + - `handle_paused_state()`: ~40 lines + - **Total:** ~290 lines, but **much more readable** + +### Cognitive Complexity +- **Current:** High - need to track 7+ variables and their interactions +- **Refactored:** Low - each state handler is self-contained + +### Testability +- **Current:** Hard - need to mock the entire runtime to test specific scenarios +- **Refactored:** Easy - can test individual state handlers in isolation + +## Migration Path + +You don't have to do this all at once! Here's a suggested migration: + +1. **Phase 1:** Create the new state enum and `ActorResources` struct +2. **Phase 2:** Extract one state handler (e.g., `handle_idle_state()`) +3. **Phase 3:** Gradually migrate other states +4. **Phase 4:** Replace the old implementation once all states are migrated +5. **Phase 5:** Add tests for individual state handlers + +## Additional Benefits + +### Documentation +The state machine *is* the documentation: +```rust +enum ActorState { + Starting { /* ... */ }, // Actor is loading + Idle { /* ... */ }, // Waiting for work + Processing { /* ... */ }, // Executing operation + Paused { /* ... */ }, // Paused by user + ShuttingDown, // Cleaning up +} +``` + +Anyone can understand the actor lifecycle at a glance! + +### Debugging +State transitions are logged: +``` +Actor abc123 state: Starting -> Idle +Actor abc123 state: Idle -> Processing (operation: calculate) +Actor abc123 state: Processing -> Idle +Actor abc123 state: Idle -> ShuttingDown +``` + +Much easier to debug than tracking boolean flags! + +### Future Extensions +Want to add a new state like "Suspended" or "Upgrading"? Just add it to the enum: + +```rust +enum ActorState { + // ... existing states ... + Suspended { + resources: ActorResources, + snapshot: Snapshot, + }, +} +``` + +And implement `handle_suspended_state()`. The compiler will tell you everywhere you need to handle it! + +## Conclusion + +The explicit state machine: +- ✅ Makes impossible states unrepresentable +- ✅ Makes state transitions clear and explicit +- ✅ Reduces cognitive load +- ✅ Improves testability +- ✅ Better error handling +- ✅ Self-documenting +- ✅ Easier to extend + +**This is a high-impact refactoring that will pay dividends for the lifetime of the project.** diff --git a/crates/theater/REFACTORING_SUMMARY.md b/crates/theater/REFACTORING_SUMMARY.md new file mode 100644 index 00000000..c880de95 --- /dev/null +++ b/crates/theater/REFACTORING_SUMMARY.md @@ -0,0 +1,264 @@ +# Theater Refactoring: Summary & Recommendations + +## What We Analyzed + +Your Theater project is a WebAssembly actor system with: +- ~62KB `theater_runtime.rs` +- ~41KB `actor/runtime.rs` +- Complex state management with implicit state machines +- Long function signatures and scattered error handling + +**You were right** - this needs refactoring! But the good news is the core architecture is solid, you just need better organization. + +## The Core Problem + +Both `theater_runtime.rs` and `actor/runtime.rs` suffer from the same issue: **implicit state machines** managed with boolean flags and `Option` types. + +Your actor runtime has ~7 pieces of mutable state: +```rust +let mut actor_instance: Option>> = None; +let mut metrics: Option>> = None; +let mut handler_tasks: Vec> = Vec::new(); +let mut current_operation: Option> = None; +let mut shutdown_requested = bool = false; +let mut shutdown_response_tx: Option> = None; +let mut current_status = String = "Starting"; +``` + +This creates a **state explosion** where it's hard to know: +- Which states are valid +- What transitions are allowed +- Which messages can be handled in which states + +## The Solution: Explicit State Machine + +Make the state machine explicit: + +```rust +enum ActorState { + Starting { setup_task, status_rx, current_status, pending_shutdown }, + Idle { resources }, + Processing { resources, current_operation, operation_name, pending_shutdown }, + Paused { resources }, + ShuttingDown, +} +``` + +Then handle each state separately: + +```rust +loop { + state = match state { + ActorState::Starting { .. } => handle_starting_state().await, + ActorState::Idle { .. } => handle_idle_state().await, + ActorState::Processing { .. } => handle_processing_state().await, + ActorState::Paused { .. } => handle_paused_state().await, + ActorState::ShuttingDown => break, + } +} +``` + +## Three Paths Forward + +### Path A: Quick Wins Only (2-3 hours) +**Best for:** Getting immediate improvements without major commitment + +**What you do:** +1. Extract helper methods to reduce main loop size +2. Add status enum for type safety +3. Use builder pattern for complex function calls +4. Extract channel handling logic +5. Add tracing spans + +**Result:** +- Code is 20-30% more readable +- Foundation for future refactors +- Zero risk to existing functionality + +See: `QUICK_WINS.md` + +### Path B: Incremental State Machine (1-2 weeks) +**Best for:** Systematic improvement while maintaining stability + +**What you do:** +1. Week 1: Implement quick wins + define state types +2. Week 2: Extract Paused state, then Idle, then Processing +3. Test after each extraction +4. Finally replace main loop + +**Result:** +- Major improvement in code clarity +- State transitions become explicit +- Much easier to test +- Can roll back at any phase if needed + +See: `MIGRATION_GUIDE.md` + +### Path C: Full Rewrite (3-4 weeks) +**Best for:** You have time and want the cleanest result + +**What you do:** +1. Create new `runtime_refactored.rs` +2. Implement full state machine from scratch +3. Extensive testing +4. Switch over when confident +5. Remove old code + +**Result:** +- Cleanest possible result +- No baggage from old implementation +- Higher risk during development + +See: `runtime_refactored.rs` + +## My Recommendation + +**Start with Path A (Quick Wins), then do Path B (Incremental Migration)** + +Why? +1. Quick wins give you immediate value (~2-3 hours) +2. You'll understand the codebase better after quick wins +3. Incremental migration is lower risk than full rewrite +4. You can stop at any point and still have improvements +5. Total time investment: ~30-40 hours over 2-3 weeks + +## Expected Benefits + +### Code Metrics +- **Lines in main loop:** 400 → ~100 +- **Cognitive complexity:** High → Low +- **Test coverage:** Difficult → Easy +- **Onboarding time:** Hours → Minutes + +### Development Velocity +- Adding new features: Easier (clear where to add code) +- Debugging: Much easier (clear state transitions) +- Testing: Much easier (test individual states) +- Code review: Much easier (smaller, focused changes) + +### Architecture Quality +- **Impossible states:** Unrepresentable (compiler enforces) +- **State transitions:** Explicit and visible +- **Error handling:** Centralized and consistent +- **Documentation:** Code is self-documenting + +## What About TheaterRuntime? + +The same principles apply! After refactoring `ActorRuntime`, you can apply the same pattern to `TheaterRuntime`: + +1. Extract command handlers into methods +2. Create manager structs (ActorManager, ChannelManager, etc.) +3. Use builder pattern for complex spawning +4. Consider state machine for runtime lifecycle + +But start with `ActorRuntime` - it's more complex and will teach you the pattern. + +## Timeline + +### Optimistic (Full-time focus) +- Week 1: Quick wins + state types +- Week 2: Migrate Paused + Idle states +- Week 3: Migrate Processing + Starting states +- Week 4: Replace main loop + cleanup + +### Realistic (Part-time, with other work) +- Week 1-2: Quick wins when you have time +- Week 3-4: Extract Paused state, test thoroughly +- Week 5-6: Extract Idle state +- Week 7-8: Extract Processing state +- Week 9-10: Extract Starting state +- Week 11-12: Replace main loop + final cleanup + +### Conservative (Slow and steady) +- Month 1: Quick wins + understanding +- Month 2: Paused + Idle states +- Month 3: Processing + Starting states +- Month 4: Main loop replacement + refinement + +## Risk Mitigation + +Every phase: +1. ✅ Write tests first +2. ✅ Keep old code paths working +3. ✅ Add logging for state transitions +4. ✅ Can roll back if issues arise +5. ✅ Get incremental value + +This is **not** a risky big-bang rewrite! + +## Getting Started + +### Today (30 minutes) +1. Read `QUICK_WINS.md` +2. Pick Quick Win #4 (named constants) +3. Implement it +4. Run tests +5. Commit + +### This Week (3-4 hours) +1. Implement Quick Wins #1-6 +2. Measure improvements +3. Share with team +4. Decide on next steps + +### Next Week +1. Read `MIGRATION_GUIDE.md` +2. Implement Phase 1 (define types) +3. Plan Phase 2 (extract Paused state) + +## Questions to Consider + +Before starting: +- [ ] Do you have test coverage? (If not, add some first) +- [ ] Can you dedicate 2-4 hours/week for 2-3 weeks? +- [ ] Is the team on board? +- [ ] Can you pause feature work briefly? + +If you answered yes to most of these, you're ready to start! + +## Success Stories + +This pattern is used in: +- **Tokio's runtime:** State machine for task execution +- **Kubernetes controllers:** Reconciliation loops +- **Game engines:** Entity state machines +- **Embedded systems:** Protocol handlers + +It's a proven approach for managing complex async systems. + +## Final Thoughts + +Your instinct that "this needs refactoring" is **100% correct**. The code is: +- ✅ Functionally sound (good architecture) +- ❌ Organizationally messy (hard to maintain) + +The state machine refactor will: +- ✅ Keep the good architecture +- ✅ Fix the organizational issues +- ✅ Make future changes easier + +**You can do this incrementally and safely!** + +## Next Steps + +1. Pick your path (A, B, or C) +2. Read the relevant document +3. Start with one small change +4. Build momentum +5. Keep going! + +I'm confident this will make a huge difference in your codebase. The fact that you recognized the problem means you'll implement the solution well. + +Good luck! 🚀 + +--- + +## Files in This Refactoring Package + +- **`REFACTORING_ANALYSIS.md`** - Detailed comparison of before/after +- **`MIGRATION_GUIDE.md`** - Step-by-step incremental migration +- **`QUICK_WINS.md`** - Small improvements you can do today +- **`runtime_refactored.rs`** - Complete example of refactored code +- **`THIS FILE`** - Summary and recommendations + +Read them in order, or jump to what's most relevant to you! diff --git a/crates/theater/STATE_MACHINE_VISUAL.md b/crates/theater/STATE_MACHINE_VISUAL.md new file mode 100644 index 00000000..d5770b38 --- /dev/null +++ b/crates/theater/STATE_MACHINE_VISUAL.md @@ -0,0 +1,415 @@ +# Actor State Machine Visual Guide + +## Current Implementation (Implicit State) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Giant Select Loop │ +│ │ +│ • actor_instance: Option<...> │ +│ • metrics: Option<...> │ +│ • handler_tasks: Vec<...> │ +│ • current_operation: Option<...> │ +│ • shutdown_requested: bool │ +│ • shutdown_response_tx: Option<...> │ +│ • current_status: String │ +│ • paused: Arc> │ +│ │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ tokio::select! { │ │ +│ │ setup completion => { ... } │ │ +│ │ operation recv => { if guards... } │ │ +│ │ operation complete => { ... } │ │ +│ │ info request => { ... } │ │ +│ │ control command => { if this, if that... } │ │ +│ │ parent shutdown => { ... } │ │ +│ │ status update => { ... } │ │ +│ │ } │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ +│ State is implicit - hard to reason about! │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Problems:** +- Can't tell at a glance what state we're in +- Boolean flags interact in complex ways +- Guards on select branches hide logic +- Testing is difficult + +--- + +## Proposed Implementation (Explicit State) + +``` + ┌──────────────┐ + │ Starting │ + │ │ + │ • setup_task │ + │ • status_rx │ + └──────┬───────┘ + │ + setup complete + │ + ▼ + ┌──────────────┐◄────────┐ + ┌────────┤ Idle │ │ + │ │ │ │ + │ │ • resources │ │ + pause │ └──────┬───────┘ │ + │ │ │ + │ new operation operation + │ │ complete + │ ▼ │ + │ ┌──────────────┐ │ + ├───────►│ Processing ├─────────┘ + │ │ │ + │ │ • resources │ + │ │ • operation │ + resume │ └──────┬───────┘ + │ │ + │ shutdown/error + │ │ + │ ▼ + │ ┌──────────────┐ + └───────►│ Paused │ + │ │ + │ • resources │ + └──────┬───────┘ + │ + shutdown/error + │ + ▼ + ┌──────────────┐ + │ ShuttingDown │ + │ │ + │ (terminal) │ + └──────────────┘ +``` + +**Benefits:** +- States are explicit and named +- Transitions are clear arrows +- Each state has only what it needs +- Easy to reason about! + +--- + +## State Details + +### Starting State +```rust +Starting { + setup_task: JoinHandle>, + status_rx: Receiver, + current_status: String, + pending_shutdown: Option>>, +} +``` + +**What it means:** +- Actor is initializing +- Loading WASM, setting up handlers +- Can receive: status updates, info requests, control commands +- Cannot receive: operations (not ready yet!) +- Transitions to: Idle (success) or ShuttingDown (error/terminate) + +**Key insight:** If shutdown is requested during startup, we store the response channel and complete shutdown after setup finishes. + +--- + +### Idle State +```rust +Idle { + resources: ActorResources, +} + +struct ActorResources { + instance: Arc>, + metrics: Arc>, + handler_tasks: Vec>, + shutdown_controller: ShutdownController, +} +``` + +**What it means:** +- Actor is ready and waiting +- Has all resources initialized +- Can receive: operations, info requests, control commands +- Transitions to: Processing (new operation), Paused (pause), ShuttingDown (shutdown) + +**Key insight:** All resources are guaranteed to exist - no Option unwrapping! + +--- + +### Processing State +```rust +Processing { + resources: ActorResources, + current_operation: JoinHandle, ActorError>>, + operation_name: String, + pending_shutdown: Option>>, +} +``` + +**What it means:** +- Actor is executing an operation +- Cannot accept new operations +- Can receive: info requests, control commands +- Transitions to: Idle (operation complete), ShuttingDown (terminate) + +**Key insight:** If shutdown is requested during operation, we store it and complete after operation finishes (graceful) or abort immediately (forced). + +--- + +### Paused State +```rust +Paused { + resources: ActorResources, +} +``` + +**What it means:** +- Actor is paused by user +- Will not accept operations +- Can receive: info requests, control commands +- Transitions to: Idle (resume), ShuttingDown (shutdown) + +**Key insight:** Simple state - just holds resources and waits for resume. + +--- + +### ShuttingDown State +```rust +ShuttingDown +``` + +**What it means:** +- Actor is cleaning up +- Terminal state (exit the loop) +- No data needed - cleanup happens in transition + +**Key insight:** This is just a marker - actual cleanup is done before entering this state. + +--- + +## Message Handling by State + +### Starting State +| Message | Action | +|---------|--------| +| Setup complete | → Idle (or ShuttingDown if pending) | +| Setup failed | → ShuttingDown (notify error) | +| Info request | Handle (some work, some don't) | +| Operation | ❌ Ignored (not ready) | +| Pause | ❌ Reject (can't pause during startup) | +| Shutdown (graceful) | Mark pending, → Idle after setup | +| Shutdown (forced) | Abort setup, → ShuttingDown | + +### Idle State +| Message | Action | +|---------|--------| +| Operation | Start operation, → Processing | +| Info request | Handle and stay in Idle | +| Pause | → Paused | +| Resume | ❌ Reject (not paused) | +| Shutdown | → ShuttingDown | + +### Processing State +| Message | Action | +|---------|--------| +| Operation complete | → Idle (or ShuttingDown if pending) | +| Operation | ❌ Ignored (already processing) | +| Info request | Handle and stay in Processing | +| Pause | ❌ Reject (can't pause during operation) | +| Shutdown (graceful) | Mark pending, → ShuttingDown after operation | +| Shutdown (forced) | Abort operation, → ShuttingDown | + +### Paused State +| Message | Action | +|---------|--------| +| Operation | ❌ Ignored (paused) | +| Info request | Handle and stay in Paused | +| Pause | Acknowledge (already paused) | +| Resume | → Idle | +| Shutdown | → ShuttingDown | + +--- + +## Transition Logic + +### From Starting +```rust +match setup_task.await { + Ok(Ok(setup)) => { + let resources = create_resources(setup); + if pending_shutdown.is_some() { + return StateTransition::Shutdown; + } + StateTransition::Continue(ActorState::Idle { resources }) + } + Ok(Err(error)) => StateTransition::Error(error), + Err(panic) => StateTransition::Error(ActorError::Panic), +} +``` + +### From Idle +```rust +match message { + Operation => StateTransition::Continue(ActorState::Processing { ... }), + Pause => StateTransition::Continue(ActorState::Paused { resources }), + Shutdown => StateTransition::Shutdown, + _ => StateTransition::Continue(ActorState::Idle { resources }), +} +``` + +### From Processing +```rust +match event { + OperationComplete => { + if pending_shutdown.is_some() { + StateTransition::Shutdown + } else { + StateTransition::Continue(ActorState::Idle { resources }) + } + } + Shutdown(Graceful) => StateTransition::Continue(ActorState::Processing { + pending_shutdown: Some(response_tx), + .. + }), + Shutdown(Force) => { + operation.abort(); + StateTransition::Shutdown + } +} +``` + +### From Paused +```rust +match message { + Resume => StateTransition::Continue(ActorState::Idle { resources }), + Shutdown => StateTransition::Shutdown, + _ => StateTransition::Continue(ActorState::Paused { resources }), +} +``` + +--- + +## Code Size Comparison + +### Current Implementation +``` +start() method: ~400 lines +└─ select! block: ~350 lines + ├─ setup branch: ~50 lines + ├─ status branch: ~5 lines + ├─ operation branch: ~60 lines + ├─ complete branch: ~15 lines + ├─ info branch: ~80 lines + ├─ control branch: ~80 lines + └─ shutdown branch: ~60 lines +``` + +### Refactored Implementation +``` +run() loop: ~40 lines +├─ handle_starting_state(): ~80 lines +│ └─ Focused on startup logic +├─ handle_idle_state(): ~60 lines +│ └─ Focused on accepting work +├─ handle_processing_state(): ~70 lines +│ └─ Focused on operation execution +└─ handle_paused_state(): ~40 lines + └─ Focused on pause/resume + +Total: ~290 lines, but MUCH more readable! +``` + +--- + +## Testing Comparison + +### Current: Hard to Test +```rust +// How do you test "pause during operation"? +// You need to: +// 1. Set up entire runtime +// 2. Mock channels +// 3. Send operation +// 4. Wait for it to start +// 5. Send pause +// 6. Check... what? The boolean flag? +``` + +### Refactored: Easy to Test +```rust +#[tokio::test] +async fn test_pause_during_processing() { + let state = ActorState::Processing { + resources: test_resources(), + current_operation: test_operation(), + operation_name: "test".into(), + pending_shutdown: None, + }; + + let (tx, rx) = oneshot::channel(); + send_control(Control::Pause { response_tx: tx }); + + let transition = handle_processing_state(state).await; + + // Should reject pause during operation + assert!(matches!( + transition, + StateTransition::Continue(ActorState::Processing { .. }) + )); + assert!(rx.await.unwrap().is_err()); +} +``` + +--- + +## Debugging Experience + +### Current: Hard to Debug +``` +[DEBUG] Operation received +[DEBUG] Starting operation +... (something goes wrong) +[ERROR] Failed to send response +``` + +*Where were we in the state machine? Was setup complete? Was there already an operation running?* + +### Refactored: Easy to Debug +``` +[INFO] Actor abc123: Starting -> Idle +[INFO] Actor abc123: Idle -> Processing (operation: calculate) +[DEBUG] Operation 'calculate' started +... (something goes wrong) +[ERROR] Operation 'calculate' failed: division by zero +[INFO] Actor abc123: Processing -> Idle +``` + +*State transitions are explicit! Easy to see what was happening.* + +--- + +## Summary + +The explicit state machine: + +✅ **Clearer:** States and transitions are obvious +✅ **Safer:** Impossible states can't be represented +✅ **Testable:** Each state handler can be tested independently +✅ **Maintainable:** Adding new states is straightforward +✅ **Debuggable:** State transitions are logged explicitly + +The current implementation: + +❌ States are implicit (boolean flags) +❌ Easy to get into invalid states +❌ Hard to test (need full runtime setup) +❌ Hard to extend (where does new logic go?) +❌ Hard to debug (can't see current state) + +**This is a high-leverage refactor that will make your life much easier!** diff --git a/crates/theater/src/actor/mod.rs b/crates/theater/src/actor/mod.rs index abe024a4..9fa9056a 100644 --- a/crates/theater/src/actor/mod.rs +++ b/crates/theater/src/actor/mod.rs @@ -15,6 +15,7 @@ pub mod types; // Public re-exports pub use handle::ActorHandle; pub use runtime::ActorRuntime; +pub use runtime::ActorRuntimeError; pub use runtime::StartActorResult; pub use store::ActorStore; pub use types::ActorError; diff --git a/crates/theater/src/actor/runtime.rs b/crates/theater/src/actor/runtime.rs index 2ec14e9c..0f7a8c06 100644 --- a/crates/theater/src/actor/runtime.rs +++ b/crates/theater/src/actor/runtime.rs @@ -8,43 +8,31 @@ use crate::actor::handle::ActorHandle; use crate::actor::store::ActorStore; use crate::actor::types::ActorError; use crate::actor::types::ActorOperation; -use crate::config::permissions::HandlerPermission; use crate::events::theater_runtime::TheaterRuntimeEventData; use crate::events::wasm::WasmEventData; use crate::events::{ChainEventData, EventData}; -use crate::host::environment::EnvironmentHost; -use crate::host::filesystem::FileSystemHost; -use crate::host::framework::HttpFramework; -use crate::host::handler::Handler; -use crate::host::http_client::HttpClientHost; -use crate::host::message_server::MessageServerHost; -use crate::host::process::ProcessHost; -use crate::host::random::RandomHost; -use crate::host::runtime::RuntimeHost; -use crate::host::store::StoreHost; -use crate::host::supervisor::SupervisorHost; -use crate::host::timing::TimingHost; +use crate::handler::Handler; +use crate::handler::HandlerRegistry; use crate::id::TheaterId; -use crate::messages::{ActorMessage, TheaterCommand}; +use crate::messages::TheaterCommand; use crate::metrics::MetricsCollector; -use crate::shutdown::ShutdownType; -use crate::shutdown::{ShutdownController, ShutdownReceiver}; use crate::store::ContentStore; use crate::wasm::{ActorComponent, ActorInstance}; -use crate::HandlerConfig; use crate::ManifestConfig; use crate::Result; +use crate::ShutdownController; +use crate::ShutdownType; use crate::StateChain; use serde_json::Value; use std::sync::Arc; use std::sync::RwLock as SyncRwLock; use tokio::sync::mpsc::{self, Receiver, Sender}; -use tokio::sync::oneshot; use tokio::sync::RwLock; use tokio::task::JoinHandle; use tokio::time::Instant; use tracing::{debug, error, info, warn}; +use wasmtime::Engine; use super::types::ActorControl; use super::types::ActorInfo; @@ -62,11 +50,16 @@ const SHUTDOWN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); /// setting up its capabilities via handlers, executing operations, and ensuring proper shutdown. pub struct ActorRuntime { /// Unique identifier for this actor - pub actor_id: TheaterId, - /// Handles to the running handler tasks - pub handler_tasks: Vec>, - /// Controller for graceful shutdown of all components - pub shutdown_controller: ShutdownController, + pub id: TheaterId, + config: ManifestConfig, + handlers: Vec>, + actor_instance: ActorInstance, + metrics: MetricsCollector, + operation_rx: Receiver, + info_rx: Receiver, + control_rx: Receiver, + theater_tx: Sender, + actor_phase_manager: ActorPhaseManager, } /// # Result of starting an actor @@ -86,977 +79,834 @@ pub enum StartActorResult { Error(String), } -impl ActorRuntime { - pub async fn start( - id: TheaterId, - config: &ManifestConfig, - initial_state: Option, - theater_tx: Sender, - actor_sender: Sender, - actor_mailbox: Receiver, - mut operation_rx: Receiver, - operation_tx: Sender, - mut info_rx: Receiver, - info_tx: Sender, - mut control_rx: Receiver, - control_tx: Sender, - init: bool, - mut parent_shutdown_receiver: ShutdownReceiver, - engine: wasmtime::Engine, - parent_permissions: HandlerPermission, - chain: Arc>, - ) { - info!("Actor runtime starting communication loops"); - let paused = Arc::new(RwLock::new(false)); - - // Create setup task with status reporting - let (status_tx, mut status_rx) = mpsc::channel::(10); - let mut setup_task = Some(tokio::spawn(Self::build_actor_resources( - id.clone(), - config.clone(), - initial_state, - theater_tx.clone(), - actor_sender, - actor_mailbox, - operation_tx.clone(), - info_tx.clone(), - control_tx.clone(), - init, - engine, - parent_permissions, - status_tx, - chain, - ))); - - // These will be set once setup completes - let mut actor_instance: Option>> = None; - let mut metrics: Option>> = None; - let mut handler_tasks: Vec> = Vec::new(); - - let mut current_operation: Option> = None; - let mut shutdown_requested = false; - let mut shutdown_response_tx: Option>> = None; - let mut shutdown_controller = ShutdownController::new(); - let mut current_status = "Starting".to_string(); - - loop { - tokio::select! { - // Handle setup completion - result = async { - match setup_task.as_mut() { - Some(task) => task.await, - None => std::future::pending().await, - } - } => { - match result { - Ok(Ok((instance, handlers, metrics_collector))) => { - info!("Actor setup completed successfully"); - actor_instance = Some(Arc::new(RwLock::new(instance))); - metrics = Some(Arc::new(RwLock::new(metrics_collector))); - setup_task = None; // Clear the task - current_status = "Running".to_string(); - let actor_handle = ActorHandle::new(operation_tx.clone(), info_tx.clone(), control_tx.clone()); - - let (init_tx, mut init_rx) = tokio::sync::broadcast::channel(1); - - // Call init function if needed - if init { - let init_id = id.clone(); - let actor_handle = actor_handle.clone(); - let init_tx = init_tx.clone(); - info!("Calling init function for actor: {:?}", init_id); - tokio::spawn(async move { - match actor_handle - .call_function::<(String,), ()>( - "theater:simple/actor.init".to_string(), - (init_id.to_string(),), - ) - .await - { - Ok(_) => { - debug!("Successfully called init function for actor: {:?}", init_id); - // Notify that init is complete - if let Err(e) = init_tx.send(()) { - error!("Failed to send init completion: {:?}", e); - } - } - Err(e) => { - error!("Failed to call init function for actor {}: {}", init_id, e); - } - } - }); - } else { - // If no init function, just send completion - if let Err(e) = init_tx.send(()) { - error!("Failed to send init completion: {:?}", e); - } - } +#[derive(Debug)] +pub enum ActorRuntimeError { + SetupError { + message: String, + }, + ActorInstanceNotFound { + message: String, + }, + ActorPhaseError { + expected: ActorPhase, + found: ActorPhase, + message: String, + }, + ActorError(ActorError), + UnknownError(anyhow::Error), +} - // Start the handler tasks - for mut handler in handlers { - info!("Starting handler: {:?}", handler.name()); - let handler_actor_handle = actor_handle.clone(); - let handler_shutdown = shutdown_controller.subscribe(); - let theater_tx = theater_tx.clone(); - let id = id.clone(); - let mut init_rx = init_tx.subscribe(); - let handler_task = tokio::spawn(async move { - // Wait for init to complete before starting handler - if let Err(e) = init_rx.recv().await { - error!("Failed to receive init completion for handler {}: {:?}", handler.name(), e); - return; - } - match handler.start( - handler_actor_handle, - handler_shutdown, - ).await { - Ok(_) => { - info!("Handler {} started successfully", handler.name()); - } - Err(e) => { - error!("Failed to start handler {}: {:?}", handler.name(), e); - // Notify theater runtime of the error - let _ = theater_tx.send(TheaterCommand::ActorError { - actor_id: id.clone(), - error: ActorError::HandlerError(e.to_string()), - }).await; - } - } - }); - handler_tasks.push(handler_task); - } - info!("All handlers started successfully for actor: {}", id); +impl From for ActorRuntimeError { + fn from(error: ActorError) -> Self { + ActorRuntimeError::ActorError(error) + } +} - // If shutdown was requested during startup, handle it now - if shutdown_requested { - info!("Shutdown was requested during startup, shutting down now"); - if let Some(response_tx) = shutdown_response_tx.take() { - let _ = response_tx.send(Ok(())); - } - break; // Exit the loop - } - } - Ok(Err(e)) => { - error!("Actor setup failed: {:?}", e); +impl From for ActorRuntimeError { + fn from(error: anyhow::Error) -> Self { + ActorRuntimeError::UnknownError(error) + } +} - // Notify theater runtime of the error - let _ = theater_tx.send(TheaterCommand::ActorError { - actor_id: id.clone(), - error: e.clone(), - }).await; +impl std::fmt::Display for ActorRuntimeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ActorRuntimeError::SetupError { message } => write!(f, "Setup Error: {}", message), + ActorRuntimeError::ActorInstanceNotFound { message } => { + write!(f, "Actor Instance Not Found: {}", message) + } + ActorRuntimeError::ActorPhaseError { + expected, + found, + message, + } => write!( + f, + "Actor Phase Error: expected {:?}, found {:?}. {}", + expected, found, message + ), + ActorRuntimeError::ActorError(err) => write!(f, "Actor Error: {}", err), + ActorRuntimeError::UnknownError(err) => write!(f, "Unknown Error: {}", err), + } + } +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ActorPhase { + Starting, + Running, + Paused, + ShuttingDown, +} - // Handle any pending shutdown request - if let Some(response_tx) = shutdown_response_tx.take() { - let _ = response_tx.send(Err(e)); - } - break; // Exit on setup failure - } - Err(e) => { - error!("Setup task panicked: {:?}", e); +impl Default for ActorPhase { + fn default() -> Self { + ActorPhase::Starting + } +} - // Handle any pending shutdown request - if let Some(response_tx) = shutdown_response_tx.take() { - let _ = response_tx.send(Err(ActorError::UnexpectedError(e.to_string()))); - } - break; - } - } - } +impl std::fmt::Display for ActorPhase { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ActorPhase::Starting => write!(f, "Starting"), + ActorPhase::Running => write!(f, "Running"), + ActorPhase::Paused => write!(f, "Paused"), + ActorPhase::ShuttingDown => write!(f, "Shutting Down"), + } + } +} - // Handle status updates from setup process - Some(new_status) = status_rx.recv() => { - current_status = new_status; - debug!("Actor {} startup status: {}", id, current_status); - } +#[derive(Clone)] +pub struct ActorPhaseManager { + current_phase: Arc>, + notify: Arc, +} - // Handle operations only after setup is complete - // not a huge fan of all these reads and awaits, but it works for now - Some(op) = operation_rx.recv(), if actor_instance.is_some() && current_operation.is_none() && !*paused.read().await => { - let actor_instance = actor_instance.as_ref().unwrap(); - let metrics = metrics.as_ref().unwrap(); - - info!("Received operation: {:?}", op); - match op { - ActorOperation::CallFunction { name, params, response_tx } => { - info!("Processing function call: {}", name); - let theater_tx = theater_tx.clone(); - let metrics = metrics.clone(); - let actor_instance = actor_instance.clone(); - let paused = paused.clone(); - - current_operation = Some(tokio::spawn(async move { - let mut actor_instance = actor_instance.write().await; - let metrics = metrics.write().await; - match Self::execute_call( - &mut actor_instance, - &name, - params, - &theater_tx, - &metrics, - ).await { - Ok(result) => { - if let Err(e) = response_tx.send(Ok(result)) { - error!("Failed to send function call response for operation '{}': {:?}", name, e); - } - } - Err(actor_error) => { - let _ = theater_tx - .send(TheaterCommand::ActorError { - actor_id: actor_instance.id().clone(), - error: actor_error.clone(), - }) - .await; - - error!("Operation '{}' failed with error: {:?}", name, actor_error); - if let Err(send_err) = response_tx.send(Err(actor_error)) { - error!("Failed to send function call error response for operation '{}': {:?}", name, send_err); - } - - *paused.write().await = true; - } - } - })); - } - ActorOperation::UpdateComponent { component_address: _, response_tx } => { - error!("UpdateComponent operation is not implemented yet"); - if let Err(e) = response_tx.send(Err(ActorError::UpdateComponentError("Not implemented".to_string()))) { - error!("Failed to send update component response: {:?}", e); - } - } - } - } +impl ActorPhaseManager { + pub fn new() -> Self { + Self { + current_phase: Arc::new(RwLock::new(ActorPhase::Starting)), + notify: Arc::new(tokio::sync::Notify::new()), + } + } - // Clean up completed operations - _ = async { - match current_operation.as_mut() { - Some(task) => task.await, - None => std::future::pending().await, - } - } => { - info!("Operation completed"); - current_operation = None; - - // Check if shutdown was requested and no more operations are running - if shutdown_requested { - info!("Shutdown requested and operation completed - shutting down gracefully"); - if let Some(response_tx) = shutdown_response_tx.take() { - let _ = response_tx.send(Ok(())); - } - break; - } - } + pub async fn set_phase(&self, phase: ActorPhase) { + let mut current_phase = self.current_phase.write().await; + *current_phase = phase; + self.notify.notify_waiters(); + } - // Handle info requests (works during startup too!) - Some(info) = info_rx.recv() => { - info!("Received info request: {:?}", info); - match info { - ActorInfo::GetStatus { response_tx } => { - let status = if shutdown_requested { - if setup_task.is_some() { - "Shutting down (during startup)".to_string() - } else if current_operation.is_some() { - "Shutting down (waiting for operation)".to_string() - } else { - "Shutting down".to_string() - } - } else if setup_task.is_some() { - current_status.clone() - } else if *paused.read().await { - "Paused".to_string() - } else if current_operation.is_some() { - "Processing".to_string() - } else { - "Idle".to_string() - }; - - if let Err(e) = response_tx.send(Ok(status)) { - error!("Failed to send status response: {:?}", e); - } - } - ActorInfo::GetState { response_tx } => { - if let Some(ref actor_instance) = actor_instance { - let actor_instance = actor_instance.read().await; - let state = actor_instance.store.data().get_state(); - if let Err(e) = response_tx.send(Ok(state)) { - error!("Failed to send state response: {:?}", e); - } - } else { - let _ = response_tx.send(Err(ActorError::UnexpectedError("Actor still starting".to_string()))); - } - } - ActorInfo::GetChain { response_tx } => { - if let Some(ref actor_instance) = actor_instance { - let actor_instance = actor_instance.read().await; - let chain = actor_instance.store.data().get_chain(); - if let Err(e) = response_tx.send(Ok(chain)) { - error!("Failed to send chain response: {:?}", e); - } - } else { - let _ = response_tx.send(Err(ActorError::UnexpectedError("Actor still starting".to_string()))); - } - } - ActorInfo::GetMetrics { response_tx } => { - if let Some(ref metrics) = metrics { - let metrics = metrics.read().await; - let metrics_data = metrics.get_metrics().await; - if let Err(e) = response_tx.send(Ok(metrics_data)) { - error!("Failed to send metrics response: {:?}", e); - } - } else { - let _ = response_tx.send(Err(ActorError::UnexpectedError("Actor still starting".to_string()))); - } - } - ActorInfo::SaveChain { response_tx } => { - if let Some(ref actor_instance) = actor_instance { - let actor_instance = actor_instance.read().await; - match actor_instance.save_chain() { - Ok(_) => { - if let Err(e) = response_tx.send(Ok(())) { - error!("Failed to send save chain response: {:?}", e); - } - } - Err(e) => { - if let Err(send_err) = response_tx.send(Err(ActorError::UnexpectedError(e.to_string()))) { - error!("Failed to send save chain error response: {:?}", send_err); - } - } - } - } else { - let _ = response_tx.send(Err(ActorError::UnexpectedError("Actor still starting".to_string()))); - } - } - } - } + pub async fn get_phase(&self) -> ActorPhase { + let current_phase = self.current_phase.read().await; + current_phase.clone() + } - // Handle control commands (works during startup too!) - Some(control) = control_rx.recv() => { - info!("Received control command: {:?}", control); - match control { - ActorControl::Shutdown { response_tx } => { - info!("Shutdown requested"); - if setup_task.is_some() { - // Still setting up - mark for shutdown after setup completes - info!("Shutdown requested during setup - will shutdown after setup completes"); - shutdown_requested = true; - shutdown_response_tx = Some(response_tx); - current_status = "Shutting down (during startup)".to_string(); - } else if current_operation.is_some() { - // Operation running - mark for shutdown after completion - info!("Operation running - will shutdown after completion"); - shutdown_requested = true; - shutdown_response_tx = Some(response_tx); - } else { - // No operation running - shutdown immediately - info!("No operation running - shutting down immediately"); - let _ = response_tx.send(Ok(())); - break; - } - } - ActorControl::Terminate { response_tx } => { - info!("Terminate requested"); - // Abort setup or current operation - if let Some(task) = setup_task.take() { - task.abort(); - } - if let Some(task) = current_operation.take() { - task.abort(); - } - if let Err(e) = response_tx.send(Ok(())) { - error!("Failed to send terminate confirmation: {:?}", e); - } - break; - } - ActorControl::Pause { response_tx } => { - if setup_task.is_some() { - let _ = response_tx.send(Err(ActorError::UnexpectedError("Cannot pause during startup".to_string()))); - } else if shutdown_requested { - let _ = response_tx.send(Err(ActorError::ShuttingDown)); - } else { - *paused.write().await = true; - let _ = response_tx.send(Ok(())); - } - } - ActorControl::Resume { response_tx } => { - if setup_task.is_some() { - let _ = response_tx.send(Err(ActorError::UnexpectedError("Cannot resume during startup".to_string()))); - } else if shutdown_requested { - let _ = response_tx.send(Err(ActorError::ShuttingDown)); - } else { - let response = if *paused.read().await { - *paused.write().await = false; - Ok(()) - } else { - Err(ActorError::NotPaused) - }; - let _ = response_tx.send(response); - } - } - } - } + pub async fn is_phase(&self, phase: ActorPhase) -> bool { + let current_phase = self.current_phase.read().await; + *current_phase == phase + } - // Handle parent shutdown signals - shutdown_signal = &mut parent_shutdown_receiver.receiver => { - info!("Received shutdown signal from parent"); - match shutdown_signal { - Ok(shutdown_signal) => { - info!("Shutdown signal received: {:?}", shutdown_signal); - match shutdown_signal.shutdown_type { - ShutdownType::Graceful => { - info!("Graceful shutdown requested"); - if setup_task.is_some() || current_operation.is_some() { - info!("Setup/operation running - will shutdown after completion"); - shutdown_requested = true; - } else { - info!("No setup/operation running - shutting down immediately"); - break; - } - } - ShutdownType::Force => { - info!("Forceful shutdown requested"); - if let Some(task) = setup_task.take() { - task.abort(); - } - if let Some(task) = current_operation.take() { - task.abort(); - } - break; - } - } - } - Err(e) => { - error!("Failed to receive shutdown signal: {:?}", e); - info!("Exiting runtime communication loop due to shutdown signal error"); - if setup_task.is_some() || current_operation.is_some() { - shutdown_requested = true; - } else { - break; - } - } - } + pub async fn wait_for_phase(&self, phase: ActorPhase) { + loop { + let notified = self.notify.notified(); // Subscribe first + { + let current_phase = self.current_phase.read().await; + if *current_phase == phase { + break; } } - } - info!("Actor runtime communication loop exiting, performing cleanup"); - if let Some(ref metrics) = metrics { - let metrics = metrics.read().await; - Self::perform_cleanup(shutdown_controller, handler_tasks, &metrics).await; - } else { - info!("Actor was shut down during startup, no cleanup needed"); + notified.await; } } +} - /// Builds the complete actor instance using existing startup logic - async fn build_actor_resources( +impl ActorRuntime { + pub async fn build_actor_resources( id: TheaterId, - config: ManifestConfig, + config: &ManifestConfig, initial_state: Option, + engine: Engine, + chain: Arc>, + mut handler_registry: HandlerRegistry, theater_tx: Sender, - actor_sender: Sender, - actor_mailbox: Receiver, operation_tx: Sender, info_tx: Sender, control_tx: Sender, - init: bool, - engine: wasmtime::Engine, - parent_permissions: HandlerPermission, - status_tx: Sender, - chain: Arc>, - ) -> Result<(ActorInstance, Vec, MetricsCollector), ActorError> { - let actor_handle = ActorHandle::new(operation_tx, info_tx, control_tx); - - let _ = status_tx.send("Setting up actor store".to_string()).await; - - // Setup actor store and manifest - let (actor_store, _manifest_id) = Self::setup_actor_store( - id.clone(), - theater_tx.clone(), - actor_handle.clone(), - &config, - chain, - ) - .await - .map_err(|e| ActorError::UnexpectedError(format!("Failed to setup actor store: {}", e)))?; - - let _ = status_tx.send("Validating permissions".to_string()).await; + actor_phase_manager: ActorPhaseManager, + ) -> Result<(ActorInstance, ShutdownController, Vec>), ActorRuntimeError> { + // ---------------- Checkpoint Setup Initial ---------------- + + debug!("Setting up actor store"); + + if actor_phase_manager.is_phase(ActorPhase::Starting).await { + let curr_phase = actor_phase_manager.get_phase().await; + return Err(ActorRuntimeError::ActorPhaseError { + expected: ActorPhase::Starting, + found: curr_phase, + message: "phase error found at setup task Checkpoint Setup Initial".into(), + }); + } - // Calculate effective permissions - let effective_permissions = config.calculate_effective_permissions(&parent_permissions); + let handle_operation_tx = operation_tx.clone(); + let actor_handle = ActorHandle::new(handle_operation_tx, info_tx, control_tx); + let actor_store = + ActorStore::new(id.clone(), theater_tx.clone(), None, actor_handle.clone(), chain); actor_store.record_event(ChainEventData { event_type: "theater-runtime".to_string(), - data: EventData::TheaterRuntime(TheaterRuntimeEventData::ValidatingPermissions { - permissions: effective_permissions.clone(), + data: EventData::TheaterRuntime(TheaterRuntimeEventData::ActorLoadCall { + manifest: config.clone(), }), timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: format!("Actor [{}] loaded successfully", id).into(), + description: format!("Initial values set up for [{}]", id).into(), }); - // Validate that the manifest doesn't exceed effective permissions - match crate::config::enforcement::validate_manifest_permissions( - &config, - &effective_permissions, - ) { - Ok(_) => {} - Err(e) => { - actor_store.record_event(ChainEventData { - event_type: "theater-runtime".to_string(), - data: EventData::TheaterRuntime(TheaterRuntimeEventData::ActorSetupError { - error: e.to_string(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: format!( - "Loading actor [{}] from manifest failed validation: {}", - id, e - ) - .into(), - }); - return Err(ActorError::UnexpectedError(format!( - "Permission validation failed: {}", - e - ))); - } + // ----------------- Checkpoint Store Manifest ---------------- + + debug!("Storing manifest for actor: {}", id); + + // Checkpoint 1: After manifest storage + if actor_phase_manager.is_phase(ActorPhase::Starting).await { + let curr_phase = actor_phase_manager.get_phase().await; + return Err(ActorRuntimeError::ActorPhaseError { + expected: ActorPhase::Starting, + found: curr_phase, + message: "phase error found at setup task Checkpoint Store Manifest".into(), + }); } - let _ = status_tx.send("Creating handlers".to_string()).await; + // Store manifest + let manifest_store = ContentStore::from_id("manifest"); + debug!("Storing manifest for actor: {}", id); + debug!("Manifest store: {:?}", manifest_store); + let manifest_id = manifest_store + .store( + config + .clone() + .into_fixed_bytes() + .expect("Failed to serialize manifest"), + ) + .await; actor_store.record_event(ChainEventData { + event_type: "theater-runtime".to_string(), + data: EventData::TheaterRuntime(TheaterRuntimeEventData::ActorLoadCall { + manifest: config.clone(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: format!("Manifest for actor [{}] stored at [{}]", id, manifest_id).into(), + }); + + // ----------------- Checkpoint Create Handlers ----------------- + + if actor_phase_manager.is_phase(ActorPhase::Starting).await { + let curr_phase = actor_phase_manager.get_phase().await; + return Err(ActorRuntimeError::ActorPhaseError { + expected: ActorPhase::Starting, + found: curr_phase, + message: "phase error found at setup task Checkpoint Create Handlers".into(), + }); + } + + debug!("Creating handlers"); + + let mut actor_component = ActorComponent::new( + config.name.clone(), + config.component.clone(), + actor_store, + engine, + ) + .await + .map_err(|e| { + let error_message = format!( + "Failed to create actor component for actor {}: {}", + config.name, e + ); + error!("{}", error_message); + >::into(e) + })?; + + actor_component.actor_store.record_event(ChainEventData { event_type: "theater-runtime".to_string(), data: EventData::TheaterRuntime(TheaterRuntimeEventData::CreatingHandlers), timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: format!("Creating handlers for actor [{}]", id).into(), + description: format!("Created handlers for actor [{}]", id).into(), }); - // Create handlers with effective permissions and validation - let handlers = match Self::create_handlers( - actor_sender, - actor_mailbox, - theater_tx.clone(), - &config, - actor_handle.clone(), - &effective_permissions, - ) { - Ok(handlers) => handlers, - Err(e) => { - error!("Failed to create handlers: {}", e); - actor_store.record_event(ChainEventData { - event_type: "theater-runtime".to_string(), - data: EventData::TheaterRuntime(TheaterRuntimeEventData::ActorSetupError { - error: e.clone(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: format!("Creating handlers for actor [{}] failed: {}", id, e) - .into(), - }); - return Err(ActorError::UnexpectedError(format!( - "Handler creation failed: {}", - e - ))); - } - }; + // ----------------- Checkpoint Setup Handlers ----------------- - let _ = status_tx.send("Creating component".to_string()).await; + debug!("Setting up handlers"); - actor_store.record_event(ChainEventData { + if actor_phase_manager.is_phase(ActorPhase::Starting).await { + let curr_phase = actor_phase_manager.get_phase().await; + return Err(ActorRuntimeError::ActorPhaseError { + expected: ActorPhase::Starting, + found: curr_phase, + message: "phase error found at setup task Checkpoint Setup Handlers".into(), + }); + } + + let mut handlers = handler_registry.setup_handlers(&mut actor_component); + + actor_component.actor_store.record_event(ChainEventData { event_type: "theater-runtime".to_string(), - data: EventData::TheaterRuntime(TheaterRuntimeEventData::CreatingComponent), + data: EventData::TheaterRuntime(TheaterRuntimeEventData::CreatingHandlers), timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: format!("Creating component for actor [{}]", id).into(), + description: format!("Set up handlers for actor [{}]", id).into(), }); - // Create component - let mut actor_component = - match Self::create_actor_component(&config, actor_store, engine.clone()).await { - Ok(component) => component, - Err(e) => { - error!("Failed to create actor component: {}", e); - return Err(ActorError::UnexpectedError(format!( - "Component creation failed: {}", - e - ))); - } - }; + // ----------------- Checkpoint Setup Host Functions ----------------- - let _ = status_tx - .send("Setting up host functions".to_string()) - .await; + debug!("Setting up host functions"); + + if actor_phase_manager.is_phase(ActorPhase::Starting).await { + let curr_phase = actor_phase_manager.get_phase().await; + return Err(ActorRuntimeError::ActorPhaseError { + expected: ActorPhase::Starting, + found: curr_phase, + message: "phase error found at setup task Checkpoint Setup Host Functions".into(), + }); + } + + handlers.iter_mut().for_each(|handler| { + handler.setup_host_functions(&mut actor_component); + }); actor_component.actor_store.record_event(ChainEventData { event_type: "theater-runtime".to_string(), data: EventData::TheaterRuntime(TheaterRuntimeEventData::CreatingHandlers), timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: format!("Setting up host functions for actor [{}]", id).into(), + description: format!("Set up host functions for actor [{}]", id).into(), }); - // Setup host functions - let handlers = Self::setup_host_functions(&mut actor_component, handlers) - .await - .map_err(|e| { - ActorError::UnexpectedError(format!("Host function setup failed: {}", e)) - })?; + // ----------------- Checkpoint Instantiate Actor ----------------- - let _ = status_tx.send("Instantiating component".to_string()).await; + debug!("Instantiating component"); - // Instantiate component - let mut actor_instance = Self::instantiate_component(actor_component, id.clone()) - .await - .map_err(|e| { - ActorError::UnexpectedError(format!("Component instantiation failed: {}", e)) - })?; + if actor_phase_manager.is_phase(ActorPhase::Starting).await { + let curr_phase = actor_phase_manager.get_phase().await; + return Err(ActorRuntimeError::ActorPhaseError { + expected: ActorPhase::Starting, + found: curr_phase, + message: "phase error found at setup task Checkpoint Instantiate Actor".into(), + }); + } - let _ = status_tx - .send("Setting up export functions".to_string()) - .await; + let mut actor_instance = actor_component.instantiate().await.map_err(|e| { + let error_message = format!("Failed to instantiate actor {}: {}", id, e); + error!("{}", error_message); + ActorRuntimeError::SetupError { + message: error_message, + } + })?; + + actor_instance + .actor_component + .actor_store + .record_event(ChainEventData { + event_type: "theater-runtime".to_string(), + data: EventData::TheaterRuntime(TheaterRuntimeEventData::InstantiatingActor), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: format!("Instantiated actor [{}]", id).into(), + }); + + // ----------------- Checkpoint Add Export Functions ----------------- + + debug!("Adding export functions"); + + if actor_phase_manager.is_phase(ActorPhase::Starting).await { + let curr_phase = actor_phase_manager.get_phase().await; + return Err(ActorRuntimeError::ActorPhaseError { + expected: ActorPhase::Starting, + found: curr_phase, + message: "phase error found at setup task Checkpoint Add Export Functions".into(), + }); + } + + handlers.iter_mut().for_each(|handler| { + handler.add_export_functions(&mut actor_instance); + }); + + actor_instance + .actor_component + .actor_store + .record_event(ChainEventData { + event_type: "theater-runtime".to_string(), + data: EventData::TheaterRuntime(TheaterRuntimeEventData::CreatingHandlers), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: format!("Added export functions for actor [{}]", id).into(), + }); - // Setup export functions - Self::setup_export_functions(&mut actor_instance, &handlers) - .await - .map_err(|e| { - ActorError::UnexpectedError(format!("Export function setup failed: {}", e)) - })?; + // ----------------- Checkpoint Initialize State ----------------- - let _ = status_tx.send("Initializing state".to_string()).await; + debug!("Initializing state"); + + if actor_phase_manager.is_phase(ActorPhase::Starting).await { + let curr_phase = actor_phase_manager.get_phase().await; + return Err(ActorRuntimeError::ActorPhaseError { + expected: ActorPhase::Starting, + found: curr_phase, + message: "phase error found at setup task Checkpoint Initialize State".into(), + }); + } // Initialize state if needed - let init_state = if init { - match initial_state { - Some(state) => Some(serde_json::to_vec(&state).map_err(|e| { - ActorError::UnexpectedError(format!("Failed to serialize initial state: {}", e)) - })?), - None => None, - } - } else { - None + let init_state = match initial_state { + Some(state) => Some(serde_json::to_vec(&state).map_err(|e| { + ActorError::UnexpectedError(format!("Failed to serialize initial state: {}", e)) + })?), + None => None, }; actor_instance.store.data_mut().set_state(init_state); - let _ = status_tx.send("Ready".to_string()).await; - - let metrics = MetricsCollector::new(); + actor_instance + .actor_component + .actor_store + .record_event(ChainEventData { + event_type: "theater-runtime".to_string(), + data: EventData::TheaterRuntime(TheaterRuntimeEventData::InitializingState), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: format!("Initialized state for actor [{}]", id).into(), + }); - Ok((actor_instance, handlers, metrics)) - } + // ----------------- Checkpoint Finalize Setup ----------------- - /// Sets up the actor store and stores the manifest - async fn setup_actor_store( - id: TheaterId, - theater_tx: Sender, - actor_handle: ActorHandle, - config: &ManifestConfig, - chain: Arc>, - ) -> Result<(ActorStore, String)> { - // Create actor store - let actor_store = - match ActorStore::new(id.clone(), theater_tx.clone(), actor_handle.clone(), chain) { - Ok(store) => store, - Err(e) => { - let error_message = format!("Failed to create actor store: {}", e); - error!("{}", error_message); - return Err(e.into()); - } - }; + debug!("Ready"); - // Store manifest - let manifest_store = ContentStore::from_id("manifest"); - debug!("Storing manifest for actor: {}", id); - debug!("Manifest store: {:?}", manifest_store); - let manifest_id = match manifest_store - .store( - config - .clone() - .into_fixed_bytes() - .expect("Failed to serialize manifest"), - ) - .await - { - Ok(id) => id, - Err(e) => { - let error_message = format!("Failed to store manifest: {}", e); - error!("{}", error_message); - return Err(e.into()); - } - }; + if actor_phase_manager.is_phase(ActorPhase::Starting).await { + let curr_phase = actor_phase_manager.get_phase().await; + return Err(ActorRuntimeError::ActorPhaseError { + expected: ActorPhase::Starting, + found: curr_phase, + message: "phase error found at setup task Checkpoint Finalize Setup".into(), + }); + } - actor_store.record_event(ChainEventData { - event_type: "theater-runtime".to_string(), - data: EventData::TheaterRuntime(TheaterRuntimeEventData::ActorLoadCall { - manifest: config.clone(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: format!("Loading actor [{}] from manifest [{}] ", id, manifest_id).into(), + let init_actor_handle = actor_handle.clone(); + let init_id = id.clone(); + tokio::spawn(async move { + init_actor_handle + .call_function::<(String,), ()>( + "theater:simple/actor.init".to_string(), + (init_id.to_string(),), + ) + .await + .map_err(|e| { + error!("Failed to call actor.init for actor {}: {}", id, e); + e + }) }); - Ok((actor_store, manifest_id.to_string())) + // Start the handlers + let mut handler_tasks: Vec> = vec![]; + let mut shutdown_controller = ShutdownController::new(); + let handler_actor_handle = actor_handle.clone(); + for mut handler in handlers { + let actor_handle = handler_actor_handle.clone(); + let shutdown_receiver = shutdown_controller.subscribe(); + let handler_task = tokio::spawn(async move { + handler + .start(actor_handle, shutdown_receiver) + .await + .unwrap(); + }); + handler_tasks.push(handler_task); + // Store handler task for later management + // Note: In a real implementation, you might want to store these in a more + // structured way + // For simplicity, we just push them into a vector here + // You might want to use a Mutex or RwLock if you need to modify this later + // For now, we assume they are static after startup + // handler_tasks.push(handler_task); + } + + actor_instance + .actor_component + .actor_store + .record_event(ChainEventData { + event_type: "theater-runtime".to_string(), + data: EventData::TheaterRuntime(TheaterRuntimeEventData::ActorReady), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: format!("Actor [{}] is ready", id).into(), + }); + + Ok((actor_instance, shutdown_controller, handler_tasks)) } - /// Creates all the handlers needed by the actor with permission validation - fn create_handlers( - actor_sender: Sender, - actor_mailbox: Receiver, - theater_tx: Sender, + pub async fn start( + id: TheaterId, config: &ManifestConfig, - actor_handle: ActorHandle, - effective_permissions: &crate::config::permissions::HandlerPermission, - ) -> Result, String> { - let mut handlers = Vec::new(); - - // Check if message server is permitted and requested - if config - .handlers - .iter() - .any(|h| matches!(h, HandlerConfig::MessageServer { .. })) - { - if effective_permissions.message_server.is_none() { - return Err( - "MessageServer handler requested but not permitted by effective permissions" - .to_string(), - ); - } - handlers.push(Handler::MessageServer( - MessageServerHost::new( - actor_sender, - actor_mailbox, - theater_tx.clone(), - effective_permissions.message_server.clone(), - ), - effective_permissions.message_server.clone(), - )); - } + initial_state: Option, + engine: Engine, + chain: Arc>, + handler_registry: HandlerRegistry, + theater_tx: Sender, + operation_rx: Receiver, + operation_tx: Sender, + info_rx: Receiver, + info_tx: Sender, + control_rx: Receiver, + control_tx: Sender, + ) -> () { + info!("Actor runtime starting communication loops"); + let actor_phase_manager = ActorPhaseManager::new(); - for handler_config in &config.handlers { - let handler = match handler_config { - HandlerConfig::MessageServer { .. } => { - debug!("MessageServer handler already created"); - None - } - HandlerConfig::Environment { config } => { - // Check permission before creating handler - if effective_permissions.environment.is_none() { - return Err("Environment handler requested but not permitted by effective permissions".to_string()); - } - Some(Handler::Environment( - EnvironmentHost::new( - config.clone(), - effective_permissions.environment.clone(), - ), - effective_permissions.environment.clone(), - )) - } - HandlerConfig::FileSystem { config } => { - // Check permission before creating handler - if effective_permissions.file_system.is_none() { - return Err("FileSystem handler requested but not permitted by effective permissions".to_string()); - } - Some(Handler::FileSystem( - FileSystemHost::new( - config.clone(), - effective_permissions.file_system.clone(), - ), - effective_permissions.file_system.clone(), - )) - } - HandlerConfig::HttpClient { config } => { - // Check permission before creating handler - if effective_permissions.http_client.is_none() { - return Err("HttpClient handler requested but not permitted by effective permissions".to_string()); + // These will be set once setup completes + let actor_instance_wrapper: Arc>> = + Arc::new(RwLock::new(None)); + let metrics: Arc> = Arc::new(RwLock::new(MetricsCollector::new())); + let handler_tasks: Arc>>> = Arc::new(RwLock::new(vec![])); + let handlers_shutdown_controller: Arc>> = + Arc::new(RwLock::new(None)); + let operation_rx = operation_rx; + let info_rx = info_rx; + let mut control_rx = control_rx; + + let setup_handle = { + let actor_instance_wrapper = actor_instance_wrapper.clone(); + let actor_phase_manager = actor_phase_manager.clone(); + let config = config.clone(); + let theater_tx = theater_tx.clone(); + let handler_tasks = handler_tasks.clone(); + let handlers_shutdown_controller = handlers_shutdown_controller.clone(); + + tokio::spawn(async move { + match Self::build_actor_resources( + id, + &config, + initial_state, + engine, + chain, + handler_registry, + theater_tx, + operation_tx, + info_tx, + control_tx, + actor_phase_manager.clone(), + ) + .await + { + Ok((actor_instance, shutdown_controller, handlers)) => { + { + let mut instance_guard = actor_instance_wrapper.write().await; + *instance_guard = Some(actor_instance); + } + { + let mut handler_tasks_guard = handler_tasks.write().await; + *handler_tasks_guard = handlers; + } + { + let mut shutdown_controller_guard = + handlers_shutdown_controller.write().await; + *shutdown_controller_guard = Some(shutdown_controller); + } + + actor_phase_manager.set_phase(ActorPhase::Running).await; + info!("Actor setup complete, now running"); } - Some(Handler::HttpClient( - HttpClientHost::new( - config.clone(), - effective_permissions.http_client.clone(), - ), - effective_permissions.http_client.clone(), - )) - } - HandlerConfig::HttpFramework { .. } => { - // Check permission before creating handler - if effective_permissions.http_framework.is_none() { - return Err("HttpFramework handler requested but not permitted by effective permissions".to_string()); + Err(e) => { + error!("Failed to set up actor runtime: {}", e); + // Handle setup failure (e.g., notify theater runtime) } - Some(Handler::HttpFramework( - HttpFramework::new(effective_permissions.http_framework.clone()), - effective_permissions.http_framework.clone(), - )) } - HandlerConfig::Runtime { config } => { - // Check permission before creating handler - if effective_permissions.runtime.is_none() { - return Err( - "Runtime handler requested but not permitted by effective permissions" - .to_string(), - ); + }) + }; + + let info_handle = { + let actor_instance_wrapper = actor_instance_wrapper.clone(); + let metrics = metrics.clone(); + let actor_phase_manager = actor_phase_manager.clone(); + + tokio::spawn(Self::info_loop( + info_rx, + actor_instance_wrapper, + metrics, + actor_phase_manager, + )) + }; + + let operation_handle = { + let actor_instance_wrapper = actor_instance_wrapper.clone(); + let metrics = metrics.clone(); + let theater_tx = theater_tx.clone(); + let actor_phase_manager = actor_phase_manager.clone(); + + tokio::spawn(Self::operation_loop( + operation_rx, + actor_instance_wrapper, + metrics, + theater_tx, + actor_phase_manager, + )) + }; + + while let Some(control) = control_rx.recv().await { + info!("Received control command: {:?}", control); + match control { + ActorControl::Shutdown { response_tx } => { + info!("Shutdown requested"); + actor_phase_manager + .set_phase(ActorPhase::ShuttingDown) + .await; + + // Wait for operation and info loops to finish gracefully + let (_, _, _) = tokio::join!(operation_handle, info_handle, setup_handle); + + match handlers_shutdown_controller.write().await.take() { + Some(controller) => { + controller.signal_shutdown(ShutdownType::Graceful).await; + } + None => { + warn!("No handlers shutdown controller found"); + } } - Some(Handler::Runtime( - RuntimeHost::new( - config.clone(), - theater_tx.clone(), - effective_permissions.runtime.clone(), - ), - effective_permissions.runtime.clone(), - )) - } - HandlerConfig::Supervisor { config } => { - // Check permission before creating handler - if effective_permissions.supervisor.is_none() { - return Err("Supervisor handler requested but not permitted by effective permissions".to_string()); + + if let Err(e) = response_tx.send(Ok(())) { + error!("Failed to send shutdown confirmation: {:?}", e); } - Some(Handler::Supervisor( - SupervisorHost::new( - config.clone(), - effective_permissions.supervisor.clone(), - ), - effective_permissions.supervisor.clone(), - )) + break; } - HandlerConfig::Process { config } => { - // Check permission before creating handler - if effective_permissions.process.is_none() { - return Err( - "Process handler requested but not permitted by effective permissions" - .to_string(), - ); + ActorControl::Terminate { response_tx } => { + info!("Terminate requested"); + // Abort info and operation loops + operation_handle.abort(); + info_handle.abort(); + setup_handle.abort(); + match handlers_shutdown_controller.write().await.take() { + Some(controller) => { + controller.signal_shutdown(ShutdownType::Force).await; + } + None => { + warn!("No handlers shutdown controller found"); + } } - Some(Handler::Process( - ProcessHost::new( - config.clone(), - actor_handle.clone(), - effective_permissions.process.clone(), - ), - effective_permissions.process.clone(), - )) - } - HandlerConfig::Store { config } => { - // Check permission before creating handler - if effective_permissions.store.is_none() { - return Err( - "Store handler requested but not permitted by effective permissions" - .to_string(), - ); + if let Err(e) = response_tx.send(Ok(())) { + error!("Failed to send terminate confirmation: {:?}", e); } - Some(Handler::Store( - StoreHost::new(config.clone(), effective_permissions.store.clone()), - effective_permissions.store.clone(), - )) + break; } - HandlerConfig::Timing { config } => { - // Check permission before creating handler - if effective_permissions.timing.is_none() { - return Err( - "Timing handler requested but not permitted by effective permissions" - .to_string(), - ); + ActorControl::Pause { response_tx } => { + if actor_phase_manager.is_phase(ActorPhase::ShuttingDown).await { + let _ = response_tx.send(Err(ActorError::ShuttingDown)); + } else { + actor_phase_manager.set_phase(ActorPhase::Paused).await; + let _ = response_tx.send(Ok(())); } - Some(Handler::Timing( - TimingHost::new(config.clone(), effective_permissions.timing.clone()), - effective_permissions.timing.clone(), - )) } - HandlerConfig::Random { config } => { - // Check permission before creating handler - if effective_permissions.random.is_none() { - return Err( - "Random handler requested but not permitted by effective permissions" - .to_string(), - ); + ActorControl::Resume { response_tx } => { + match actor_phase_manager.get_phase().await { + ActorPhase::Starting | ActorPhase::Running => { + let _ = response_tx.send(Err(ActorError::NotPaused)); + } + ActorPhase::ShuttingDown => { + let _ = response_tx.send(Err(ActorError::ShuttingDown)); + } + ActorPhase::Paused => { + actor_phase_manager.set_phase(ActorPhase::Running).await; + let _ = response_tx.send(Ok(())); + } } - Some(Handler::Random( - RandomHost::new(config.clone(), effective_permissions.random.clone()), - effective_permissions.random.clone(), - )) } - }; - if let Some(handler) = handler { - handlers.push(handler); } } - Ok(handlers) - } + // Gonna have to send the shutdown signal to all our handlers / respond to the shutdown + // request - /// Creates and initializes the actor component - async fn create_actor_component( - config: &ManifestConfig, - actor_store: ActorStore, - engine: wasmtime::Engine, - ) -> Result { - match ActorComponent::new( - config.name.clone(), - config.component.clone(), - actor_store, - engine, - ) - .await - { - Ok(component) => Ok(component), - Err(e) => { - let error_message = format!( - "Failed to create actor component for actor {}: {}", - config.name, e - ); - error!("{}", error_message); - Err(e.into()) + info!("Actor runtime communication loop exiting, performing cleanup"); + let metrics = metrics.read().await; + + // If any handlers are still running, abort them + let handler_tasks = handler_tasks.read().await; + for handle in handler_tasks.iter() { + if !handle.is_finished() { + info!("Aborting handler task"); + handle.abort(); } } + + // Log final metrics + let final_metrics = metrics.get_metrics().await; + info!("Final metrics at shutdown: {:?}", final_metrics); + + info!("Actor runtime cleanup complete"); } - /// Sets up host functions for all handlers - async fn setup_host_functions( - actor_component: &mut ActorComponent, - mut handlers: Vec, - ) -> Result> { - for handler in &mut handlers { - info!( - "Setting up host functions for handler: {:?}", - handler.name() - ); - if let Err(e) = handler.setup_host_functions(actor_component).await { - let error_message = format!( - "Failed to set up host functions for handler {}: {}", - handler.name(), - e - ); - error!("{}", error_message); - return Err(e.into()); + async fn operation_loop( + mut operation_rx: Receiver, + actor_instance_wrapper: Arc>>, + metrics: Arc>, + theater_tx: Sender, + actor_phase_manager: ActorPhaseManager, + ) { + actor_phase_manager + .wait_for_phase(ActorPhase::Running) + .await; + + loop { + tokio::select! { + biased; + + _ = actor_phase_manager.wait_for_phase(ActorPhase::ShuttingDown) => { + break; + } + + _ = actor_phase_manager.wait_for_phase(ActorPhase::Paused) => { + actor_phase_manager.wait_for_phase(ActorPhase::Running).await; + } + + Some(op) = operation_rx.recv() => { + Self::process_operation( + op, &actor_instance_wrapper, &metrics, &theater_tx, actor_phase_manager.clone() + ).await + } + + else => break, } } - Ok(handlers) } - /// Instantiates the actor component - async fn instantiate_component( - actor_component: ActorComponent, - id: TheaterId, - ) -> Result { - match actor_component.instantiate().await { - Ok(instance) => Ok(instance), - Err(e) => { - let error_message = format!("Failed to instantiate actor {}: {}", id, e); - error!("{}", error_message); - Err(e.into()) + async fn process_operation( + op: ActorOperation, + actor_instance_wrapper: &Arc>>, + metrics: &Arc>, + theater_tx: &Sender, + actor_phase_manager: ActorPhaseManager, + ) -> () { + match op { + ActorOperation::CallFunction { + name, + params, + response_tx, + } => { + info!("Processing function call: {}", name); + let mut actor_instance_guard = actor_instance_wrapper.write().await; + let actor_instance = match &mut *actor_instance_guard { + Some(instance) => instance, + None => { + let err = ActorRuntimeError::ActorInstanceNotFound { + message: "Actor instance not found".to_string(), + }; + + let _ = theater_tx + .send(TheaterCommand::ActorRuntimeError { error: err }) + .await; + + let actor_err = + ActorError::UnexpectedError("Actor instance not found".to_string()); + + if let Err(e) = response_tx.send(Err(actor_err)) { + error!( + "Failed to send function call error response for operation '{}': {:?}", + name, e + ); + } + return; + } + }; + let metrics = metrics.write().await; + match Self::execute_call(actor_instance, &name, params, &theater_tx, &metrics).await + { + Ok(result) => { + if let Err(e) = response_tx.send(Ok(result)) { + error!( + "Failed to send function call response for operation '{}': {:?}", + name, e + ); + } + } + Err(actor_error) => { + let _ = theater_tx + .send(TheaterCommand::ActorError { + actor_id: actor_instance.id().clone(), + error: actor_error.clone(), + }) + .await; + + error!("Operation '{}' failed with error: {:?}", name, actor_error); + if let Err(send_err) = response_tx.send(Err(actor_error)) { + error!("Failed to send function call error response for operation '{}': {:?}", name, send_err); + } + + // Pause the actor on error + actor_phase_manager.set_phase(ActorPhase::Paused).await; + } + } } } } - /// Sets up export functions for all handlers - async fn setup_export_functions( - actor_instance: &mut ActorInstance, - handlers: &[Handler], - ) -> Result<()> { - for handler in handlers { - info!("Creating functions for handler: {:?}", handler.name()); - if let Err(e) = handler.add_export_functions(actor_instance).await { - let error_message = format!( - "Failed to create export functions for handler {}: {}", - handler.name(), - e - ); - error!("{}", error_message); - return Err(e.into()); + async fn info_loop( + mut info_rx: Receiver, + actor_instance_wrapper: Arc>>, + metrics: Arc>, + actor_phase_manager: ActorPhaseManager, + ) { + // Handle info requests + while let Some(info) = info_rx.recv().await { + info!("Received info request: {:?}", info); + match info { + ActorInfo::GetStatus { response_tx } => { + let status = actor_phase_manager.get_phase().await.to_string(); + + if let Err(e) = response_tx.send(Ok(status)) { + error!("Failed to send status response: {:?}", e); + } + } + ActorInfo::GetState { response_tx } => { + match &*actor_instance_wrapper.read().await { + Some(instance) => { + let state = instance.store.data().get_state(); + if let Err(e) = response_tx.send(Ok(state)) { + error!("Failed to send state response: {:?}", e); + } + } + None => { + let err = + ActorError::UnexpectedError("Actor instance not found".to_string()); + if let Err(e) = response_tx.send(Err(err)) { + error!("Failed to send state error response: {:?}", e); + } + } + } + } + ActorInfo::GetChain { response_tx } => { + match &*actor_instance_wrapper.read().await { + None => { + let err = + ActorError::UnexpectedError("Actor instance not found".to_string()); + if let Err(e) = response_tx.send(Err(err)) { + error!("Failed to send chain error response: {:?}", e); + } + return; + } + Some(instance) => { + let chain = instance.store.data().get_chain(); + if let Err(e) = response_tx.send(Ok(chain)) { + error!("Failed to send chain response: {:?}", e); + } + } + }; + } + ActorInfo::GetMetrics { response_tx } => { + let metrics = metrics.read().await; + let metrics_data = metrics.get_metrics().await; + if let Err(e) = response_tx.send(Ok(metrics_data)) { + error!("Failed to send metrics response: {:?}", e); + } + } + ActorInfo::SaveChain { response_tx } => { + match &mut *actor_instance_wrapper.write().await { + None => { + let err = + ActorError::UnexpectedError("Actor instance not found".to_string()); + if let Err(e) = response_tx.send(Err(err)) { + error!("Failed to send save chain error response: {:?}", e); + } + return; + } + Some(instance) => match instance.save_chain() { + Ok(_) => { + if let Err(e) = response_tx.send(Ok(())) { + error!("Failed to send save chain response: {:?}", e); + } + } + Err(e) => { + if let Err(send_err) = response_tx + .send(Err(ActorError::UnexpectedError(e.to_string()))) + { + error!( + "Failed to send save chain error response: {:?}", + send_err + ); + } + } + }, + }; + } } } - Ok(()) } /// @@ -1160,234 +1010,4 @@ impl ActorRuntime { Ok(results) } - - /// Updates the WebAssembly component of an actor instance. - /// - /// This method implements hot-swapping of the WebAssembly component while preserving - /// the actor's state and reusing the existing setup logic. - /// - /// ## Parameters - /// - /// * `actor_instance` - Mutable reference to the actor instance to update - /// * `component_address` - The address of the new component to load - /// * `handlers` - The existing handlers - /// - /// ## Returns - /// - /// * `Ok(())` - If the component was successfully updated - /// * `Err(ActorError)` - If the update failed - #[allow(dead_code)] - async fn update_component( - actor_instance: &mut ActorInstance, - component_address: &str, - config: &ManifestConfig, - engine: wasmtime::Engine, - ) -> Result<(), ActorError> { - let actor_id = actor_instance.id(); - info!( - "Updating component for actor {} to: {}", - actor_id, component_address - ); - - let mut new_config = config.clone(); - - // Get current state before updating - let current_state = actor_instance.store.data().get_state(); - - // Record update started event - actor_instance - .store - .data_mut() - .record_event(ChainEventData { - event_type: "theater-runtime".to_string(), - data: EventData::TheaterRuntime(TheaterRuntimeEventData::ActorUpdateStart { - new_component_address: component_address.to_string(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: format!("Starting update to component [{}]", component_address).into(), - }); - - // Create a temporary config for the new component - new_config.component = component_address.to_string(); - - // Set up actor store - reuse the existing one without creating a new one - let actor_store = actor_instance.actor_component.actor_store.clone(); - - let (actor_sender_spoof, actor_mailbox_spoof) = mpsc::channel::(1); - let (theater_tx_spoof, _) = mpsc::channel::(1); - - // Calculate effective permissions for update (use root permissions for now) - let parent_permissions = crate::config::permissions::HandlerPermission::root(); - let effective_permissions = new_config.calculate_effective_permissions(&parent_permissions); - - let handlers = match Self::create_handlers( - actor_sender_spoof, - actor_mailbox_spoof, - theater_tx_spoof, - &new_config, - actor_instance - .actor_component - .actor_store - .get_actor_handle(), - &effective_permissions, - ) { - Ok(handlers) => handlers, - Err(e) => { - error!("Handler creation failed during update: {}", e); - todo!(); - //return Err(ActorError::UpdateError(format!("Handler creation failed: {}", e))); - } - }; - - // Create new component - reusing existing method - let mut new_actor_component = - match Self::create_actor_component(&new_config, actor_store, engine).await { - Ok(component) => component, - Err(e) => { - let error_message = format!("Failed to create actor component: {}", e); - Self::record_update_error(actor_instance, component_address, &error_message); - return Err(ActorError::UpdateComponentError(error_message)); - } - }; - - // Setup host functions - reusing existing method - let handlers = match Self::setup_host_functions(&mut new_actor_component, handlers).await { - Ok(handlers) => handlers, - Err(e) => { - let error_message = format!("Failed to setup host functions: {}", e); - Self::record_update_error(actor_instance, component_address, &error_message); - return Err(ActorError::UpdateComponentError(error_message)); - } - }; - - // Instantiate component - reusing existing method - let mut new_instance = - match Self::instantiate_component(new_actor_component, actor_id.clone()).await { - Ok(instance) => instance, - Err(e) => { - let error_message = format!("Failed to instantiate component: {}", e); - Self::record_update_error(actor_instance, component_address, &error_message); - return Err(ActorError::UpdateComponentError(error_message)); - } - }; - - // Setup export functions - reusing existing method - if let Err(e) = Self::setup_export_functions(&mut new_instance, &handlers).await { - let error_message = format!("Failed to setup export functions: {}", e); - Self::record_update_error(actor_instance, component_address, &error_message); - return Err(ActorError::UpdateComponentError(error_message)); - } - - // Swap the instance - std::mem::swap(actor_instance, &mut new_instance); - - // Restore state - actor_instance.store.data_mut().set_state(current_state); - - // Record update success - actor_instance - .store - .data_mut() - .record_event(ChainEventData { - event_type: "theater-runtime".to_string(), - data: EventData::TheaterRuntime(TheaterRuntimeEventData::ActorUpdateComplete { - new_component_address: component_address.to_string(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: format!("Successfully updated to component [{}]", component_address) - .into(), - }); - - info!("Component updated successfully for actor {}", actor_id); - Ok(()) - } - - /// Helper method to record update errors in the actor's chain - #[allow(dead_code)] - fn record_update_error( - actor_instance: &mut ActorInstance, - component_address: &str, - error_message: &str, - ) { - error!("{}", error_message); - actor_instance - .store - .data_mut() - .record_event(ChainEventData { - event_type: "theater-runtime".to_string(), - data: EventData::TheaterRuntime(TheaterRuntimeEventData::ActorUpdateError { - new_component_address: component_address.to_string(), - error: error_message.to_string(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: format!( - "Failed to update to component [{}]: {}", - component_address, error_message - ) - .into(), - }); - } - - /// # Perform final cleanup when shutting down - /// - /// This method is called during shutdown to release resources and log - /// final metrics before the actor terminates. - async fn perform_cleanup( - shutdown_controller: ShutdownController, - handler_tasks: Vec>, - metrics: &MetricsCollector, - ) { - info!("Performing final cleanup"); - - // Signal shutdown to all handler components - info!("Signaling shutdown to all handler components"); - shutdown_controller - .signal_shutdown(ShutdownType::Graceful) - .await; - - // If any handlers are still running, abort them - for task in handler_tasks { - if !task.is_finished() { - debug!("Aborting handler task that didn't shut down gracefully"); - task.abort(); - } - } - - // Log final metrics - let final_metrics = metrics.get_metrics().await; - info!("Final metrics at shutdown: {:?}", final_metrics); - - info!("Actor runtime cleanup complete"); - } - - /// # Stop the actor runtime - /// - /// Gracefully shuts down the actor runtime and all its components. - /// This method is retained for API compatibility but delegates to the - /// shutdown controller. - /// - /// ## Returns - /// - /// * `Ok(())` - The runtime was successfully shut down - /// * `Err(anyhow::Error)` - An error occurred during shutdown - pub async fn stop(mut self) -> Result<()> { - info!("Initiating actor runtime shutdown"); - - // Signal shutdown to all components - info!("Signaling shutdown to all components"); - self.shutdown_controller - .signal_shutdown(ShutdownType::Graceful) - .await; - - // If any handlers are still running, abort them - for task in self.handler_tasks.drain(..) { - if !task.is_finished() { - debug!("Aborting handler task that didn't shut down gracefully"); - task.abort(); - } - } - - info!("Actor runtime shutdown complete"); - Ok(()) - } } diff --git a/crates/theater/src/actor/store.rs b/crates/theater/src/actor/store.rs index 53075779..ac072911 100644 --- a/crates/theater/src/actor/store.rs +++ b/crates/theater/src/actor/store.rs @@ -6,9 +6,9 @@ use crate::actor::handle::ActorHandle; use crate::chain::{ChainEvent, StateChain}; -use crate::events::ChainEventData; +use crate::events::{ChainEventData, EventData, EventPayload}; use crate::id::TheaterId; -use crate::messages::TheaterCommand; +use crate::messages::{MessageCommand, TheaterCommand}; use std::sync::{Arc, RwLock}; use tokio::sync::mpsc::Sender; @@ -24,15 +24,22 @@ use tokio::sync::mpsc::Sender; /// - The actor's current state data /// - A handle to interact with the actor #[derive(Clone)] -pub struct ActorStore { +pub struct ActorStore +where + E: EventPayload, +{ /// Unique identifier for the actor pub id: TheaterId, /// Channel for sending commands to the Theater runtime pub theater_tx: Sender, + /// Optional channel for sending message commands to the message-server handler + /// This is only available when the message-server handler is loaded + pub message_tx: Option>, + /// The event chain that records all actor operations for verification and audit - pub chain: Arc>, + pub chain: Arc>>, /// The current state of the actor, stored as a binary blob pub state: Option>, @@ -41,7 +48,10 @@ pub struct ActorStore { pub actor_handle: ActorHandle, } -impl ActorStore { +impl ActorStore +where + E: EventPayload, +{ /// # Create a new ActorStore /// /// Creates a new instance of the ActorStore with the given parameters. @@ -50,6 +60,7 @@ impl ActorStore { /// /// * `id` - Unique identifier for the actor /// * `theater_tx` - Channel for sending commands to the Theater runtime + /// * `message_tx` - Optional channel for sending message commands to the message-server handler /// * `actor_handle` - Handle for interacting with the actor /// /// ## Returns @@ -58,16 +69,18 @@ impl ActorStore { pub fn new( id: TheaterId, theater_tx: Sender, + message_tx: Option>, actor_handle: ActorHandle, - chain: Arc>, - ) -> anyhow::Result { - Ok(Self { + chain: Arc>>, + ) -> Self { + Self { id: id.clone(), theater_tx: theater_tx.clone(), + message_tx, chain, state: Some(vec![]), actor_handle, - }) + } } /// # Get the actor's ID @@ -129,7 +142,7 @@ impl ActorStore { /// ## Returns /// /// The ChainEvent that was created and added to the chain. - pub fn record_event(&self, event_data: ChainEventData) -> ChainEvent { + pub fn record_event(&self, event_data: ChainEventData) -> ChainEvent { let mut chain = self.chain.write().unwrap(); chain .add_typed_event(event_data) diff --git a/crates/theater/src/actor/types.rs b/crates/theater/src/actor/types.rs index 0961d8f4..0de73e81 100644 --- a/crates/theater/src/actor/types.rs +++ b/crates/theater/src/actor/types.rs @@ -174,13 +174,6 @@ pub enum ActorOperation { /// Channel to send the result back to the caller response_tx: oneshot::Sender, ActorError>>, }, - /// Update a WebAssembly component in the actor - UpdateComponent { - /// Address of the component to update - component_address: String, - /// Channel to send the result back to the caller - response_tx: oneshot::Sender>, - }, } #[derive(Debug)] diff --git a/crates/theater/src/chain/mod.rs b/crates/theater/src/chain/mod.rs index 2d63471f..f3ae7422 100644 --- a/crates/theater/src/chain/mod.rs +++ b/crates/theater/src/chain/mod.rs @@ -32,15 +32,17 @@ use console::style; use serde::{Deserialize, Serialize}; // use sha1::Digest; use std::fmt; +use std::marker::PhantomData; use std::path::Path; use tokio::sync::mpsc::Sender; use tracing::debug; use wasmtime::component::{ComponentType, Lift, Lower}; -use crate::events::ChainEventData; +use crate::events::{ChainEventData, EventData, EventPayload}; use crate::messages::TheaterCommand; use crate::store::ContentRef; use crate::TheaterId; +use theater_chain::event::EventType; /// # Chain Event /// @@ -187,6 +189,16 @@ impl fmt::Display for ChainEvent { } } +impl EventType for ChainEvent { + fn event_type(&self) -> String { + self.event_type.clone() + } + + fn len(&self) -> usize { + self.data.len() + } +} + // implement Eq for ChainEvent impl PartialEq for ChainEvent { fn eq(&self, other: &Self) -> bool { @@ -247,7 +259,10 @@ impl PartialEq for ChainEvent { /// all actors in the system. The state chain can also be persisted to disk for /// long-term storage or debugging. #[derive(Debug, Clone, Serialize)] -pub struct StateChain { +pub struct StateChain +where + E: EventPayload, +{ /// The ordered sequence of events in this chain, from oldest to newest. events: Vec, /// Hash of the most recent event in the chain, or None if the chain is empty. @@ -260,9 +275,14 @@ pub struct StateChain { /// This is excluded from serialization as it's determined by context. #[serde(skip)] actor_id: TheaterId, + #[serde(skip)] + marker: PhantomData, } -impl StateChain { +impl StateChain +where + E: EventPayload, +{ /// Creates a new empty state chain for an actor. /// /// ## Purpose @@ -301,6 +321,7 @@ impl StateChain { current_hash: None, theater_tx, actor_id, + marker: PhantomData, } } @@ -365,7 +386,7 @@ impl StateChain { /// the notification to be delivered. pub fn add_typed_event( &mut self, - event_data: ChainEventData, + event_data: ChainEventData, ) -> Result { // Create initial event structure without hash let mut event = event_data.to_chain_event(self.current_hash.clone()); diff --git a/crates/theater/src/config/enforcement.rs b/crates/theater/src/config/enforcement.rs index 4fb882b6..7d339e2f 100644 --- a/crates/theater/src/config/enforcement.rs +++ b/crates/theater/src/config/enforcement.rs @@ -1,33 +1,52 @@ -use crate::config::permissions::*; use crate::config::actor_manifest::*; +use crate::config::permissions::*; use thiserror::Error; #[derive(Error, Debug)] pub enum PermissionError { #[error("Handler configuration exceeds granted permissions: {reason}")] ConfigExceedsPermissions { reason: String }, - + #[error("Operation denied: {operation} not permitted by {permission_type} permissions")] - OperationDenied { operation: String, permission_type: String }, - + OperationDenied { + operation: String, + permission_type: String, + }, + #[error("Handler type '{handler_type}' not permitted by effective permissions")] HandlerNotPermitted { handler_type: String }, - + #[error("Path '{path}' not in allowed paths: {allowed_paths:?}")] - PathNotAllowed { path: String, allowed_paths: Vec }, - + PathNotAllowed { + path: String, + allowed_paths: Vec, + }, + #[error("Command '{command}' not in allowed commands: {allowed_commands:?}")] - CommandNotAllowed { command: String, allowed_commands: Vec }, - + CommandNotAllowed { + command: String, + allowed_commands: Vec, + }, + #[error("Host '{host}' not in allowed hosts: {allowed_hosts:?}")] - HostNotAllowed { host: String, allowed_hosts: Vec }, - + HostNotAllowed { + host: String, + allowed_hosts: Vec, + }, + #[error("Method '{method}' not in allowed methods: {allowed_methods:?}")] - MethodNotAllowed { method: String, allowed_methods: Vec }, - + MethodNotAllowed { + method: String, + allowed_methods: Vec, + }, + #[error("Resource limit exceeded: {resource} = {requested} > {limit}")] - ResourceLimitExceeded { resource: String, requested: usize, limit: usize }, - + ResourceLimitExceeded { + resource: String, + requested: usize, + limit: usize, + }, + #[error("Environment variable '{var}' access denied")] EnvVarDenied { var: String }, } @@ -99,16 +118,18 @@ fn validate_filesystem_config( config: &FileSystemHandlerConfig, permissions: &Option, ) -> PermissionResult<()> { - let perms = permissions.as_ref().ok_or_else(|| PermissionError::HandlerNotPermitted { - handler_type: "filesystem".to_string(), - })?; + let perms = permissions + .as_ref() + .ok_or_else(|| PermissionError::HandlerNotPermitted { + handler_type: "filesystem".to_string(), + })?; // Check if requested path is allowed if let Some(config_path) = &config.path { if let Some(allowed_paths) = &perms.allowed_paths { - let path_allowed = allowed_paths.iter().any(|allowed| { - config_path.starts_with(allowed) - }); + let path_allowed = allowed_paths + .iter() + .any(|allowed| config_path.starts_with(allowed)); if !path_allowed { return Err(PermissionError::PathNotAllowed { path: config_path.to_string_lossy().to_string(), @@ -171,9 +192,11 @@ fn validate_process_config( config: &ProcessHostConfig, permissions: &Option, ) -> PermissionResult<()> { - let perms = permissions.as_ref().ok_or_else(|| PermissionError::HandlerNotPermitted { - handler_type: "process".to_string(), - })?; + let perms = permissions + .as_ref() + .ok_or_else(|| PermissionError::HandlerNotPermitted { + handler_type: "process".to_string(), + })?; // Check max_processes limit if config.max_processes > perms.max_processes { @@ -227,9 +250,11 @@ fn validate_environment_config( config: &EnvironmentHandlerConfig, permissions: &Option, ) -> PermissionResult<()> { - let perms = permissions.as_ref().ok_or_else(|| PermissionError::HandlerNotPermitted { - handler_type: "environment".to_string(), - })?; + let perms = permissions + .as_ref() + .ok_or_else(|| PermissionError::HandlerNotPermitted { + handler_type: "environment".to_string(), + })?; // Check if allow_list_all is permitted if config.allow_list_all && !perms.allow_list_all { @@ -243,9 +268,7 @@ fn validate_environment_config( if let Some(allowed_vars) = &perms.allowed_vars { for var in config_vars { if !allowed_vars.contains(var) { - return Err(PermissionError::EnvVarDenied { - var: var.clone(), - }); + return Err(PermissionError::EnvVarDenied { var: var.clone() }); } } } @@ -258,9 +281,11 @@ fn validate_random_config( config: &RandomHandlerConfig, permissions: &Option, ) -> PermissionResult<()> { - let perms = permissions.as_ref().ok_or_else(|| PermissionError::HandlerNotPermitted { - handler_type: "random".to_string(), - })?; + let perms = permissions + .as_ref() + .ok_or_else(|| PermissionError::HandlerNotPermitted { + handler_type: "random".to_string(), + })?; // Check max_bytes limit if config.max_bytes > perms.max_bytes { @@ -294,9 +319,11 @@ fn validate_timing_config( config: &TimingHostConfig, permissions: &Option, ) -> PermissionResult<()> { - let perms = permissions.as_ref().ok_or_else(|| PermissionError::HandlerNotPermitted { - handler_type: "timing".to_string(), - })?; + let perms = permissions + .as_ref() + .ok_or_else(|| PermissionError::HandlerNotPermitted { + handler_type: "timing".to_string(), + })?; // Check max_sleep_duration limit if config.max_sleep_duration > perms.max_sleep_duration { @@ -331,10 +358,12 @@ impl PermissionChecker { path: Option<&str>, command: Option<&str>, ) -> PermissionResult<()> { - let perms = permissions.as_ref().ok_or_else(|| PermissionError::OperationDenied { - operation: operation.to_string(), - permission_type: "filesystem".to_string(), - })?; + let perms = permissions + .as_ref() + .ok_or_else(|| PermissionError::OperationDenied { + operation: operation.to_string(), + permission_type: "filesystem".to_string(), + })?; match operation { "read" => { @@ -366,12 +395,18 @@ impl PermissionChecker { // Check path restrictions - FAIL CLOSED: require explicit path allowlist if let Some(path) = path { - let allowed_paths = perms.allowed_paths.as_ref().ok_or_else(|| PermissionError::PathNotAllowed { - path: path.to_string(), - allowed_paths: vec!["".to_string()], - })?; - - let path_allowed = allowed_paths.iter().any(|allowed| path.starts_with(allowed)); + let allowed_paths = + perms + .allowed_paths + .as_ref() + .ok_or_else(|| PermissionError::PathNotAllowed { + path: path.to_string(), + allowed_paths: vec!["".to_string()], + })?; + + let path_allowed = allowed_paths + .iter() + .any(|allowed| path.starts_with(allowed)); if !path_allowed { return Err(PermissionError::PathNotAllowed { path: path.to_string(), @@ -401,10 +436,12 @@ impl PermissionChecker { method: &str, host: &str, ) -> PermissionResult<()> { - let perms = permissions.as_ref().ok_or_else(|| PermissionError::OperationDenied { - operation: format!("{} {}", method, host), - permission_type: "http_client".to_string(), - })?; + let perms = permissions + .as_ref() + .ok_or_else(|| PermissionError::OperationDenied { + operation: format!("{} {}", method, host), + permission_type: "http_client".to_string(), + })?; // Check method if let Some(allowed_methods) = &perms.allowed_methods { @@ -434,10 +471,12 @@ impl PermissionChecker { permissions: &Option, var_name: &str, ) -> PermissionResult<()> { - let perms = permissions.as_ref().ok_or_else(|| PermissionError::OperationDenied { - operation: format!("access env var {}", var_name), - permission_type: "environment".to_string(), - })?; + let perms = permissions + .as_ref() + .ok_or_else(|| PermissionError::OperationDenied { + operation: format!("access env var {}", var_name), + permission_type: "environment".to_string(), + })?; // Check denied list first if let Some(denied_vars) = &perms.denied_vars { @@ -459,7 +498,9 @@ impl PermissionChecker { // Check prefixes if let Some(allowed_prefixes) = &perms.allowed_prefixes { - let has_allowed_prefix = allowed_prefixes.iter().any(|prefix| var_name.starts_with(prefix)); + let has_allowed_prefix = allowed_prefixes + .iter() + .any(|prefix| var_name.starts_with(prefix)); if !has_allowed_prefix { return Err(PermissionError::EnvVarDenied { var: var_name.to_string(), @@ -476,10 +517,12 @@ impl PermissionChecker { program: &str, current_process_count: usize, ) -> PermissionResult<()> { - let perms = permissions.as_ref().ok_or_else(|| PermissionError::OperationDenied { - operation: format!("execute {}", program), - permission_type: "process".to_string(), - })?; + let perms = permissions + .as_ref() + .ok_or_else(|| PermissionError::OperationDenied { + operation: format!("execute {}", program), + permission_type: "process".to_string(), + })?; // Check process count limit if current_process_count >= perms.max_processes { @@ -509,10 +552,12 @@ impl PermissionChecker { bytes_requested: Option, max_value: Option, ) -> PermissionResult<()> { - let perms = permissions.as_ref().ok_or_else(|| PermissionError::OperationDenied { - operation: operation.to_string(), - permission_type: "random".to_string(), - })?; + let perms = permissions + .as_ref() + .ok_or_else(|| PermissionError::OperationDenied { + operation: operation.to_string(), + permission_type: "random".to_string(), + })?; // Check byte limit if let Some(bytes) = bytes_requested { @@ -545,10 +590,12 @@ impl PermissionChecker { operation: &str, duration_ms: u64, ) -> PermissionResult<()> { - let perms = permissions.as_ref().ok_or_else(|| PermissionError::OperationDenied { - operation: operation.to_string(), - permission_type: "timing".to_string(), - })?; + let perms = permissions + .as_ref() + .ok_or_else(|| PermissionError::OperationDenied { + operation: operation.to_string(), + permission_type: "timing".to_string(), + })?; // Check max duration limit if duration_ms > perms.max_sleep_duration { diff --git a/crates/theater/src/config/inheritance.rs b/crates/theater/src/config/inheritance.rs index b30cff8e..8bf5af28 100644 --- a/crates/theater/src/config/inheritance.rs +++ b/crates/theater/src/config/inheritance.rs @@ -19,9 +19,9 @@ impl Default for HandlerInheritance { } } -impl PartialEq for HandlerInheritance -where - T: PartialEq +impl PartialEq for HandlerInheritance +where + T: PartialEq, { fn eq(&self, other: &Self) -> bool { match (self, other) { @@ -34,47 +34,69 @@ where } // Helper functions for skip_serializing_if -fn is_inherit_message_server(val: &HandlerInheritance) -> bool { +fn is_inherit_message_server( + val: &HandlerInheritance, +) -> bool { matches!(val, HandlerInheritance::Inherit) } -fn is_inherit_file_system(val: &HandlerInheritance) -> bool { +fn is_inherit_file_system( + val: &HandlerInheritance, +) -> bool { matches!(val, HandlerInheritance::Inherit) } -fn is_inherit_http_client(val: &HandlerInheritance) -> bool { +fn is_inherit_http_client( + val: &HandlerInheritance, +) -> bool { matches!(val, HandlerInheritance::Inherit) } -fn is_inherit_http_framework(val: &HandlerInheritance) -> bool { +fn is_inherit_http_framework( + val: &HandlerInheritance, +) -> bool { matches!(val, HandlerInheritance::Inherit) } -fn is_inherit_runtime(val: &HandlerInheritance) -> bool { +fn is_inherit_runtime( + val: &HandlerInheritance, +) -> bool { matches!(val, HandlerInheritance::Inherit) } -fn is_inherit_supervisor(val: &HandlerInheritance) -> bool { +fn is_inherit_supervisor( + val: &HandlerInheritance, +) -> bool { matches!(val, HandlerInheritance::Inherit) } -fn is_inherit_store(val: &HandlerInheritance) -> bool { +fn is_inherit_store( + val: &HandlerInheritance, +) -> bool { matches!(val, HandlerInheritance::Inherit) } -fn is_inherit_timing(val: &HandlerInheritance) -> bool { +fn is_inherit_timing( + val: &HandlerInheritance, +) -> bool { matches!(val, HandlerInheritance::Inherit) } -fn is_inherit_process(val: &HandlerInheritance) -> bool { +fn is_inherit_process( + val: &HandlerInheritance, +) -> bool { matches!(val, HandlerInheritance::Inherit) } -fn is_inherit_environment(val: &HandlerInheritance) -> bool { +fn is_inherit_environment( + val: &HandlerInheritance, +) -> bool { matches!(val, HandlerInheritance::Inherit) } -fn is_inherit_random(val: &HandlerInheritance) -> bool { +fn is_inherit_random( + val: &HandlerInheritance, +) -> bool { matches!(val, HandlerInheritance::Inherit) } diff --git a/crates/theater/src/config/permissions.rs b/crates/theater/src/config/permissions.rs index 073947f4..838ede2a 100644 --- a/crates/theater/src/config/permissions.rs +++ b/crates/theater/src/config/permissions.rs @@ -96,7 +96,7 @@ fn intersect_path_options( parent_list.iter().any(|parent_path| { let child_canonical = std::path::Path::new(child_path); let parent_canonical = std::path::Path::new(parent_path); - + // Check if child path starts with parent path child_canonical.starts_with(parent_canonical) }) @@ -419,9 +419,9 @@ impl HandlerPermission { }), http_client: Some(HttpClientPermissions { allowed_methods: None, // None means all methods allowed - allowed_hosts: None, // None means all hosts allowed - max_redirects: None, // None means unlimited - timeout: None, // None means no timeout restriction + allowed_hosts: None, // None means all hosts allowed + max_redirects: None, // None means unlimited + timeout: None, // None means no timeout restriction }), http_framework: Some(HttpFrameworkPermissions { allowed_routes: None, @@ -461,7 +461,7 @@ impl HandlerPermission { policy: &crate::config::inheritance::HandlerPermissionPolicy, ) -> HandlerPermission { use crate::config::inheritance::apply_inheritance_policy; - + HandlerPermission { message_server: apply_inheritance_policy( &parent_permissions.message_server, @@ -479,34 +479,19 @@ impl HandlerPermission { &parent_permissions.http_framework, &policy.http_framework, ), - runtime: apply_inheritance_policy( - &parent_permissions.runtime, - &policy.runtime, - ), + runtime: apply_inheritance_policy(&parent_permissions.runtime, &policy.runtime), supervisor: apply_inheritance_policy( &parent_permissions.supervisor, &policy.supervisor, ), - store: apply_inheritance_policy( - &parent_permissions.store, - &policy.store, - ), - timing: apply_inheritance_policy( - &parent_permissions.timing, - &policy.timing, - ), - process: apply_inheritance_policy( - &parent_permissions.process, - &policy.process, - ), + store: apply_inheritance_policy(&parent_permissions.store, &policy.store), + timing: apply_inheritance_policy(&parent_permissions.timing, &policy.timing), + process: apply_inheritance_policy(&parent_permissions.process, &policy.process), environment: apply_inheritance_policy( &parent_permissions.environment, &policy.environment, ), - random: apply_inheritance_policy( - &parent_permissions.random, - &policy.random, - ), + random: apply_inheritance_policy(&parent_permissions.random, &policy.random), } } } @@ -560,8 +545,13 @@ impl RestrictWith for FileSystemPermissions { read: self.read && restriction.read, write: self.write && restriction.write, execute: self.execute && restriction.execute, - allowed_commands: intersect_options(&self.allowed_commands, &restriction.allowed_commands), - new_dir: self.new_dir.and_then(|p| restriction.new_dir.map(|r| p && r)), + allowed_commands: intersect_options( + &self.allowed_commands, + &restriction.allowed_commands, + ), + new_dir: self + .new_dir + .and_then(|p| restriction.new_dir.map(|r| p && r)), allowed_paths: intersect_path_options(&self.allowed_paths, &restriction.allowed_paths), } } @@ -605,7 +595,10 @@ impl RestrictWith for ProcessPermissions { ProcessPermissions { max_processes: self.max_processes.min(restriction.max_processes), max_output_buffer: self.max_output_buffer.min(restriction.max_output_buffer), - allowed_programs: intersect_options(&self.allowed_programs, &restriction.allowed_programs), + allowed_programs: intersect_options( + &self.allowed_programs, + &restriction.allowed_programs, + ), allowed_paths: intersect_path_options(&self.allowed_paths, &restriction.allowed_paths), } } @@ -621,13 +614,16 @@ impl RestrictWith for EnvironmentPermissions { combined.extend_from_slice(restrict); combined.dedup(); Some(combined) - }, + } (Some(parent), None) => Some(parent.clone()), (None, Some(restrict)) => Some(restrict.clone()), (None, None) => None, }, allow_list_all: self.allow_list_all && restriction.allow_list_all, - allowed_prefixes: intersect_options(&self.allowed_prefixes, &restriction.allowed_prefixes), + allowed_prefixes: intersect_options( + &self.allowed_prefixes, + &restriction.allowed_prefixes, + ), } } } @@ -679,7 +675,6 @@ impl RestrictWith for StorePermissions { #[cfg(test)] mod tests { use super::*; - // Helper function to create a full-capability filesystem permission fn full_filesystem_permissions() -> FileSystemPermissions { @@ -687,9 +682,17 @@ mod tests { read: true, write: true, execute: true, - allowed_commands: Some(vec!["ls".to_string(), "cat".to_string(), "echo".to_string()]), + allowed_commands: Some(vec![ + "ls".to_string(), + "cat".to_string(), + "echo".to_string(), + ]), new_dir: Some(true), - allowed_paths: Some(vec!["/home".to_string(), "/tmp".to_string(), "/data".to_string()]), + allowed_paths: Some(vec![ + "/home".to_string(), + "/tmp".to_string(), + "/data".to_string(), + ]), } } diff --git a/crates/theater/src/events/environment.rs b/crates/theater/src/events/environment.rs index 95ac4f1f..9c614bb1 100644 --- a/crates/theater/src/events/environment.rs +++ b/crates/theater/src/events/environment.rs @@ -12,14 +12,14 @@ pub enum EnvironmentEventData { value_found: bool, timestamp: DateTime, }, - + #[serde(rename = "permission_denied")] PermissionDenied { operation: String, variable_name: String, reason: String, }, - + #[serde(rename = "error")] Error { operation: String, diff --git a/crates/theater/src/events/http.rs b/crates/theater/src/events/http.rs index f5661d81..044fc332 100644 --- a/crates/theater/src/events/http.rs +++ b/crates/theater/src/events/http.rs @@ -148,7 +148,7 @@ pub enum HttpEventData { path: String, message: String, }, - + // Permission events PermissionDenied { operation: String, diff --git a/crates/theater/src/events/mod.rs b/crates/theater/src/events/mod.rs index 875debef..2cd74e92 100644 --- a/crates/theater/src/events/mod.rs +++ b/crates/theater/src/events/mod.rs @@ -1,5 +1,19 @@ use crate::chain::ChainEvent; use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +/// Trait implemented by any payload type that can be recorded in the Theater +/// event chain. External handler crates can implement this to integrate their +/// custom event enums with the runtime. +pub trait EventPayload: + Serialize + for<'de> Deserialize<'de> + Send + Sync + Debug + 'static +{ +} + +impl EventPayload for T where + T: Serialize + for<'de> Deserialize<'de> + Send + Sync + Debug + 'static +{ +} /// # Chain Event Data /// @@ -39,12 +53,19 @@ use serde::{Deserialize, Serialize}; /// converted to a `ChainEvent` for inclusion in an actor's event chain using /// the `to_chain_event` method. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChainEventData { +#[serde(bound( + serialize = "E: Serialize", + deserialize = "E: serde::de::DeserializeOwned" +))] +pub struct ChainEventData +where + E: EventPayload, +{ /// The type identifier for this event, used for filtering and routing. /// This should be a dot-separated string like "subsystem.action". pub event_type: String, /// The specific event data payload, containing domain-specific information. - pub data: EventData, + pub data: E, /// Unix timestamp (in seconds) when the event was created. pub timestamp: u64, /// Optional human-readable description of the event for logging and debugging. @@ -119,7 +140,10 @@ pub enum EventData { TheaterRuntime(theater_runtime::TheaterRuntimeEventData), } -impl ChainEventData { +impl ChainEventData +where + E: EventPayload, +{ /// Gets the event type identifier string. /// /// ## Purpose @@ -268,12 +292,12 @@ impl ChainEventData { /// /// // Later, create child events in the chain /// // Create a child event -/// let child_event_data = ChainEventData { -/// event_type: "child.event".to_string(), -/// data: EventData::Runtime(RuntimeEventData::Log { level: "info".to_string(), message: "child event".to_string() }), -/// timestamp: 0, -/// description: None, -/// }; + /// let child_event_data = ChainEventData { + /// event_type: "child.event".to_string(), + /// data: EventData::Runtime(RuntimeEventData::Log { level: "info".to_string(), message: "child event".to_string() }), + /// timestamp: 0, + /// description: None, + /// }; /// let child_chain_event = child_event_data.to_chain_event(Some(chain_event.hash.clone())); /// ``` /// diff --git a/crates/theater/src/events/process.rs b/crates/theater/src/events/process.rs index 7f1239bf..c3953c36 100644 --- a/crates/theater/src/events/process.rs +++ b/crates/theater/src/events/process.rs @@ -70,7 +70,7 @@ pub enum ProcessEventData { /// Error message message: String, }, - + /// Permission denied PermissionDenied { /// Operation that was denied diff --git a/crates/theater/src/events/theater_runtime.rs b/crates/theater/src/events/theater_runtime.rs index afef91d1..aa94bf16 100644 --- a/crates/theater/src/events/theater_runtime.rs +++ b/crates/theater/src/events/theater_runtime.rs @@ -1,4 +1,4 @@ -use crate::{config::permissions::HandlerPermission, store::ContentRef, ManifestConfig}; +use crate::{config::permissions::HandlerPermission, ManifestConfig}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -46,6 +46,10 @@ pub enum TheaterRuntimeEventData { /// Error message describing the failure error: String, }, + + InstantiatingActor, + InitializingState, + ActorReady, } pub struct TheaterRuntimeEvent { diff --git a/crates/theater/src/events/timing/mod.rs b/crates/theater/src/events/timing/mod.rs index b72237b0..45691872 100644 --- a/crates/theater/src/events/timing/mod.rs +++ b/crates/theater/src/events/timing/mod.rs @@ -14,15 +14,8 @@ pub enum TimingEventData { // Handler setup events HandlerSetupStart, HandlerSetupSuccess, - HandlerSetupError { - error: String, - step: String, - }, + HandlerSetupError { error: String, step: String }, LinkerInstanceSuccess, - FunctionSetupStart { - function_name: String, - }, - FunctionSetupSuccess { - function_name: String, - }, + FunctionSetupStart { function_name: String }, + FunctionSetupSuccess { function_name: String }, } diff --git a/crates/theater/src/handler/mod.rs b/crates/theater/src/handler/mod.rs new file mode 100644 index 00000000..f1416ff3 --- /dev/null +++ b/crates/theater/src/handler/mod.rs @@ -0,0 +1,87 @@ +use crate::actor::handle::ActorHandle; +use crate::shutdown::ShutdownReceiver; +use crate::wasm::{ActorComponent, ActorInstance}; +use anyhow::Result; +use std::future::Future; +use std::pin::Pin; + +pub struct HandlerRegistry { + handlers: Vec>, +} + +impl HandlerRegistry { + pub fn new() -> Self { + Self { + handlers: Vec::new(), + } + } + + pub fn register(&mut self, handler: H) { + self.handlers.push(Box::new(handler)); + } + + pub fn setup_handlers( + &mut self, + actor_component: &mut ActorComponent, + ) -> Vec> { + let component_imports = actor_component.import_types.clone(); // What the component imports + let component_exports = actor_component.export_types.clone(); // What the component exports + + let mut active_handlers = Vec::new(); + + for handler in &self.handlers { + let needs_this_handler = handler.imports().map_or(false, |import| { + component_imports.iter().any(|(name, _)| name == &import) + }) || handler.exports().map_or(false, |export| { + component_exports.iter().any(|(name, _)| name == &export) + }); + + if needs_this_handler { + active_handlers.push(handler.create_instance()); + } + } + + active_handlers + } +} + +impl Clone for HandlerRegistry { + fn clone(&self) -> Self { + let mut new_registry = HandlerRegistry::new(); + for handler in &self.handlers { + // Each handler creates a fresh instance of itself + new_registry.handlers.push(handler.create_instance()); + } + new_registry + } +} + +/// Trait describing the lifecycle hooks every handler must implement. +/// +/// External handler crates can implement this trait and register their handlers +/// with the Theater runtime without depending on the concrete `Handler` enum. +pub trait Handler: Send + Sync + 'static { + fn create_instance(&self) -> Box; + + fn start( + &mut self, + actor_handle: ActorHandle, + shutdown_receiver: ShutdownReceiver, + ) -> Pin> + Send>>; + + fn setup_host_functions( + &mut self, + actor_component: &mut ActorComponent, + ) -> Result<()>; + + fn add_export_functions( + &self, + actor_instance: &mut ActorInstance, + ) -> Result<()>; + + fn name(&self) -> &str; + + fn imports(&self) -> Option; + + fn exports(&self) -> Option; +} diff --git a/crates/theater/src/host/environment.rs b/crates/theater/src/host/environment.rs index f2081b19..b51a6788 100644 --- a/crates/theater/src/host/environment.rs +++ b/crates/theater/src/host/environment.rs @@ -31,8 +31,14 @@ pub struct EnvironmentHost { } impl EnvironmentHost { - pub fn new(config: EnvironmentHandlerConfig, permissions: Option) -> Self { - Self { config, permissions } + pub fn new( + config: EnvironmentHandlerConfig, + permissions: Option, + ) -> Self { + Self { + config, + permissions, + } } pub async fn start( @@ -84,7 +90,10 @@ impl EnvironmentHost { timestamp: chrono::Utc::now().timestamp_millis() as u64, description: Some(format!("Failed to create linker instance: {}", e)), }); - return Err(anyhow::anyhow!("Could not instantiate theater:simple/environment: {}", e)); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/environment: {}", + e + )); } }; @@ -98,12 +107,9 @@ impl EnvironmentHost { (var_name,): (String,)| -> Result<(Option,)> { let now = Utc::now().timestamp_millis() as u64; - + // PERMISSION CHECK BEFORE OPERATION - if let Err(e) = PermissionChecker::check_env_var_access( - &permissions, - &var_name, - ) { + if let Err(e) = PermissionChecker::check_env_var_access(&permissions, &var_name) { // Record permission denied event ctx.data_mut().record_event(ChainEventData { event_type: "theater:simple/environment/permission-denied".to_string(), @@ -153,12 +159,11 @@ impl EnvironmentHost { (var_name,): (String,)| -> Result<(bool,)> { let now = Utc::now().timestamp_millis() as u64; - + // PERMISSION CHECK BEFORE OPERATION - if let Err(e) = PermissionChecker::check_env_var_access( - &permissions_clone, - &var_name, - ) { + if let Err(e) = + PermissionChecker::check_env_var_access(&permissions_clone, &var_name) + { // Record permission denied event ctx.data_mut().record_event(ChainEventData { event_type: "theater:simple/environment/permission-denied".to_string(), @@ -260,7 +265,9 @@ impl EnvironmentHost { event_type: "environment-setup".to_string(), data: EventData::Environment(EnvironmentEventData::HandlerSetupSuccess), timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some("Environment host functions setup completed successfully".to_string()), + description: Some( + "Environment host functions setup completed successfully".to_string(), + ), }); Ok(()) diff --git a/crates/theater/src/host/filesystem.rs b/crates/theater/src/host/filesystem.rs index b072d461..7a565dd8 100644 --- a/crates/theater/src/host/filesystem.rs +++ b/crates/theater/src/host/filesystem.rs @@ -9,6 +9,7 @@ use crate::shutdown::ShutdownReceiver; use crate::wasm::ActorComponent; use crate::wasm::ActorInstance; use anyhow::Result; +use dunce; use rand::Rng; use serde::{Deserialize, Serialize}; use std::fs::File; @@ -19,7 +20,6 @@ use thiserror::Error; use tokio::process::Command as AsyncCommand; use tracing::{error, info}; use wasmtime::StoreContextMut; -use dunce; #[derive(Debug, Clone, Serialize, Deserialize)] enum FileSystemCommand { @@ -103,14 +103,14 @@ impl FileSystemHost { let resolved_validation_path = dunce::canonicalize(path_to_validate).map_err(|e| { if is_creation { format!( - "Failed to resolve parent directory '{}' for creation operation: {}", - path_to_validate.display(), + "Failed to resolve parent directory '{}' for creation operation: {}", + path_to_validate.display(), e ) } else { format!( - "Failed to resolve path '{}': {}", - path_to_validate.display(), + "Failed to resolve path '{}': {}", + path_to_validate.display(), e ) } @@ -123,12 +123,12 @@ impl FileSystemHost { // Canonicalize the allowed path for comparison using dunce let allowed_canonical = dunce::canonicalize(allowed_path) .unwrap_or_else(|_| PathBuf::from(allowed_path)); - + // Check if resolved path is within the allowed directory - resolved_validation_path == allowed_canonical || - resolved_validation_path.starts_with(&allowed_canonical) + resolved_validation_path == allowed_canonical + || resolved_validation_path.starts_with(&allowed_canonical) }); - + if !is_allowed { return Err(if is_creation { format!( @@ -138,8 +138,8 @@ impl FileSystemHost { ) } else { format!( - "Path '{}' not in allowed paths: {:?}", - resolved_validation_path.display(), + "Path '{}' not in allowed paths: {:?}", + resolved_validation_path.display(), allowed_paths ) }); @@ -154,17 +154,20 @@ impl FileSystemHost { // by appending the filename/dirname to the canonicalized parent directory let final_component = full_path.file_name().ok_or_else(|| { format!( - "Cannot determine target name for {} operation on path '{}'", - operation, - requested_path + "Cannot determine target name for {} operation on path '{}'", + operation, requested_path ) })?; - + Ok(resolved_validation_path.join(final_component)) } else { // For read/delete, return the canonicalized path Ok(dunce::canonicalize(&full_path).map_err(|e| { - format!("Failed to resolve target path '{}': {}", full_path.display(), e) + format!( + "Failed to resolve target path '{}': {}", + full_path.display(), + e + ) })?) } } diff --git a/crates/theater/src/host/framework/http_framework.rs b/crates/theater/src/host/framework/http_framework.rs index 1cc5b104..441319a5 100644 --- a/crates/theater/src/host/framework/http_framework.rs +++ b/crates/theater/src/host/framework/http_framework.rs @@ -149,7 +149,10 @@ impl HttpFramework { timestamp: chrono::Utc::now().timestamp_millis() as u64, description: Some(format!("Failed to create linker instance: {}", e)), }); - return Err(anyhow::anyhow!("Could not instantiate theater:simple/http-framework: {}", e)); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/http-framework: {}", + e + )); } }; @@ -262,15 +265,15 @@ impl HttpFramework { EventData::Http(HttpEventData::ServerStartHttps { server_id, port, - cert_path: server.get_tls_cert_path().unwrap_or("unknown").to_string(), + cert_path: server + .get_tls_cert_path() + .unwrap_or("unknown") + .to_string(), }) } else { - EventData::Http(HttpEventData::ServerStart { - server_id, - port, - }) + EventData::Http(HttpEventData::ServerStart { server_id, port }) }; - + let description = if server.is_https() { format!("Started HTTPS server {} on port {}", server_id, port) } else { @@ -1113,7 +1116,9 @@ impl HttpFramework { event_type: "http-framework-setup".to_string(), data: EventData::Http(HttpEventData::HandlerSetupSuccess), timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some("HTTP framework host functions setup completed successfully".to_string()), + description: Some( + "HTTP framework host functions setup completed successfully".to_string(), + ), }); info!("HTTP framework host functions set up"); @@ -1124,48 +1129,54 @@ impl HttpFramework { pub async fn add_export_functions(&self, actor_instance: &mut ActorInstance) -> Result<()> { info!("Adding export functions for HTTP framework"); - match actor_instance - .register_function::<(u64, HttpRequest), (HttpResponse,)>( - "theater:simple/http-handlers", - "handle-request", - ) - { + match actor_instance.register_function::<(u64, HttpRequest), (HttpResponse,)>( + "theater:simple/http-handlers", + "handle-request", + ) { Ok(_) => { info!("Successfully registered handle-request function"); } Err(e) => { error!("Failed to register handle-request function: {}", e); - return Err(anyhow::anyhow!("Failed to register handle-request function: {}", e)); + return Err(anyhow::anyhow!( + "Failed to register handle-request function: {}", + e + )); } } - match actor_instance - .register_function::<(u64, HttpRequest), (MiddlewareResult,)>( - "theater:simple/http-handlers", - "handle-middleware", - ) - { + match actor_instance.register_function::<(u64, HttpRequest), (MiddlewareResult,)>( + "theater:simple/http-handlers", + "handle-middleware", + ) { Ok(_) => { info!("Successfully registered handle-middleware function"); } Err(e) => { error!("Failed to register handle-middleware function: {}", e); - return Err(anyhow::anyhow!("Failed to register handle-middleware function: {}", e)); + return Err(anyhow::anyhow!( + "Failed to register handle-middleware function: {}", + e + )); } } - match actor_instance - .register_function_no_result::<(u64, u64, String, Option)>( - "theater:simple/http-handlers", - "handle-websocket-connect", - ) - { + match actor_instance.register_function_no_result::<(u64, u64, String, Option)>( + "theater:simple/http-handlers", + "handle-websocket-connect", + ) { Ok(_) => { info!("Successfully registered handle-websocket-connect function"); } Err(e) => { - error!("Failed to register handle-websocket-connect function: {}", e); - return Err(anyhow::anyhow!("Failed to register handle-websocket-connect function: {}", e)); + error!( + "Failed to register handle-websocket-connect function: {}", + e + ); + return Err(anyhow::anyhow!( + "Failed to register handle-websocket-connect function: {}", + e + )); } } @@ -1173,29 +1184,38 @@ impl HttpFramework { .register_function::<(u64, u64, WebSocketMessage), (Vec,)>( "theater:simple/http-handlers", "handle-websocket-message", - ) - { + ) { Ok(_) => { info!("Successfully registered handle-websocket-message function"); } Err(e) => { - error!("Failed to register handle-websocket-message function: {}", e); - return Err(anyhow::anyhow!("Failed to register handle-websocket-message function: {}", e)); + error!( + "Failed to register handle-websocket-message function: {}", + e + ); + return Err(anyhow::anyhow!( + "Failed to register handle-websocket-message function: {}", + e + )); } } - match actor_instance - .register_function_no_result::<(u64, u64)>( - "theater:simple/http-handlers", - "handle-websocket-disconnect", - ) - { + match actor_instance.register_function_no_result::<(u64, u64)>( + "theater:simple/http-handlers", + "handle-websocket-disconnect", + ) { Ok(_) => { info!("Successfully registered handle-websocket-disconnect function"); } Err(e) => { - error!("Failed to register handle-websocket-disconnect function: {}", e); - return Err(anyhow::anyhow!("Failed to register handle-websocket-disconnect function: {}", e)); + error!( + "Failed to register handle-websocket-disconnect function: {}", + e + ); + return Err(anyhow::anyhow!( + "Failed to register handle-websocket-disconnect function: {}", + e + )); } } diff --git a/crates/theater/src/host/framework/server_instance.rs b/crates/theater/src/host/framework/server_instance.rs index 2be5de12..fc047684 100644 --- a/crates/theater/src/host/framework/server_instance.rs +++ b/crates/theater/src/host/framework/server_instance.rs @@ -1,23 +1,23 @@ use crate::actor::handle::ActorHandle; -use std::panic; use axum::{ - extract::{Path, State, WebSocketUpgrade, MatchedPath}, + extract::{MatchedPath, Path, State, WebSocketUpgrade}, http::{HeaderName, HeaderValue, Request, StatusCode}, response::Response, - routing::{any, get, post, put, delete, patch, head, options}, + routing::{any, delete, get, head, options, patch, post, put}, Router, }; use futures::{SinkExt, StreamExt}; use std::collections::HashMap; +use std::panic; +use anyhow::Result; use std::sync::Arc; use tokio::sync::{mpsc, RwLock}; use tokio::task::JoinHandle; use tracing::{debug, error, info, warn}; -use anyhow::Result; -use super::types::*; use super::tls::{create_tls_config, validate_tls_config}; +use super::types::*; // Route configuration #[derive(Clone)] @@ -62,7 +62,7 @@ pub struct RouteHandlerState { pub server_state: Arc, } -// New: State for WebSocket handlers +// New: State for WebSocket handlers #[derive(Clone)] pub struct WebSocketHandlerState { pub config: WebSocketConfig, @@ -133,7 +133,10 @@ impl ServerInstance { } pub fn get_tls_cert_path(&self) -> Option<&str> { - self.config.tls_config.as_ref().map(|tls| tls.cert_path.as_str()) + self.config + .tls_config + .as_ref() + .map(|tls| tls.cert_path.as_str()) } // Existing route/middleware/websocket management methods remain the same @@ -177,7 +180,12 @@ impl ServerInstance { self.routes.contains_key(&route_id) } - pub fn add_middleware(&mut self, middleware_id: u64, path: String, handler_id: u64) -> Result<()> { + pub fn add_middleware( + &mut self, + middleware_id: u64, + path: String, + handler_id: u64, + ) -> Result<()> { let middleware_config = MiddlewareConfig { id: middleware_id, path, @@ -234,7 +242,7 @@ impl ServerInstance { // NEW: Completely rewritten router building using Axum's native routing fn build_router(&self, actor_handle: ActorHandle) -> Router { let mut router = Router::new(); - + // Create shared base state let base_state = Arc::new(ServerState { id: self.id, @@ -251,7 +259,10 @@ impl ServerInstance { server_state: base_state.clone(), }; - debug!("Adding route: {} {} -> handler {}", route.method, route.path, route.handler_id); + debug!( + "Adding route: {} {} -> handler {}", + route.method, route.path, route.handler_id + ); // Use the appropriate HTTP method handler - Axum handles all the wildcard magic! let method_handler = match route.method.as_str() { @@ -279,7 +290,7 @@ impl ServerInstance { config: ws_config.clone(), server_state: base_state.clone(), }; - + debug!("Adding WebSocket route: {}", ws_path); router = router.route( ws_path, @@ -287,7 +298,11 @@ impl ServerInstance { ); } - info!("Built router with {} routes and {} WebSocket endpoints", self.routes.len(), self.websockets.len()); + info!( + "Built router with {} routes and {} WebSocket endpoints", + self.routes.len(), + self.websockets.len() + ); router } @@ -299,19 +314,27 @@ impl ServerInstance { path_params: Option>>, req: Request, ) -> Response { - let actual_path = req.uri().path().to_string(); let method = req.method().as_str().to_uppercase(); - - debug!("Handling {} request to {} (matched pattern: {})", method, actual_path, matched_path.as_str()); - + + debug!( + "Handling {} request to {} (matched pattern: {})", + method, + actual_path, + matched_path.as_str() + ); + // Convert request, including path parameters extracted by Axum let mut theater_request = convert_request_with_axum_params(req, path_params).await; - + // Apply middlewares (using the actual requested path, not the pattern) let middlewares = route_state.server_state.find_middlewares(&actual_path); for middleware in middlewares { - match route_state.server_state.call_middleware(middleware.handler_id, theater_request.clone()).await { + match route_state + .server_state + .call_middleware(middleware.handler_id, theater_request.clone()) + .await + { Ok(result) => { if !result.proceed { debug!("Middleware {} rejected request", middleware.handler_id); @@ -333,13 +356,22 @@ impl ServerInstance { } // Call the specific handler for this route - match route_state.server_state.call_route_handler(route_state.handler_id, theater_request).await { + match route_state + .server_state + .call_route_handler(route_state.handler_id, theater_request) + .await + { Ok(response) => { debug!("Handler {} completed successfully", route_state.handler_id); convert_response(response) } Err(e) => { - error!("Handler error for route {} ({}): {}", matched_path.as_str(), route_state.handler_id, e); + error!( + "Handler error for route {} ({}): {}", + matched_path.as_str(), + route_state.handler_id, + e + ); Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(format!("Handler error: {}", e).into()) @@ -355,7 +387,7 @@ impl ServerInstance { req: Request, ) -> Response { let path = req.uri().path().to_string(); - + ws.on_upgrade(move |socket| async move { Self::handle_websocket_connection(ws_state, socket, path).await; }) @@ -373,15 +405,20 @@ impl ServerInstance { // Store connection { let mut connections = ws_state.server_state.active_ws_connections.write().await; - connections.insert(connection_id, WebSocketConnection { - id: connection_id, - sender: tx, - }); + connections.insert( + connection_id, + WebSocketConnection { + id: connection_id, + sender: tx, + }, + ); } // Handle connect event if let Some(connect_handler_id) = ws_state.config.connect_handler_id { - if let Err(e) = ws_state.server_state.actor_handle + if let Err(e) = ws_state + .server_state + .actor_handle .call_function::<(u64, u64, String, Option), ()>( "theater:simple/http-handlers.handle-websocket-connect".to_string(), (connect_handler_id, connection_id, path.clone(), None), @@ -444,7 +481,9 @@ impl ServerInstance { text: Some(text.to_string()), }; - if let Err(e) = ws_state.server_state.actor_handle + if let Err(e) = ws_state + .server_state + .actor_handle .call_function::<(u64, u64, WebSocketMessage), (Vec,)>( "theater:simple/http-handlers.handle-websocket-message".to_string(), (ws_state.config.message_handler_id, connection_id, message), @@ -461,7 +500,9 @@ impl ServerInstance { text: None, }; - if let Err(e) = ws_state.server_state.actor_handle + if let Err(e) = ws_state + .server_state + .actor_handle .call_function::<(u64, u64, WebSocketMessage), (Vec,)>( "theater:simple/http-handlers.handle-websocket-message".to_string(), (ws_state.config.message_handler_id, connection_id, message), @@ -490,7 +531,9 @@ impl ServerInstance { // Handle disconnect event if let Some(disconnect_handler_id) = ws_state.config.disconnect_handler_id { - if let Err(e) = ws_state.server_state.actor_handle + if let Err(e) = ws_state + .server_state + .actor_handle .call_function::<(u64, u64), ()>( "theater:simple/http-handlers.handle-websocket-disconnect".to_string(), (disconnect_handler_id, connection_id), @@ -518,9 +561,15 @@ impl ServerInstance { } async fn start_http(&mut self, actor_handle: ActorHandle) -> Result { - let router = self.build_router_safe(actor_handle).map_err(|e| anyhow::anyhow!(e))?; - let host = self.config.host.clone().unwrap_or_else(|| "0.0.0.0".to_string()); - + let router = self + .build_router_safe(actor_handle) + .map_err(|e| anyhow::anyhow!(e))?; + let host = self + .config + .host + .clone() + .unwrap_or_else(|| "0.0.0.0".to_string()); + let addr = if self.port == 0 { format!("{}:0", host) } else { @@ -535,10 +584,9 @@ impl ServerInstance { self.shutdown_tx = Some(shutdown_tx); let server_handle = tokio::spawn(async move { - let graceful = axum::serve(listener, router) - .with_graceful_shutdown(async { - shutdown_rx.await.ok(); - }); + let graceful = axum::serve(listener, router).with_graceful_shutdown(async { + shutdown_rx.await.ok(); + }); if let Err(e) = graceful.await { error!("Server error: {}", e); @@ -552,16 +600,26 @@ impl ServerInstance { Ok(self.port) } - async fn start_https(&mut self, actor_handle: ActorHandle, tls_config: &TlsConfig) -> Result { + async fn start_https( + &mut self, + actor_handle: ActorHandle, + tls_config: &TlsConfig, + ) -> Result { // Validate TLS configuration early debug!("Validating TLS configuration for server {}", self.id); validate_tls_config(&tls_config.cert_path, &tls_config.key_path) .map_err(|e| anyhow::anyhow!("TLS validation failed: {}", e))?; // Build router - let router = self.build_router_safe(actor_handle).map_err(|e| anyhow::anyhow!(e))?; - let host = self.config.host.clone().unwrap_or_else(|| "0.0.0.0".to_string()); - + let router = self + .build_router_safe(actor_handle) + .map_err(|e| anyhow::anyhow!(e))?; + let host = self + .config + .host + .clone() + .unwrap_or_else(|| "0.0.0.0".to_string()); + let addr = if self.port == 0 { format!("{}:0", host) } else { @@ -583,12 +641,12 @@ impl ServerInstance { // Convert to std::net::TcpListener for axum-server let std_listener = listener.into_std()?; - + // Start HTTPS server using axum-server let server_handle = tokio::spawn(async move { let server = axum_server::from_tcp_rustls(std_listener, rustls_config) .serve(router.into_make_service()); - + tokio::select! { result = server => { if let Err(e) = result { @@ -619,7 +677,7 @@ impl ServerInstance { if let Some(handle) = self.server_handle.take() { handle.abort(); - + let connections_count = self.active_ws_connections.read().await.len(); if connections_count > 0 { debug!( @@ -639,9 +697,8 @@ impl ServerInstance { } fn build_router_safe(&self, actor_handle: ActorHandle) -> Result { - let result = panic::catch_unwind(panic::AssertUnwindSafe(|| { - self.build_router(actor_handle) - })); + let result = + panic::catch_unwind(panic::AssertUnwindSafe(|| self.build_router(actor_handle))); match result { Ok(router) => Ok(router), @@ -747,7 +804,7 @@ async fn convert_request_with_axum_params( ) -> HttpRequest { let method = req.method().as_str().to_string(); let uri = req.uri().to_string(); - + // Extract headers let mut headers = Vec::new(); for (name, value) in req.headers() { @@ -755,7 +812,7 @@ async fn convert_request_with_axum_params( headers.push((name.to_string(), value_str.to_string())); } } - + // Add path parameters as special headers so WASM handlers can access them if let Some(Path(params)) = path_params { for (key, value) in params { @@ -763,7 +820,7 @@ async fn convert_request_with_axum_params( debug!("Extracted path parameter: {} = {}", key, value); } } - + // Read body let body = match axum::body::to_bytes(req.into_body(), 100 * 1024 * 1024).await { Ok(bytes) => Some(bytes.to_vec()), @@ -787,15 +844,16 @@ fn convert_response(response: HttpResponse) -> Response { // Add headers for (name, value) in response.headers { - if let (Ok(header_name), Ok(header_value)) = ( - HeaderName::try_from(name), - HeaderValue::try_from(value), - ) { + if let (Ok(header_name), Ok(header_value)) = + (HeaderName::try_from(name), HeaderValue::try_from(value)) + { builder = builder.header(header_name, header_value); } } // Set body let body = response.body.unwrap_or_default(); - builder.body(axum::body::Body::from(body)).unwrap_or_default() + builder + .body(axum::body::Body::from(body)) + .unwrap_or_default() } diff --git a/crates/theater/src/host/framework/tls.rs b/crates/theater/src/host/framework/tls.rs index 9308a0ab..a011d9d1 100644 --- a/crates/theater/src/host/framework/tls.rs +++ b/crates/theater/src/host/framework/tls.rs @@ -12,78 +12,100 @@ use tracing::{debug, info}; /// Load certificates from a PEM file pub fn load_certs(path: &str) -> Result>> { let path = Path::new(path); - + if !path.exists() { - return Err(anyhow::anyhow!("Certificate file not found: {}", path.display())); + return Err(anyhow::anyhow!( + "Certificate file not found: {}", + path.display() + )); } - + if !path.is_file() { - return Err(anyhow::anyhow!("Certificate path is not a file: {}", path.display())); + return Err(anyhow::anyhow!( + "Certificate path is not a file: {}", + path.display() + )); } - + debug!("Loading certificates from: {}", path.display()); - + let file = File::open(path) .with_context(|| format!("Failed to open certificate file: {}", path.display()))?; - + let mut reader = BufReader::new(file); let certs: Result, _> = certs(&mut reader).collect(); - let certs = certs - .with_context(|| format!("Failed to parse certificates from: {}", path.display()))?; - + let certs = + certs.with_context(|| format!("Failed to parse certificates from: {}", path.display()))?; + if certs.is_empty() { - return Err(anyhow::anyhow!("No certificates found in file: {}", path.display())); + return Err(anyhow::anyhow!( + "No certificates found in file: {}", + path.display() + )); } - - info!("Loaded {} certificate(s) from: {}", certs.len(), path.display()); + + info!( + "Loaded {} certificate(s) from: {}", + certs.len(), + path.display() + ); Ok(certs) } /// Load private key from a PEM file pub fn load_private_key(path: &str) -> Result> { let path = Path::new(path); - + if !path.exists() { - return Err(anyhow::anyhow!("Private key file not found: {}", path.display())); + return Err(anyhow::anyhow!( + "Private key file not found: {}", + path.display() + )); } - + if !path.is_file() { - return Err(anyhow::anyhow!("Private key path is not a file: {}", path.display())); + return Err(anyhow::anyhow!( + "Private key path is not a file: {}", + path.display() + )); } - + debug!("Loading private key from: {}", path.display()); - + let file = File::open(path) .with_context(|| format!("Failed to open private key file: {}", path.display()))?; - + let mut reader = BufReader::new(file); let key = private_key(&mut reader) .with_context(|| format!("Failed to parse private key from: {}", path.display()))? .ok_or_else(|| anyhow::anyhow!("No private key found in file: {}", path.display()))?; - + info!("Loaded private key from: {}", path.display()); Ok(key) } /// Create a rustls ServerConfig from certificate and key paths pub fn create_tls_config(cert_path: &str, key_path: &str) -> Result { - debug!("Creating TLS configuration with cert: {}, key: {}", cert_path, key_path); - + debug!( + "Creating TLS configuration with cert: {}, key: {}", + cert_path, key_path + ); + // Load certificates let cert_chain = load_certs(cert_path)?; - + // Load private key let private_key = load_private_key(key_path)?; - + // Create TLS configuration let server_config = ServerConfig::builder() .with_no_client_auth() .with_single_cert(cert_chain, private_key) .with_context(|| "Failed to create TLS configuration")?; - + // Convert to RustlsConfig for axum-server let rustls_config = RustlsConfig::from_config(Arc::new(server_config)); - + info!("TLS configuration created successfully"); Ok(rustls_config) } @@ -92,31 +114,43 @@ pub fn create_tls_config(cert_path: &str, key_path: &str) -> Result Result<()> { debug!("Validating TLS configuration paths"); - + // Check if files exist and are readable let cert_path = Path::new(cert_path); let key_path = Path::new(key_path); - + if !cert_path.exists() { - return Err(anyhow::anyhow!("Certificate file not found: {}", cert_path.display())); + return Err(anyhow::anyhow!( + "Certificate file not found: {}", + cert_path.display() + )); } - + if !key_path.exists() { - return Err(anyhow::anyhow!("Private key file not found: {}", key_path.display())); + return Err(anyhow::anyhow!( + "Private key file not found: {}", + key_path.display() + )); } - + if !cert_path.is_file() { - return Err(anyhow::anyhow!("Certificate path is not a file: {}", cert_path.display())); + return Err(anyhow::anyhow!( + "Certificate path is not a file: {}", + cert_path.display() + )); } - + if !key_path.is_file() { - return Err(anyhow::anyhow!("Private key path is not a file: {}", key_path.display())); + return Err(anyhow::anyhow!( + "Private key path is not a file: {}", + key_path.display() + )); } - + // Try to load them to make sure they're valid let _certs = load_certs(cert_path.to_str().unwrap())?; let _key = load_private_key(key_path.to_str().unwrap())?; - + debug!("TLS configuration validation successful"); Ok(()) } @@ -126,7 +160,7 @@ mod tests { use super::*; use std::fs; use tempfile::tempdir; - + fn create_test_cert() -> &'static str { // A minimal self-signed certificate for testing r#"-----BEGIN CERTIFICATE----- @@ -138,7 +172,7 @@ rJxWNK5fAgMBAAEwDQYJKoZIhvcNAQELBQADQQB8J+HnF9E5HbGlGZJQJW6Q5L5E oqwV3CXm9oQJmVGWTKhJ5KXF2X2w9Q9QBEoX3oX9Q5YbhVwqBWK9F7hKKX6g -----END CERTIFICATE-----"# } - + fn create_test_private_key() -> &'static str { // A minimal private key for testing r#"-----BEGIN PRIVATE KEY----- @@ -151,36 +185,36 @@ W5kJm4qP+8X3JEwF7QzJ7yVF0q4XAiEA1VY8W5kJm4qP+8X3JEwF7QzJ7yVF0q uFwIgNVWPFuZCZuKj/vF9yRMBe0Mye8lRdKuF9VWPFuZCZuKj/vA=== -----END PRIVATE KEY-----"# } - + #[test] fn test_validate_nonexistent_files() { let result = validate_tls_config("/nonexistent/cert.pem", "/nonexistent/key.pem"); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("not found")); } - + #[test] fn test_load_valid_cert_and_key() -> Result<()> { let temp_dir = tempdir()?; - + // Create test certificate file let cert_path = temp_dir.path().join("test.crt"); fs::write(&cert_path, create_test_cert())?; - + // Create test private key file let key_path = temp_dir.path().join("test.key"); fs::write(&key_path, create_test_private_key())?; - + // For now, just test that the files exist since we need real certificates // In a full implementation, we'd use proper test certificates - + // Test that the functions would fail appropriately with invalid content let certs_result = load_certs(cert_path.to_str().unwrap()); let key_result = load_private_key(key_path.to_str().unwrap()); - + // These should fail because our test data isn't real certificates assert!(certs_result.is_err() || key_result.is_err()); - + Ok(()) } } diff --git a/crates/theater/src/host/handler.rs b/crates/theater/src/host/handler.rs deleted file mode 100644 index 23d11462..00000000 --- a/crates/theater/src/host/handler.rs +++ /dev/null @@ -1,369 +0,0 @@ -use crate::actor::handle::ActorHandle; -use crate::config::permissions::*; -use crate::events::{ - environment::EnvironmentEventData, - filesystem::FilesystemEventData, - http::HttpEventData, - message::MessageEventData, - process::ProcessEventData, - random::RandomEventData, - runtime::RuntimeEventData, - store::StoreEventData, - supervisor::SupervisorEventData, - timing::TimingEventData, - ChainEventData, EventData, -}; -use crate::host::environment::EnvironmentHost; -use crate::host::filesystem::FileSystemHost; -use crate::host::framework::HttpFramework; -use crate::host::http_client::HttpClientHost; -use crate::host::message_server::MessageServerHost; -use crate::host::process::ProcessHost; -use crate::host::random::RandomHost; -use crate::host::runtime::RuntimeHost; -use crate::host::store::StoreHost; -use crate::host::supervisor::SupervisorHost; -use crate::host::timing::TimingHost; -use crate::shutdown::ShutdownReceiver; -use crate::wasm::{ActorComponent, ActorInstance}; -use anyhow::Result; - -pub enum Handler { - MessageServer(MessageServerHost, Option), - Environment(EnvironmentHost, Option), - FileSystem(FileSystemHost, Option), - HttpClient(HttpClientHost, Option), - HttpFramework(HttpFramework, Option), - Process(ProcessHost, Option), - Runtime(RuntimeHost, Option), - Supervisor(SupervisorHost, Option), - Store(StoreHost, Option), - Timing(TimingHost, Option), - Random(RandomHost, Option), -} - -impl Handler { - pub async fn start( - &mut self, - actor_handle: ActorHandle, - shutdown_receiver: ShutdownReceiver, - ) -> Result<()> { - match self { - Handler::MessageServer(h, _) => Ok(h - .start(actor_handle, shutdown_receiver) - .await - .expect("Error starting message server")), - Handler::Environment(h, _) => Ok(h - .start(actor_handle, shutdown_receiver) - .await - .expect("Error starting environment handler")), - Handler::FileSystem(h, _) => Ok(h - .start(actor_handle, shutdown_receiver) - .await - .expect("Error starting filesystem")), - Handler::HttpClient(h, _) => Ok(h - .start(actor_handle, shutdown_receiver) - .await - .expect("Error starting http client")), - Handler::HttpFramework(h, _) => Ok(h - .start(actor_handle, shutdown_receiver) - .await - .expect("Error starting http framework")), - Handler::Process(h, _) => Ok(h - .start(actor_handle, shutdown_receiver) - .await - .expect("Error starting process handler")), - Handler::Runtime(h, _) => Ok(h - .start(actor_handle, shutdown_receiver) - .await - .expect("Error starting runtime")), - Handler::Supervisor(h, _) => Ok(h - .start(actor_handle, shutdown_receiver) - .await - .expect("Error starting supervisor")), - Handler::Store(h, _) => Ok(h - .start(actor_handle, shutdown_receiver) - .await - .expect("Error starting store")), - Handler::Timing(h, _) => Ok(h - .start(actor_handle, shutdown_receiver) - .await - .expect("Error starting timing")), - Handler::Random(h, _) => Ok(h - .start(actor_handle, shutdown_receiver) - .await - .expect("Error starting random")), - } - } - - pub async fn setup_host_functions( - &mut self, - actor_component: &mut ActorComponent, - ) -> Result<()> { - match self { - Handler::MessageServer(h, _) => Ok(h - .setup_host_functions(actor_component) - .await - .expect("Error setting up message server host functions")), - Handler::Environment(h, _) => Ok(h - .setup_host_functions(actor_component) - .await - .expect("Error setting up environment host functions")), - Handler::FileSystem(h, _) => Ok(h - .setup_host_functions(actor_component) - .await - .expect("Error setting up filesystem host functions")), - Handler::HttpClient(h, _) => Ok(h - .setup_host_functions(actor_component) - .await - .expect("Error setting up http client host functions")), - Handler::HttpFramework(h, _) => Ok(h - .setup_host_functions(actor_component) - .await - .expect("Error setting up http framework host functions")), - Handler::Process(h, _) => Ok(h - .setup_host_functions(actor_component) - .await - .expect("Error setting up process host functions")), - Handler::Runtime(h, _) => Ok(h - .setup_host_functions(actor_component) - .await - .expect("Error setting up runtime host functions")), - Handler::Supervisor(h, _) => Ok(h - .setup_host_functions(actor_component) - .await - .expect("Error setting up supervisor host functions")), - Handler::Store(h, _) => Ok(h - .setup_host_functions(actor_component) - .await - .expect("Error setting up store host functions")), - Handler::Timing(h, _) => Ok(h - .setup_host_functions(actor_component) - .await - .expect("Error setting up timing host functions")), - Handler::Random(h, _) => Ok(h - .setup_host_functions(actor_component) - .await - .expect("Error setting up random host functions")), - } - } - - pub async fn add_export_functions(&self, actor_instance: &mut ActorInstance) -> Result<()> { - match self { - Handler::MessageServer(handler, _) => { - match handler.add_export_functions(actor_instance).await { - Ok(_) => Ok(()), - Err(e) => { - let error_msg = format!("Error adding functions to message server: {}", e); - actor_instance.actor_component.actor_store.record_event(ChainEventData { - event_type: "message-server-export-setup".to_string(), - data: EventData::Message(MessageEventData::HandlerSetupError { - error: error_msg.clone(), - step: "add_export_functions".to_string(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some(error_msg), - }); - Err(e) - } - } - } - Handler::Environment(handler, _) => { - match handler.add_export_functions(actor_instance).await { - Ok(_) => Ok(()), - Err(e) => { - let error_msg = format!("Error adding functions to environment handler: {}", e); - actor_instance.actor_component.actor_store.record_event(ChainEventData { - event_type: "environment-export-setup".to_string(), - data: EventData::Environment(EnvironmentEventData::HandlerSetupError { - error: error_msg.clone(), - step: "add_export_functions".to_string(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some(error_msg), - }); - Err(e) - } - } - } - Handler::FileSystem(handler, _) => { - match handler.add_export_functions(actor_instance).await { - Ok(_) => Ok(()), - Err(e) => { - let error_msg = format!("Error adding functions to filesystem: {}", e); - actor_instance.actor_component.actor_store.record_event(ChainEventData { - event_type: "filesystem-export-setup".to_string(), - data: EventData::Filesystem(FilesystemEventData::HandlerSetupError { - error: error_msg.clone(), - step: "add_export_functions".to_string(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some(error_msg), - }); - Err(e) - } - } - } - Handler::HttpClient(handler, _) => { - match handler.add_export_functions(actor_instance).await { - Ok(_) => Ok(()), - Err(e) => { - let error_msg = format!("Error adding functions to http client: {}", e); - actor_instance.actor_component.actor_store.record_event(ChainEventData { - event_type: "http-client-export-setup".to_string(), - data: EventData::Http(HttpEventData::HandlerSetupError { - error: error_msg.clone(), - step: "add_export_functions".to_string(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some(error_msg), - }); - Err(e) - } - } - } - Handler::HttpFramework(handler, _) => { - match handler.add_export_functions(actor_instance).await { - Ok(_) => Ok(()), - Err(e) => { - let error_msg = format!("Error adding functions to http framework: {}", e); - actor_instance.actor_component.actor_store.record_event(ChainEventData { - event_type: "http-framework-export-setup".to_string(), - data: EventData::Http(HttpEventData::HandlerSetupError { - error: error_msg.clone(), - step: "add_export_functions".to_string(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some(error_msg), - }); - Err(e) - } - } - } - Handler::Process(handler, _) => { - match handler.add_export_functions(actor_instance).await { - Ok(_) => Ok(()), - Err(e) => { - let error_msg = format!("Error adding functions to process handler: {}", e); - actor_instance.actor_component.actor_store.record_event(ChainEventData { - event_type: "process-export-setup".to_string(), - data: EventData::Process(ProcessEventData::HandlerSetupError { - error: error_msg.clone(), - step: "add_export_functions".to_string(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some(error_msg), - }); - Err(e) - } - } - } - Handler::Runtime(handler, _) => { - match handler.add_export_functions(actor_instance).await { - Ok(_) => Ok(()), - Err(e) => { - let error_msg = format!("Error adding functions to runtime: {}", e); - actor_instance.actor_component.actor_store.record_event(ChainEventData { - event_type: "runtime-export-setup".to_string(), - data: EventData::Runtime(RuntimeEventData::HandlerSetupError { - error: error_msg.clone(), - step: "add_export_functions".to_string(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some(error_msg), - }); - Err(e) - } - } - } - Handler::Supervisor(handler, _) => { - match handler.add_export_functions(actor_instance).await { - Ok(_) => Ok(()), - Err(e) => { - let error_msg = format!("Error adding functions to supervisor: {}", e); - actor_instance.actor_component.actor_store.record_event(ChainEventData { - event_type: "supervisor-export-setup".to_string(), - data: EventData::Supervisor(SupervisorEventData::HandlerSetupError { - error: error_msg.clone(), - step: "add_export_functions".to_string(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some(error_msg), - }); - Err(e) - } - } - } - Handler::Store(handler, _) => { - match handler.add_export_functions(actor_instance).await { - Ok(_) => Ok(()), - Err(e) => { - let error_msg = format!("Error adding functions to store: {}", e); - actor_instance.actor_component.actor_store.record_event(ChainEventData { - event_type: "store-export-setup".to_string(), - data: EventData::Store(StoreEventData::HandlerSetupError { - error: error_msg.clone(), - step: "add_export_functions".to_string(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some(error_msg), - }); - Err(e) - } - } - } - Handler::Timing(handler, _) => { - match handler.add_export_functions(actor_instance).await { - Ok(_) => Ok(()), - Err(e) => { - let error_msg = format!("Error adding functions to timing: {}", e); - actor_instance.actor_component.actor_store.record_event(ChainEventData { - event_type: "timing-export-setup".to_string(), - data: EventData::Timing(TimingEventData::HandlerSetupError { - error: error_msg.clone(), - step: "add_export_functions".to_string(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some(error_msg), - }); - Err(e) - } - } - } - Handler::Random(handler, _) => { - match handler.add_export_functions(actor_instance).await { - Ok(_) => Ok(()), - Err(e) => { - let error_msg = format!("Error adding functions to random: {}", e); - actor_instance.actor_component.actor_store.record_event(ChainEventData { - event_type: "random-export-setup".to_string(), - data: EventData::Random(RandomEventData::HandlerSetupError { - error: error_msg.clone(), - step: "add_export_functions".to_string(), - }), - timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some(error_msg), - }); - Err(e) - } - } - } - } - } - - pub fn name(&self) -> &str { - match self { - Handler::MessageServer(_, _) => "message-server", - Handler::Environment(_, _) => "environment", - Handler::FileSystem(_, _) => "filesystem", - Handler::HttpClient(_, _) => "http-client", - Handler::HttpFramework(_, _) => "http-framework", - Handler::Process(_, _) => "process", - Handler::Runtime(_, _) => "runtime", - Handler::Supervisor(_, _) => "supervisor", - Handler::Store(_, _) => "store", - Handler::Timing(_, _) => "timing", - Handler::Random(_, _) => "random", - } - } -} diff --git a/crates/theater/src/host/handler.rs.old b/crates/theater/src/host/handler.rs.old new file mode 100644 index 00000000..ab343139 --- /dev/null +++ b/crates/theater/src/host/handler.rs.old @@ -0,0 +1,463 @@ +use crate::actor::handle::ActorHandle; +use crate::config::permissions::*; +use crate::events::{ + environment::EnvironmentEventData, filesystem::FilesystemEventData, http::HttpEventData, + message::MessageEventData, process::ProcessEventData, random::RandomEventData, + runtime::RuntimeEventData, store::StoreEventData, supervisor::SupervisorEventData, + timing::TimingEventData, ChainEventData, EventData, +}; +use crate::handler::Handler; +use crate::host::environment::EnvironmentHost; +use crate::host::filesystem::FileSystemHost; +use crate::host::framework::HttpFramework; +use crate::host::http_client::HttpClientHost; +use crate::host::message_server::MessageServerHost; +use crate::host::process::ProcessHost; +use crate::host::random::RandomHost; +use crate::host::runtime::RuntimeHost; +use crate::host::store::StoreHost; +use crate::host::supervisor::SupervisorHost; +use crate::host::timing::TimingHost; +use crate::shutdown::ShutdownReceiver; +use crate::wasm::{ActorComponent, ActorInstance}; +use anyhow::Result; + +pub enum SimpleHandler { + MessageServer(MessageServerHost, Option), + Environment(EnvironmentHost, Option), + FileSystem(FileSystemHost, Option), + HttpClient(HttpClientHost, Option), + HttpFramework(HttpFramework, Option), + Process(ProcessHost, Option), + Runtime(RuntimeHost, Option), + Supervisor(SupervisorHost, Option), + Store(StoreHost, Option), + Timing(TimingHost, Option), + Random(RandomHost, Option), +} + +impl Handler for SimpleHandler { + fn start( + &mut self, + actor_handle: ActorHandle, + shutdown_receiver: ShutdownReceiver, + ) -> impl std::future::Future> + Send { + Box::pin(self.start(actor_handle, shutdown_receiver)) + } + + fn setup_host_functions( + &mut self, + actor_component: &mut ActorComponent, + ) -> impl std::future::Future> + Send { + Box::pin(self.setup_host_functions(actor_component)) + } + + fn add_export_functions( + &self, + actor_instance: &mut ActorInstance, + ) -> impl std::future::Future> + Send { + Box::pin(self.add_export_functions(actor_instance)) + } + + fn name(&self) -> &str { + self.name() + } + + fn provide_imports(&self) -> Option { + match self { + SimpleHandler::MessageServer(h, _) => h.provide_imports(), + SimpleHandler::Environment(h, _) => h.provide_imports(), + SimpleHandler::FileSystem(h, _) => h.provide_imports(), + SimpleHandler::HttpClient(h, _) => h.provide_imports(), + SimpleHandler::HttpFramework(h, _) => h.provide_imports(), + SimpleHandler::Process(h, _) => h.provide_imports(), + SimpleHandler::Runtime(h, _) => h.provide_imports(), + SimpleHandler::Supervisor(h, _) => h.provide_imports(), + SimpleHandler::Store(h, _) => h.provide_imports(), + SimpleHandler::Timing(h, _) => h.provide_imports(), + SimpleHandler::Random(h, _) => h.provide_imports(), + } + } + + fn provide_exports(&self) -> Option { + match self { + SimpleHandler::MessageServer(h, _) => h.provide_exports(), + SimpleHandler::Environment(h, _) => h.provide_exports(), + SimpleHandler::FileSystem(h, _) => h.provide_exports(), + SimpleHandler::HttpClient(h, _) => h.provide_exports(), + SimpleHandler::HttpFramework(h, _) => h.provide_exports(), + SimpleHandler::Process(h, _) => h.provide_exports(), + SimpleHandler::Runtime(h, _) => h.provide_exports(), + SimpleHandler::Supervisor(h, _) => h.provide_exports(), + SimpleHandler::Store(h, _) => h.provide_exports(), + SimpleHandler::Timing(h, _) => h.provide_exports(), + SimpleHandler::Random(h, _) => h.provide_exports(), + } + } +} + +impl SimpleHandler { + pub async fn start( + &mut self, + actor_handle: ActorHandle, + shutdown_receiver: ShutdownReceiver, + ) -> Result<()> { + match self { + SimpleHandler::MessageServer(h, _) => Ok(h + .start(actor_handle, shutdown_receiver) + .await + .expect("Error starting message server")), + SimpleHandler::Environment(h, _) => Ok(h + .start(actor_handle, shutdown_receiver) + .await + .expect("Error starting environment handler")), + SimpleHandler::FileSystem(h, _) => Ok(h + .start(actor_handle, shutdown_receiver) + .await + .expect("Error starting filesystem")), + SimpleHandler::HttpClient(h, _) => Ok(h + .start(actor_handle, shutdown_receiver) + .await + .expect("Error starting http client")), + SimpleHandler::HttpFramework(h, _) => Ok(h + .start(actor_handle, shutdown_receiver) + .await + .expect("Error starting http framework")), + SimpleHandler::Process(h, _) => Ok(h + .start(actor_handle, shutdown_receiver) + .await + .expect("Error starting process handler")), + SimpleHandler::Runtime(h, _) => Ok(h + .start(actor_handle, shutdown_receiver) + .await + .expect("Error starting runtime")), + SimpleHandler::Supervisor(h, _) => Ok(h + .start(actor_handle, shutdown_receiver) + .await + .expect("Error starting supervisor")), + SimpleHandler::Store(h, _) => Ok(h + .start(actor_handle, shutdown_receiver) + .await + .expect("Error starting store")), + SimpleHandler::Timing(h, _) => Ok(h + .start(actor_handle, shutdown_receiver) + .await + .expect("Error starting timing")), + SimpleHandler::Random(h, _) => Ok(h + .start(actor_handle, shutdown_receiver) + .await + .expect("Error starting random")), + } + } + + pub async fn setup_host_functions( + &mut self, + actor_component: &mut ActorComponent, + ) -> Result<()> { + match self { + SimpleHandler::MessageServer(h, _) => Ok(h + .setup_host_functions(actor_component) + .await + .expect("Error setting up message server host functions")), + SimpleHandler::Environment(h, _) => Ok(h + .setup_host_functions(actor_component) + .await + .expect("Error setting up environment host functions")), + SimpleHandler::FileSystem(h, _) => Ok(h + .setup_host_functions(actor_component) + .await + .expect("Error setting up filesystem host functions")), + SimpleHandler::HttpClient(h, _) => Ok(h + .setup_host_functions(actor_component) + .await + .expect("Error setting up http client host functions")), + SimpleHandler::HttpFramework(h, _) => Ok(h + .setup_host_functions(actor_component) + .await + .expect("Error setting up http framework host functions")), + SimpleHandler::Process(h, _) => Ok(h + .setup_host_functions(actor_component) + .await + .expect("Error setting up process host functions")), + SimpleHandler::Runtime(h, _) => Ok(h + .setup_host_functions(actor_component) + .await + .expect("Error setting up runtime host functions")), + SimpleHandler::Supervisor(h, _) => Ok(h + .setup_host_functions(actor_component) + .await + .expect("Error setting up supervisor host functions")), + SimpleHandler::Store(h, _) => Ok(h + .setup_host_functions(actor_component) + .await + .expect("Error setting up store host functions")), + SimpleHandler::Timing(h, _) => Ok(h + .setup_host_functions(actor_component) + .await + .expect("Error setting up timing host functions")), + SimpleHandler::Random(h, _) => Ok(h + .setup_host_functions(actor_component) + .await + .expect("Error setting up random host functions")), + } + } + + pub async fn add_export_functions(&self, actor_instance: &mut ActorInstance) -> Result<()> { + match self { + SimpleHandler::MessageServer(handler, _) => { + match handler.add_export_functions(actor_instance).await { + Ok(_) => Ok(()), + Err(e) => { + let error_msg = format!("Error adding functions to message server: {}", e); + actor_instance + .actor_component + .actor_store + .record_event(ChainEventData { + event_type: "message-server-export-setup".to_string(), + data: EventData::Message(MessageEventData::HandlerSetupError { + error: error_msg.clone(), + step: "add_export_functions".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(error_msg), + }); + Err(e) + } + } + } + SimpleHandler::Environment(handler, _) => { + match handler.add_export_functions(actor_instance).await { + Ok(_) => Ok(()), + Err(e) => { + let error_msg = + format!("Error adding functions to environment handler: {}", e); + actor_instance + .actor_component + .actor_store + .record_event(ChainEventData { + event_type: "environment-export-setup".to_string(), + data: EventData::Environment( + EnvironmentEventData::HandlerSetupError { + error: error_msg.clone(), + step: "add_export_functions".to_string(), + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(error_msg), + }); + Err(e) + } + } + } + SimpleHandler::FileSystem(handler, _) => { + match handler.add_export_functions(actor_instance).await { + Ok(_) => Ok(()), + Err(e) => { + let error_msg = format!("Error adding functions to filesystem: {}", e); + actor_instance + .actor_component + .actor_store + .record_event(ChainEventData { + event_type: "filesystem-export-setup".to_string(), + data: EventData::Filesystem( + FilesystemEventData::HandlerSetupError { + error: error_msg.clone(), + step: "add_export_functions".to_string(), + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(error_msg), + }); + Err(e) + } + } + } + SimpleHandler::HttpClient(handler, _) => { + match handler.add_export_functions(actor_instance).await { + Ok(_) => Ok(()), + Err(e) => { + let error_msg = format!("Error adding functions to http client: {}", e); + actor_instance + .actor_component + .actor_store + .record_event(ChainEventData { + event_type: "http-client-export-setup".to_string(), + data: EventData::Http(HttpEventData::HandlerSetupError { + error: error_msg.clone(), + step: "add_export_functions".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(error_msg), + }); + Err(e) + } + } + } + SimpleHandler::HttpFramework(handler, _) => { + match handler.add_export_functions(actor_instance).await { + Ok(_) => Ok(()), + Err(e) => { + let error_msg = format!("Error adding functions to http framework: {}", e); + actor_instance + .actor_component + .actor_store + .record_event(ChainEventData { + event_type: "http-framework-export-setup".to_string(), + data: EventData::Http(HttpEventData::HandlerSetupError { + error: error_msg.clone(), + step: "add_export_functions".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(error_msg), + }); + Err(e) + } + } + } + SimpleHandler::Process(handler, _) => { + match handler.add_export_functions(actor_instance).await { + Ok(_) => Ok(()), + Err(e) => { + let error_msg = format!("Error adding functions to process handler: {}", e); + actor_instance + .actor_component + .actor_store + .record_event(ChainEventData { + event_type: "process-export-setup".to_string(), + data: EventData::Process(ProcessEventData::HandlerSetupError { + error: error_msg.clone(), + step: "add_export_functions".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(error_msg), + }); + Err(e) + } + } + } + SimpleHandler::Runtime(handler, _) => { + match handler.add_export_functions(actor_instance).await { + Ok(_) => Ok(()), + Err(e) => { + let error_msg = format!("Error adding functions to runtime: {}", e); + actor_instance + .actor_component + .actor_store + .record_event(ChainEventData { + event_type: "runtime-export-setup".to_string(), + data: EventData::Runtime(RuntimeEventData::HandlerSetupError { + error: error_msg.clone(), + step: "add_export_functions".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(error_msg), + }); + Err(e) + } + } + } + SimpleHandler::Supervisor(handler, _) => { + match handler.add_export_functions(actor_instance).await { + Ok(_) => Ok(()), + Err(e) => { + let error_msg = format!("Error adding functions to supervisor: {}", e); + actor_instance + .actor_component + .actor_store + .record_event(ChainEventData { + event_type: "supervisor-export-setup".to_string(), + data: EventData::Supervisor( + SupervisorEventData::HandlerSetupError { + error: error_msg.clone(), + step: "add_export_functions".to_string(), + }, + ), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(error_msg), + }); + Err(e) + } + } + } + SimpleHandler::Store(handler, _) => { + match handler.add_export_functions(actor_instance).await { + Ok(_) => Ok(()), + Err(e) => { + let error_msg = format!("Error adding functions to store: {}", e); + actor_instance + .actor_component + .actor_store + .record_event(ChainEventData { + event_type: "store-export-setup".to_string(), + data: EventData::Store(StoreEventData::HandlerSetupError { + error: error_msg.clone(), + step: "add_export_functions".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(error_msg), + }); + Err(e) + } + } + } + SimpleHandler::Timing(handler, _) => { + match handler.add_export_functions(actor_instance).await { + Ok(_) => Ok(()), + Err(e) => { + let error_msg = format!("Error adding functions to timing: {}", e); + actor_instance + .actor_component + .actor_store + .record_event(ChainEventData { + event_type: "timing-export-setup".to_string(), + data: EventData::Timing(TimingEventData::HandlerSetupError { + error: error_msg.clone(), + step: "add_export_functions".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(error_msg), + }); + Err(e) + } + } + } + SimpleHandler::Random(handler, _) => { + match handler.add_export_functions(actor_instance).await { + Ok(_) => Ok(()), + Err(e) => { + let error_msg = format!("Error adding functions to random: {}", e); + actor_instance + .actor_component + .actor_store + .record_event(ChainEventData { + event_type: "random-export-setup".to_string(), + data: EventData::Random(RandomEventData::HandlerSetupError { + error: error_msg.clone(), + step: "add_export_functions".to_string(), + }), + timestamp: chrono::Utc::now().timestamp_millis() as u64, + description: Some(error_msg), + }); + Err(e) + } + } + } + } + } + + pub fn name(&self) -> &str { + match self { + SimpleHandler::MessageServer(_, _) => "message-server", + SimpleHandler::Environment(_, _) => "environment", + SimpleHandler::FileSystem(_, _) => "filesystem", + SimpleHandler::HttpClient(_, _) => "http-client", + SimpleHandler::HttpFramework(_, _) => "http-framework", + SimpleHandler::Process(_, _) => "process", + SimpleHandler::Runtime(_, _) => "runtime", + SimpleHandler::Supervisor(_, _) => "supervisor", + SimpleHandler::Store(_, _) => "store", + SimpleHandler::Timing(_, _) => "timing", + SimpleHandler::Random(_, _) => "random", + } + } +} diff --git a/crates/theater/src/host/http_client.rs b/crates/theater/src/host/http_client.rs index 2ed7f4dd..d27a83bf 100644 --- a/crates/theater/src/host/http_client.rs +++ b/crates/theater/src/host/http_client.rs @@ -56,7 +56,10 @@ pub enum HttpClientError { } impl HttpClientHost { - pub fn new(_config: HttpClientHandlerConfig, permissions: Option) -> Self { + pub fn new( + _config: HttpClientHandlerConfig, + permissions: Option, + ) -> Self { Self { permissions } } @@ -96,7 +99,10 @@ impl HttpClientHost { timestamp: chrono::Utc::now().timestamp_millis() as u64, description: Some(format!("Failed to create linker instance: {}", e)), }); - return Err(anyhow::anyhow!("Could not instantiate theater:simple/http-client: {}", e)); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/http-client: {}", + e + )); } }; @@ -269,7 +275,9 @@ impl HttpClientHost { event_type: "http-client-setup".to_string(), data: EventData::Http(HttpEventData::HandlerSetupSuccess), timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some("HTTP client host functions setup completed successfully".to_string()), + description: Some( + "HTTP client host functions setup completed successfully".to_string(), + ), }); info!("Host functions set up for http-client"); diff --git a/crates/theater/src/host/message_server.rs b/crates/theater/src/host/message_server.rs index 4148f14d..0a9898ea 100644 --- a/crates/theater/src/host/message_server.rs +++ b/crates/theater/src/host/message_server.rs @@ -95,7 +95,9 @@ impl MessageServerHost { event_type: "message-server-setup".to_string(), data: EventData::Message(MessageEventData::LinkerInstanceSuccess), timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some("Successfully created linker instance for message-server-host".to_string()), + description: Some( + "Successfully created linker instance for message-server-host".to_string(), + ), }); interface } @@ -110,7 +112,10 @@ impl MessageServerHost { timestamp: chrono::Utc::now().timestamp_millis() as u64, description: Some(format!("Failed to create linker instance: {}", e)), }); - return Err(anyhow::anyhow!("Could not instantiate theater:simple/message-server-host: {}", e)); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/message-server-host: {}", + e + )); } }; @@ -373,7 +378,9 @@ impl MessageServerHost { function_name: "list-outstanding-requests".to_string(), }), timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some("Setting up 'list-outstanding-requests' function wrapper".to_string()), + description: Some( + "Setting up 'list-outstanding-requests' function wrapper".to_string(), + ), }); // Use a thread-safe reference to the outstanding_requests field @@ -428,9 +435,15 @@ impl MessageServerHost { step: "list_outstanding_requests_function_wrap".to_string(), }), timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some(format!("Failed to set up 'list-outstanding-requests' function wrapper: {}", e)), + description: Some(format!( + "Failed to set up 'list-outstanding-requests' function wrapper: {}", + e + )), }); - anyhow::anyhow!("Failed to wrap async list-outstanding-requests function: {}", e) + anyhow::anyhow!( + "Failed to wrap async list-outstanding-requests function: {}", + e + ) })?; // Record successful 'list-outstanding-requests' function setup @@ -440,7 +453,9 @@ impl MessageServerHost { function_name: "list-outstanding-requests".to_string(), }), timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some("Successfully set up 'list-outstanding-requests' function wrapper".to_string()), + description: Some( + "Successfully set up 'list-outstanding-requests' function wrapper".to_string(), + ), }); // Use a thread-safe reference to the outstanding_requests field @@ -548,7 +563,9 @@ impl MessageServerHost { function_name: "respond-to-request".to_string(), }), timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some("Successfully set up 'respond-to-request' function wrapper".to_string()), + description: Some( + "Successfully set up 'respond-to-request' function wrapper".to_string(), + ), }); // Use a thread-safe reference to the outstanding_requests field @@ -1053,7 +1070,9 @@ impl MessageServerHost { event_type: "message-server-setup".to_string(), data: EventData::Message(MessageEventData::HandlerSetupSuccess), timestamp: chrono::Utc::now().timestamp_millis() as u64, - description: Some("Message server host functions setup completed successfully".to_string()), + description: Some( + "Message server host functions setup completed successfully".to_string(), + ), }); Ok(()) @@ -1079,19 +1098,25 @@ impl MessageServerHost { "theater:simple/message-server-client", "handle-channel-open", ) - .map_err(|e| anyhow::anyhow!("Failed to register handle-channel-open function: {}", e))?; + .map_err(|e| { + anyhow::anyhow!("Failed to register handle-channel-open function: {}", e) + })?; actor_instance .register_function_no_result::<(String, Vec)>( "theater:simple/message-server-client", "handle-channel-message", ) - .map_err(|e| anyhow::anyhow!("Failed to register handle-channel-message function: {}", e))?; + .map_err(|e| { + anyhow::anyhow!("Failed to register handle-channel-message function: {}", e) + })?; actor_instance .register_function_no_result::<(String,)>( "theater:simple/message-server-client", "handle-channel-close", ) - .map_err(|e| anyhow::anyhow!("Failed to register handle-channel-close function: {}", e))?; + .map_err(|e| { + anyhow::anyhow!("Failed to register handle-channel-close function: {}", e) + })?; Ok(()) } diff --git a/crates/theater/src/host/mod.rs b/crates/theater/src/host/mod.rs index ec3357e7..93836a43 100644 --- a/crates/theater/src/host/mod.rs +++ b/crates/theater/src/host/mod.rs @@ -1,17 +1,17 @@ pub mod environment; pub mod filesystem; pub mod framework; -pub mod handler; +// pub mod handler; // Migrated to separate handler crates pub mod http_client; -pub mod message_server; +// pub mod message_server; // Temporarily disabled due to compilation errors pub mod process; pub mod random; pub mod runtime; -pub mod store; +// pub mod store; // Temporarily disabled due to compilation errors pub mod supervisor; pub mod timing; pub use framework::HttpFramework; -pub use handler::Handler; +// pub use handler::SimpleHandler; // Migrated to separate handler crates pub use process::ProcessHost; pub use random::RandomHost; pub use timing::TimingHost; diff --git a/crates/theater/src/host/process.rs b/crates/theater/src/host/process.rs index ff01c1b6..d24523c0 100644 --- a/crates/theater/src/host/process.rs +++ b/crates/theater/src/host/process.rs @@ -164,7 +164,11 @@ pub struct ProcessHost { impl ProcessHost { /// Create a new ProcessHost with the given configuration - pub fn new(config: ProcessHostConfig, actor_handle: ActorHandle, permissions: Option) -> Self { + pub fn new( + config: ProcessHostConfig, + actor_handle: ActorHandle, + permissions: Option, + ) -> Self { Self { config, processes: Arc::new(Mutex::new(HashMap::new())), @@ -189,48 +193,51 @@ impl ProcessHost { info!("Adding export functions for process handling"); // Register the process handler export functions - match actor_instance - .register_function_no_result::<(u64, Vec)>( - "theater:simple/process-handlers", - "handle-stdout", - ) - { + match actor_instance.register_function_no_result::<(u64, Vec)>( + "theater:simple/process-handlers", + "handle-stdout", + ) { Ok(_) => { info!("Successfully registered handle-stdout function"); } Err(e) => { error!("Failed to register handle-stdout function: {}", e); - return Err(anyhow::anyhow!("Failed to register handle-stdout function: {}", e)); + return Err(anyhow::anyhow!( + "Failed to register handle-stdout function: {}", + e + )); } } - match actor_instance - .register_function_no_result::<(u64, Vec)>( - "theater:simple/process-handlers", - "handle-stderr", - ) - { + match actor_instance.register_function_no_result::<(u64, Vec)>( + "theater:simple/process-handlers", + "handle-stderr", + ) { Ok(_) => { info!("Successfully registered handle-stderr function"); } Err(e) => { error!("Failed to register handle-stderr function: {}", e); - return Err(anyhow::anyhow!("Failed to register handle-stderr function: {}", e)); + return Err(anyhow::anyhow!( + "Failed to register handle-stderr function: {}", + e + )); } } - match actor_instance - .register_function_no_result::<(u64, i32)>( - "theater:simple/process-handlers", - "handle-exit", - ) - { + match actor_instance.register_function_no_result::<(u64, i32)>( + "theater:simple/process-handlers", + "handle-exit", + ) { Ok(_) => { info!("Successfully registered handle-exit function"); } Err(e) => { error!("Failed to register handle-exit function: {}", e); - return Err(anyhow::anyhow!("Failed to register handle-exit function: {}", e)); + return Err(anyhow::anyhow!( + "Failed to register handle-exit function: {}", + e + )); } } @@ -471,10 +478,7 @@ impl ProcessHost { info!("Setting up host functions for process handling"); - let mut interface = match actor_component - .linker - .instance("theater:simple/process") - { + let mut interface = match actor_component.linker.instance("theater:simple/process") { Ok(interface) => { // Record successful linker instance creation actor_component.actor_store.record_event(ChainEventData { @@ -496,7 +500,10 @@ impl ProcessHost { timestamp: chrono::Utc::now().timestamp_millis() as u64, description: Some(format!("Failed to create linker instance: {}", e)), }); - return Err(anyhow::anyhow!("Could not instantiate theater:simple/process: {}", e)); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/process: {}", + e + )); } }; @@ -1261,16 +1268,19 @@ impl ProcessHost { actor_id: crate::id::TheaterId, ) { const GRACE_PERIOD_SECS: u64 = 5; - + // First, try SIGTERM let sigterm_result = Self::send_signal_to_process(process_id, 15, &processes).await; // SIGTERM = 15 - + if sigterm_result.is_ok() { - info!("Sent SIGTERM to process {}, waiting {} seconds for graceful exit", process_id, GRACE_PERIOD_SECS); - + info!( + "Sent SIGTERM to process {}, waiting {} seconds for graceful exit", + process_id, GRACE_PERIOD_SECS + ); + // Wait for grace period tokio::time::sleep(Duration::from_secs(GRACE_PERIOD_SECS)).await; - + // Check if process is still running let still_running = { let processes_lock = processes.lock().unwrap(); @@ -1280,10 +1290,13 @@ impl ProcessHost { false } }; - + if still_running { - info!("Process {} did not exit gracefully, sending SIGKILL", process_id); - + info!( + "Process {} did not exit gracefully, sending SIGKILL", + process_id + ); + // Record escalation event let escalation_event = ChainEventData { event_type: "process/timeout".to_string(), @@ -1298,25 +1311,32 @@ impl ProcessHost { process_id )), }; - + // Send escalation event - if let Err(e) = theater_tx.send(crate::messages::TheaterCommand::NewEvent { - actor_id: actor_id.clone(), - event: escalation_event.to_chain_event(None), - }).await { + if let Err(e) = theater_tx + .send(crate::messages::TheaterCommand::NewEvent { + actor_id: actor_id.clone(), + event: escalation_event.to_chain_event(None), + }) + .await + { error!("Failed to send timeout escalation event: {}", e); } - + // Send SIGKILL - let _sigkill_result = Self::send_signal_to_process(process_id, 9, &processes).await; // SIGKILL = 9 + let _sigkill_result = Self::send_signal_to_process(process_id, 9, &processes).await; + // SIGKILL = 9 } } else { // If SIGTERM failed, try direct kill - info!("SIGTERM failed for process {}, attempting direct kill", process_id); + info!( + "SIGTERM failed for process {}, attempting direct kill", + process_id + ); Self::kill_process_directly(process_id, &processes).await; } } - + /// Send signal to process (helper function) async fn send_signal_to_process( process_id: u64, @@ -1331,7 +1351,7 @@ impl ProcessHost { return Err("Process not found".to_string()); } }; - + if let Some(os_pid) = os_pid { #[cfg(unix)] { @@ -1339,11 +1359,15 @@ impl ProcessHost { if libc::kill(os_pid as i32, signal) == 0 { Ok(()) } else { - Err(format!("Failed to send signal {}: {}", signal, std::io::Error::last_os_error())) + Err(format!( + "Failed to send signal {}: {}", + signal, + std::io::Error::last_os_error() + )) } } } - + #[cfg(not(unix))] { // On non-Unix platforms, we can only kill @@ -1358,7 +1382,7 @@ impl ProcessHost { Err("Process OS PID not available".to_string()) } } - + /// Kill process directly using Child::kill() async fn kill_process_directly( process_id: u64, @@ -1372,7 +1396,7 @@ impl ProcessHost { None } }; - + if let Some(mut child) = child_opt { if let Err(e) = child.kill().await { error!("Failed to kill process {}: {}", process_id, e); diff --git a/crates/theater/src/host/random.rs b/crates/theater/src/host/random.rs index 9d24603c..f2c475bf 100644 --- a/crates/theater/src/host/random.rs +++ b/crates/theater/src/host/random.rs @@ -42,7 +42,10 @@ pub enum RandomError { } impl RandomHost { - pub fn new(config: RandomHandlerConfig, permissions: Option) -> Self { + pub fn new( + config: RandomHandlerConfig, + permissions: Option, + ) -> Self { let rng = if let Some(seed) = config.seed { info!("Initializing random host with seed: {}", seed); Arc::new(Mutex::new(ChaCha20Rng::seed_from_u64(seed))) @@ -51,7 +54,11 @@ impl RandomHost { Arc::new(Mutex::new(ChaCha20Rng::from_entropy())) }; - Self { config, rng, permissions } + Self { + config, + rng, + permissions, + } } pub async fn setup_host_functions( @@ -68,10 +75,7 @@ impl RandomHost { info!("Setting up random number generator host functions"); - let mut interface = match actor_component - .linker - .instance("theater:simple/random") - { + let mut interface = match actor_component.linker.instance("theater:simple/random") { Ok(interface) => { // Record successful linker instance creation actor_component.actor_store.record_event(ChainEventData { @@ -93,7 +97,10 @@ impl RandomHost { timestamp: chrono::Utc::now().timestamp_millis() as u64, description: Some(format!("Failed to create linker instance: {}", e)), }); - return Err(anyhow::anyhow!("Could not instantiate theater:simple/random: {}", e)); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/random: {}", + e + )); } }; diff --git a/crates/theater/src/host/runtime.rs b/crates/theater/src/host/runtime.rs index edc1e012..6bb2939c 100644 --- a/crates/theater/src/host/runtime.rs +++ b/crates/theater/src/host/runtime.rs @@ -82,10 +82,7 @@ impl RuntimeHost { pub async fn setup_host_functions(&self, actor_component: &mut ActorComponent) -> Result<()> { info!("Setting up runtime host functions"); let name = actor_component.name.clone(); - let mut interface = match actor_component - .linker - .instance("theater:simple/runtime") - { + let mut interface = match actor_component.linker.instance("theater:simple/runtime") { Ok(interface) => { // Record successful linker instance creation actor_component.actor_store.record_event(ChainEventData { @@ -107,7 +104,10 @@ impl RuntimeHost { timestamp: chrono::Utc::now().timestamp_millis() as u64, description: Some(format!("Failed to create linker instance: {}", e)), }); - return Err(anyhow::anyhow!("Could not instantiate theater:simple/runtime: {}", e)); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/runtime: {}", + e + )); } }; diff --git a/crates/theater/src/host/store.rs b/crates/theater/src/host/store.rs index 54d63953..95fbfad2 100644 --- a/crates/theater/src/host/store.rs +++ b/crates/theater/src/host/store.rs @@ -32,7 +32,10 @@ pub struct StoreHost { } impl StoreHost { - pub fn new(_config: StoreHandlerConfig, permissions: Option) -> Self { + pub fn new( + _config: StoreHandlerConfig, + permissions: Option, + ) -> Self { Self { permissions } } @@ -47,10 +50,7 @@ impl StoreHost { info!("Setting up store host functions"); - let mut interface = match actor_component - .linker - .instance("theater:simple/store") - { + let mut interface = match actor_component.linker.instance("theater:simple/store") { Ok(interface) => { // Record successful linker instance creation actor_component.actor_store.record_event(ChainEventData { @@ -72,7 +72,10 @@ impl StoreHost { timestamp: chrono::Utc::now().timestamp_millis() as u64, description: Some(format!("Failed to create linker instance: {}", e)), }); - return Err(anyhow::anyhow!("Could not instantiate theater:simple/store: {}", e)); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/store: {}", + e + )); } }; diff --git a/crates/theater/src/host/supervisor.rs b/crates/theater/src/host/supervisor.rs index 2a2e599b..cde955c0 100644 --- a/crates/theater/src/host/supervisor.rs +++ b/crates/theater/src/host/supervisor.rs @@ -43,7 +43,10 @@ struct SupervisorEvent { } impl SupervisorHost { - pub fn new(_config: SupervisorHostConfig, permissions: Option) -> Self { + pub fn new( + _config: SupervisorHostConfig, + permissions: Option, + ) -> Self { let (channel_tx, channel_rx) = tokio::sync::mpsc::channel(100); Self { channel_tx, @@ -63,10 +66,7 @@ impl SupervisorHost { info!("Setting up host functions for supervisor"); - let mut interface = match actor_component - .linker - .instance("theater:simple/supervisor") - { + let mut interface = match actor_component.linker.instance("theater:simple/supervisor") { Ok(interface) => { // Record successful linker instance creation actor_component.actor_store.record_event(ChainEventData { @@ -88,14 +88,17 @@ impl SupervisorHost { timestamp: chrono::Utc::now().timestamp_millis() as u64, description: Some(format!("Failed to create linker instance: {}", e)), }); - return Err(anyhow::anyhow!("Could not instantiate theater:simple/supervisor: {}", e)); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/supervisor: {}", + e + )); } }; let supervisor_tx = self.channel_tx.clone(); - + info!("DEBUG: About to start function registrations"); - + // spawn-child implementation info!("DEBUG: Registering spawn function"); let _ = interface @@ -955,48 +958,54 @@ impl SupervisorHost { pub async fn add_export_functions(&self, actor_instance: &mut ActorInstance) -> Result<()> { info!("Adding export functions for supervisor"); - match actor_instance - .register_function_no_result::<(String, WitActorError)>( - "theater:simple/supervisor-handlers", - "handle-child-error", - ) - { + match actor_instance.register_function_no_result::<(String, WitActorError)>( + "theater:simple/supervisor-handlers", + "handle-child-error", + ) { Ok(_) => { info!("Successfully registered handle-child-error function"); } Err(e) => { error!("Failed to register handle-child-error function: {}", e); - return Err(anyhow::anyhow!("Failed to register handle-child-error function: {}", e)); + return Err(anyhow::anyhow!( + "Failed to register handle-child-error function: {}", + e + )); } } - match actor_instance - .register_function_no_result::<(String, Option>)>( - "theater:simple/supervisor-handlers", - "handle-child-exit", - ) - { + match actor_instance.register_function_no_result::<(String, Option>)>( + "theater:simple/supervisor-handlers", + "handle-child-exit", + ) { Ok(_) => { info!("Successfully registered handle-child-exit function"); } Err(e) => { error!("Failed to register handle-child-exit function: {}", e); - return Err(anyhow::anyhow!("Failed to register handle-child-exit function: {}", e)); + return Err(anyhow::anyhow!( + "Failed to register handle-child-exit function: {}", + e + )); } } - match actor_instance - .register_function_no_result::<(String,)>( - "theater:simple/supervisor-handlers", - "handle-child-external-stop", - ) - { + match actor_instance.register_function_no_result::<(String,)>( + "theater:simple/supervisor-handlers", + "handle-child-external-stop", + ) { Ok(_) => { info!("Successfully registered handle-child-external-stop function"); } Err(e) => { - error!("Failed to register handle-child-external-stop function: {}", e); - return Err(anyhow::anyhow!("Failed to register handle-child-external-stop function: {}", e)); + error!( + "Failed to register handle-child-external-stop function: {}", + e + ); + return Err(anyhow::anyhow!( + "Failed to register handle-child-external-stop function: {}", + e + )); } } diff --git a/crates/theater/src/host/timing.rs b/crates/theater/src/host/timing.rs index bb4e104c..d826ab22 100644 --- a/crates/theater/src/host/timing.rs +++ b/crates/theater/src/host/timing.rs @@ -41,8 +41,14 @@ pub enum TimingError { } impl TimingHost { - pub fn new(config: TimingHostConfig, permissions: Option) -> Self { - Self { config, permissions } + pub fn new( + config: TimingHostConfig, + permissions: Option, + ) -> Self { + Self { + config, + permissions, + } } pub async fn setup_host_functions(&self, actor_component: &mut ActorComponent) -> Result<()> { @@ -56,10 +62,7 @@ impl TimingHost { info!("Setting up timing host functions"); - let mut interface = match actor_component - .linker - .instance("theater:simple/timing") - { + let mut interface = match actor_component.linker.instance("theater:simple/timing") { Ok(interface) => { // Record successful linker instance creation actor_component.actor_store.record_event(ChainEventData { @@ -81,7 +84,10 @@ impl TimingHost { timestamp: chrono::Utc::now().timestamp_millis() as u64, description: Some(format!("Failed to create linker instance: {}", e)), }); - return Err(anyhow::anyhow!("Could not instantiate theater:simple/timing: {}", e)); + return Err(anyhow::anyhow!( + "Could not instantiate theater:simple/timing: {}", + e + )); } }; diff --git a/crates/theater/src/id.rs b/crates/theater/src/id.rs index 14a84a83..6d56e7ec 100644 --- a/crates/theater/src/id.rs +++ b/crates/theater/src/id.rs @@ -39,7 +39,7 @@ use uuid::Uuid; /// /// Internally, TheaterId is implemented using UUIDs (Universally Unique Identifiers) /// to ensure uniqueness across distributed systems without requiring central coordination. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Copy)] pub struct TheaterId(Uuid); impl TheaterId { diff --git a/crates/theater/src/lib.rs b/crates/theater/src/lib.rs index 2f9babd2..bc89203c 100644 --- a/crates/theater/src/lib.rs +++ b/crates/theater/src/lib.rs @@ -37,6 +37,7 @@ pub mod chain; pub mod config; pub mod errors; pub mod events; +pub mod handler; pub mod host; pub mod id; @@ -47,7 +48,7 @@ pub mod shutdown; pub mod store; pub mod theater_runtime; pub mod utils; -mod wasm; +pub mod wasm; pub use actor::ActorError; @@ -70,4 +71,4 @@ pub use metrics::{ pub use shutdown::{ShutdownController, ShutdownReceiver, ShutdownSignal, ShutdownType}; pub use store::{ContentRef, ContentStore, Label}; pub use theater_runtime::TheaterRuntime; -pub use wasm::{MemoryStats, WasmError}; +pub use wasm::{ActorComponent, ActorInstance, MemoryStats, WasmError}; diff --git a/crates/theater/src/messages.rs b/crates/theater/src/messages.rs index a7a889ad..dac2a4b1 100644 --- a/crates/theater/src/messages.rs +++ b/crates/theater/src/messages.rs @@ -1,4 +1,5 @@ use crate::actor::ActorError; +use crate::actor::ActorRuntimeError; /// # Theater Message System /// /// Defines the message types used for communication between different components @@ -184,32 +185,6 @@ pub enum TheaterCommand { data: Option>, }, - /// # Update an actor's component - /// - /// ## Parameters - /// - /// * `actor_id` - ID of the actor to update - /// * `component` - The new component address - /// * `response_tx` - Channel to receive the result (success or error) - UpdateActorComponent { - actor_id: TheaterId, - component: String, - response_tx: oneshot::Sender>, - }, - - /// # Send a message to an actor - /// - /// Sends a message to a specific actor for processing. - /// - /// ## Parameters - /// - /// * `actor_id` - ID of the actor to send the message to - /// * `actor_message` - The message to send - SendMessage { - actor_id: TheaterId, - actor_message: ActorMessage, - }, - /// # Record a new event /// /// Records an event in an actor's event chain. @@ -236,6 +211,10 @@ pub enum TheaterCommand { error: ActorError, }, + ActorRuntimeError { + error: ActorRuntimeError, + }, + /// # Get all actors /// /// Retrieves a list of all actor IDs in the system. @@ -364,100 +343,6 @@ pub enum TheaterCommand { event_tx: Sender>, }, - /// # Open a communication channel - /// - /// Opens a bidirectional communication channel between two participants. - /// - /// ## Parameters - /// - /// * `initiator_id` - The participant initiating the channel - /// * `target_id` - The target participant for the channel - /// * `channel_id` - The unique ID for this channel - /// * `initial_message` - The first message to send on the channel - /// * `response_tx` - Channel to receive the result (success or error) - ChannelOpen { - initiator_id: ChannelParticipant, - target_id: ChannelParticipant, - channel_id: ChannelId, - initial_message: Vec, - response_tx: oneshot::Sender>, - }, - - /// # Send a message on a channel - /// - /// Sends data through an established channel. - /// - /// ## Parameters - /// - /// * `channel_id` - The ID of the channel to send on - /// * `sender_id` - The participant sending the message - /// * `message` - The message data to send - ChannelMessage { - channel_id: ChannelId, - sender_id: ChannelParticipant, - message: Vec, - }, - - /// # Close a channel - /// - /// Closes an open communication channel. - /// - /// ## Parameters - /// - /// * `channel_id` - The ID of the channel to close - ChannelClose { channel_id: ChannelId }, - - /// # List active channels - /// - /// Retrieves a list of all active communication channels. - /// - /// ## Parameters - /// - /// * `response_tx` - Channel to receive the result (list of channel IDs and participants) - /// - /// ## Security - /// - /// This operation is only available to the system or to actors with - /// appropriate monitoring permissions. - ListChannels { - response_tx: oneshot::Sender)>>>, - }, - - /// # Get channel status - /// - /// Retrieves information about a specific channel. - /// - /// ## Parameters - /// - /// * `channel_id` - The ID of the channel to query - /// * `response_tx` - Channel to receive the result (channel participant info) - /// - /// ## Security - /// - /// This operation is only available to participants in the channel, - /// the system, or actors with appropriate monitoring permissions. - GetChannelStatus { - channel_id: ChannelId, - response_tx: oneshot::Sender>>>, - }, - - /// # Register a new channel - /// - /// Registers a new channel in the system (internal use). - /// - /// ## Parameters - /// - /// * `channel_id` - The ID of the channel to register - /// * `participants` - The participants in the channel - /// - /// ## Security - /// - /// This operation is only available to the system itself. - RegisterChannel { - channel_id: ChannelId, - participants: Vec, - }, - /// # Create a new content store /// /// Creates a new content-addressable storage instance. @@ -486,13 +371,6 @@ impl TheaterCommand { TheaterCommand::ResumeActor { manifest_path, .. } => { format!("ResumeActor: {}", manifest_path) } - TheaterCommand::UpdateActorComponent { - actor_id, - component, - .. - } => { - format!("UpdateActorComponent: {} -> {}", actor_id, component) - } TheaterCommand::StopActor { actor_id, .. } => { format!("StopActor: {:?}", actor_id) } @@ -506,15 +384,13 @@ impl TheaterCommand { data.as_ref().map(|d| String::from_utf8_lossy(d)) ) } - TheaterCommand::SendMessage { actor_id, .. } => { - format!("SendMessage: {:?}", actor_id) - } TheaterCommand::NewEvent { actor_id, .. } => { format!("NewEvent: {:?}", actor_id) } TheaterCommand::ActorError { actor_id, .. } => { format!("ActorError: {:?}", actor_id) } + TheaterCommand::ActorRuntimeError { .. } => "ActorRuntimeError".to_string(), TheaterCommand::GetActors { .. } => "GetActors".to_string(), TheaterCommand::GetActorManifest { actor_id, .. } => { format!("GetActorManifest: {:?}", actor_id) @@ -540,37 +416,6 @@ impl TheaterCommand { TheaterCommand::SubscribeToActor { actor_id, .. } => { format!("SubscribeToActor: {:?}", actor_id) } - TheaterCommand::ChannelOpen { - initiator_id, - target_id, - channel_id, - .. - } => { - format!( - "ChannelOpen: {} -> {} (channel: {})", - initiator_id, target_id, channel_id - ) - } - TheaterCommand::ChannelMessage { channel_id, .. } => { - format!("ChannelMessage: {}", channel_id) - } - TheaterCommand::ChannelClose { channel_id } => { - format!("ChannelClose: {}", channel_id) - } - TheaterCommand::ListChannels { .. } => "ListChannels".to_string(), - TheaterCommand::GetChannelStatus { channel_id, .. } => { - format!("GetChannelStatus: {}", channel_id) - } - TheaterCommand::RegisterChannel { - channel_id, - participants, - } => { - format!( - "RegisterChannel: {} with {} participants", - channel_id, - participants.len() - ) - } TheaterCommand::NewStore { .. } => "NewStore".to_string(), } } @@ -662,6 +507,37 @@ impl ChannelId { pub fn as_str(&self) -> &str { &self.0 } + + /// # Parse a channel ID from a string + /// + /// Creates a ChannelId from its string representation. + /// + /// ## Parameters + /// + /// * `s` - The string to parse (should be in the format "ch_XXXXXXXXXXXXXXXX") + /// + /// ## Returns + /// + /// * `Ok(ChannelId)` - Successfully parsed channel ID + /// * `Err` - Invalid format or empty string + /// + /// ## Example + /// + /// ```rust + /// use theater::messages::ChannelId; + /// + /// let channel_id = ChannelId::parse("ch_0123456789abcdef").unwrap(); + /// assert_eq!(channel_id.as_str(), "ch_0123456789abcdef"); + /// ``` + pub fn parse(s: &str) -> Result { + if s.is_empty() { + anyhow::bail!("Channel ID cannot be empty"); + } + if !s.starts_with("ch_") { + anyhow::bail!("Channel ID must start with 'ch_' prefix"); + } + Ok(ChannelId(s.to_string())) + } } /// # Channel Participant @@ -794,10 +670,12 @@ pub struct ActorSend { pub struct ActorChannelOpen { /// The unique ID for this channel pub channel_id: ChannelId, + /// The participant initiating the channel + pub initiator_id: ChannelParticipant, /// Channel to receive the result of the open request pub response_tx: oneshot::Sender>, /// Initial message data (may contain authentication/metadata) - pub data: Vec, + pub initial_msg: Vec, } /// # Actor Channel Message @@ -818,7 +696,7 @@ pub struct ActorChannelMessage { /// The ID of the channel to send on pub channel_id: ChannelId, /// Message data - pub data: Vec, + pub msg: Vec, } /// # Actor Channel Close @@ -908,6 +786,118 @@ pub enum ActorMessage { ChannelInitiated(ActorChannelInitiated), } +/// # Actor Lifecycle Event +/// +/// Notifications sent from the runtime to the message-server handler about actor lifecycle changes. +/// +/// ## Purpose +/// +/// ActorLifecycleEvent enables the message-server handler to maintain its own actor registry +/// independently from the runtime. When actors are spawned or stopped, the runtime sends +/// notifications through a dedicated channel. +/// +/// ## Architecture +/// +/// This is the key integration point between the runtime and message-server: +/// - Runtime creates actors and sends ActorSpawned events +/// - Message-server creates mailboxes and registers actors +/// - Message-server starts consuming actor mailboxes +/// - When actors stop, runtime sends ActorStopped events +/// - Message-server cleans up mailboxes and registry entries +/// +/// ## Usage +/// +/// ```rust +/// use theater::messages::ActorLifecycleEvent; +/// use theater::id::TheaterId; +/// use theater::actor::handle::ActorHandle; +/// +/// // Runtime sends this when spawning an actor +/// let event = ActorLifecycleEvent::ActorSpawned { +/// actor_id: TheaterId::generate(), +/// actor_handle: actor_handle.clone(), +/// }; +/// lifecycle_tx.send(event).await.unwrap(); +/// ``` +#[derive(Debug)] +pub enum ActorLifecycleEvent { + /// An actor has been spawned and is ready to receive messages + ActorSpawned { + actor_id: TheaterId, + actor_handle: crate::actor::handle::ActorHandle, + }, + /// An actor has been stopped and should be unregistered + ActorStopped { + actor_id: TheaterId, + }, +} + +/// # Message Command +/// +/// Commands for the message-server handler's messaging infrastructure. +/// +/// ## Purpose +/// +/// MessageCommand provides a separate command space from TheaterCommand specifically +/// for actor-to-actor messaging operations. This separation allows the message-server +/// handler to manage messaging independently from the core runtime. +/// +/// ## Design +/// +/// MessageCommand enables complete architectural separation: +/// - Message-server handler maintains its own actor registry +/// - Message routing is handled externally from the runtime +/// - Lifecycle integration happens through ActorLifecycleEvent +/// +/// ## Integration +/// +/// Actor WASM host functions send MessageCommands to route messages: +/// - send() → MessageCommand::SendMessage +/// - request() → MessageCommand::SendMessage (with Request type) +/// - open-channel() → MessageCommand::OpenChannel +/// - send-on-channel() → MessageCommand::ChannelMessage +/// - close-channel() → MessageCommand::ChannelClose +#[derive(Debug)] +pub enum MessageCommand { + /// Send a one-way message to an actor + /// + /// Delivers a message to the target actor's mailbox without waiting for a response. + SendMessage { + target_id: TheaterId, + message: ActorMessage, + response_tx: oneshot::Sender>, + }, + + /// Open a bidirectional channel between actors + /// + /// Initiates a channel creation between two participants. The target actor + /// receives a ChannelOpen message and can accept or reject the channel. + OpenChannel { + initiator_id: ChannelParticipant, + target_id: ChannelParticipant, + channel_id: ChannelId, + initial_message: Vec, + response_tx: oneshot::Sender>, + }, + + /// Send a message on an established channel + /// + /// Transmits data over an existing channel to the other participant. + ChannelMessage { + channel_id: ChannelId, + message: Vec, + response_tx: oneshot::Sender>, + }, + + /// Close a channel + /// + /// Terminates a channel, notifying both participants. + ChannelClose { + channel_id: ChannelId, + response_tx: oneshot::Sender>, + }, +} + /// # Actor Status /// /// Represents the current operational status of an actor. diff --git a/crates/theater/src/store/mod.rs b/crates/theater/src/store/mod.rs index fa3b6c46..04664c69 100644 --- a/crates/theater/src/store/mod.rs +++ b/crates/theater/src/store/mod.rs @@ -267,12 +267,10 @@ impl ContentStore { } /// Store content and return its ContentRef - pub async fn store(&self, content: Vec) -> Result { + pub async fn store(&self, content: Vec) -> ContentRef { let content_ref = ContentRef::from_content(&content); + content_ref.store_content(&self.base_path(), &content).await; content_ref - .store_content(&self.base_path(), &content) - .await?; - Ok(content_ref) } /// Store content synchronously and return its ContentRef diff --git a/crates/theater/src/theater_runtime.rs b/crates/theater/src/theater_runtime.rs index c5e0cff3..43d3bea1 100644 --- a/crates/theater/src/theater_runtime.rs +++ b/crates/theater/src/theater_runtime.rs @@ -7,15 +7,14 @@ use crate::actor::runtime::ActorRuntime; use crate::actor::types::{ActorControl, ActorError, ActorInfo, ActorOperation}; use crate::chain::ChainEvent; -use crate::config::permissions::HandlerPermission; use crate::events::runtime::RuntimeEventData; use crate::events::EventData; +use crate::handler::HandlerRegistry; use crate::id::TheaterId; +use crate::messages::{ActorLifecycleEvent, ActorMessage, ActorStatus, TheaterCommand}; use crate::messages::{ - ActorChannelClose, ActorChannelMessage, ActorChannelOpen, ActorResult, ChannelId, - ChannelParticipant, ChildError, ChildExternalStop, ChildResult, + ActorResult, ChannelId, ChannelParticipant, ChildError, ChildExternalStop, ChildResult, }; -use crate::messages::{ActorMessage, ActorStatus, TheaterCommand}; use crate::metrics::ActorMetrics; use crate::shutdown::{ShutdownController, ShutdownType}; use crate::utils::{self, resolve_reference}; @@ -25,8 +24,10 @@ use crate::{ManifestConfig, StateChain}; use serde_json::Value; use std::collections::HashMap; use std::collections::HashSet; +use std::marker::PhantomData; use std::path::PathBuf; use std::sync::{Arc, RwLock}; +use theater_chain::event::EventType; use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Sender; use tokio::sync::{mpsc, oneshot}; @@ -88,13 +89,13 @@ use tracing::{debug, error, info, warn}; /// The runtime uses a command-based architecture where all operations are sent as messages /// through channels. This allows for asynchronous processing and helps maintain isolation /// between components. -pub struct TheaterRuntime { +pub struct TheaterRuntime { /// Map of active actors indexed by their ID actors: HashMap, /// Map of chains index by actor ID chains: HashMap>>, /// Sender for commands to the runtime - pub theater_tx: Sender, + theater_tx: Sender, /// Receiver for commands to the runtime theater_rx: Receiver, /// Map of event subscriptions for actors @@ -105,8 +106,11 @@ pub struct TheaterRuntime { channel_events_tx: Option>, /// wasm engine wasm_engine: wasmtime::Engine, - /// Runtime permissions - permissions: HandlerPermission, + /// Handler registry + pub handler_registry: HandlerRegistry, + /// Optional channel for sending actor lifecycle events to message-server handler + message_lifecycle_tx: Option>, + marker: PhantomData, } /// # ActorProcess @@ -129,7 +133,7 @@ pub struct ActorProcess { /// Actor Name pub name: String, /// Task handle for the running actor - pub process: JoinHandle, + pub process: JoinHandle<()>, /// Channel for sending messages to the actor pub mailbox_tx: mpsc::Sender, /// Channel for sending operations to the actor @@ -152,7 +156,10 @@ pub struct ActorProcess { pub supervisor_tx: Option>, } -impl TheaterRuntime { +impl TheaterRuntime +where + E: EventType, +{ /// Creates a new TheaterRuntime with the given communication channels. /// /// ## Parameters @@ -160,6 +167,7 @@ impl TheaterRuntime { /// * `theater_tx` - Sender for commands to the runtime /// * `theater_rx` - Receiver for commands to the runtime /// * `channel_events_tx` - Optional channel for sending events to external systems + /// * `message_lifecycle_tx` - Optional channel for sending actor lifecycle events to message-server /// /// ## Returns /// @@ -175,7 +183,7 @@ impl TheaterRuntime { /// # /// # async fn example() -> Result<()> { /// let (theater_tx, theater_rx) = mpsc::channel::(100); - /// let runtime = TheaterRuntime::new(theater_tx, theater_rx, None, Default::default()).await?; + /// let runtime = TheaterRuntime::new(theater_tx, theater_rx, None, None, Default::default()).await?; /// # Ok(()) /// # } /// ``` @@ -183,7 +191,8 @@ impl TheaterRuntime { theater_tx: Sender, theater_rx: Receiver, channel_events_tx: Option>, - permissions: HandlerPermission, + message_lifecycle_tx: Option>, + handler_registry: HandlerRegistry, ) -> Result { info!("Theater runtime initializing"); let engine = wasmtime::Engine::new(wasmtime::Config::new().async_support(true))?; @@ -197,7 +206,9 @@ impl TheaterRuntime { channels: HashMap::new(), channel_events_tx, wasm_engine: engine, - permissions, + handler_registry, + message_lifecycle_tx, + marker: PhantomData, }) } @@ -405,39 +416,6 @@ impl TheaterRuntime { } } } - TheaterCommand::UpdateActorComponent { - actor_id, - component, - response_tx, - } => { - debug!("Updating actor component: {:?}", actor_id); - match self.update_actor_component(actor_id, component).await { - Ok(_) => { - info!("Actor component updated successfully"); - let _ = response_tx.send(Ok(())); - } - Err(e) => { - error!("Failed to update actor component: {}", e); - let _ = response_tx.send(Err(e)); - } - } - } - TheaterCommand::SendMessage { - actor_id, - actor_message, - } => { - debug!("Sending message to actor: {:?}", actor_id); - if let Some(proc) = self.actors.get_mut(&actor_id) { - if let Err(e) = proc.mailbox_tx.send(actor_message).await { - error!("Failed to send message to actor: {}", e); - } - } else { - warn!( - "Attempted to send message to non-existent actor: {:?}", - actor_id - ); - } - } TheaterCommand::NewEvent { actor_id, event } => { debug!("Received new event from actor {:?}", actor_id); @@ -452,6 +430,9 @@ impl TheaterRuntime { error!("Failed to handle actor error event: {}", e); } } + TheaterCommand::ActorRuntimeError { error } => { + error!("Theater runtime error: {}", error); + } TheaterCommand::GetActors { response_tx } => { debug!("Getting list of actors"); let actor_info: Vec<_> = self @@ -513,302 +494,6 @@ impl TheaterRuntime { .expect("Failed to subscribe"); } // Channel-related commands - TheaterCommand::ChannelOpen { - initiator_id, - target_id, - channel_id, - initial_message, - response_tx, - } => { - debug!("Opening channel from {:?} to {:?}", initiator_id, target_id); - if initiator_id == target_id { - warn!("Attempted to open channel to self: {:?}", initiator_id); - let _ = - response_tx.send(Err(anyhow::anyhow!("Cannot open channel to self"))); - continue; - } - match target_id { - ChannelParticipant::Actor(ref actor_id) => { - if let Some(proc) = self.actors.get_mut(&actor_id) { - // Create a oneshot channel to intercept the response - let (inner_tx, inner_rx) = tokio::sync::oneshot::channel(); - - // Create channel open message - let actor_message = ActorMessage::ChannelOpen(ActorChannelOpen { - channel_id: channel_id.clone(), - response_tx: inner_tx, - data: initial_message, - }); - - // Send the message to the target actor - if let Err(e) = proc.mailbox_tx.send(actor_message).await { - error!("Failed to send channel open message to actor: {}", e); - let _ = response_tx.send(Err(anyhow::anyhow!( - "Failed to send channel open message" - ))); - continue; - } - - // Process the response asynchronously - let channel_id_clone = channel_id.clone(); - let initiator_id_clone = initiator_id.clone(); - let target_id_clone = target_id.clone(); - let theater_tx_clone = self.theater_tx.clone(); - - tokio::spawn(async move { - match inner_rx.await { - Ok(result) => { - if let Ok(true) = &result { - // Channel was accepted, register both participants via a new command - // This avoids holding a mutable reference across an await point - let register_cmd = - TheaterCommand::RegisterChannel { - channel_id: channel_id_clone.clone(), - participants: vec![ - initiator_id_clone.clone(), - target_id_clone.clone(), - ], - }; - - if let Err(e) = - theater_tx_clone.send(register_cmd).await - { - error!( - "Failed to register channel participants: {}", - e - ); - } else { - debug!("Requested registration of channel {:?} with participants {:?} and {:?}", - channel_id_clone, initiator_id_clone, target_id_clone); - } - } - - // Forward the result to the original requester - let _ = response_tx.send(result); - } - Err(e) => { - error!( - "Failed to receive channel open response: {}", - e - ); - let _ = response_tx.send(Err(anyhow::anyhow!( - "Failed to receive channel open response" - ))); - } - } - }); - } else { - warn!( - "Attempted to open channel to non-existent actor: {:?}", - target_id - ); - let _ = response_tx - .send(Err(anyhow::anyhow!("Target actor not found"))); - } - } - ChannelParticipant::External => { - warn!( - "External channel participants cannot be targeted for channel open" - ); - let _ = response_tx.send(Err(anyhow::anyhow!( - "External participants cannot be targeted for channel open" - ))); - } - } - } - TheaterCommand::ChannelMessage { - channel_id, - message, - sender_id, - } => { - debug!("Sending message on channel: {:?}", channel_id); - - // Look up the participants for this channel - if let Some(participant_ids) = self.channels.get(&channel_id) { - let mut successful_delivery = false; - - for participant in participant_ids { - debug!("Delivering message to participant {:?}", participant); - if *participant == sender_id { - debug!("Skipping message delivery to sender"); - continue; - } - match participant { - ChannelParticipant::Actor(actor_id) => { - if let Some(proc) = self.actors.get_mut(actor_id) { - let actor_message = - ActorMessage::ChannelMessage(ActorChannelMessage { - channel_id: channel_id.clone(), - data: message.clone(), - }); - - match proc.mailbox_tx.send(actor_message).await { - Ok(_) => { - successful_delivery = true; - debug!( - "Delivered channel message to actor {:?}", - actor_id - ); - } - Err(e) => { - error!( - "Failed to send channel message to actor {:?}: {}", - actor_id, e - ); - } - } - } else { - warn!( - "Actor {:?} registered for channel {:?} no longer exists", - actor_id, channel_id - ); - } - } - ChannelParticipant::External => { - debug!("Sending message to server"); - // Send the message to the server - if let Some(tx) = &self.channel_events_tx { - let channel_event = - crate::messages::ChannelEvent::Message { - channel_id: channel_id.clone(), - sender_id: sender_id.clone(), - message: message.clone(), - }; - - if let Err(e) = tx.send(channel_event).await { - error!("Failed to send message to server: {}", e); - } else { - successful_delivery = true; - debug!( - "Delivered message to server for channel {:?}", - channel_id - ); - } - } - } - } - } - - if !successful_delivery { - warn!( - "Failed to deliver message to any actor for channel {:?}", - channel_id - ); - } - } else { - warn!("No actors registered for channel: {:?}", channel_id); - } - } - TheaterCommand::ChannelClose { channel_id } => { - debug!("Closing channel: {:?}", channel_id); - - // Get participant IDs before removing the channel - let participant_ids = if let Some(ids) = self.channels.get(&channel_id) { - ids.clone() - } else { - HashSet::new() - }; - - // Remove the channel from the registry - self.channels.remove(&channel_id); - debug!("Removed channel {:?} from registry", channel_id); - - // Notify participants about channel closure - let mut successful_notification = false; - for participant in &participant_ids { - match participant { - ChannelParticipant::Actor(actor_id) => { - if let Some(proc) = self.actors.get_mut(actor_id) { - let actor_message = - ActorMessage::ChannelClose(ActorChannelClose { - channel_id: channel_id.clone(), - }); - - match proc.mailbox_tx.send(actor_message).await { - Ok(_) => { - successful_notification = true; - debug!( - "Notified actor {:?} about channel {:?} closure", - actor_id, channel_id - ); - } - Err(e) => { - error!( - "Failed to send channel close message to actor {:?}: {}", - actor_id, e - ); - } - } - } else { - warn!("Actor {:?} registered for channel {:?} no longer exists during closure", - actor_id, channel_id); - } - } - ChannelParticipant::External => { - // Send the message to the server - if let Some(tx) = &self.channel_events_tx { - let channel_event = crate::messages::ChannelEvent::Close { - channel_id: channel_id.clone(), - }; - - if let Err(e) = tx.send(channel_event).await { - error!("Failed to send close event to server: {}", e); - } else { - debug!( - "Notified server about closure of channel {:?}", - channel_id - ); - } - } - } - } - } - - if !successful_notification && !participant_ids.is_empty() { - warn!( - "Failed to notify any actors about channel {:?} closure", - channel_id - ); - } - } - TheaterCommand::ListChannels { response_tx } => { - debug!("Getting list of channels"); - let channels = self.list_channels().await; - - if let Err(e) = response_tx.send(Ok(channels)) { - error!("Failed to send channel list: {:?}", e); - } - } - TheaterCommand::GetChannelStatus { - channel_id, - response_tx, - } => { - debug!("Getting status for channel: {:?}", channel_id); - let status = self.get_channel_status(&channel_id).await; - - if let Err(e) = response_tx.send(Ok(status)) { - error!("Failed to send channel status: {:?}", e); - } - } - TheaterCommand::RegisterChannel { - channel_id, - participants, - } => { - debug!( - "Registering channel {:?} with {} participants", - channel_id, - participants.len() - ); - - // Convert the Vec to a HashSet - let participant_set: HashSet = - participants.into_iter().collect(); - - // Register the channel with its participants - self.channels.insert(channel_id.clone(), participant_set); - - debug!("Successfully registered channel {:?}", channel_id); - } TheaterCommand::NewStore { response_tx } => { debug!("Creating new content store"); let store_id = crate::store::ContentStore::new(); @@ -929,35 +614,24 @@ impl TheaterRuntime { let actor_name = manifest.name.clone(); let manifest_clone = manifest.clone(); let engine = self.wasm_engine.clone(); - let permissions = self.permissions.clone(); + let handler_registry = self.handler_registry.clone(); let actor_runtime_process = tokio::spawn(async move { - ActorRuntime::start( + let actor_runtime = ActorRuntime::start( actor_id_for_task.clone(), &manifest_clone, init_value, + engine, + chain, + handler_registry, theater_tx, - actor_sender, - mailbox_rx, operation_rx, actor_operation_tx, info_rx, actor_info_tx, control_rx, actor_control_tx, - init, - shutdown_receiver_clone, - engine, - permissions, - chain, ) .await; - - // Return a dummy struct to maintain API compatibility - ActorRuntime { - actor_id: actor_id_for_task, - handler_tasks: Vec::new(), - shutdown_controller: ShutdownController::new(), - } }); let process = ActorProcess { @@ -991,6 +665,18 @@ impl TheaterRuntime { self.actors.insert(actor_id.clone(), process); debug!("Actor process registered with runtime"); + + // Notify message-server about new actor + if let Some(lifecycle_tx) = &self.message_lifecycle_tx { + let event = ActorLifecycleEvent::ActorSpawned { + actor_id: actor_id.clone(), + actor_handle: handler_actor_handle.clone(), + }; + if let Err(e) = lifecycle_tx.send(event).await { + warn!("Failed to send ActorSpawned event to message-server: {}", e); + } + } + Ok(actor_id) } @@ -1015,8 +701,8 @@ impl TheaterRuntime { let should_remove = if let std::collections::hash_map::Entry::Occupied(mut entry) = self.subscriptions.entry(actor_id.clone()) { - let subscribers = entry.get_mut(); - let mut to_remove = Vec::new(); + let subscribers: &mut Vec>> = entry.get_mut(); + let mut to_remove: Vec = Vec::new(); // Send events and track failures for (index, subscriber) in subscribers.iter().enumerate() { @@ -1207,6 +893,16 @@ impl TheaterRuntime { debug!("Successfully stopped child {:?}", child_id); } + // Notify message-server that actor is stopping + if let Some(lifecycle_tx) = &self.message_lifecycle_tx { + let event = ActorLifecycleEvent::ActorStopped { + actor_id: actor_id.clone(), + }; + if let Err(e) = lifecycle_tx.send(event).await { + warn!("Failed to send ActorStopped event to message-server: {}", e); + } + } + // Signal this specific actor to shutdown - we need to get the actor again since // we may have changed the actors map when stopping children let proc = self.actors.remove(&actor_id); @@ -1297,37 +993,6 @@ impl TheaterRuntime { Ok(()) } - async fn update_actor_component( - &mut self, - actor_id: TheaterId, - component: String, - ) -> Result<()> { - debug!("Updating actor component for: {:?}", actor_id); - - if let Some(proc) = self.actors.get(&actor_id) { - // Send a message to update the actor's component - let (tx, rx): ( - oneshot::Sender>, - oneshot::Receiver>, - ) = oneshot::channel(); - proc.operation_tx - .send(ActorOperation::UpdateComponent { - component_address: component, - response_tx: tx, - }) - .await?; - - match rx.await { - Ok(result) => result?, - Err(e) => return Err(anyhow::anyhow!("Failed to receive update result: {}", e)), - } - } else { - return Err(anyhow::anyhow!("Actor not found")); - } - - Ok(()) - } - async fn restart_actor(&mut self, actor_id: TheaterId) -> Result<()> { debug!("Starting actor restart process for: {:?}", actor_id); diff --git a/crates/theater/src/utils/template.rs b/crates/theater/src/utils/template.rs index f28a6a15..b1f20645 100644 --- a/crates/theater/src/utils/template.rs +++ b/crates/theater/src/utils/template.rs @@ -1,31 +1,28 @@ // Variable substitution using Handlebars +use handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError}; use serde_json::Value; use thiserror::Error; -use handlebars::{Handlebars, Context, RenderContext, Helper, HelperResult, Output, RenderError}; #[derive(Error, Debug)] pub enum TemplateError { #[error("Template rendering failed: {0}")] RenderError(#[from] RenderError), - + #[error("Template compilation failed: {0}")] CompilationError(String), } /// Substitute variables in TOML content using Handlebars templating -pub fn substitute_variables( - toml_content: &str, - state: &Value, -) -> Result { +pub fn substitute_variables(toml_content: &str, state: &Value) -> Result { let mut handlebars = Handlebars::new(); - + // Register a helper for default values: {{default server.port "8080"}} handlebars.register_helper("default", Box::new(default_helper)); - + // Compile and render the template let result = handlebars.render_template(toml_content, state)?; - + Ok(result) } @@ -40,12 +37,10 @@ fn default_helper( ) -> HelperResult { // Get the first parameter (the variable we're trying to access) let value = h.param(0); - - // Get the default value (second parameter) - let default = h.param(1) - .and_then(|v| v.value().as_str()) - .unwrap_or(""); - + + // Get the default value (second parameter) + let default = h.param(1).and_then(|v| v.value().as_str()).unwrap_or(""); + match value { Some(param) => { // If the value exists and isn't null/empty, use it @@ -60,7 +55,7 @@ fn default_helper( out.write(default)?; } } - + Ok(()) } @@ -77,7 +72,7 @@ mod tests { "name": "test-app" } }); - + let result = substitute_variables(toml, &state).unwrap(); assert_eq!(result, r#"name = "test-app""#); } @@ -92,7 +87,7 @@ mod tests { } } }); - + let result = substitute_variables(toml, &state).unwrap(); assert_eq!(result, r#"path = "/var/data""#); } @@ -106,7 +101,7 @@ mod tests { {"hostname": "server2.example.com"} ] }); - + let result = substitute_variables(toml, &state).unwrap(); assert_eq!(result, r#"host = "server1.example.com""#); } @@ -121,7 +116,7 @@ count = {{config.max_items}} "feature": {"enabled": true}, "config": {"max_items": 42} }); - + let result = substitute_variables(toml, &state).unwrap(); assert!(result.contains("enabled = true")); assert!(result.contains("count = 42")); @@ -131,7 +126,7 @@ count = {{config.max_items}} fn test_default_helper() { let toml = r#"port = {{default server.port "8080"}}"#; let state = json!({}); - + let result = substitute_variables(toml, &state).unwrap(); assert_eq!(result, r#"port = 8080"#); } @@ -144,7 +139,7 @@ count = {{config.max_items}} "port": 9000 } }); - + let result = substitute_variables(toml, &state).unwrap(); assert_eq!(result, r#"port = 9000"#); } @@ -154,10 +149,10 @@ count = {{config.max_items}} let toml = r#"url = "{{protocol}}://{{host}}:{{port}}""#; let state = json!({ "protocol": "https", - "host": "api.example.com", + "host": "api.example.com", "port": 443 }); - + let result = substitute_variables(toml, &state).unwrap(); assert_eq!(result, r#"url = "https://api.example.com:443""#); } @@ -181,7 +176,7 @@ type = "http-client" base_url = "{{default api.endpoint "https://api.default.com"}}" timeout = {{default api.timeout_ms "5000"}} "#; - + let state = json!({ "app": { "name": "dynamic-processor", @@ -205,11 +200,11 @@ timeout = {{default api.timeout_ms "5000"}} "create_dirs": true } }); - + let result = substitute_variables(toml, &state).unwrap(); - + println!("Rendered result:\n{}", result); - + // Verify key substitutions assert!(result.contains(r#"name = "dynamic-processor""#)); assert!(result.contains(r#"version = "0.1.0""#)); @@ -226,7 +221,7 @@ timeout = {{default api.timeout_ms "5000"}} fn test_missing_variable_renders_empty() { let toml = r#"value = "{{missing.variable}}""#; let state = json!({}); - + let result = substitute_variables(toml, &state).unwrap(); // Handlebars renders missing variables as empty strings assert_eq!(result, r#"value = """#); diff --git a/crates/theater/src/wasm.rs b/crates/theater/src/wasm.rs index 0e61b6e6..c08df5f0 100644 --- a/crates/theater/src/wasm.rs +++ b/crates/theater/src/wasm.rs @@ -49,8 +49,8 @@ use crate::events::wasm::WasmEventData; use crate::events::{ChainEventData, EventData}; // use crate::config::ManifestConfig; use crate::id::TheaterId; +use crate::store; use crate::utils::resolve_reference; -use crate::{store, ChainEvent}; use tracing::{debug, error, info}; use wasmtime::component::types::ComponentItem; @@ -214,6 +214,8 @@ pub struct ActorComponent { pub actor_store: ActorStore, pub linker: Linker, pub engine: Engine, + pub import_types: Vec<(String, ComponentItem)>, + pub export_types: Vec<(String, ComponentItem)>, pub exports: HashMap, } @@ -333,6 +335,17 @@ impl ActorComponent { }; let linker = Linker::new(&engine); + let component_type = component.component_type(); + let mut import_types = Vec::new(); + for (import_name, import) in component_type.imports(&engine) { + import_types.push((import_name.to_string(), import)) + } + + let mut export_types = Vec::new(); + for (export_name, export) in component_type.exports(&engine) { + export_types.push((export_name.to_string(), export)) + } + Ok(ActorComponent { name, component, @@ -340,6 +353,8 @@ impl ActorComponent { linker, engine, exports: HashMap::new(), + import_types, + export_types, }) } @@ -396,12 +411,7 @@ impl ActorComponent { })?; info!("Component bytes loaded from path: {}", component_path); // Store the component bytes in the content store for future use - let bytes_ref = component_store.store(bytes.clone()).await.map_err(|e| { - WasmError::WasmError { - context: "storing component bytes", - message: e.to_string(), - } - })?; + let bytes_ref = component_store.store(bytes.clone()).await; // Label the stored bytes for future retrieval component_store diff --git a/crates/theater/tests/common/helpers.rs b/crates/theater/tests/common/helpers.rs index 4365a7df..88a2af0f 100644 --- a/crates/theater/tests/common/helpers.rs +++ b/crates/theater/tests/common/helpers.rs @@ -1,11 +1,11 @@ use chrono::Utc; -use theater::ActorOperation; use theater::chain::StateChain; -use theater::{HandlerConfig, ManifestConfig, MessageServerConfig}; use theater::events::message::MessageEventData; use theater::events::{ChainEventData, EventData}; use theater::id::TheaterId; use theater::messages::{ActorMessage, TheaterCommand}; +use theater::ActorOperation; +use theater::{HandlerConfig, ManifestConfig, MessageServerConfig}; use theater::{ShutdownController, ShutdownReceiver}; use tokio::sync::mpsc; @@ -38,9 +38,9 @@ pub fn create_test_manifest(name: &str) -> ManifestConfig { }; // Add a message server handler - config - .handlers - .push(HandlerConfig::MessageServer { config: MessageServerConfig {} }); + config.handlers.push(HandlerConfig::MessageServer { + config: MessageServerConfig {}, + }); config } diff --git a/crates/theater/tests/integration/permission_tests.rs b/crates/theater/tests/integration/permission_tests.rs index 510ba610..79c48d9d 100644 --- a/crates/theater/tests/integration/permission_tests.rs +++ b/crates/theater/tests/integration/permission_tests.rs @@ -1,6 +1,8 @@ -use theater::config::actor_manifest::{HandlerConfig, ManifestConfig, FileSystemHandlerConfig, EnvironmentHandlerConfig}; +use theater::config::actor_manifest::{ + EnvironmentHandlerConfig, FileSystemHandlerConfig, HandlerConfig, ManifestConfig, +}; use theater::config::permissions::{ - HandlerPermission, FileSystemPermissions, EnvironmentPermissions, + EnvironmentPermissions, FileSystemPermissions, HandlerPermission, }; /// Test that handler creation validation works correctly @@ -25,27 +27,27 @@ async fn test_handler_creation_permission_validation() { }), ..Default::default() }; - + // Create a manifest that requests these handlers let _manifest = ManifestConfig { name: "test-actor".to_string(), component: "test.wasm".to_string(), handlers: vec![ - HandlerConfig::FileSystem { + HandlerConfig::FileSystem { config: FileSystemHandlerConfig { path: Some("/tmp/test".into()), new_dir: Some(false), allowed_commands: None, - } + }, }, - HandlerConfig::Environment { + HandlerConfig::Environment { config: EnvironmentHandlerConfig { allowed_vars: None, denied_vars: None, allow_list_all: false, allowed_prefixes: None, - } - } + }, + }, ], version: "1.0.0".to_string(), description: None, @@ -54,7 +56,7 @@ async fn test_handler_creation_permission_validation() { permission_policy: Default::default(), init_state: None, }; - + // Test case 2: Missing permissions should be detected let _invalid_permissions = HandlerPermission { // Missing file_system permissions @@ -67,9 +69,9 @@ async fn test_handler_creation_permission_validation() { }), ..Default::default() }; - + println!("✅ Permission validation logic tested"); - + // The actual validation would happen in create_handlers() function // which we've already implemented to check permissions before creating handlers } @@ -95,7 +97,7 @@ async fn test_permission_inheritance() { }), ..Default::default() }; - + // Test child permissions (more restrictive) let _child_permissions = HandlerPermission { file_system: Some(FileSystemPermissions { @@ -114,10 +116,10 @@ async fn test_permission_inheritance() { }), ..Default::default() }; - + // In a real implementation, effective permissions would be the intersection // of parent and child permissions (child permissions cannot exceed parent) - + println!("✅ Permission inheritance logic structure verified"); } @@ -126,7 +128,7 @@ async fn test_permission_inheritance() { #[tokio::test] async fn test_permission_checker_integration() { use theater::config::enforcement::PermissionChecker; - + // Test filesystem permissions let fs_permissions = Some(FileSystemPermissions { read: true, @@ -136,31 +138,34 @@ async fn test_permission_checker_integration() { new_dir: Some(false), allowed_paths: Some(vec!["/tmp/allowed".to_string()]), }); - + // Should allow read in allowed path assert!(PermissionChecker::check_filesystem_operation( &fs_permissions, "read", Some("/tmp/allowed/file.txt"), None, - ).is_ok()); - + ) + .is_ok()); + // Should deny write (not in allowed operations) assert!(PermissionChecker::check_filesystem_operation( &fs_permissions, "write", Some("/tmp/allowed/file.txt"), None, - ).is_err()); - + ) + .is_err()); + // Should deny read in disallowed path assert!(PermissionChecker::check_filesystem_operation( &fs_permissions, "read", Some("/tmp/forbidden/file.txt"), None, - ).is_err()); - + ) + .is_err()); + println!("✅ Permission checker integration working correctly"); } @@ -172,21 +177,21 @@ async fn test_permission_denial_events() { // 1. The operation is blocked // 2. A PermissionDenied event is logged to the chain // 3. An appropriate error is returned - + // We can test this by verifying the event data structure use theater::events::filesystem::FilesystemEventData; - + let denial_event = FilesystemEventData::PermissionDenied { operation: "write".to_string(), path: "/tmp/forbidden/file.txt".to_string(), reason: "Write operation not permitted".to_string(), }; - + // Verify the event can be serialized (important for the event chain) let serialized = serde_json::to_string(&denial_event).expect("Failed to serialize event"); assert!(serialized.contains("PermissionDenied")); assert!(serialized.contains("write")); assert!(serialized.contains("/tmp/forbidden/file.txt")); - + println!("✅ Permission denial event structure verified"); } diff --git a/crates/theater/tests/unit/mod.rs b/crates/theater/tests/unit/mod.rs index 36f0ca28..df2f9d36 100644 --- a/crates/theater/tests/unit/mod.rs +++ b/crates/theater/tests/unit/mod.rs @@ -4,5 +4,5 @@ // pub mod actor_store_tests; // Disabled - API changed significantly pub mod chain_tests; pub mod messages_tests; -pub mod store_tests; pub mod permission_enforcement_tests; +pub mod store_tests; diff --git a/crates/theater/tests/unit/permission_enforcement_tests.rs b/crates/theater/tests/unit/permission_enforcement_tests.rs index b6247d30..a4b54f6c 100644 --- a/crates/theater/tests/unit/permission_enforcement_tests.rs +++ b/crates/theater/tests/unit/permission_enforcement_tests.rs @@ -118,7 +118,7 @@ fn test_filesystem_fail_closed_no_allowed_paths() { execute: false, allowed_commands: None, new_dir: Some(false), - allowed_paths: None, // No paths configured - should deny all + allowed_paths: None, // No paths configured - should deny all }); // Should deny all path operations when no allowed_paths is configured @@ -143,7 +143,7 @@ fn test_filesystem_fail_closed_no_allowed_paths() { assert!(PermissionChecker::check_filesystem_operation( &permissions_no_paths, "read", - None, // No path specified + None, // No path specified None, ) .is_ok()); @@ -159,36 +159,28 @@ fn test_http_permission_checking() { }); // Should allow GET request to allowed host - assert!(PermissionChecker::check_http_operation( - &http_permissions, - "GET", - "api.example.com", - ) - .is_ok()); + assert!( + PermissionChecker::check_http_operation(&http_permissions, "GET", "api.example.com",) + .is_ok() + ); // Should allow POST request to allowed host - assert!(PermissionChecker::check_http_operation( - &http_permissions, - "POST", - "api.example.com", - ) - .is_ok()); + assert!( + PermissionChecker::check_http_operation(&http_permissions, "POST", "api.example.com",) + .is_ok() + ); // Should deny PUT request (not in allowed methods) - assert!(PermissionChecker::check_http_operation( - &http_permissions, - "PUT", - "api.example.com", - ) - .is_err()); + assert!( + PermissionChecker::check_http_operation(&http_permissions, "PUT", "api.example.com",) + .is_err() + ); // Should deny request to forbidden host - assert!(PermissionChecker::check_http_operation( - &http_permissions, - "GET", - "forbidden.com", - ) - .is_err()); + assert!( + PermissionChecker::check_http_operation(&http_permissions, "GET", "forbidden.com",) + .is_err() + ); } #[test] @@ -201,24 +193,12 @@ fn test_environment_permission_checking() { }); // Should allow reading allowed environment variables - assert!(PermissionChecker::check_env_var_access( - &env_permissions, - "HOME", - ) - .is_ok()); + assert!(PermissionChecker::check_env_var_access(&env_permissions, "HOME",).is_ok()); - assert!(PermissionChecker::check_env_var_access( - &env_permissions, - "PATH", - ) - .is_ok()); + assert!(PermissionChecker::check_env_var_access(&env_permissions, "PATH",).is_ok()); // Should deny reading forbidden environment variables - assert!(PermissionChecker::check_env_var_access( - &env_permissions, - "SECRET_KEY", - ) - .is_err()); + assert!(PermissionChecker::check_env_var_access(&env_permissions, "SECRET_KEY",).is_err()); } #[test] @@ -231,17 +211,13 @@ fn test_environment_wildcard_permissions() { }); // Should allow access to any environment variable with allow_list_all - assert!(PermissionChecker::check_env_var_access( - &wildcard_permissions, - "ANY_VARIABLE", - ) - .is_ok()); + assert!( + PermissionChecker::check_env_var_access(&wildcard_permissions, "ANY_VARIABLE",).is_ok() + ); - assert!(PermissionChecker::check_env_var_access( - &wildcard_permissions, - "ANOTHER_VARIABLE", - ) - .is_ok()); + assert!( + PermissionChecker::check_env_var_access(&wildcard_permissions, "ANOTHER_VARIABLE",).is_ok() + ); } #[test] @@ -336,18 +312,10 @@ fn test_timing_permission_checking() { }); // Should allow sleep within limits - assert!(PermissionChecker::check_timing_operation( - &timing_permissions, - "sleep", - 3000, - ) - .is_ok()); + assert!(PermissionChecker::check_timing_operation(&timing_permissions, "sleep", 3000,).is_ok()); // Should deny sleep exceeding limits - assert!(PermissionChecker::check_timing_operation( - &timing_permissions, - "sleep", - 10000, - ) - .is_err()); + assert!( + PermissionChecker::check_timing_operation(&timing_permissions, "sleep", 10000,).is_err() + ); } diff --git a/crates/theater/tests/unit/store_tests.rs b/crates/theater/tests/unit/store_tests.rs index 25fd325c..6a39d826 100644 --- a/crates/theater/tests/unit/store_tests.rs +++ b/crates/theater/tests/unit/store_tests.rs @@ -67,21 +67,42 @@ async fn test_content_store_labeling() { let label2 = Label::new("test-label-2"); */ - store.label(&Label::from_str("test-label-1"), &ref1.clone()).await.unwrap(); - store.label(&Label::from_str("test-label-2"), &ref2.clone()).await.unwrap(); + store + .label(&Label::from_str("test-label-1"), &ref1.clone()) + .await + .unwrap(); + store + .label(&Label::from_str("test-label-2"), &ref2.clone()) + .await + .unwrap(); // Lookup by label - let found_ref1 = store.get_by_label(&Label::from_str("test-label-1")).await.unwrap().unwrap(); - let found_ref2 = store.get_by_label(&Label::from_str("test-label-2")).await.unwrap().unwrap(); + let found_ref1 = store + .get_by_label(&Label::from_str("test-label-1")) + .await + .unwrap() + .unwrap(); + let found_ref2 = store + .get_by_label(&Label::from_str("test-label-2")) + .await + .unwrap() + .unwrap(); assert_eq!(found_ref1, ref1); assert_eq!(found_ref2, ref2); // Update label - store.label(&Label::from_str("test-label-1"), &ref2.clone()).await.unwrap(); + store + .label(&Label::from_str("test-label-1"), &ref2.clone()) + .await + .unwrap(); // Verify update - let updated_ref = store.get_by_label(&Label::from_str("test-label-1")).await.unwrap().unwrap(); + let updated_ref = store + .get_by_label(&Label::from_str("test-label-1")) + .await + .unwrap() + .unwrap(); assert_eq!(updated_ref, ref2); } @@ -98,23 +119,38 @@ async fn test_content_store_delete() { let content_ref = store.store(test_content.clone()).await.unwrap(); // Create a label for this content - store.label(&Label::from_str(label_name), &content_ref).await.unwrap(); + store + .label(&Label::from_str(label_name), &content_ref) + .await + .unwrap(); // Verify content exists and label points to it assert!(store.exists(&content_ref).await); - let label_ref = store.get_by_label(&Label::from_str(label_name)).await.unwrap(); + let label_ref = store + .get_by_label(&Label::from_str(label_name)) + .await + .unwrap(); assert_eq!(Some(content_ref.clone()), label_ref); // Delete the label - store.remove_label(&Label::from_str(label_name)).await.unwrap(); + store + .remove_label(&Label::from_str(label_name)) + .await + .unwrap(); // The content should still exist, but the label should be gone assert!(store.exists(&content_ref).await); // Content still exists - let label_ref_after = store.get_by_label(&Label::from_str(label_name)).await.unwrap(); + let label_ref_after = store + .get_by_label(&Label::from_str(label_name)) + .await + .unwrap(); assert_eq!(None, label_ref_after); // Label is gone // Getting content by label should return None - let result = store.get_content_by_label(&Label::from_str(label_name)).await.unwrap(); + let result = store + .get_content_by_label(&Label::from_str(label_name)) + .await + .unwrap(); assert_eq!(None, result); } diff --git a/test b/test new file mode 100755 index 00000000..2c3b3a7b Binary files /dev/null and b/test differ