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 SignalDispositionAction {
441 Default,
442 Ignore,
443 User,
444}
445
446#[derive(Debug, Clone, PartialEq, Eq)]
447pub struct SignalHandlerRegistration {
448 pub action: SignalDispositionAction,
449 pub mask: Vec<u32>,
450 pub flags: u32,
451}
452
453#[derive(Debug, Clone, PartialEq, Eq)]
454pub struct ExecutionSignalState {
455 pub vm_id: String,
456 pub execution_id: String,
457 pub signal: u32,
458 pub registration: SignalHandlerRegistration,
459}
460
461#[derive(Debug, Clone, PartialEq, Eq)]
462pub enum ExecutionEvent {
463 Stdout(OutputChunk),
464 Stderr(OutputChunk),
465 Exited(ExecutionExited),
466 GuestRequest(GuestKernelCall),
467 SignalState(ExecutionSignalState),
468}
469
470pub trait ExecutionBridge: BridgeTypes {
471 fn create_javascript_context(
472 &mut self,
473 request: CreateJavascriptContextRequest,
474 ) -> Result<GuestContextHandle, Self::Error>;
475 fn create_wasm_context(
476 &mut self,
477 request: CreateWasmContextRequest,
478 ) -> Result<GuestContextHandle, Self::Error>;
479 fn start_execution(
480 &mut self,
481 request: StartExecutionRequest,
482 ) -> Result<StartedExecution, Self::Error>;
483 fn write_stdin(&mut self, request: WriteExecutionStdinRequest) -> Result<(), Self::Error>;
484 fn close_stdin(&mut self, request: ExecutionHandleRequest) -> Result<(), Self::Error>;
485 fn kill_execution(&mut self, request: KillExecutionRequest) -> Result<(), Self::Error>;
486 fn poll_execution_event(
487 &mut self,
488 request: PollExecutionEventRequest,
489 ) -> Result<Option<ExecutionEvent>, Self::Error>;
490}
491
492pub trait HostBridge:
493 FilesystemBridge
494 + PermissionBridge
495 + PersistenceBridge
496 + ClockBridge
497 + RandomBridge
498 + EventBridge
499 + ExecutionBridge
500{
501}
502
503impl<T> HostBridge for T where
504 T: FilesystemBridge
505 + PermissionBridge
506 + PersistenceBridge
507 + ClockBridge
508 + RandomBridge
509 + EventBridge
510 + ExecutionBridge
511{
512}
513
514#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
515#[serde(rename_all = "camelCase")]
516pub enum BridgeCallConvention {
517 Sync,
518 Async,
519 SyncPromise,
520}
521
522#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
523#[serde(rename_all = "camelCase")]
524pub struct BridgeContractGroup {
525 pub convention: BridgeCallConvention,
526 #[serde(default)]
527 pub argument_types: Vec<String>,
528 pub return_type: String,
529 pub names: Vec<String>,
530}
531
532#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
533#[serde(rename_all = "camelCase")]
534pub struct BridgeDispatchTarget {
535 pub method: String,
536 #[serde(default)]
537 pub translate_args: bool,
538}
539
540#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
541#[serde(rename_all = "camelCase")]
542pub struct BridgeContract {
543 pub version: u32,
544 pub groups: Vec<BridgeContractGroup>,
545 #[serde(default)]
546 pub dispatch: BTreeMap<String, BridgeDispatchTarget>,
547}
548
549static BRIDGE_CONTRACT: OnceLock<BridgeContract> = OnceLock::new();
550
551pub fn bridge_contract() -> &'static BridgeContract {
552 BRIDGE_CONTRACT.get_or_init(|| {
553 serde_json::from_str(include_str!("../bridge-contract.json"))
554 .expect("bridge-contract.json must be valid")
555 })
556}
557
558#[cfg(test)]
559mod tests {
560 use super::{bridge_contract, BridgeCallConvention};
561
562 #[test]
563 fn bridge_contract_has_version_and_unique_method_names() {
564 let contract = bridge_contract();
565 assert!(
566 contract.version > 0,
567 "bridge contract version must be positive"
568 );
569
570 let mut seen = std::collections::BTreeSet::new();
571 for group in &contract.groups {
572 assert!(
573 !group.names.is_empty(),
574 "every bridge contract group must list at least one method"
575 );
576 for name in &group.names {
577 assert!(
578 seen.insert(name.clone()),
579 "duplicate bridge contract method: {name}"
580 );
581 }
582 }
583 }
584
585 #[test]
586 fn bridge_contract_dispatch_targets_are_declared_methods() {
587 let contract = bridge_contract();
588 let names: std::collections::BTreeSet<_> = contract
589 .groups
590 .iter()
591 .flat_map(|group| group.names.iter())
592 .collect();
593
594 for name in contract.dispatch.keys() {
595 assert!(
596 names.contains(name),
597 "bridge dispatch target {name} must be listed in bridge contract names"
598 );
599 }
600
601 for required in [
602 "_fsReadFile",
603 "_fsReadFileBinary",
604 "_fsLutimes",
605 "_fsLutimesAsync",
606 "fs.futimesSync",
607 "_cryptoHashDigest",
608 "_cryptoDiffieHellmanSessionDestroy",
609 "_netSocketConnectRaw",
610 "_kernelStdioWriteRaw",
611 "_ptySetRawMode",
612 ] {
613 assert!(
614 contract.dispatch.contains_key(required),
615 "bridge dispatch metadata missing for {required}"
616 );
617 }
618 }
619
620 #[test]
621 fn bridge_contract_lists_each_convention() {
622 let contract = bridge_contract();
623 for convention in [
624 BridgeCallConvention::Sync,
625 BridgeCallConvention::Async,
626 BridgeCallConvention::SyncPromise,
627 ] {
628 assert!(
629 contract
630 .groups
631 .iter()
632 .any(|group| group.convention == convention),
633 "missing bridge contract group for {convention:?}"
634 );
635 }
636 }
637
638 #[test]
639 fn bridge_contract_module_loading_signatures_match_runtime_calls() {
640 let contract = bridge_contract();
641
642 let find_group = |method: &str| {
643 contract
644 .groups
645 .iter()
646 .find(|group| group.names.iter().any(|name| name == method))
647 .unwrap_or_else(|| panic!("missing bridge contract method {method}"))
648 };
649
650 let resolve_group = find_group("_resolveModule");
651 assert_eq!(resolve_group.convention, BridgeCallConvention::SyncPromise);
652 assert_eq!(
653 resolve_group.argument_types,
654 vec![
655 "specifier: string",
656 "fromDir: string",
657 "mode?: \"require\" | \"import\""
658 ]
659 );
660 assert_eq!(
661 resolve_group.names,
662 vec!["_resolveModule", "_resolveModuleSync"]
663 );
664
665 let load_group = find_group("_loadFile");
666 assert_eq!(load_group.convention, BridgeCallConvention::SyncPromise);
667 assert_eq!(load_group.argument_types, vec!["path: string"]);
668 assert_eq!(load_group.names, vec!["_loadFile", "_loadFileSync"]);
669
670 let format_group = find_group("_moduleFormat");
671 assert_eq!(format_group.convention, BridgeCallConvention::SyncPromise);
672 assert_eq!(format_group.argument_types, vec!["filename: string"]);
673 assert_eq!(
674 format_group.return_type,
675 "\"module\" | \"commonjs\" | \"json\" | null"
676 );
677 assert_eq!(format_group.names, vec!["_moduleFormat"]);
678 }
679
680 #[test]
681 fn bridge_contract_includes_diffie_hellman_session_lifecycle_callbacks() {
682 let contract = bridge_contract();
683 let crypto_group = contract
684 .groups
685 .iter()
686 .find(|group| {
687 group
688 .names
689 .iter()
690 .any(|name| name == "_cryptoDiffieHellmanSessionCreate")
691 })
692 .expect("crypto bridge group");
693
694 for method in [
695 "_cryptoDiffieHellmanSessionCreate",
696 "_cryptoDiffieHellmanSessionCall",
697 "_cryptoDiffieHellmanSessionDestroy",
698 ] {
699 assert!(
700 crypto_group.names.iter().any(|name| name == method),
701 "missing Diffie-Hellman session bridge callback {method}"
702 );
703 }
704 }
705}