Skip to main content

secure_exec_sidecar/
wire.rs

1//! Generated Secure Exec sidecar wire protocol surface.
2//!
3//! This module is the public generated protocol entrypoint. The hand-written
4//! `protocol` module remains an internal compatibility layer while callers move
5//! to generated wire frames.
6
7use std::error::Error;
8use std::fmt;
9
10pub use crate::generated_protocol::v1::*;
11
12// The generated BARE types intentionally omit `Copy`/`Default`; restore them on the
13// crate-local generated types so the wider sidecar keeps the ergonomics it relies on
14// after the hand-written protocol types were replaced with these aliases. These live in
15// `wire` (not `protocol`) because `protocol.rs` is `#[path]`-included by integration
16// tests, where the generated types would be foreign and the impls would break the orphan rule.
17impl Copy for crate::generated_protocol::v1::GuestFilesystemOperation {}
18impl Copy for crate::generated_protocol::v1::RootFilesystemMode {}
19impl Copy for crate::generated_protocol::v1::WasmPermissionTier {}
20
21// `derive(Default)` cannot be added: these are foreign generated types, so the
22// `Default` impl must be written by hand here (orphan rule).
23#[allow(clippy::derivable_impls)]
24impl Default for crate::generated_protocol::v1::RootFilesystemEntryKind {
25    fn default() -> Self {
26        Self::File
27    }
28}
29
30impl Default for crate::generated_protocol::v1::RootFilesystemEntry {
31    fn default() -> Self {
32        Self {
33            path: String::new(),
34            kind: crate::generated_protocol::v1::RootFilesystemEntryKind::File,
35            mode: None,
36            uid: None,
37            gid: None,
38            content: None,
39            encoding: None,
40            target: None,
41            executable: false,
42        }
43    }
44}
45
46#[allow(clippy::derivable_impls)]
47impl Default for crate::generated_protocol::v1::RootFilesystemMode {
48    fn default() -> Self {
49        Self::Ephemeral
50    }
51}
52
53#[allow(clippy::derivable_impls)]
54impl Default for crate::generated_protocol::v1::RootFilesystemDescriptor {
55    fn default() -> Self {
56        Self {
57            mode: crate::generated_protocol::v1::RootFilesystemMode::default(),
58            disable_default_base_layer: false,
59            lowers: Vec::new(),
60            bootstrap_entries: Vec::new(),
61        }
62    }
63}
64
65impl crate::generated_protocol::v1::PermissionsPolicy {
66    pub fn deny_all() -> Self {
67        use crate::generated_protocol::v1::{
68            FsPermissionScope, PatternPermissionScope, PermissionMode,
69        };
70        Self {
71            fs: Some(FsPermissionScope::PermissionMode(PermissionMode::Deny)),
72            network: Some(PatternPermissionScope::PermissionMode(PermissionMode::Deny)),
73            child_process: Some(PatternPermissionScope::PermissionMode(PermissionMode::Deny)),
74            process: Some(PatternPermissionScope::PermissionMode(PermissionMode::Deny)),
75            env: Some(PatternPermissionScope::PermissionMode(PermissionMode::Deny)),
76            tool: Some(PatternPermissionScope::PermissionMode(PermissionMode::Deny)),
77        }
78    }
79
80    pub fn allow_all() -> Self {
81        use crate::generated_protocol::v1::{
82            FsPermissionScope, PatternPermissionScope, PermissionMode,
83        };
84        Self {
85            fs: Some(FsPermissionScope::PermissionMode(PermissionMode::Allow)),
86            network: Some(PatternPermissionScope::PermissionMode(
87                PermissionMode::Allow,
88            )),
89            child_process: Some(PatternPermissionScope::PermissionMode(
90                PermissionMode::Allow,
91            )),
92            process: Some(PatternPermissionScope::PermissionMode(
93                PermissionMode::Allow,
94            )),
95            env: Some(PatternPermissionScope::PermissionMode(
96                PermissionMode::Allow,
97            )),
98            tool: Some(PatternPermissionScope::PermissionMode(
99                PermissionMode::Allow,
100            )),
101        }
102    }
103}
104
105impl Default for crate::generated_protocol::v1::PermissionsPolicy {
106    fn default() -> Self {
107        Self::allow_all()
108    }
109}
110
111impl crate::generated_protocol::v1::CreateVmRequest {
112    pub fn json_config(
113        runtime: crate::generated_protocol::v1::GuestRuntimeKind,
114        config: secure_exec_vm_config::CreateVmConfig,
115    ) -> Self {
116        Self {
117            runtime,
118            config: serde_json::to_string(&config).expect("serialize create VM config"),
119        }
120    }
121
122    pub fn legacy_test_config(
123        runtime: crate::generated_protocol::v1::GuestRuntimeKind,
124        metadata: std::collections::HashMap<String, String>,
125        root_filesystem: crate::generated_protocol::v1::RootFilesystemDescriptor,
126        permissions: Option<crate::generated_protocol::v1::PermissionsPolicy>,
127    ) -> Self {
128        let metadata: std::collections::BTreeMap<_, _> = metadata.into_iter().collect();
129        let mut config = secure_exec_vm_config::CreateVmConfig {
130            cwd: metadata.get("cwd").cloned(),
131            env: legacy_env_config(&metadata),
132            root_filesystem: legacy_root_filesystem_config(root_filesystem),
133            permissions: permissions.map(legacy_permissions_config),
134            limits: legacy_limits_config(&metadata),
135            dns: legacy_dns_config(&metadata),
136            native_root: legacy_native_root_config(&metadata),
137            listen: legacy_listen_config(&metadata),
138            ..Default::default()
139        };
140        config.loopback_exempt_ports = legacy_loopback_exempt_ports(&config.env);
141        Self::json_config(runtime, config)
142    }
143}
144
145fn legacy_env_config(
146    metadata: &std::collections::BTreeMap<String, String>,
147) -> std::collections::BTreeMap<String, String> {
148    metadata
149        .iter()
150        .filter_map(|(key, value)| {
151            key.strip_prefix("env.")
152                .map(|name| (name.to_string(), value.clone()))
153        })
154        .collect()
155}
156
157fn legacy_root_filesystem_config(
158    descriptor: crate::generated_protocol::v1::RootFilesystemDescriptor,
159) -> secure_exec_vm_config::RootFilesystemConfig {
160    secure_exec_vm_config::RootFilesystemConfig {
161        mode: match descriptor.mode {
162            crate::generated_protocol::v1::RootFilesystemMode::Ephemeral => {
163                secure_exec_vm_config::RootFilesystemMode::Ephemeral
164            }
165            crate::generated_protocol::v1::RootFilesystemMode::ReadOnly => {
166                secure_exec_vm_config::RootFilesystemMode::ReadOnly
167            }
168        },
169        disable_default_base_layer: descriptor.disable_default_base_layer,
170        lowers: descriptor
171            .lowers
172            .into_iter()
173            .map(legacy_root_lower_config)
174            .collect(),
175        bootstrap_entries: descriptor
176            .bootstrap_entries
177            .into_iter()
178            .map(legacy_root_entry_config)
179            .collect(),
180    }
181}
182
183fn legacy_root_lower_config(
184    lower: crate::generated_protocol::v1::RootFilesystemLowerDescriptor,
185) -> secure_exec_vm_config::RootFilesystemLowerDescriptor {
186    match lower {
187        crate::generated_protocol::v1::RootFilesystemLowerDescriptor::SnapshotRootFilesystemLower(
188            snapshot,
189        ) => secure_exec_vm_config::RootFilesystemLowerDescriptor::Snapshot {
190            entries: snapshot
191                .entries
192                .into_iter()
193                .map(legacy_root_entry_config)
194                .collect(),
195        },
196        crate::generated_protocol::v1::RootFilesystemLowerDescriptor::BundledBaseFilesystemLower => {
197            secure_exec_vm_config::RootFilesystemLowerDescriptor::BundledBaseFilesystem
198        }
199    }
200}
201
202fn legacy_root_entry_config(
203    entry: crate::generated_protocol::v1::RootFilesystemEntry,
204) -> secure_exec_vm_config::RootFilesystemEntry {
205    secure_exec_vm_config::RootFilesystemEntry {
206        path: entry.path,
207        kind: match entry.kind {
208            crate::generated_protocol::v1::RootFilesystemEntryKind::File => {
209                secure_exec_vm_config::RootFilesystemEntryKind::File
210            }
211            crate::generated_protocol::v1::RootFilesystemEntryKind::Directory => {
212                secure_exec_vm_config::RootFilesystemEntryKind::Directory
213            }
214            crate::generated_protocol::v1::RootFilesystemEntryKind::Symlink => {
215                secure_exec_vm_config::RootFilesystemEntryKind::Symlink
216            }
217        },
218        mode: entry.mode,
219        uid: entry.uid,
220        gid: entry.gid,
221        content: entry.content,
222        encoding: entry.encoding.map(|encoding| match encoding {
223            crate::generated_protocol::v1::RootFilesystemEntryEncoding::Utf8 => {
224                secure_exec_vm_config::RootFilesystemEntryEncoding::Utf8
225            }
226            crate::generated_protocol::v1::RootFilesystemEntryEncoding::Base64 => {
227                secure_exec_vm_config::RootFilesystemEntryEncoding::Base64
228            }
229        }),
230        target: entry.target,
231        executable: entry.executable,
232    }
233}
234
235fn legacy_permissions_config(
236    permissions: crate::generated_protocol::v1::PermissionsPolicy,
237) -> secure_exec_vm_config::PermissionsPolicy {
238    secure_exec_vm_config::PermissionsPolicy {
239        fs: permissions.fs.map(legacy_fs_permission_scope_config),
240        network: permissions
241            .network
242            .map(legacy_pattern_permission_scope_config),
243        child_process: permissions
244            .child_process
245            .map(legacy_pattern_permission_scope_config),
246        process: permissions
247            .process
248            .map(legacy_pattern_permission_scope_config),
249        env: permissions.env.map(legacy_pattern_permission_scope_config),
250        tool: permissions.tool.map(legacy_pattern_permission_scope_config),
251    }
252}
253
254fn legacy_permission_mode_config(
255    mode: crate::generated_protocol::v1::PermissionMode,
256) -> secure_exec_vm_config::PermissionMode {
257    match mode {
258        crate::generated_protocol::v1::PermissionMode::Allow => {
259            secure_exec_vm_config::PermissionMode::Allow
260        }
261        crate::generated_protocol::v1::PermissionMode::Ask => {
262            secure_exec_vm_config::PermissionMode::Ask
263        }
264        crate::generated_protocol::v1::PermissionMode::Deny => {
265            secure_exec_vm_config::PermissionMode::Deny
266        }
267    }
268}
269
270fn legacy_fs_permission_scope_config(
271    scope: crate::generated_protocol::v1::FsPermissionScope,
272) -> secure_exec_vm_config::FsPermissionScope {
273    match scope {
274        crate::generated_protocol::v1::FsPermissionScope::PermissionMode(mode) => {
275            secure_exec_vm_config::FsPermissionScope::Mode(legacy_permission_mode_config(mode))
276        }
277        crate::generated_protocol::v1::FsPermissionScope::FsPermissionRuleSet(rules) => {
278            secure_exec_vm_config::FsPermissionScope::Rules(
279                secure_exec_vm_config::FsPermissionRuleSet {
280                    default: rules.default.map(legacy_permission_mode_config),
281                    rules: rules
282                        .rules
283                        .into_iter()
284                        .map(|rule| secure_exec_vm_config::FsPermissionRule {
285                            mode: legacy_permission_mode_config(rule.mode),
286                            operations: rule.operations,
287                            paths: rule.paths,
288                        })
289                        .collect(),
290                },
291            )
292        }
293    }
294}
295
296fn legacy_pattern_permission_scope_config(
297    scope: crate::generated_protocol::v1::PatternPermissionScope,
298) -> secure_exec_vm_config::PatternPermissionScope {
299    match scope {
300        crate::generated_protocol::v1::PatternPermissionScope::PermissionMode(mode) => {
301            secure_exec_vm_config::PatternPermissionScope::Mode(legacy_permission_mode_config(mode))
302        }
303        crate::generated_protocol::v1::PatternPermissionScope::PatternPermissionRuleSet(rules) => {
304            secure_exec_vm_config::PatternPermissionScope::Rules(
305                secure_exec_vm_config::PatternPermissionRuleSet {
306                    default: rules.default.map(legacy_permission_mode_config),
307                    rules: rules
308                        .rules
309                        .into_iter()
310                        .map(|rule| secure_exec_vm_config::PatternPermissionRule {
311                            mode: legacy_permission_mode_config(rule.mode),
312                            operations: rule.operations,
313                            patterns: rule.patterns,
314                        })
315                        .collect(),
316                },
317            )
318        }
319    }
320}
321
322fn legacy_dns_config(
323    metadata: &std::collections::BTreeMap<String, String>,
324) -> Option<secure_exec_vm_config::VmDnsConfig> {
325    let mut dns = secure_exec_vm_config::VmDnsConfig::default();
326    if let Some(value) = metadata.get("network.dns.servers") {
327        dns.name_servers = value
328            .split(',')
329            .map(str::trim)
330            .filter(|entry| !entry.is_empty())
331            .map(str::to_string)
332            .collect();
333    }
334    for (key, value) in metadata {
335        let Some(hostname) = key.strip_prefix("network.dns.override.") else {
336            continue;
337        };
338        dns.overrides.insert(
339            hostname.to_string(),
340            value
341                .split(',')
342                .map(str::trim)
343                .filter(|entry| !entry.is_empty())
344                .map(str::to_string)
345                .collect(),
346        );
347    }
348    if dns.name_servers.is_empty() && dns.overrides.is_empty() {
349        None
350    } else {
351        Some(dns)
352    }
353}
354
355fn legacy_native_root_config(
356    metadata: &std::collections::BTreeMap<String, String>,
357) -> Option<secure_exec_vm_config::NativeRootFilesystemConfig> {
358    let id = metadata.get("rootFilesystem.nativePlugin.id")?;
359    let config = metadata
360        .get("rootFilesystem.nativePlugin.config")
361        .map(|value| serde_json::from_str(value).expect("parse native root plugin config"))
362        .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
363    let read_only = metadata
364        .get("rootFilesystem.nativePlugin.readOnly")
365        .map(|value| value.parse::<bool>().expect("parse native root readOnly"))
366        .unwrap_or(false);
367    Some(secure_exec_vm_config::NativeRootFilesystemConfig {
368        plugin: secure_exec_vm_config::MountPluginDescriptor {
369            id: id.clone(),
370            config,
371        },
372        read_only,
373    })
374}
375
376fn legacy_listen_config(
377    metadata: &std::collections::BTreeMap<String, String>,
378) -> Option<secure_exec_vm_config::VmListenPolicyConfig> {
379    let listen = secure_exec_vm_config::VmListenPolicyConfig {
380        port_min: metadata
381            .get("network.listen.port_min")
382            .map(|value| value.parse::<u16>().expect("parse network.listen.port_min")),
383        port_max: metadata
384            .get("network.listen.port_max")
385            .map(|value| value.parse::<u16>().expect("parse network.listen.port_max")),
386        allow_privileged: metadata
387            .get("network.listen.allow_privileged")
388            .map(|value| {
389                value
390                    .parse::<bool>()
391                    .expect("parse network.listen.allow_privileged")
392            }),
393    };
394    if listen.port_min.is_none() && listen.port_max.is_none() && listen.allow_privileged.is_none() {
395        None
396    } else {
397        Some(listen)
398    }
399}
400
401fn legacy_loopback_exempt_ports(env: &std::collections::BTreeMap<String, String>) -> Vec<u16> {
402    let Some(value) = env.get("AGENT_OS_LOOPBACK_EXEMPT_PORTS") else {
403        return Vec::new();
404    };
405    serde_json::from_str::<Vec<u16>>(value).unwrap_or_default()
406}
407
408fn legacy_limits_config(
409    metadata: &std::collections::BTreeMap<String, String>,
410) -> Option<secure_exec_vm_config::VmLimitsConfig> {
411    let resources = secure_exec_vm_config::ResourceLimitsConfig {
412        cpu_count: legacy_u64(metadata, "resource.cpu_count"),
413        max_processes: legacy_u64(metadata, "resource.max_processes"),
414        max_open_fds: legacy_u64(metadata, "resource.max_open_fds"),
415        max_pipes: legacy_u64(metadata, "resource.max_pipes"),
416        max_ptys: legacy_u64(metadata, "resource.max_ptys"),
417        max_sockets: legacy_u64(metadata, "resource.max_sockets"),
418        max_connections: legacy_u64(metadata, "resource.max_connections"),
419        max_socket_buffered_bytes: legacy_u64(metadata, "resource.max_socket_buffered_bytes"),
420        max_socket_datagram_queue_len: legacy_u64(
421            metadata,
422            "resource.max_socket_datagram_queue_len",
423        ),
424        max_filesystem_bytes: legacy_u64(metadata, "resource.max_filesystem_bytes"),
425        max_inode_count: legacy_u64(metadata, "resource.max_inode_count"),
426        max_blocking_read_ms: legacy_u64(metadata, "resource.max_blocking_read_ms"),
427        max_pread_bytes: legacy_u64(metadata, "resource.max_pread_bytes"),
428        max_fd_write_bytes: legacy_u64(metadata, "resource.max_fd_write_bytes"),
429        max_process_argv_bytes: legacy_u64(metadata, "resource.max_process_argv_bytes"),
430        max_process_env_bytes: legacy_u64(metadata, "resource.max_process_env_bytes"),
431        max_readdir_entries: legacy_u64(metadata, "resource.max_readdir_entries"),
432        max_wasm_fuel: legacy_u64(metadata, "resource.max_wasm_fuel"),
433        max_wasm_memory_bytes: legacy_u64(metadata, "resource.max_wasm_memory_bytes"),
434        max_wasm_stack_bytes: legacy_u64(metadata, "resource.max_wasm_stack_bytes"),
435    };
436    let http = secure_exec_vm_config::HttpLimitsConfig {
437        max_fetch_response_bytes: legacy_u64(metadata, "limits.http.max_fetch_response_bytes"),
438    };
439    let tools = secure_exec_vm_config::ToolLimitsConfig {
440        default_tool_timeout_ms: legacy_u64(metadata, "limits.tools.default_tool_timeout_ms"),
441        max_tool_timeout_ms: legacy_u64(metadata, "limits.tools.max_tool_timeout_ms"),
442        max_registered_toolkits: legacy_u64(metadata, "limits.tools.max_registered_toolkits"),
443        max_registered_tools_per_vm: legacy_u64(
444            metadata,
445            "limits.tools.max_registered_tools_per_vm",
446        ),
447        max_tools_per_toolkit: legacy_u64(metadata, "limits.tools.max_tools_per_toolkit"),
448        max_tool_schema_bytes: legacy_u64(metadata, "limits.tools.max_tool_schema_bytes"),
449        max_tool_examples_per_tool: legacy_u64(metadata, "limits.tools.max_tool_examples_per_tool"),
450        max_tool_example_input_bytes: legacy_u64(
451            metadata,
452            "limits.tools.max_tool_example_input_bytes",
453        ),
454    };
455    let plugins = secure_exec_vm_config::PluginLimitsConfig {
456        max_persisted_manifest_bytes: legacy_u64(
457            metadata,
458            "limits.plugins.max_persisted_manifest_bytes",
459        ),
460        max_persisted_manifest_file_bytes: legacy_u64(
461            metadata,
462            "limits.plugins.max_persisted_manifest_file_bytes",
463        ),
464    };
465    let acp = secure_exec_vm_config::AcpLimitsConfig {
466        max_read_line_bytes: legacy_u64(metadata, "limits.acp.max_read_line_bytes"),
467        stdout_buffer_byte_limit: legacy_u64(metadata, "limits.acp.stdout_buffer_byte_limit"),
468    };
469    let js_runtime = secure_exec_vm_config::JsRuntimeLimitsConfig {
470        v8_heap_limit_mb: legacy_u64(metadata, "limits.js_runtime.v8_heap_limit_mb"),
471        captured_output_limit_bytes: legacy_u64(
472            metadata,
473            "limits.js_runtime.captured_output_limit_bytes",
474        ),
475        stdin_buffer_limit_bytes: legacy_u64(
476            metadata,
477            "limits.js_runtime.stdin_buffer_limit_bytes",
478        ),
479        event_payload_limit_bytes: legacy_u64(
480            metadata,
481            "limits.js_runtime.event_payload_limit_bytes",
482        ),
483        v8_ipc_max_frame_bytes: legacy_u64(metadata, "limits.js_runtime.v8_ipc_max_frame_bytes"),
484    };
485    let python = secure_exec_vm_config::PythonLimitsConfig {
486        output_buffer_max_bytes: legacy_u64(metadata, "limits.python.output_buffer_max_bytes"),
487        execution_timeout_ms: legacy_u64(metadata, "limits.python.execution_timeout_ms"),
488        vfs_rpc_timeout_ms: legacy_u64(metadata, "limits.python.vfs_rpc_timeout_ms"),
489    };
490    let wasm = secure_exec_vm_config::WasmLimitsConfig {
491        max_module_file_bytes: legacy_u64(metadata, "limits.wasm.max_module_file_bytes"),
492        captured_output_limit_bytes: legacy_u64(
493            metadata,
494            "limits.wasm.captured_output_limit_bytes",
495        ),
496        sync_read_limit_bytes: legacy_u64(metadata, "limits.wasm.sync_read_limit_bytes"),
497    };
498
499    let config = secure_exec_vm_config::VmLimitsConfig {
500        resources: legacy_has_resource_limits(&resources).then_some(resources),
501        http: http.max_fetch_response_bytes.is_some().then_some(http),
502        tools: legacy_has_tool_limits(&tools).then_some(tools),
503        plugins: legacy_has_plugin_limits(&plugins).then_some(plugins),
504        acp: legacy_has_acp_limits(&acp).then_some(acp),
505        js_runtime: legacy_has_js_runtime_limits(&js_runtime).then_some(js_runtime),
506        python: legacy_has_python_limits(&python).then_some(python),
507        wasm: legacy_has_wasm_limits(&wasm).then_some(wasm),
508    };
509
510    if config.resources.is_none()
511        && config.http.is_none()
512        && config.tools.is_none()
513        && config.plugins.is_none()
514        && config.acp.is_none()
515        && config.js_runtime.is_none()
516        && config.python.is_none()
517        && config.wasm.is_none()
518    {
519        None
520    } else {
521        Some(config)
522    }
523}
524
525fn legacy_u64(metadata: &std::collections::BTreeMap<String, String>, key: &str) -> Option<u64> {
526    metadata.get(key).map(|value| {
527        value
528            .parse::<u64>()
529            .unwrap_or_else(|error| panic!("parse {key}: {error}"))
530    })
531}
532
533fn legacy_has_resource_limits(config: &secure_exec_vm_config::ResourceLimitsConfig) -> bool {
534    config.cpu_count.is_some()
535        || config.max_processes.is_some()
536        || config.max_open_fds.is_some()
537        || config.max_pipes.is_some()
538        || config.max_ptys.is_some()
539        || config.max_sockets.is_some()
540        || config.max_connections.is_some()
541        || config.max_socket_buffered_bytes.is_some()
542        || config.max_socket_datagram_queue_len.is_some()
543        || config.max_filesystem_bytes.is_some()
544        || config.max_inode_count.is_some()
545        || config.max_blocking_read_ms.is_some()
546        || config.max_pread_bytes.is_some()
547        || config.max_fd_write_bytes.is_some()
548        || config.max_process_argv_bytes.is_some()
549        || config.max_process_env_bytes.is_some()
550        || config.max_readdir_entries.is_some()
551        || config.max_wasm_fuel.is_some()
552        || config.max_wasm_memory_bytes.is_some()
553        || config.max_wasm_stack_bytes.is_some()
554}
555
556fn legacy_has_tool_limits(config: &secure_exec_vm_config::ToolLimitsConfig) -> bool {
557    config.default_tool_timeout_ms.is_some()
558        || config.max_tool_timeout_ms.is_some()
559        || config.max_registered_toolkits.is_some()
560        || config.max_registered_tools_per_vm.is_some()
561        || config.max_tools_per_toolkit.is_some()
562        || config.max_tool_schema_bytes.is_some()
563        || config.max_tool_examples_per_tool.is_some()
564        || config.max_tool_example_input_bytes.is_some()
565}
566
567fn legacy_has_plugin_limits(config: &secure_exec_vm_config::PluginLimitsConfig) -> bool {
568    config.max_persisted_manifest_bytes.is_some()
569        || config.max_persisted_manifest_file_bytes.is_some()
570}
571
572fn legacy_has_acp_limits(config: &secure_exec_vm_config::AcpLimitsConfig) -> bool {
573    config.max_read_line_bytes.is_some() || config.stdout_buffer_byte_limit.is_some()
574}
575
576fn legacy_has_js_runtime_limits(config: &secure_exec_vm_config::JsRuntimeLimitsConfig) -> bool {
577    config.v8_heap_limit_mb.is_some()
578        || config.captured_output_limit_bytes.is_some()
579        || config.stdin_buffer_limit_bytes.is_some()
580        || config.event_payload_limit_bytes.is_some()
581        || config.v8_ipc_max_frame_bytes.is_some()
582}
583
584fn legacy_has_python_limits(config: &secure_exec_vm_config::PythonLimitsConfig) -> bool {
585    config.output_buffer_max_bytes.is_some()
586        || config.execution_timeout_ms.is_some()
587        || config.vfs_rpc_timeout_ms.is_some()
588}
589
590fn legacy_has_wasm_limits(config: &secure_exec_vm_config::WasmLimitsConfig) -> bool {
591    config.max_module_file_bytes.is_some()
592        || config.captured_output_limit_bytes.is_some()
593        || config.sync_read_limit_bytes.is_some()
594}
595
596// Ownership-scope constructor ergonomics. The generated BARE union exposes only the
597// tuple-wrapped variants (`ConnectionOwnership`/`SessionOwnership`/`VmOwnership`); restore
598// the hand-written `connection`/`session`/`vm` helpers the sidecar relies on. These live in
599// `wire` (not `protocol`) for the same orphan-rule reason as the impls above: `protocol.rs`
600// is `#[path]`-included by integration tests where the generated type is foreign.
601impl crate::generated_protocol::v1::OwnershipScope {
602    pub fn connection(connection_id: impl Into<String>) -> Self {
603        Self::ConnectionOwnership(crate::generated_protocol::v1::ConnectionOwnership {
604            connection_id: connection_id.into(),
605        })
606    }
607
608    pub fn session(connection_id: impl Into<String>, session_id: impl Into<String>) -> Self {
609        Self::SessionOwnership(crate::generated_protocol::v1::SessionOwnership {
610            connection_id: connection_id.into(),
611            session_id: session_id.into(),
612        })
613    }
614
615    pub fn vm(
616        connection_id: impl Into<String>,
617        session_id: impl Into<String>,
618        vm_id: impl Into<String>,
619    ) -> Self {
620        Self::VmOwnership(crate::generated_protocol::v1::VmOwnership {
621            connection_id: connection_id.into(),
622            session_id: session_id.into(),
623            vm_id: vm_id.into(),
624        })
625    }
626}
627
628pub const PROTOCOL_NAME: &str = "secure-exec-sidecar";
629pub const PROTOCOL_VERSION: u16 = 7;
630pub const DEFAULT_MAX_FRAME_BYTES: usize = 1024 * 1024;
631
632#[derive(Debug, Clone, PartialEq, Eq)]
633pub enum ProtocolCodecError {
634    TruncatedFrame {
635        actual: usize,
636    },
637    LengthPrefixMismatch {
638        declared: usize,
639        actual: usize,
640    },
641    FrameTooLarge {
642        size: usize,
643        max: usize,
644    },
645    UnsupportedSchema {
646        name: String,
647        version: u16,
648    },
649    InvalidRequestId,
650    InvalidRequestDirection {
651        request_id: RequestId,
652        expected: RequestDirection,
653    },
654    EmptyOwnershipField {
655        field: &'static str,
656    },
657    EmptyAuthToken,
658    InvalidOwnershipScope {
659        required: OwnershipRequirement,
660        actual: OwnershipRequirement,
661    },
662    SerializeFailure(String),
663    DeserializeFailure(String),
664}
665
666impl fmt::Display for ProtocolCodecError {
667    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
668        match self {
669            Self::TruncatedFrame { actual } => {
670                write!(
671                    f,
672                    "protocol frame is truncated: only {actual} bytes provided"
673                )
674            }
675            Self::LengthPrefixMismatch { declared, actual } => write!(
676                f,
677                "protocol frame length prefix mismatch: declared {declared} bytes, got {actual}",
678            ),
679            Self::FrameTooLarge { size, max } => {
680                write!(f, "protocol frame is {size} bytes, limit is {max}")
681            }
682            Self::UnsupportedSchema { name, version } => write!(
683                f,
684                "unsupported protocol schema {name}@{version}; expected {PROTOCOL_NAME}@{PROTOCOL_VERSION}",
685            ),
686            Self::InvalidRequestId => write!(f, "protocol request identifiers must be non-zero"),
687            Self::InvalidRequestDirection {
688                request_id,
689                expected,
690            } => write!(f, "protocol request id {request_id} must be {expected}",),
691            Self::EmptyOwnershipField { field } => {
692                write!(f, "protocol ownership field `{field}` cannot be empty")
693            }
694            Self::EmptyAuthToken => {
695                write!(f, "authenticate requests require a non-empty auth token")
696            }
697            Self::InvalidOwnershipScope { required, actual } => write!(
698                f,
699                "protocol frame requires {required} ownership but carried {actual}",
700            ),
701            Self::SerializeFailure(message) => {
702                write!(f, "protocol frame serialization failed: {message}")
703            }
704            Self::DeserializeFailure(message) => {
705                write!(f, "protocol frame deserialization failed: {message}")
706            }
707        }
708    }
709}
710
711impl Error for ProtocolCodecError {}
712
713#[derive(Debug, Clone, Copy, PartialEq, Eq)]
714pub enum OwnershipRequirement {
715    Any,
716    Connection,
717    Session,
718    Vm,
719    SessionOrVm,
720}
721
722impl fmt::Display for OwnershipRequirement {
723    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
724        match self {
725            Self::Any => write!(f, "any"),
726            Self::Connection => write!(f, "connection"),
727            Self::Session => write!(f, "session"),
728            Self::Vm => write!(f, "vm"),
729            Self::SessionOrVm => write!(f, "session-or-vm"),
730        }
731    }
732}
733
734#[derive(Debug, Clone, Copy, PartialEq, Eq)]
735pub enum RequestDirection {
736    Host,
737    Sidecar,
738}
739
740impl fmt::Display for RequestDirection {
741    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
742        match self {
743            Self::Host => write!(f, "positive"),
744            Self::Sidecar => write!(f, "negative"),
745        }
746    }
747}
748
749#[derive(Debug, Clone, PartialEq, Eq)]
750pub struct WireDispatchResult {
751    pub response: ResponseFrame,
752    pub events: Vec<EventFrame>,
753}
754
755#[derive(Debug, Clone)]
756pub struct WireFrameCodec {
757    max_frame_bytes: usize,
758}
759
760impl WireFrameCodec {
761    pub fn new(max_frame_bytes: usize) -> Self {
762        Self { max_frame_bytes }
763    }
764
765    pub fn max_frame_bytes(&self) -> usize {
766        self.max_frame_bytes
767    }
768
769    pub fn encode(&self, frame: &ProtocolFrame) -> Result<Vec<u8>, ProtocolCodecError> {
770        validate_frame(frame)?;
771
772        let payload = serde_bare::to_vec(frame)
773            .map_err(|error| ProtocolCodecError::SerializeFailure(error.to_string()))?;
774        if payload.len() > self.max_frame_bytes {
775            return Err(ProtocolCodecError::FrameTooLarge {
776                size: payload.len(),
777                max: self.max_frame_bytes,
778            });
779        }
780
781        let length =
782            u32::try_from(payload.len()).map_err(|_| ProtocolCodecError::FrameTooLarge {
783                size: payload.len(),
784                max: u32::MAX as usize,
785            })?;
786
787        let mut encoded = Vec::with_capacity(4 + payload.len());
788        encoded.extend_from_slice(&length.to_be_bytes());
789        encoded.extend_from_slice(&payload);
790        Ok(encoded)
791    }
792
793    pub fn decode(&self, bytes: &[u8]) -> Result<ProtocolFrame, ProtocolCodecError> {
794        let payload = self.checked_payload(bytes)?;
795        let frame = serde_bare::from_slice(payload)
796            .map_err(|error| ProtocolCodecError::DeserializeFailure(error.to_string()))?;
797        validate_frame(&frame)?;
798        Ok(frame)
799    }
800
801    fn checked_payload<'a>(&self, bytes: &'a [u8]) -> Result<&'a [u8], ProtocolCodecError> {
802        if bytes.len() < 4 {
803            return Err(ProtocolCodecError::TruncatedFrame {
804                actual: bytes.len(),
805            });
806        }
807
808        let declared =
809            u32::from_be_bytes(bytes[..4].try_into().expect("length prefix is four bytes"))
810                as usize;
811        if declared > self.max_frame_bytes {
812            return Err(ProtocolCodecError::FrameTooLarge {
813                size: declared,
814                max: self.max_frame_bytes,
815            });
816        }
817
818        let actual = bytes.len() - 4;
819        if declared != actual {
820            return Err(ProtocolCodecError::LengthPrefixMismatch { declared, actual });
821        }
822
823        Ok(&bytes[4..])
824    }
825}
826
827impl Default for WireFrameCodec {
828    fn default() -> Self {
829        Self::new(DEFAULT_MAX_FRAME_BYTES)
830    }
831}
832
833pub fn protocol_schema() -> ProtocolSchema {
834    ProtocolSchema::current()
835}
836
837impl ProtocolSchema {
838    pub fn current() -> Self {
839        Self {
840            name: PROTOCOL_NAME.to_string(),
841            version: PROTOCOL_VERSION,
842        }
843    }
844}
845
846impl Default for ProtocolSchema {
847    fn default() -> Self {
848        Self::current()
849    }
850}
851
852pub(crate) fn request_frame_to_compat(
853    request: RequestFrame,
854) -> Result<crate::protocol::RequestFrame, ProtocolCodecError> {
855    match crate::protocol::from_generated_protocol_frame(ProtocolFrame::RequestFrame(request))? {
856        crate::protocol::ProtocolFrame::Request(request) => Ok(request),
857        crate::protocol::ProtocolFrame::Response(_)
858        | crate::protocol::ProtocolFrame::Event(_)
859        | crate::protocol::ProtocolFrame::SidecarRequest(_)
860        | crate::protocol::ProtocolFrame::SidecarResponse(_) => {
861            Err(ProtocolCodecError::DeserializeFailure(String::from(
862                "wire request frame converted to non-request compatibility frame",
863            )))
864        }
865    }
866}
867
868pub(crate) fn ownership_scope_to_compat(
869    ownership: OwnershipScope,
870) -> crate::protocol::OwnershipScope {
871    crate::protocol::from_generated_ownership_scope(ownership)
872}
873
874pub(crate) fn request_payload_to_compat(
875    ownership: &crate::protocol::OwnershipScope,
876    payload: RequestPayload,
877) -> Result<crate::protocol::RequestPayload, ProtocolCodecError> {
878    match crate::protocol::from_generated_protocol_frame(ProtocolFrame::RequestFrame(
879        RequestFrame {
880            schema: protocol_schema(),
881            request_id: 1,
882            ownership: crate::protocol::to_generated_ownership_scope(ownership),
883            payload,
884        },
885    ))? {
886        crate::protocol::ProtocolFrame::Request(request) => Ok(request.payload),
887        crate::protocol::ProtocolFrame::Response(_)
888        | crate::protocol::ProtocolFrame::Event(_)
889        | crate::protocol::ProtocolFrame::SidecarRequest(_)
890        | crate::protocol::ProtocolFrame::SidecarResponse(_) => {
891            Err(ProtocolCodecError::DeserializeFailure(String::from(
892                "wire request payload converted to non-request compatibility frame",
893            )))
894        }
895    }
896}
897
898pub(crate) fn response_payload_from_compat(
899    ownership: &crate::protocol::OwnershipScope,
900    payload: crate::protocol::ResponsePayload,
901) -> Result<ResponsePayload, ProtocolCodecError> {
902    match crate::protocol::to_generated_protocol_frame(&crate::protocol::ProtocolFrame::Response(
903        crate::protocol::ResponseFrame::new(1, ownership.clone(), payload),
904    ))? {
905        ProtocolFrame::ResponseFrame(response) => Ok(response.payload),
906        ProtocolFrame::RequestFrame(_)
907        | ProtocolFrame::EventFrame(_)
908        | ProtocolFrame::SidecarRequestFrame(_)
909        | ProtocolFrame::SidecarResponseFrame(_) => Err(ProtocolCodecError::SerializeFailure(
910            String::from("compatibility response payload converted to non-response wire frame"),
911        )),
912    }
913}
914
915pub(crate) fn event_frame_from_compat(
916    event: crate::protocol::EventFrame,
917) -> Result<EventFrame, ProtocolCodecError> {
918    match crate::protocol::to_generated_protocol_frame(&crate::protocol::ProtocolFrame::Event(
919        event,
920    ))? {
921        ProtocolFrame::EventFrame(event) => Ok(event),
922        ProtocolFrame::RequestFrame(_)
923        | ProtocolFrame::ResponseFrame(_)
924        | ProtocolFrame::SidecarRequestFrame(_)
925        | ProtocolFrame::SidecarResponseFrame(_) => Err(ProtocolCodecError::SerializeFailure(
926            String::from("compatibility event converted to non-event wire frame"),
927        )),
928    }
929}
930
931pub(crate) fn event_frame_to_compat(
932    event: EventFrame,
933) -> Result<crate::protocol::EventFrame, ProtocolCodecError> {
934    match crate::protocol::from_generated_protocol_frame(ProtocolFrame::EventFrame(event))? {
935        crate::protocol::ProtocolFrame::Event(event) => Ok(event),
936        crate::protocol::ProtocolFrame::Request(_)
937        | crate::protocol::ProtocolFrame::Response(_)
938        | crate::protocol::ProtocolFrame::SidecarRequest(_)
939        | crate::protocol::ProtocolFrame::SidecarResponse(_) => {
940            Err(ProtocolCodecError::DeserializeFailure(String::from(
941                "wire event converted to non-event compatibility frame",
942            )))
943        }
944    }
945}
946
947pub(crate) fn sidecar_request_frame_from_compat(
948    request: crate::protocol::SidecarRequestFrame,
949) -> Result<SidecarRequestFrame, ProtocolCodecError> {
950    match crate::protocol::to_generated_protocol_frame(
951        &crate::protocol::ProtocolFrame::SidecarRequest(request),
952    )? {
953        ProtocolFrame::SidecarRequestFrame(request) => Ok(request),
954        ProtocolFrame::RequestFrame(_)
955        | ProtocolFrame::ResponseFrame(_)
956        | ProtocolFrame::EventFrame(_)
957        | ProtocolFrame::SidecarResponseFrame(_) => {
958            Err(ProtocolCodecError::SerializeFailure(String::from(
959                "compatibility sidecar request converted to non-sidecar-request wire frame",
960            )))
961        }
962    }
963}
964
965pub(crate) fn sidecar_request_payload_to_compat(
966    ownership: &crate::protocol::OwnershipScope,
967    payload: SidecarRequestPayload,
968) -> Result<crate::protocol::SidecarRequestPayload, ProtocolCodecError> {
969    match crate::protocol::from_generated_protocol_frame(ProtocolFrame::SidecarRequestFrame(
970        SidecarRequestFrame {
971            schema: protocol_schema(),
972            request_id: -1,
973            ownership: crate::protocol::to_generated_ownership_scope(ownership),
974            payload,
975        },
976    ))? {
977        crate::protocol::ProtocolFrame::SidecarRequest(request) => Ok(request.payload),
978        crate::protocol::ProtocolFrame::Request(_)
979        | crate::protocol::ProtocolFrame::Response(_)
980        | crate::protocol::ProtocolFrame::Event(_)
981        | crate::protocol::ProtocolFrame::SidecarResponse(_) => {
982            Err(ProtocolCodecError::DeserializeFailure(String::from(
983                "wire sidecar request payload converted to non-sidecar-request compatibility frame",
984            )))
985        }
986    }
987}
988
989pub(crate) fn sidecar_response_frame_to_compat(
990    response: SidecarResponseFrame,
991) -> Result<crate::protocol::SidecarResponseFrame, ProtocolCodecError> {
992    match crate::protocol::from_generated_protocol_frame(ProtocolFrame::SidecarResponseFrame(
993        response,
994    ))? {
995        crate::protocol::ProtocolFrame::SidecarResponse(response) => Ok(response),
996        crate::protocol::ProtocolFrame::Request(_)
997        | crate::protocol::ProtocolFrame::Response(_)
998        | crate::protocol::ProtocolFrame::Event(_)
999        | crate::protocol::ProtocolFrame::SidecarRequest(_) => {
1000            Err(ProtocolCodecError::DeserializeFailure(String::from(
1001                "wire sidecar response converted to non-sidecar-response compatibility frame",
1002            )))
1003        }
1004    }
1005}
1006
1007pub(crate) fn sidecar_response_frame_from_compat(
1008    response: crate::protocol::SidecarResponseFrame,
1009) -> Result<SidecarResponseFrame, ProtocolCodecError> {
1010    match crate::protocol::to_generated_protocol_frame(
1011        &crate::protocol::ProtocolFrame::SidecarResponse(response),
1012    )? {
1013        ProtocolFrame::SidecarResponseFrame(response) => Ok(response),
1014        ProtocolFrame::RequestFrame(_)
1015        | ProtocolFrame::ResponseFrame(_)
1016        | ProtocolFrame::EventFrame(_)
1017        | ProtocolFrame::SidecarRequestFrame(_) => {
1018            Err(ProtocolCodecError::SerializeFailure(String::from(
1019                "compatibility sidecar response converted to non-sidecar-response wire frame",
1020            )))
1021        }
1022    }
1023}
1024
1025pub(crate) fn dispatch_result_from_compat(
1026    result: crate::state::DispatchResult,
1027) -> Result<WireDispatchResult, ProtocolCodecError> {
1028    let response = match crate::protocol::to_generated_protocol_frame(
1029        &crate::protocol::ProtocolFrame::Response(result.response),
1030    )? {
1031        ProtocolFrame::ResponseFrame(response) => response,
1032        ProtocolFrame::RequestFrame(_)
1033        | ProtocolFrame::EventFrame(_)
1034        | ProtocolFrame::SidecarRequestFrame(_)
1035        | ProtocolFrame::SidecarResponseFrame(_) => {
1036            return Err(ProtocolCodecError::SerializeFailure(String::from(
1037                "compatibility dispatch response converted to non-response wire frame",
1038            )));
1039        }
1040    };
1041
1042    let events = result
1043        .events
1044        .into_iter()
1045        .map(|event| {
1046            match crate::protocol::to_generated_protocol_frame(
1047                &crate::protocol::ProtocolFrame::Event(event),
1048            )? {
1049                ProtocolFrame::EventFrame(event) => Ok(event),
1050                ProtocolFrame::RequestFrame(_)
1051                | ProtocolFrame::ResponseFrame(_)
1052                | ProtocolFrame::SidecarRequestFrame(_)
1053                | ProtocolFrame::SidecarResponseFrame(_) => {
1054                    Err(ProtocolCodecError::SerializeFailure(String::from(
1055                        "compatibility dispatch event converted to non-event wire frame",
1056                    )))
1057                }
1058            }
1059        })
1060        .collect::<Result<Vec<_>, _>>()?;
1061
1062    Ok(WireDispatchResult { response, events })
1063}
1064
1065fn validate_frame(frame: &ProtocolFrame) -> Result<(), ProtocolCodecError> {
1066    match frame {
1067        ProtocolFrame::RequestFrame(frame) => {
1068            validate_schema(&frame.schema)?;
1069            validate_request_id(frame.request_id)
1070        }
1071        ProtocolFrame::ResponseFrame(frame) => {
1072            validate_schema(&frame.schema)?;
1073            validate_request_id(frame.request_id)
1074        }
1075        ProtocolFrame::EventFrame(frame) => validate_schema(&frame.schema),
1076        ProtocolFrame::SidecarRequestFrame(frame) => {
1077            validate_schema(&frame.schema)?;
1078            validate_request_id(frame.request_id)
1079        }
1080        ProtocolFrame::SidecarResponseFrame(frame) => {
1081            validate_schema(&frame.schema)?;
1082            validate_request_id(frame.request_id)
1083        }
1084    }
1085}
1086
1087fn validate_schema(schema: &ProtocolSchema) -> Result<(), ProtocolCodecError> {
1088    if schema.name != PROTOCOL_NAME || schema.version != PROTOCOL_VERSION {
1089        return Err(ProtocolCodecError::UnsupportedSchema {
1090            name: schema.name.clone(),
1091            version: schema.version,
1092        });
1093    }
1094    Ok(())
1095}
1096
1097fn validate_request_id(request_id: RequestId) -> Result<(), ProtocolCodecError> {
1098    if request_id == 0 {
1099        return Err(ProtocolCodecError::InvalidRequestId);
1100    }
1101    Ok(())
1102}