From fa3ca7c2354ad9b197a7a8282b8d4d547f7e17a8 Mon Sep 17 00:00:00 2001 From: Surma Date: Thu, 22 Oct 2026 23:37:35 +0100 Subject: [PATCH] Workspace skeleton for plugins: sausage-vfs, sausage-interface, sausage-plugin, sausage-plugin-macros The agent leaves the workspace until it is rewritten on the new SDK. --- Cargo.lock | 174 +++--------------------------- Cargo.toml | 7 +- interface/Cargo.toml | 8 ++ interface/src/compat.rs | 2 + interface/src/lib.rs | 4 + sdk/Cargo.toml | 13 +-- sdk/macros/Cargo.toml | 10 ++ sdk/macros/src/lib.rs | 1 + sdk/src/lib.rs | 230 +--------------------------------------- vfs/Cargo.toml | 7 ++ vfs/src/lib.rs | 1 + 11 files changed, 58 insertions(+), 399 deletions(-) create mode 100644 interface/Cargo.toml create mode 100644 interface/src/compat.rs create mode 100644 interface/src/lib.rs create mode 100644 sdk/macros/Cargo.toml create mode 100644 sdk/macros/src/lib.rs create mode 100644 vfs/Cargo.toml create mode 100644 vfs/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ca4874c..6cc1bf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,16 +11,6 @@ dependencies = [ "gimli", ] -[[package]] -name = "agent" -version = "0.1.0" -dependencies = [ - "sausage-actor", - "serde", - "serde_json", - "wstd", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -95,24 +85,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - [[package]] name = "async-trait" version = "0.1.92" @@ -268,15 +240,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "core_detect" version = "1.0.0" @@ -599,35 +562,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "event-listener" -version = "5.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" -dependencies = [ - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - [[package]] name = "fastrand" version = "2.5.0" @@ -723,21 +657,6 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - [[package]] name = "futures-sink" version = "0.3.34" @@ -1095,15 +1014,6 @@ dependencies = [ "libc", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "io-extras" version = "0.19.0" @@ -1370,12 +1280,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "parking_lot" version = "0.12.5" @@ -1687,15 +1591,27 @@ dependencies = [ ] [[package]] -name = "sausage-actor" +name = "sausage-interface" version = "0.1.0" dependencies = [ - "async-channel", - "serde", "serde_json", - "wstd", ] +[[package]] +name = "sausage-plugin" +version = "0.1.0" +dependencies = [ + "sausage-plugin-macros", +] + +[[package]] +name = "sausage-plugin-macros" +version = "0.1.0" + +[[package]] +name = "sausage-vfs" +version = "0.1.0" + [[package]] name = "scopeguard" version = "1.2.0" @@ -1885,7 +1801,7 @@ version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "fastrand 2.5.0", + "fastrand", "getrandom 0.4.3", "once_cell", "rustix", @@ -2156,12 +2072,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - [[package]] name = "walkdir" version = "2.5.0" @@ -2187,15 +2097,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" version = "0.2.129" @@ -2926,15 +2827,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -dependencies = [ - "bitflags", -] - [[package]] name = "wit-component" version = "0.258.3" @@ -2991,38 +2883,6 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" -[[package]] -name = "wstd" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29b52936db10a79bb724dadd1c2c3aac958e8229dcb1f1c7f2b7044ca9fc6a3a" -dependencies = [ - "anyhow", - "async-task", - "bytes", - "futures-lite", - "http", - "http-body", - "http-body-util", - "itoa", - "pin-project-lite", - "serde", - "serde_json", - "slab", - "wasip2", - "wstd-macro", -] - -[[package]] -name = "wstd-macro" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "153db9b65508bf6c2efe26617169840c03671ba5719a35868db7682a5261f7d4" -dependencies = [ - "quote", - "syn 2.0.119", -] - [[package]] name = "yoke" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index c0057ce..aeb3e8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,11 @@ [workspace] resolver = "3" -members = ["cli", "agent", "sdk"] +members = ["cli", "vfs", "interface", "sdk", "sdk/macros"] default-members = ["cli"] -exclude = ["examples"] +# The agent is rewritten on top of the new SDK. Until then it is not a member. +exclude = ["examples", "agent"] # Wasmtime compiles components with Cranelift. Optimize dependencies in dev -# builds, so that tests and debug runs start actors quickly. +# builds, so that tests and debug runs start plugins quickly. [profile.dev.package."*"] opt-level = 2 diff --git a/interface/Cargo.toml b/interface/Cargo.toml new file mode 100644 index 0000000..4ce14ef --- /dev/null +++ b/interface/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "sausage-interface" +version = "0.1.0" +edition = "2024" +description = "Sausage plugin interfaces: OpenRPC documents, the compatibility check, and client code generation." + +[dependencies] +serde_json = "1" diff --git a/interface/src/compat.rs b/interface/src/compat.rs new file mode 100644 index 0000000..8bbff24 --- /dev/null +++ b/interface/src/compat.rs @@ -0,0 +1,2 @@ +//! The compatibility check between a consumer's import and a provider's +//! interface (PROPOSAL.md, section 11.2). diff --git a/interface/src/lib.rs b/interface/src/lib.rs new file mode 100644 index 0000000..23832e3 --- /dev/null +++ b/interface/src/lib.rs @@ -0,0 +1,4 @@ +//! Sausage plugin interfaces: OpenRPC documents, the compatibility check +//! (`compat`), and the generator of client code for `import!`. + +pub mod compat; diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index a72480c..2cde9ab 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -1,15 +1,8 @@ [package] -name = "sausage-actor" +name = "sausage-plugin" version = "0.1.0" edition = "2024" -description = "SDK for Sausage actors." - -[features] -default = [] -async = ["dep:async-channel", "dep:wstd"] +description = "SDK for Sausage plugins." [dependencies] -async-channel = { version = "2", optional = true } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -wstd = { version = "0.6", optional = true } +sausage-plugin-macros = { version = "0.1", path = "macros" } diff --git a/sdk/macros/Cargo.toml b/sdk/macros/Cargo.toml new file mode 100644 index 0000000..1567106 --- /dev/null +++ b/sdk/macros/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "sausage-plugin-macros" +version = "0.1.0" +edition = "2024" +description = "Macros of the Sausage plugin SDK. Use them through `sausage-plugin`." + +[lib] +proc-macro = true + +[dependencies] diff --git a/sdk/macros/src/lib.rs b/sdk/macros/src/lib.rs new file mode 100644 index 0000000..1fa50a5 --- /dev/null +++ b/sdk/macros/src/lib.rs @@ -0,0 +1 @@ +//! Macros of the Sausage plugin SDK. Use them through `sausage-plugin`. diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 3acb758..eccbf7b 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -1,229 +1 @@ -//! Small runtime helpers for Sausage actors. - -pub use serde_json::{Value, json}; - -use serde::{Deserialize, Serialize}; -use std::io::BufRead; - -/// A message that an actor receives from the broker. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct Message { - /// The message ID. - pub id: String, - /// The ID of the sending actor. - pub from: String, - /// The ID of the message that this message answers. - #[serde(skip_serializing_if = "Option::is_none")] - pub reply_to: Option, - /// The message body. - pub body: Value, -} - -impl Message { - /// Return the body's `type` string, or an empty string when it is missing. - pub fn kind(&self) -> &str { - self.body["type"].as_str().unwrap_or("") - } -} - -fn parse(line: &[u8]) -> Option { - serde_json::from_slice::(line).ok() -} - -fn is_shutdown(message: &Message) -> bool { - message.from == "broker" && message.kind() == "shutdown" -} - -/// Return this actor's ID from `SAUSAGE_ACTOR`, or an empty string if unset. -pub fn me() -> String { - std::env::var("SAUSAGE_ACTOR").unwrap_or_default() -} - -/// Read messages from stdin until EOF or a shutdown message from the broker. -/// -/// Invalid JSON lines are skipped. -pub fn messages() -> impl Iterator { - let stdin = std::io::stdin(); - let mut lines = stdin.lock().lines(); - std::iter::from_fn(move || { - loop { - let Ok(line) = lines.next()? else { - continue; - }; - let Some(message) = parse(line.as_bytes()) else { - continue; - }; - return (!is_shutdown(&message)).then_some(message); - } - }) -} - -/// Send a message to `to` and return its full ID (`/`). -pub fn send(to: &str, body: Value) -> String { - let id = next_id(); - write_message(json!({"to": to, "id": id, "body": body})); - format!("{}/{id}", me()) -} - -/// Reply to `message` and return the new message's full ID. -pub fn reply(message: &Message, body: Value) -> String { - let id = next_id(); - write_message(json!({ - "to": message.from, - "id": id, - "reply_to": message.id, - "body": body, - })); - format!("{}/{id}", me()) -} - -/// Write a file so that readers see either the old or the new content, -/// never half of it: write a temporary file next to `path`, then rename it. -pub fn write_atomic(path: impl AsRef, contents: impl AsRef<[u8]>) -> std::io::Result<()> { - let path = path.as_ref(); - let tmp = path.with_file_name(format!(".{}.tmp-{}", path.file_name().unwrap_or_default().to_string_lossy(), next_id())); - std::fs::write(&tmp, contents)?; - std::fs::rename(&tmp, path) -} - -/// Publish a message to this actor's subscribers and return its full ID. -pub fn publish(body: Value) -> String { - let id = next_id(); - write_message(json!({"id": id, "body": body})); - format!("{}/{id}", me()) -} - -fn write_message(message: Value) { - use std::io::Write; - - let mut stdout = std::io::stdout().lock(); - writeln!(stdout, "{message}").unwrap(); - stdout.flush().unwrap(); -} - -fn next_id() -> String { - use std::sync::OnceLock; - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::time::{SystemTime, UNIX_EPOCH}; - - static START: OnceLock = OnceLock::new(); - static SEQUENCE: AtomicUsize = AtomicUsize::new(0); - let start = START.get_or_init(|| { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - }); - format!("{start}-{}", SEQUENCE.fetch_add(1, Ordering::Relaxed)) -} - -/// An asynchronous inbox for actors that use HTTP or timers. -#[cfg(feature = "async")] -pub struct Inbox { - rx: async_channel::Receiver, - _reader: wstd::runtime::Task<()>, -} - -#[cfg(feature = "async")] -impl Inbox { - /// Start the stdin reader inside the wstd runtime and return its inbox. - /// The inbox owns the reader task and must be dropped to cancel it. - pub fn start() -> Self { - let (tx, rx) = async_channel::unbounded(); - let reader = wstd::runtime::spawn(read_messages(tx)); - Self { rx, _reader: reader } - } - - /// Wait for the next message, or return `None` at EOF or shutdown. - pub async fn recv(&self) -> Option { - self.rx.recv().await.ok() - } - - /// Return the next available message without waiting. - pub fn try_recv(&self) -> Option { - self.rx.try_recv().ok() - } - - /// Return whether the reader closed the inbox, even when messages remain buffered. - pub fn is_closed(&self) -> bool { - self.rx.is_closed() - } -} - -#[cfg(feature = "async")] -async fn read_messages(tx: async_channel::Sender) { - use wstd::io::AsyncRead; - - let mut stdin = wstd::io::stdin(); - let mut buf = Vec::new(); - let mut chunk = [0; 8192]; - loop { - let Ok(n) = stdin.read(&mut chunk).await else { - break; - }; - if n == 0 { - break; - } - buf.extend_from_slice(&chunk[..n]); - while let Some(pos) = buf.iter().position(|b| *b == b'\n') { - let message = parse(&buf[..pos]); - buf.drain(..=pos); - let Some(message) = message else { - continue; - }; - if is_shutdown(&message) { - tx.close(); - return; - } - if tx.send(message).await.is_err() { - return; - } - } - } - tx.close(); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn kind_reads_body_type() { - let message: Message = serde_json::from_value(json!({ - "id": "broker/1", - "from": "broker", - "body": {"type": "ping"} - })) - .unwrap(); - assert_eq!(message.kind(), "ping"); - assert_eq!(message.reply_to, None); - } - - #[test] - fn kind_is_empty_when_type_is_missing() { - let message: Message = serde_json::from_value(json!({ - "id": "broker/1", - "from": "broker", - "body": {} - })) - .unwrap(); - assert_eq!(message.kind(), ""); - } - - #[test] - fn write_atomic_replaces_the_file() { - let dir = std::env::temp_dir().join(format!("sausage-actor-{}", next_id())); - std::fs::create_dir_all(&dir).unwrap(); - let path = dir.join("state.json"); - write_atomic(&path, "old").unwrap(); - write_atomic(&path, "new").unwrap(); - assert_eq!(std::fs::read_to_string(&path).unwrap(), "new"); - assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 1); - std::fs::remove_dir_all(dir).unwrap(); - } - - #[test] - fn generated_ids_differ() { - assert_ne!(next_id(), next_id()); - } -} +//! SDK for Sausage plugins. diff --git a/vfs/Cargo.toml b/vfs/Cargo.toml new file mode 100644 index 0000000..6704733 --- /dev/null +++ b/vfs/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "sausage-vfs" +version = "0.1.0" +edition = "2024" +description = "A composable virtual filesystem for WASI guests in Wasmtime." + +[dependencies] diff --git a/vfs/src/lib.rs b/vfs/src/lib.rs new file mode 100644 index 0000000..f38233f --- /dev/null +++ b/vfs/src/lib.rs @@ -0,0 +1 @@ +//! A composable virtual filesystem for WASI guests in Wasmtime.