1#![forbid(unsafe_code)]
2
3use std::collections::BTreeMap;
6use std::sync::OnceLock;
7use std::time::{Duration, SystemTime};
8
9use serde::Deserialize;
10
11pub trait BridgeTypes {
13 type Error;
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum FileKind {
18 File,
19 Directory,
20 SymbolicLink,
21 Other,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct FileMetadata {
26 pub mode: u32,
27 pub size: u64,
28 pub kind: FileKind,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct DirectoryEntry {
33 pub name: String,
34 pub kind: FileKind,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct PathRequest {
39 pub vm_id: String,
40 pub path: String,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ReadFileRequest {
45 pub vm_id: String,
46 pub path: String,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct WriteFileRequest {
51 pub vm_id: String,
52 pub path: String,
53 pub contents: Vec<u8>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ReadDirRequest {
58 pub vm_id: String,
59 pub path: String,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct CreateDirRequest {
64 pub vm_id: String,
65 pub path: String,
66 pub recursive: bool,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct RenameRequest {
71 pub vm_id: String,
72 pub from_path: String,
73 pub to_path: String,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct SymlinkRequest {
78 pub vm_id: String,
79 pub target_path: String,
80 pub link_path: String,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ChmodRequest {
85 pub vm_id: String,
86 pub path: String,
87 pub mode: u32,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct TruncateRequest {
92 pub vm_id: String,
93 pub path: String,
94 pub len: u64,
95}
96
97pub trait FilesystemBridge: BridgeTypes {
98 fn read_file(&mut self, request: ReadFileRequest) -> Result<Vec<u8>, Self::Error>;
99 fn write_file(&mut self, request: WriteFileRequest) -> Result<(), Self::Error>;
100 fn stat(&mut self, request: PathRequest) -> Result<FileMetadata, Self::Error>;
101 fn lstat(&mut self, request: PathRequest) -> Result<FileMetadata, Self::Error>;
102 fn read_dir(&mut self, request: ReadDirRequest) -> Result<Vec<DirectoryEntry>, Self::Error>;
103 fn create_dir(&mut self, request: CreateDirRequest) -> Result<(), Self::Error>;
104 fn remove_file(&mut self, request: PathRequest) -> Result<(), Self::Error>;
105 fn remove_dir(&mut self, request: PathRequest) -> Result<(), Self::Error>;
106 fn rename(&mut self, request: RenameRequest) -> Result<(), Self::Error>;
107 fn symlink(&mut self, request: SymlinkRequest) -> Result<(), Self::Error>;
108 fn read_link(&mut self, request: PathRequest) -> Result<String, Self::Error>;
109 fn chmod(&mut self, request: ChmodRequest) -> Result<(), Self::Error>;
110 fn truncate(&mut self, request: TruncateRequest) -> Result<(), Self::Error>;
111 fn exists(&mut self, request: PathRequest) -> Result<bool, Self::Error>;
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum PermissionVerdict {
116 Allow,
117 Deny,
118 Prompt,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct PermissionDecision {
123 pub verdict: PermissionVerdict,
124 pub reason: Option<String>,
125}
126
127impl PermissionDecision {
128 pub fn allow() -> Self {
129 Self {
130 verdict: PermissionVerdict::Allow,
131 reason: None,
132 }
133 }
134
135 pub fn deny(reason: impl Into<String>) -> Self {
136 Self {
137 verdict: PermissionVerdict::Deny,
138 reason: Some(reason.into()),
139 }
140 }
141
142 pub fn prompt(reason: impl Into<String>) -> Self {
143 Self {
144 verdict: PermissionVerdict::Prompt,
145 reason: Some(reason.into()),
146 }
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum FilesystemAccess {
152 Read,
153 Write,
154 Stat,
155 ReadDir,
156 CreateDir,
157 Remove,
158 Rename,
159 Symlink,
160 ReadLink,
161 Chmod,
162 Truncate,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct FilesystemPermissionRequest {
167 pub vm_id: String,
168 pub path: String,
169 pub access: FilesystemAccess,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub enum NetworkAccess {
174 Fetch,
175 Http,
176 Dns,
177 Listen,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct NetworkPermissionRequest {
182 pub vm_id: String,
183 pub access: NetworkAccess,
184 pub resource: String,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct CommandPermissionRequest {
189 pub vm_id: String,
190 pub command: String,
191 pub args: Vec<String>,
192 pub cwd: Option<String>,
193 pub env: BTreeMap<String, String>,
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum EnvironmentAccess {
198 Read,
199 Write,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct EnvironmentPermissionRequest {
204 pub vm_id: String,
205 pub access: EnvironmentAccess,
206 pub key: String,
207 pub value: Option<String>,
208}
209
210pub trait PermissionBridge: BridgeTypes {
211 fn check_filesystem_access(
212 &mut self,
213 request: FilesystemPermissionRequest,
214 ) -> Result<PermissionDecision, Self::Error>;
215 fn check_network_access(
216 &mut self,
217 request: NetworkPermissionRequest,
218 ) -> Result<PermissionDecision, Self::Error>;
219 fn check_command_execution(
220 &mut self,
221 request: CommandPermissionRequest,
222 ) -> Result<PermissionDecision, Self::Error>;
223 fn check_environment_access(
224 &mut self,
225 request: EnvironmentPermissionRequest,
226 ) -> Result<PermissionDecision, Self::Error>;
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct FilesystemSnapshot {
231 pub format: String,
232 pub bytes: Vec<u8>,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq)]
236pub struct LoadFilesystemStateRequest {
237 pub vm_id: String,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct FlushFilesystemStateRequest {
242 pub vm_id: String,
243 pub snapshot: FilesystemSnapshot,
244}
245
246pub trait PersistenceBridge: BridgeTypes {
247 fn load_filesystem_state(
248 &mut self,
249 request: LoadFilesystemStateRequest,
250 ) -> Result<Option<FilesystemSnapshot>, Self::Error>;
251 fn flush_filesystem_state(
252 &mut self,
253 request: FlushFilesystemStateRequest,
254 ) -> Result<(), Self::Error>;
255}
256
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct ClockRequest {
259 pub vm_id: String,
260}
261
262#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct ScheduleTimerRequest {
264 pub vm_id: String,
265 pub delay: Duration,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
269pub struct ScheduledTimer {
270 pub timer_id: String,
271 pub delay: Duration,
272}
273
274pub trait ClockBridge: BridgeTypes {
275 fn wall_clock(&mut self, request: ClockRequest) -> Result<SystemTime, Self::Error>;
276 fn monotonic_clock(&mut self, request: ClockRequest) -> Result<Duration, Self::Error>;
277 fn schedule_timer(
278 &mut self,
279 request: ScheduleTimerRequest,
280 ) -> Result<ScheduledTimer, Self::Error>;
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct RandomBytesRequest {
285 pub vm_id: String,
286 pub len: usize,
287}
288
289pub trait RandomBridge: BridgeTypes {
290 fn fill_random_bytes(&mut self, request: RandomBytesRequest) -> Result<Vec<u8>, Self::Error>;
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub enum LogLevel {
295 Trace,
296 Debug,
297 Info,
298 Warn,
299 Error,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq)]
303pub struct LogRecord {
304 pub vm_id: String,
305 pub level: LogLevel,
306 pub message: String,
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
310pub struct DiagnosticRecord {
311 pub vm_id: String,
312 pub message: String,
313 pub fields: BTreeMap<String, String>,
314}
315
316#[derive(Debug, Clone, PartialEq, Eq)]
317pub struct StructuredEventRecord {
318 pub vm_id: String,
319 pub name: String,
320 pub fields: BTreeMap<String, String>,
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324pub enum LifecycleState {
325 Starting,
326 Ready,
327 Busy,
328 Terminated,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq)]
332pub struct LifecycleEventRecord {
333 pub vm_id: String,
334 pub state: LifecycleState,
335 pub detail: Option<String>,
336}
337
338pub trait EventBridge: BridgeTypes {
339 fn emit_structured_event(&mut self, event: StructuredEventRecord) -> Result<(), Self::Error>;
340 fn emit_diagnostic(&mut self, event: DiagnosticRecord) -> Result<(), Self::Error>;
341 fn emit_log(&mut self, event: LogRecord) -> Result<(), Self::Error>;
342 fn emit_lifecycle(&mut self, event: LifecycleEventRecord) -> Result<(), Self::Error>;
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
346pub enum GuestRuntime {
347 JavaScript,
348 WebAssembly,
349}
350
351#[derive(Debug, Clone, PartialEq, Eq)]
352pub struct CreateJavascriptContextRequest {
353 pub vm_id: String,
354 pub bootstrap_module: Option<String>,
355}
356
357#[derive(Debug, Clone, PartialEq, Eq)]
358pub struct CreateWasmContextRequest {
359 pub vm_id: String,
360 pub module_path: Option<String>,
361}
362
363#[derive(Debug, Clone, PartialEq, Eq)]
364pub struct GuestContextHandle {
365 pub context_id: String,
366 pub runtime: GuestRuntime,
367}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
370pub struct StartExecutionRequest {
371 pub vm_id: String,
372 pub context_id: String,
373 pub argv: Vec<String>,
374 pub env: BTreeMap<String, String>,
375 pub cwd: String,
376}
377
378#[derive(Debug, Clone, PartialEq, Eq)]
379pub struct StartedExecution {
380 pub execution_id: String,
381}
382
383#[derive(Debug, Clone, PartialEq, Eq)]
384pub struct ExecutionHandleRequest {
385 pub vm_id: String,
386 pub execution_id: String,
387}
388
389#[derive(Debug, Clone, PartialEq, Eq)]
390pub struct WriteExecutionStdinRequest {
391 pub vm_id: String,
392 pub execution_id: String,
393 pub chunk: Vec<u8>,
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397pub enum ExecutionSignal {
398 Terminate,
399 Interrupt,
400 Kill,
401}
402
403#[derive(Debug, Clone, PartialEq, Eq)]
404pub struct KillExecutionRequest {
405 pub vm_id: String,
406 pub execution_id: String,
407 pub signal: ExecutionSignal,
408}
409
410#[derive(Debug, Clone, PartialEq, Eq)]
411pub struct PollExecutionEventRequest {
412 pub vm_id: String,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq)]
416pub struct OutputChunk {
417 pub vm_id: String,
418 pub execution_id: String,
419 pub chunk: Vec<u8>,
420}
421
422#[derive(Debug, Clone, PartialEq, Eq)]
423pub struct ExecutionExited {
424 pub vm_id: String,
425 pub execution_id: String,
426 pub exit_code: i32,
427}
428
429#[derive(Debug, Clone, PartialEq, Eq)]
430pub struct GuestKernelCall {
431 pub vm_id: String,
432 pub execution_id: String,
433 pub operation: String,
434 pub payload: Vec<u8>,
435}
436
437#[derive(Debug, Clone, PartialEq, Eq)]
438pub enum ExecutionEvent {
439 Stdout(OutputChunk),
440 Stderr(OutputChunk),
441 Exited(ExecutionExited),
442 GuestRequest(GuestKernelCall),
443}
444
445pub trait ExecutionBridge: BridgeTypes {
446 fn create_javascript_context(
447 &mut self,
448 request: CreateJavascriptContextRequest,
449 ) -> Result<GuestContextHandle, Self::Error>;
450 fn create_wasm_context(
451 &mut self,
452 request: CreateWasmContextRequest,
453 ) -> Result<GuestContextHandle, Self::Error>;
454 fn start_execution(
455 &mut self,
456 request: StartExecutionRequest,
457 ) -> Result<StartedExecution, Self::Error>;
458 fn write_stdin(&mut self, request: WriteExecutionStdinRequest) -> Result<(), Self::Error>;
459 fn close_stdin(&mut self, request: ExecutionHandleRequest) -> Result<(), Self::Error>;
460 fn kill_execution(&mut self, request: KillExecutionRequest) -> Result<(), Self::Error>;
461 fn poll_execution_event(
462 &mut self,
463 request: PollExecutionEventRequest,
464 ) -> Result<Option<ExecutionEvent>, Self::Error>;
465}
466
467pub trait HostBridge:
468 FilesystemBridge
469 + PermissionBridge
470 + PersistenceBridge
471 + ClockBridge
472 + RandomBridge
473 + EventBridge
474 + ExecutionBridge
475{
476}
477
478impl<T> HostBridge for T where
479 T: FilesystemBridge
480 + PermissionBridge
481 + PersistenceBridge
482 + ClockBridge
483 + RandomBridge
484 + EventBridge
485 + ExecutionBridge
486{
487}
488
489#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
490#[serde(rename_all = "camelCase")]
491pub enum BridgeCallConvention {
492 Sync,
493 Async,
494 SyncPromise,
495}
496
497#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
498#[serde(rename_all = "camelCase")]
499pub struct BridgeContractGroup {
500 pub convention: BridgeCallConvention,
501 #[serde(default)]
502 pub argument_types: Vec<String>,
503 pub return_type: String,
504 pub names: Vec<String>,
505}
506
507#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
508#[serde(rename_all = "camelCase")]
509pub struct BridgeContract {
510 pub version: u32,
511 pub groups: Vec<BridgeContractGroup>,
512}
513
514static BRIDGE_CONTRACT: OnceLock<BridgeContract> = OnceLock::new();
515
516pub fn bridge_contract() -> &'static BridgeContract {
517 BRIDGE_CONTRACT.get_or_init(|| {
518 serde_json::from_str(include_str!("../bridge-contract.json"))
519 .expect("bridge-contract.json must be valid")
520 })
521}
522
523#[cfg(test)]
524mod tests {
525 use super::{bridge_contract, BridgeCallConvention};
526
527 #[test]
528 fn bridge_contract_has_version_and_unique_method_names() {
529 let contract = bridge_contract();
530 assert!(
531 contract.version > 0,
532 "bridge contract version must be positive"
533 );
534
535 let mut seen = std::collections::BTreeSet::new();
536 for group in &contract.groups {
537 assert!(
538 !group.names.is_empty(),
539 "every bridge contract group must list at least one method"
540 );
541 for name in &group.names {
542 assert!(
543 seen.insert(name.clone()),
544 "duplicate bridge contract method: {name}"
545 );
546 }
547 }
548 }
549
550 #[test]
551 fn bridge_contract_lists_each_convention() {
552 let contract = bridge_contract();
553 for convention in [
554 BridgeCallConvention::Sync,
555 BridgeCallConvention::Async,
556 BridgeCallConvention::SyncPromise,
557 ] {
558 assert!(
559 contract
560 .groups
561 .iter()
562 .any(|group| group.convention == convention),
563 "missing bridge contract group for {convention:?}"
564 );
565 }
566 }
567
568 #[test]
569 fn bridge_contract_module_loading_signatures_match_runtime_calls() {
570 let contract = bridge_contract();
571
572 let find_group = |method: &str| {
573 contract
574 .groups
575 .iter()
576 .find(|group| group.names.iter().any(|name| name == method))
577 .unwrap_or_else(|| panic!("missing bridge contract method {method}"))
578 };
579
580 let resolve_group = find_group("_resolveModule");
581 assert_eq!(resolve_group.convention, BridgeCallConvention::SyncPromise);
582 assert_eq!(
583 resolve_group.argument_types,
584 vec![
585 "specifier: string",
586 "fromDir: string",
587 "mode?: \"require\" | \"import\""
588 ]
589 );
590 assert_eq!(
591 resolve_group.names,
592 vec!["_resolveModule", "_resolveModuleSync"]
593 );
594
595 let load_group = find_group("_loadFile");
596 assert_eq!(load_group.convention, BridgeCallConvention::SyncPromise);
597 assert_eq!(load_group.argument_types, vec!["path: string"]);
598 assert_eq!(load_group.names, vec!["_loadFile", "_loadFileSync"]);
599
600 let format_group = find_group("_moduleFormat");
601 assert_eq!(format_group.convention, BridgeCallConvention::SyncPromise);
602 assert_eq!(format_group.argument_types, vec!["filename: string"]);
603 assert_eq!(
604 format_group.return_type,
605 "\"module\" | \"commonjs\" | \"json\" | null"
606 );
607 assert_eq!(format_group.names, vec!["_moduleFormat"]);
608 }
609}