Skip to main content

agentos_bridge/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Shared bridge contracts between the secure-exec kernel and execution planes.
4
5pub mod queue_tracker;
6
7use std::collections::BTreeMap;
8use std::sync::OnceLock;
9use std::time::{Duration, SystemTime};
10
11use serde::Deserialize;
12
13/// Shared associated types for bridge implementations.
14pub 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}