Skip to main content

wasmsh_protocol/
lib.rs

1//! Message protocol for wasmsh host adapters.
2//!
3//! Defines versioned, serializable messages exchanged between a host and
4//! `wasmsh-runtime`, including the progressive `StartRun` / `PollRun`
5//! execution flow in addition to one-shot `Run`.
6//!
7//! An experimental typed WIT projection of the same surface lives in
8//! `crates/wasmsh-protocol/wit/worker-protocol.wit`. The serde enums remain
9//! the canonical contract today; the WIT world is additive and intended for
10//! future component-model embedders.
11//!
12//! ## Protocol version
13//! Current version: `0.1.0`
14
15#![warn(missing_docs)]
16
17/// Protocol version string.
18pub const PROTOCOL_VERSION: &str = "0.1.0";
19
20/// A command sent from the host to the worker.
21#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22#[non_exhaustive]
23pub enum HostCommand {
24    /// Initialize the shell runtime with optional configuration.
25    Init {
26        /// Maximum step budget per execution (0 = unlimited).
27        step_budget: u64,
28        /// Hostnames/IPs allowed for network access (empty = no network).
29        ///
30        /// Patterns: exact host (`api.example.com`), wildcard (`*.example.com`),
31        /// IP (`192.168.1.100`), host with port (`api.example.com:8080`).
32        #[serde(default)]
33        allowed_hosts: Vec<String>,
34    },
35    /// Execute a shell command string.
36    Run {
37        /// The shell source text to execute.
38        input: String,
39    },
40    /// Start a progressive shell execution without polling it to completion.
41    StartRun {
42        /// The shell source text to execute.
43        input: String,
44    },
45    /// Poll the active progressive execution.
46    PollRun,
47    /// Deliver a POSIX signal name or number to the shell runtime.
48    Signal {
49        /// Signal name (`TERM`, `SIGINT`) or decimal number (`15`).
50        signal: String,
51    },
52    /// Cancel the currently running execution.
53    Cancel,
54    /// Mount a virtual filesystem at the given path.
55    Mount {
56        /// Absolute path at which to mount the filesystem.
57        path: String,
58    },
59    /// Read a file from the virtual filesystem.
60    ReadFile {
61        /// Absolute path of the file to read.
62        path: String,
63    },
64    /// Write data to a file in the virtual filesystem.
65    WriteFile {
66        /// Absolute path of the file to write.
67        path: String,
68        /// Raw bytes to write into the file.
69        data: Vec<u8>,
70    },
71    /// List directory contents.
72    ListDir {
73        /// Absolute path of the directory to list.
74        path: String,
75    },
76}
77
78/// An event sent from the worker to the host.
79#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
80#[non_exhaustive]
81pub enum WorkerEvent {
82    /// Shell produced stdout output.
83    Stdout(Vec<u8>),
84    /// Shell produced stderr output.
85    Stderr(Vec<u8>),
86    /// Command execution finished with exit code.
87    Exit(i32),
88    /// Command execution is still active and needs another poll.
89    Yielded,
90    /// A diagnostic message (warning, info, trace).
91    Diagnostic(DiagnosticLevel, String),
92    /// A file in the VFS was changed.
93    FsChanged(String),
94    /// Protocol version announcement (sent on Init).
95    Version(String),
96}
97
98/// Diagnostic severity level.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
100#[non_exhaustive]
101pub enum DiagnosticLevel {
102    /// Informational message.
103    Info,
104    /// Non-fatal warning.
105    Warning,
106    /// Error-level diagnostic.
107    Error,
108    /// Low-level trace output.
109    Trace,
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn protocol_version() {
118        assert!(!PROTOCOL_VERSION.is_empty());
119    }
120
121    #[test]
122    fn host_command_variants() {
123        let cmd = HostCommand::Run {
124            input: "echo hello".into(),
125        };
126        assert!(matches!(cmd, HostCommand::Run { .. }));
127
128        let cmd = HostCommand::StartRun {
129            input: "echo hello".into(),
130        };
131        assert!(matches!(cmd, HostCommand::StartRun { .. }));
132
133        let cmd = HostCommand::Signal {
134            signal: "TERM".into(),
135        };
136        assert!(matches!(cmd, HostCommand::Signal { .. }));
137
138        assert_eq!(HostCommand::PollRun, HostCommand::PollRun);
139    }
140
141    #[test]
142    fn worker_event_variants() {
143        let evt = WorkerEvent::Exit(0);
144        assert_eq!(evt, WorkerEvent::Exit(0));
145
146        assert_eq!(WorkerEvent::Yielded, WorkerEvent::Yielded);
147
148        let evt = WorkerEvent::Diagnostic(DiagnosticLevel::Warning, "test".into());
149        assert!(matches!(
150            evt,
151            WorkerEvent::Diagnostic(DiagnosticLevel::Warning, _)
152        ));
153    }
154
155    #[test]
156    fn progressive_commands_roundtrip_json() {
157        let start = HostCommand::StartRun {
158            input: "echo hello".into(),
159        };
160        let encoded = serde_json::to_string(&start).unwrap();
161        assert_eq!(encoded, r#"{"StartRun":{"input":"echo hello"}}"#);
162        let decoded: HostCommand = serde_json::from_str(&encoded).unwrap();
163        assert_eq!(decoded, start);
164
165        let encoded = serde_json::to_string(&HostCommand::PollRun).unwrap();
166        assert_eq!(encoded, r#""PollRun""#);
167        let decoded: HostCommand = serde_json::from_str(&encoded).unwrap();
168        assert_eq!(decoded, HostCommand::PollRun);
169
170        let signal = HostCommand::Signal {
171            signal: "TERM".into(),
172        };
173        let encoded = serde_json::to_string(&signal).unwrap();
174        assert_eq!(encoded, r#"{"Signal":{"signal":"TERM"}}"#);
175        let decoded: HostCommand = serde_json::from_str(&encoded).unwrap();
176        assert_eq!(decoded, signal);
177    }
178
179    #[test]
180    fn yielded_event_roundtrips_json() {
181        let encoded = serde_json::to_string(&WorkerEvent::Yielded).unwrap();
182        assert_eq!(encoded, r#""Yielded""#);
183        let decoded: WorkerEvent = serde_json::from_str(&encoded).unwrap();
184        assert_eq!(decoded, WorkerEvent::Yielded);
185    }
186}