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