Skip to main content

agentos_sidecar_protocol/
wire.rs

1//! Generated AgentOS language execution 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            binding: 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            binding: 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::deny_all()
108    }
109}
110
111impl crate::generated_protocol::v1::CreateVmRequest {
112    pub fn json_config(
113        runtime: crate::generated_protocol::v1::GuestRuntimeKind,
114        config: agentos_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 = agentos_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(permissions_policy_config_from_wire),
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) -> agentos_vm_config::RootFilesystemConfig {
160    agentos_vm_config::RootFilesystemConfig {
161        mode: match descriptor.mode {
162            crate::generated_protocol::v1::RootFilesystemMode::Ephemeral => {
163                agentos_vm_config::RootFilesystemMode::Ephemeral
164            }
165            crate::generated_protocol::v1::RootFilesystemMode::ReadOnly => {
166                agentos_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) -> agentos_vm_config::RootFilesystemLowerDescriptor {
186    match lower {
187        crate::generated_protocol::v1::RootFilesystemLowerDescriptor::SnapshotRootFilesystemLower(
188            snapshot,
189        ) => agentos_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            agentos_vm_config::RootFilesystemLowerDescriptor::BundledBaseFilesystem
198        }
199    }
200}
201
202fn legacy_root_entry_config(
203    entry: crate::generated_protocol::v1::RootFilesystemEntry,
204) -> agentos_vm_config::RootFilesystemEntry {
205    agentos_vm_config::RootFilesystemEntry {
206        path: entry.path,
207        kind: match entry.kind {
208            crate::generated_protocol::v1::RootFilesystemEntryKind::File => {
209                agentos_vm_config::RootFilesystemEntryKind::File
210            }
211            crate::generated_protocol::v1::RootFilesystemEntryKind::Directory => {
212                agentos_vm_config::RootFilesystemEntryKind::Directory
213            }
214            crate::generated_protocol::v1::RootFilesystemEntryKind::Symlink => {
215                agentos_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                agentos_vm_config::RootFilesystemEntryEncoding::Utf8
225            }
226            crate::generated_protocol::v1::RootFilesystemEntryEncoding::Base64 => {
227                agentos_vm_config::RootFilesystemEntryEncoding::Base64
228            }
229        }),
230        target: entry.target,
231        executable: entry.executable,
232    }
233}
234
235pub fn permissions_policy_config_from_wire(
236    permissions: crate::generated_protocol::v1::PermissionsPolicy,
237) -> agentos_vm_config::PermissionsPolicy {
238    agentos_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        binding: permissions
251            .binding
252            .map(legacy_pattern_permission_scope_config),
253    }
254}
255
256fn legacy_permission_mode_config(
257    mode: crate::generated_protocol::v1::PermissionMode,
258) -> agentos_vm_config::PermissionMode {
259    match mode {
260        crate::generated_protocol::v1::PermissionMode::Allow => {
261            agentos_vm_config::PermissionMode::Allow
262        }
263        crate::generated_protocol::v1::PermissionMode::Ask => {
264            agentos_vm_config::PermissionMode::Ask
265        }
266        crate::generated_protocol::v1::PermissionMode::Deny => {
267            agentos_vm_config::PermissionMode::Deny
268        }
269    }
270}
271
272fn legacy_fs_permission_scope_config(
273    scope: crate::generated_protocol::v1::FsPermissionScope,
274) -> agentos_vm_config::FsPermissionScope {
275    match scope {
276        crate::generated_protocol::v1::FsPermissionScope::PermissionMode(mode) => {
277            agentos_vm_config::FsPermissionScope::Mode(legacy_permission_mode_config(mode))
278        }
279        crate::generated_protocol::v1::FsPermissionScope::FsPermissionRuleSet(rules) => {
280            agentos_vm_config::FsPermissionScope::Rules(agentos_vm_config::FsPermissionRuleSet {
281                default: rules.default.map(legacy_permission_mode_config),
282                rules: rules
283                    .rules
284                    .into_iter()
285                    .map(|rule| agentos_vm_config::FsPermissionRule {
286                        mode: legacy_permission_mode_config(rule.mode),
287                        operations: rule.operations,
288                        paths: rule.paths,
289                    })
290                    .collect(),
291            })
292        }
293    }
294}
295
296fn legacy_pattern_permission_scope_config(
297    scope: crate::generated_protocol::v1::PatternPermissionScope,
298) -> agentos_vm_config::PatternPermissionScope {
299    match scope {
300        crate::generated_protocol::v1::PatternPermissionScope::PermissionMode(mode) => {
301            agentos_vm_config::PatternPermissionScope::Mode(legacy_permission_mode_config(mode))
302        }
303        crate::generated_protocol::v1::PatternPermissionScope::PatternPermissionRuleSet(rules) => {
304            agentos_vm_config::PatternPermissionScope::Rules(
305                agentos_vm_config::PatternPermissionRuleSet {
306                    default: rules.default.map(legacy_permission_mode_config),
307                    rules: rules
308                        .rules
309                        .into_iter()
310                        .map(|rule| agentos_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<agentos_vm_config::VmDnsConfig> {
325    let mut dns = agentos_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<agentos_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(agentos_vm_config::NativeRootFilesystemConfig {
368        plugin: agentos_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<agentos_vm_config::VmListenPolicyConfig> {
379    let listen = agentos_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("AGENTOS_LOOPBACK_EXEMPT_PORTS") else {
403        return Vec::new();
404    };
405    serde_json::from_str::<Vec<serde_json::Value>>(value)
406        .unwrap_or_default()
407        .into_iter()
408        .filter_map(|value| match value {
409            serde_json::Value::Number(number) => number.as_u64(),
410            serde_json::Value::String(value) => value.parse::<u64>().ok(),
411            _ => None,
412        })
413        .filter_map(|port| u16::try_from(port).ok())
414        .collect()
415}
416
417fn legacy_limits_config(
418    metadata: &std::collections::BTreeMap<String, String>,
419) -> Option<agentos_vm_config::VmLimitsConfig> {
420    let resources = agentos_vm_config::ResourceLimitsConfig {
421        cpu_count: legacy_u64(metadata, "resource.cpu_count"),
422        max_processes: legacy_u64(metadata, "resource.max_processes"),
423        max_open_fds: legacy_u64(metadata, "resource.max_open_fds"),
424        max_pipes: legacy_u64(metadata, "resource.max_pipes"),
425        max_ptys: legacy_u64(metadata, "resource.max_ptys"),
426        max_sockets: legacy_u64(metadata, "resource.max_sockets"),
427        max_connections: legacy_u64(metadata, "resource.max_connections"),
428        max_socket_buffered_bytes: legacy_u64(metadata, "resource.max_socket_buffered_bytes"),
429        max_socket_datagram_queue_len: legacy_u64(
430            metadata,
431            "resource.max_socket_datagram_queue_len",
432        ),
433        max_filesystem_bytes: legacy_u64(metadata, "resource.max_filesystem_bytes"),
434        max_inode_count: legacy_u64(metadata, "resource.max_inode_count"),
435        max_blocking_read_ms: legacy_u64(metadata, "resource.max_blocking_read_ms"),
436        max_pread_bytes: legacy_u64(metadata, "resource.max_pread_bytes"),
437        max_fd_write_bytes: legacy_u64(metadata, "resource.max_fd_write_bytes"),
438        max_process_argv_bytes: legacy_u64(metadata, "resource.max_process_argv_bytes"),
439        max_process_env_bytes: legacy_u64(metadata, "resource.max_process_env_bytes"),
440        max_readdir_entries: legacy_u64(metadata, "resource.max_readdir_entries"),
441        max_recursive_fs_depth: legacy_u64(metadata, "resource.max_recursive_fs_depth"),
442        max_recursive_fs_entries: legacy_u64(metadata, "resource.max_recursive_fs_entries"),
443        max_wasm_fuel: legacy_u64(metadata, "resource.max_wasm_fuel"),
444        max_wasm_memory_bytes: legacy_u64(metadata, "resource.max_wasm_memory_bytes"),
445        max_wasm_stack_bytes: legacy_u64(metadata, "resource.max_wasm_stack_bytes"),
446    };
447    let http = agentos_vm_config::HttpLimitsConfig {
448        max_fetch_response_bytes: legacy_u64(metadata, "limits.http.max_fetch_response_bytes"),
449    };
450    let bindings = agentos_vm_config::BindingLimitsConfig {
451        default_binding_timeout_ms: legacy_u64(
452            metadata,
453            "limits.bindings.default_binding_timeout_ms",
454        ),
455        max_binding_timeout_ms: legacy_u64(metadata, "limits.bindings.max_binding_timeout_ms"),
456        max_registered_collections: legacy_u64(
457            metadata,
458            "limits.bindings.max_registered_collections",
459        ),
460        max_registered_bindings_per_vm: legacy_u64(
461            metadata,
462            "limits.bindings.max_registered_bindings_per_vm",
463        ),
464        max_bindings_per_collection: legacy_u64(
465            metadata,
466            "limits.bindings.max_bindings_per_collection",
467        ),
468        max_binding_schema_bytes: legacy_u64(metadata, "limits.bindings.max_binding_schema_bytes"),
469        max_examples_per_binding: legacy_u64(metadata, "limits.bindings.max_examples_per_binding"),
470        max_binding_example_input_bytes: legacy_u64(
471            metadata,
472            "limits.bindings.max_binding_example_input_bytes",
473        ),
474    };
475    let plugins = agentos_vm_config::PluginLimitsConfig {
476        max_persisted_manifest_bytes: legacy_u64(
477            metadata,
478            "limits.plugins.max_persisted_manifest_bytes",
479        ),
480        max_persisted_manifest_file_bytes: legacy_u64(
481            metadata,
482            "limits.plugins.max_persisted_manifest_file_bytes",
483        ),
484    };
485    let acp = agentos_vm_config::AcpLimitsConfig {
486        max_read_line_bytes: legacy_u64(metadata, "limits.acp.max_read_line_bytes"),
487        stdout_buffer_byte_limit: legacy_u64(metadata, "limits.acp.stdout_buffer_byte_limit"),
488        max_completed_message_bytes: legacy_u64(metadata, "limits.acp.max_completed_message_bytes"),
489        max_turn_output_bytes: legacy_u64(metadata, "limits.acp.max_turn_output_bytes"),
490        max_prompt_bytes: legacy_u64(metadata, "limits.acp.max_prompt_bytes"),
491        max_prompt_blocks: legacy_u64(metadata, "limits.acp.max_prompt_blocks"),
492        max_fallback_continuation_bytes: legacy_u64(
493            metadata,
494            "limits.acp.max_fallback_continuation_bytes",
495        ),
496        max_session_history_bytes: legacy_u64(metadata, "limits.acp.max_session_history_bytes"),
497        max_session_history_events: legacy_u64(metadata, "limits.acp.max_session_history_events"),
498        max_history_page_entries: legacy_u64(metadata, "limits.acp.max_history_page_entries"),
499        max_session_list_entries: legacy_u64(metadata, "limits.acp.max_session_list_entries"),
500        max_sessions_per_vm: legacy_u64(metadata, "limits.acp.max_sessions_per_vm"),
501        max_prompts_per_session: legacy_u64(metadata, "limits.acp.max_prompts_per_session"),
502        max_prompts_per_vm: legacy_u64(metadata, "limits.acp.max_prompts_per_vm"),
503        max_pending_permissions_per_session: legacy_u64(
504            metadata,
505            "limits.acp.max_pending_permissions_per_session",
506        ),
507        max_pending_permissions_per_vm: legacy_u64(
508            metadata,
509            "limits.acp.max_pending_permissions_per_vm",
510        ),
511        max_permission_outcomes_per_session: legacy_u64(
512            metadata,
513            "limits.acp.max_permission_outcomes_per_session",
514        ),
515        max_permission_outcomes_per_vm: legacy_u64(
516            metadata,
517            "limits.acp.max_permission_outcomes_per_vm",
518        ),
519    };
520    let sqlite = agentos_vm_config::SqliteLimitsConfig {
521        max_result_bytes: legacy_u64(metadata, "limits.sqlite.max_result_bytes"),
522        max_in_flight_requests: legacy_u64(metadata, "limits.sqlite.max_in_flight_requests"),
523        max_queued_request_bytes: legacy_u64(metadata, "limits.sqlite.max_queued_request_bytes"),
524    };
525    let js_runtime = agentos_vm_config::JsRuntimeLimitsConfig {
526        v8_heap_limit_mb: legacy_u64(metadata, "limits.js_runtime.v8_heap_limit_mb"),
527        sync_rpc_wait_timeout_ms: legacy_u64(
528            metadata,
529            "limits.js_runtime.sync_rpc_wait_timeout_ms",
530        ),
531        cpu_time_limit_ms: legacy_u64(metadata, "limits.js_runtime.cpu_time_limit_ms"),
532        wall_clock_limit_ms: legacy_u64(metadata, "limits.js_runtime.wall_clock_limit_ms"),
533        import_cache_materialize_timeout_ms: legacy_u64(
534            metadata,
535            "limits.js_runtime.import_cache_materialize_timeout_ms",
536        ),
537        captured_output_limit_bytes: legacy_u64(
538            metadata,
539            "limits.js_runtime.captured_output_limit_bytes",
540        ),
541        stdin_buffer_limit_bytes: legacy_u64(
542            metadata,
543            "limits.js_runtime.stdin_buffer_limit_bytes",
544        ),
545        event_payload_limit_bytes: legacy_u64(
546            metadata,
547            "limits.js_runtime.event_payload_limit_bytes",
548        ),
549        max_timers: legacy_u64(metadata, "limits.js_runtime.max_timers"),
550        v8_ipc_max_frame_bytes: legacy_u64(metadata, "limits.js_runtime.v8_ipc_max_frame_bytes"),
551    };
552    let python = agentos_vm_config::PythonLimitsConfig {
553        output_buffer_max_bytes: legacy_u64(metadata, "limits.python.output_buffer_max_bytes"),
554        execution_timeout_ms: legacy_u64(metadata, "limits.python.execution_timeout_ms"),
555        max_old_space_mb: legacy_u64(metadata, "limits.python.max_old_space_mb"),
556        vfs_rpc_timeout_ms: legacy_u64(metadata, "limits.python.vfs_rpc_timeout_ms"),
557    };
558    let wasm = agentos_vm_config::WasmLimitsConfig {
559        max_module_file_bytes: legacy_u64(metadata, "limits.wasm.max_module_file_bytes"),
560        captured_output_limit_bytes: legacy_u64(
561            metadata,
562            "limits.wasm.captured_output_limit_bytes",
563        ),
564        sync_read_limit_bytes: legacy_u64(metadata, "limits.wasm.sync_read_limit_bytes"),
565        prewarm_timeout_ms: legacy_u64(metadata, "limits.wasm.prewarm_timeout_ms"),
566        runner_heap_limit_mb: legacy_u64(metadata, "limits.wasm.runner_heap_limit_mb"),
567        runner_cpu_time_limit_ms: legacy_u64(metadata, "limits.wasm.runner_cpu_time_limit_ms"),
568    };
569    let execution = agentos_vm_config::ExecutionLimitsConfig {
570        completed_ttl_ms: legacy_u64(metadata, "limits.execution.completed_ttl_ms"),
571        max_completed_executions: legacy_u64(metadata, "limits.execution.max_completed_executions"),
572        live_execution_warning_threshold: legacy_u64(
573            metadata,
574            "limits.execution.live_execution_warning_threshold",
575        ),
576    };
577    let process = agentos_vm_config::ProcessLimitsConfig {
578        max_spawn_file_actions: legacy_u64(metadata, "limits.process.max_spawn_file_actions")
579            .or_else(|| legacy_u64(metadata, "limits.wasm.max_spawn_file_actions")),
580        max_spawn_file_action_bytes: legacy_u64(
581            metadata,
582            "limits.process.max_spawn_file_action_bytes",
583        )
584        .or_else(|| legacy_u64(metadata, "limits.wasm.max_spawn_file_action_bytes")),
585        pending_stdin_bytes: legacy_u64(metadata, "limits.process.pending_stdin_bytes"),
586        pending_event_count: legacy_u64(metadata, "limits.process.pending_event_count"),
587        pending_event_bytes: legacy_u64(metadata, "limits.process.pending_event_bytes"),
588    };
589
590    let config = agentos_vm_config::VmLimitsConfig {
591        reactor: None,
592        resources: legacy_has_resource_limits(&resources).then_some(resources),
593        http: http.max_fetch_response_bytes.is_some().then_some(http),
594        udp: None,
595        tls: None,
596        http2: None,
597        bindings: legacy_has_binding_limits(&bindings).then_some(bindings),
598        plugins: legacy_has_plugin_limits(&plugins).then_some(plugins),
599        acp: legacy_has_acp_limits(&acp).then_some(acp),
600        sqlite: (sqlite.max_result_bytes.is_some()
601            || sqlite.max_in_flight_requests.is_some()
602            || sqlite.max_queued_request_bytes.is_some())
603        .then_some(sqlite),
604        js_runtime: legacy_has_js_runtime_limits(&js_runtime).then_some(js_runtime),
605        python: legacy_has_python_limits(&python).then_some(python),
606        wasm: legacy_has_wasm_limits(&wasm).then_some(wasm),
607        execution: (execution.completed_ttl_ms.is_some()
608            || execution.max_completed_executions.is_some()
609            || execution.live_execution_warning_threshold.is_some())
610        .then_some(execution),
611        process: legacy_has_process_limits(&process).then_some(process),
612    };
613
614    if config.resources.is_none()
615        && config.http.is_none()
616        && config.bindings.is_none()
617        && config.plugins.is_none()
618        && config.acp.is_none()
619        && config.sqlite.is_none()
620        && config.js_runtime.is_none()
621        && config.python.is_none()
622        && config.wasm.is_none()
623        && config.execution.is_none()
624        && config.process.is_none()
625    {
626        None
627    } else {
628        Some(config)
629    }
630}
631
632fn legacy_u64(metadata: &std::collections::BTreeMap<String, String>, key: &str) -> Option<u64> {
633    metadata.get(key).map(|value| {
634        value
635            .parse::<u64>()
636            .unwrap_or_else(|error| panic!("parse {key}: {error}"))
637    })
638}
639
640fn legacy_has_resource_limits(config: &agentos_vm_config::ResourceLimitsConfig) -> bool {
641    config.cpu_count.is_some()
642        || config.max_processes.is_some()
643        || config.max_open_fds.is_some()
644        || config.max_pipes.is_some()
645        || config.max_ptys.is_some()
646        || config.max_sockets.is_some()
647        || config.max_connections.is_some()
648        || config.max_socket_buffered_bytes.is_some()
649        || config.max_socket_datagram_queue_len.is_some()
650        || config.max_filesystem_bytes.is_some()
651        || config.max_inode_count.is_some()
652        || config.max_blocking_read_ms.is_some()
653        || config.max_pread_bytes.is_some()
654        || config.max_fd_write_bytes.is_some()
655        || config.max_process_argv_bytes.is_some()
656        || config.max_process_env_bytes.is_some()
657        || config.max_readdir_entries.is_some()
658        || config.max_wasm_fuel.is_some()
659        || config.max_wasm_memory_bytes.is_some()
660        || config.max_wasm_stack_bytes.is_some()
661}
662
663fn legacy_has_binding_limits(config: &agentos_vm_config::BindingLimitsConfig) -> bool {
664    config.default_binding_timeout_ms.is_some()
665        || config.max_binding_timeout_ms.is_some()
666        || config.max_registered_collections.is_some()
667        || config.max_registered_bindings_per_vm.is_some()
668        || config.max_bindings_per_collection.is_some()
669        || config.max_binding_schema_bytes.is_some()
670        || config.max_examples_per_binding.is_some()
671        || config.max_binding_example_input_bytes.is_some()
672}
673
674fn legacy_has_plugin_limits(config: &agentos_vm_config::PluginLimitsConfig) -> bool {
675    config.max_persisted_manifest_bytes.is_some()
676        || config.max_persisted_manifest_file_bytes.is_some()
677}
678
679fn legacy_has_acp_limits(config: &agentos_vm_config::AcpLimitsConfig) -> bool {
680    config.max_read_line_bytes.is_some()
681        || config.stdout_buffer_byte_limit.is_some()
682        || config.max_completed_message_bytes.is_some()
683        || config.max_turn_output_bytes.is_some()
684        || config.max_prompt_bytes.is_some()
685        || config.max_prompt_blocks.is_some()
686        || config.max_fallback_continuation_bytes.is_some()
687        || config.max_session_history_bytes.is_some()
688        || config.max_session_history_events.is_some()
689        || config.max_history_page_entries.is_some()
690        || config.max_session_list_entries.is_some()
691        || config.max_sessions_per_vm.is_some()
692        || config.max_prompts_per_session.is_some()
693        || config.max_prompts_per_vm.is_some()
694        || config.max_pending_permissions_per_session.is_some()
695        || config.max_pending_permissions_per_vm.is_some()
696        || config.max_permission_outcomes_per_session.is_some()
697        || config.max_permission_outcomes_per_vm.is_some()
698}
699
700fn legacy_has_js_runtime_limits(config: &agentos_vm_config::JsRuntimeLimitsConfig) -> bool {
701    config.v8_heap_limit_mb.is_some()
702        || config.sync_rpc_wait_timeout_ms.is_some()
703        || config.cpu_time_limit_ms.is_some()
704        || config.wall_clock_limit_ms.is_some()
705        || config.import_cache_materialize_timeout_ms.is_some()
706        || config.captured_output_limit_bytes.is_some()
707        || config.stdin_buffer_limit_bytes.is_some()
708        || config.event_payload_limit_bytes.is_some()
709        || config.max_timers.is_some()
710        || config.v8_ipc_max_frame_bytes.is_some()
711}
712
713fn legacy_has_python_limits(config: &agentos_vm_config::PythonLimitsConfig) -> bool {
714    config.output_buffer_max_bytes.is_some()
715        || config.execution_timeout_ms.is_some()
716        || config.max_old_space_mb.is_some()
717        || config.vfs_rpc_timeout_ms.is_some()
718}
719
720fn legacy_has_wasm_limits(config: &agentos_vm_config::WasmLimitsConfig) -> bool {
721    config.max_module_file_bytes.is_some()
722        || config.captured_output_limit_bytes.is_some()
723        || config.sync_read_limit_bytes.is_some()
724        || config.prewarm_timeout_ms.is_some()
725        || config.runner_heap_limit_mb.is_some()
726        || config.runner_cpu_time_limit_ms.is_some()
727}
728
729fn legacy_has_process_limits(config: &agentos_vm_config::ProcessLimitsConfig) -> bool {
730    config.max_spawn_file_actions.is_some()
731        || config.max_spawn_file_action_bytes.is_some()
732        || config.pending_stdin_bytes.is_some()
733        || config.pending_event_count.is_some()
734        || config.pending_event_bytes.is_some()
735}
736
737// Ownership-scope constructor ergonomics. The generated BARE union exposes only the
738// tuple-wrapped variants (`ConnectionOwnership`/`SessionOwnership`/`VmOwnership`); restore
739// the hand-written `connection`/`session`/`vm` helpers the sidecar relies on. These live in
740// `wire` (not `protocol`) for the same orphan-rule reason as the impls above: `protocol.rs`
741// is `#[path]`-included by integration tests where the generated type is foreign.
742impl crate::generated_protocol::v1::OwnershipScope {
743    pub fn connection(connection_id: impl Into<String>) -> Self {
744        Self::ConnectionOwnership(crate::generated_protocol::v1::ConnectionOwnership {
745            connection_id: connection_id.into(),
746        })
747    }
748
749    pub fn session(connection_id: impl Into<String>, session_id: impl Into<String>) -> Self {
750        Self::SessionOwnership(crate::generated_protocol::v1::SessionOwnership {
751            connection_id: connection_id.into(),
752            session_id: session_id.into(),
753        })
754    }
755
756    pub fn vm(
757        connection_id: impl Into<String>,
758        session_id: impl Into<String>,
759        vm_id: impl Into<String>,
760    ) -> Self {
761        Self::VmOwnership(crate::generated_protocol::v1::VmOwnership {
762            connection_id: connection_id.into(),
763            session_id: session_id.into(),
764            vm_id: vm_id.into(),
765        })
766    }
767}
768
769pub const PROTOCOL_NAME: &str = "agentos-native-sidecar";
770pub const PROTOCOL_VERSION: u16 = 8;
771// 16 MiB: large enough to carry a trusted-client CreateVm config that inlines an
772// entire base-filesystem snapshot, while still bounding a single frame.
773pub const DEFAULT_MAX_FRAME_BYTES: usize = 16 * 1024 * 1024;
774
775#[derive(Debug, Clone, PartialEq, Eq)]
776pub enum ProtocolCodecError {
777    TruncatedFrame {
778        actual: usize,
779    },
780    LengthPrefixMismatch {
781        declared: usize,
782        actual: usize,
783    },
784    FrameTooLarge {
785        size: usize,
786        max: usize,
787    },
788    UnsupportedSchema {
789        name: String,
790        version: u16,
791    },
792    InvalidRequestId,
793    InvalidRequestDirection {
794        request_id: RequestId,
795        expected: RequestDirection,
796    },
797    EmptyOwnershipField {
798        field: &'static str,
799    },
800    EmptyAuthToken,
801    InvalidOwnershipScope {
802        required: OwnershipRequirement,
803        actual: OwnershipRequirement,
804    },
805    SerializeFailure(String),
806    DeserializeFailure(String),
807}
808
809impl fmt::Display for ProtocolCodecError {
810    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
811        match self {
812            Self::TruncatedFrame { actual } => {
813                write!(
814                    f,
815                    "protocol frame is truncated: only {actual} bytes provided"
816                )
817            }
818            Self::LengthPrefixMismatch { declared, actual } => write!(
819                f,
820                "protocol frame length prefix mismatch: declared {declared} bytes, got {actual}",
821            ),
822            Self::FrameTooLarge { size, max } => {
823                write!(f, "protocol frame is {size} bytes, limit is {max}")
824            }
825            Self::UnsupportedSchema { name, version } => write!(
826                f,
827                "unsupported protocol schema {name}@{version}; expected {PROTOCOL_NAME}@{PROTOCOL_VERSION}",
828            ),
829            Self::InvalidRequestId => write!(f, "protocol request identifiers must be non-zero"),
830            Self::InvalidRequestDirection {
831                request_id,
832                expected,
833            } => write!(f, "protocol request id {request_id} must be {expected}",),
834            Self::EmptyOwnershipField { field } => {
835                write!(f, "protocol ownership field `{field}` cannot be empty")
836            }
837            Self::EmptyAuthToken => {
838                write!(f, "authenticate requests require a non-empty auth token")
839            }
840            Self::InvalidOwnershipScope { required, actual } => write!(
841                f,
842                "protocol frame requires {required} ownership but carried {actual}",
843            ),
844            Self::SerializeFailure(message) => {
845                write!(f, "protocol frame serialization failed: {message}")
846            }
847            Self::DeserializeFailure(message) => {
848                write!(f, "protocol frame deserialization failed: {message}")
849            }
850        }
851    }
852}
853
854impl Error for ProtocolCodecError {}
855
856#[derive(Debug, Clone, Copy, PartialEq, Eq)]
857pub enum OwnershipRequirement {
858    Any,
859    Connection,
860    Session,
861    Vm,
862    SessionOrVm,
863}
864
865impl fmt::Display for OwnershipRequirement {
866    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
867        match self {
868            Self::Any => write!(f, "any"),
869            Self::Connection => write!(f, "connection"),
870            Self::Session => write!(f, "session"),
871            Self::Vm => write!(f, "vm"),
872            Self::SessionOrVm => write!(f, "session-or-vm"),
873        }
874    }
875}
876
877#[derive(Debug, Clone, Copy, PartialEq, Eq)]
878pub enum RequestDirection {
879    Host,
880    Sidecar,
881}
882
883impl fmt::Display for RequestDirection {
884    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
885        match self {
886            Self::Host => write!(f, "positive"),
887            Self::Sidecar => write!(f, "negative"),
888        }
889    }
890}
891
892#[derive(Debug, Clone, PartialEq, Eq)]
893pub struct WireDispatchResult {
894    pub response: ResponseFrame,
895    pub events: Vec<EventFrame>,
896}
897
898#[derive(Debug, Clone, PartialEq, Eq)]
899pub struct CompatDispatchResult {
900    pub response: crate::protocol::ResponseFrame,
901    pub events: Vec<crate::protocol::EventFrame>,
902}
903
904#[derive(Debug, Clone)]
905pub struct WireFrameCodec {
906    max_frame_bytes: usize,
907}
908
909impl WireFrameCodec {
910    pub fn new(max_frame_bytes: usize) -> Self {
911        Self { max_frame_bytes }
912    }
913
914    pub fn max_frame_bytes(&self) -> usize {
915        self.max_frame_bytes
916    }
917
918    pub fn encode(&self, frame: &ProtocolFrame) -> Result<Vec<u8>, ProtocolCodecError> {
919        validate_frame(frame)?;
920
921        let payload = serde_bare::to_vec(frame)
922            .map_err(|error| ProtocolCodecError::SerializeFailure(error.to_string()))?;
923        if payload.len() > self.max_frame_bytes {
924            return Err(ProtocolCodecError::FrameTooLarge {
925                size: payload.len(),
926                max: self.max_frame_bytes,
927            });
928        }
929
930        let length =
931            u32::try_from(payload.len()).map_err(|_| ProtocolCodecError::FrameTooLarge {
932                size: payload.len(),
933                max: u32::MAX as usize,
934            })?;
935
936        let mut encoded = Vec::with_capacity(4 + payload.len());
937        encoded.extend_from_slice(&length.to_be_bytes());
938        encoded.extend_from_slice(&payload);
939        Ok(encoded)
940    }
941
942    pub fn decode(&self, bytes: &[u8]) -> Result<ProtocolFrame, ProtocolCodecError> {
943        let payload = self.checked_payload(bytes)?;
944        let frame = serde_bare::from_slice(payload)
945            .map_err(|error| ProtocolCodecError::DeserializeFailure(error.to_string()))?;
946        validate_frame(&frame)?;
947        Ok(frame)
948    }
949
950    /// Encode a frame as a bare message WITHOUT the 4-byte length prefix.
951    ///
952    /// Stream transports (stdio) use [`encode`] so frames can be delimited in a
953    /// byte stream. Message transports where the boundary is the call itself
954    /// (the browser `pushFrame` / `postMessage` path) use this so the on-wire
955    /// bytes match the TypeScript `encodeProtocolFramePayload(frame, "bare")`,
956    /// which emits the raw bare frame with no prefix.
957    pub fn encode_message(&self, frame: &ProtocolFrame) -> Result<Vec<u8>, ProtocolCodecError> {
958        validate_frame(frame)?;
959        let payload = serde_bare::to_vec(frame)
960            .map_err(|error| ProtocolCodecError::SerializeFailure(error.to_string()))?;
961        if payload.len() > self.max_frame_bytes {
962            return Err(ProtocolCodecError::FrameTooLarge {
963                size: payload.len(),
964                max: self.max_frame_bytes,
965            });
966        }
967        Ok(payload)
968    }
969
970    /// Decode a bare message produced by [`encode_message`] (no length prefix).
971    pub fn decode_message(&self, bytes: &[u8]) -> Result<ProtocolFrame, ProtocolCodecError> {
972        if bytes.len() > self.max_frame_bytes {
973            return Err(ProtocolCodecError::FrameTooLarge {
974                size: bytes.len(),
975                max: self.max_frame_bytes,
976            });
977        }
978        let frame = serde_bare::from_slice(bytes)
979            .map_err(|error| ProtocolCodecError::DeserializeFailure(error.to_string()))?;
980        validate_frame(&frame)?;
981        Ok(frame)
982    }
983
984    fn checked_payload<'a>(&self, bytes: &'a [u8]) -> Result<&'a [u8], ProtocolCodecError> {
985        if bytes.len() < 4 {
986            return Err(ProtocolCodecError::TruncatedFrame {
987                actual: bytes.len(),
988            });
989        }
990
991        let declared =
992            u32::from_be_bytes(bytes[..4].try_into().expect("length prefix is four bytes"))
993                as usize;
994        if declared > self.max_frame_bytes {
995            return Err(ProtocolCodecError::FrameTooLarge {
996                size: declared,
997                max: self.max_frame_bytes,
998            });
999        }
1000
1001        let actual = bytes.len() - 4;
1002        if declared != actual {
1003            return Err(ProtocolCodecError::LengthPrefixMismatch { declared, actual });
1004        }
1005
1006        Ok(&bytes[4..])
1007    }
1008}
1009
1010impl Default for WireFrameCodec {
1011    fn default() -> Self {
1012        Self::new(DEFAULT_MAX_FRAME_BYTES)
1013    }
1014}
1015
1016pub fn protocol_schema() -> ProtocolSchema {
1017    ProtocolSchema::current()
1018}
1019
1020impl ProtocolSchema {
1021    pub fn current() -> Self {
1022        Self {
1023            name: PROTOCOL_NAME.to_string(),
1024            version: PROTOCOL_VERSION,
1025        }
1026    }
1027}
1028
1029impl Default for ProtocolSchema {
1030    fn default() -> Self {
1031        Self::current()
1032    }
1033}
1034
1035pub fn request_frame_to_compat(
1036    request: RequestFrame,
1037) -> Result<crate::protocol::RequestFrame, ProtocolCodecError> {
1038    match crate::protocol::from_generated_protocol_frame(ProtocolFrame::RequestFrame(request))? {
1039        crate::protocol::ProtocolFrame::Request(request) => Ok(request),
1040        crate::protocol::ProtocolFrame::Response(_)
1041        | crate::protocol::ProtocolFrame::Event(_)
1042        | crate::protocol::ProtocolFrame::SidecarRequest(_)
1043        | crate::protocol::ProtocolFrame::SidecarResponse(_)
1044        | crate::protocol::ProtocolFrame::Control(_) => {
1045            Err(ProtocolCodecError::DeserializeFailure(String::from(
1046                "wire request frame converted to non-request compatibility frame",
1047            )))
1048        }
1049    }
1050}
1051
1052pub fn ownership_scope_to_compat(ownership: OwnershipScope) -> crate::protocol::OwnershipScope {
1053    crate::protocol::from_generated_ownership_scope(ownership)
1054}
1055
1056pub fn request_payload_to_compat(
1057    ownership: &crate::protocol::OwnershipScope,
1058    payload: RequestPayload,
1059) -> Result<crate::protocol::RequestPayload, ProtocolCodecError> {
1060    match crate::protocol::from_generated_protocol_frame(ProtocolFrame::RequestFrame(
1061        RequestFrame {
1062            schema: protocol_schema(),
1063            request_id: 1,
1064            ownership: crate::protocol::to_generated_ownership_scope(ownership),
1065            payload,
1066        },
1067    ))? {
1068        crate::protocol::ProtocolFrame::Request(request) => Ok(request.payload),
1069        crate::protocol::ProtocolFrame::Response(_)
1070        | crate::protocol::ProtocolFrame::Event(_)
1071        | crate::protocol::ProtocolFrame::SidecarRequest(_)
1072        | crate::protocol::ProtocolFrame::SidecarResponse(_)
1073        | crate::protocol::ProtocolFrame::Control(_) => {
1074            Err(ProtocolCodecError::DeserializeFailure(String::from(
1075                "wire request payload converted to non-request compatibility frame",
1076            )))
1077        }
1078    }
1079}
1080
1081pub fn response_payload_from_compat(
1082    ownership: &crate::protocol::OwnershipScope,
1083    payload: crate::protocol::ResponsePayload,
1084) -> Result<ResponsePayload, ProtocolCodecError> {
1085    match crate::protocol::to_generated_protocol_frame(&crate::protocol::ProtocolFrame::Response(
1086        crate::protocol::ResponseFrame::new(1, ownership.clone(), payload),
1087    ))? {
1088        ProtocolFrame::ResponseFrame(response) => Ok(response.payload),
1089        ProtocolFrame::RequestFrame(_)
1090        | ProtocolFrame::EventFrame(_)
1091        | ProtocolFrame::SidecarRequestFrame(_)
1092        | ProtocolFrame::SidecarResponseFrame(_)
1093        | ProtocolFrame::ControlFrame(_) => Err(ProtocolCodecError::SerializeFailure(
1094            String::from("compatibility response payload converted to non-response wire frame"),
1095        )),
1096    }
1097}
1098
1099pub fn event_frame_from_compat(
1100    event: crate::protocol::EventFrame,
1101) -> Result<EventFrame, ProtocolCodecError> {
1102    match crate::protocol::to_generated_protocol_frame(&crate::protocol::ProtocolFrame::Event(
1103        event,
1104    ))? {
1105        ProtocolFrame::EventFrame(event) => Ok(event),
1106        ProtocolFrame::RequestFrame(_)
1107        | ProtocolFrame::ResponseFrame(_)
1108        | ProtocolFrame::SidecarRequestFrame(_)
1109        | ProtocolFrame::SidecarResponseFrame(_)
1110        | ProtocolFrame::ControlFrame(_) => Err(ProtocolCodecError::SerializeFailure(
1111            String::from("compatibility event converted to non-event wire frame"),
1112        )),
1113    }
1114}
1115
1116pub fn event_frame_to_compat(
1117    event: EventFrame,
1118) -> Result<crate::protocol::EventFrame, ProtocolCodecError> {
1119    match crate::protocol::from_generated_protocol_frame(ProtocolFrame::EventFrame(event))? {
1120        crate::protocol::ProtocolFrame::Event(event) => Ok(event),
1121        crate::protocol::ProtocolFrame::Request(_)
1122        | crate::protocol::ProtocolFrame::Response(_)
1123        | crate::protocol::ProtocolFrame::SidecarRequest(_)
1124        | crate::protocol::ProtocolFrame::SidecarResponse(_)
1125        | crate::protocol::ProtocolFrame::Control(_) => {
1126            Err(ProtocolCodecError::DeserializeFailure(String::from(
1127                "wire event converted to non-event compatibility frame",
1128            )))
1129        }
1130    }
1131}
1132
1133pub fn sidecar_request_frame_from_compat(
1134    request: crate::protocol::SidecarRequestFrame,
1135) -> Result<SidecarRequestFrame, ProtocolCodecError> {
1136    match crate::protocol::to_generated_protocol_frame(
1137        &crate::protocol::ProtocolFrame::SidecarRequest(request),
1138    )? {
1139        ProtocolFrame::SidecarRequestFrame(request) => Ok(request),
1140        ProtocolFrame::RequestFrame(_)
1141        | ProtocolFrame::ResponseFrame(_)
1142        | ProtocolFrame::EventFrame(_)
1143        | ProtocolFrame::SidecarResponseFrame(_)
1144        | ProtocolFrame::ControlFrame(_) => {
1145            Err(ProtocolCodecError::SerializeFailure(String::from(
1146                "compatibility sidecar request converted to non-sidecar-request wire frame",
1147            )))
1148        }
1149    }
1150}
1151
1152pub fn sidecar_request_payload_to_compat(
1153    ownership: &crate::protocol::OwnershipScope,
1154    payload: SidecarRequestPayload,
1155) -> Result<crate::protocol::SidecarRequestPayload, ProtocolCodecError> {
1156    match crate::protocol::from_generated_protocol_frame(ProtocolFrame::SidecarRequestFrame(
1157        SidecarRequestFrame {
1158            schema: protocol_schema(),
1159            request_id: -1,
1160            ownership: crate::protocol::to_generated_ownership_scope(ownership),
1161            payload,
1162        },
1163    ))? {
1164        crate::protocol::ProtocolFrame::SidecarRequest(request) => Ok(request.payload),
1165        crate::protocol::ProtocolFrame::Request(_)
1166        | crate::protocol::ProtocolFrame::Response(_)
1167        | crate::protocol::ProtocolFrame::Event(_)
1168        | crate::protocol::ProtocolFrame::SidecarResponse(_)
1169        | crate::protocol::ProtocolFrame::Control(_) => {
1170            Err(ProtocolCodecError::DeserializeFailure(String::from(
1171                "wire sidecar request payload converted to non-sidecar-request compatibility frame",
1172            )))
1173        }
1174    }
1175}
1176
1177pub fn sidecar_response_frame_to_compat(
1178    response: SidecarResponseFrame,
1179) -> Result<crate::protocol::SidecarResponseFrame, ProtocolCodecError> {
1180    match crate::protocol::from_generated_protocol_frame(ProtocolFrame::SidecarResponseFrame(
1181        response,
1182    ))? {
1183        crate::protocol::ProtocolFrame::SidecarResponse(response) => Ok(response),
1184        crate::protocol::ProtocolFrame::Request(_)
1185        | crate::protocol::ProtocolFrame::Response(_)
1186        | crate::protocol::ProtocolFrame::Event(_)
1187        | crate::protocol::ProtocolFrame::SidecarRequest(_)
1188        | crate::protocol::ProtocolFrame::Control(_) => {
1189            Err(ProtocolCodecError::DeserializeFailure(String::from(
1190                "wire sidecar response converted to non-sidecar-response compatibility frame",
1191            )))
1192        }
1193    }
1194}
1195
1196pub fn sidecar_response_frame_from_compat(
1197    response: crate::protocol::SidecarResponseFrame,
1198) -> Result<SidecarResponseFrame, ProtocolCodecError> {
1199    match crate::protocol::to_generated_protocol_frame(
1200        &crate::protocol::ProtocolFrame::SidecarResponse(response),
1201    )? {
1202        ProtocolFrame::SidecarResponseFrame(response) => Ok(response),
1203        ProtocolFrame::RequestFrame(_)
1204        | ProtocolFrame::ResponseFrame(_)
1205        | ProtocolFrame::EventFrame(_)
1206        | ProtocolFrame::SidecarRequestFrame(_)
1207        | ProtocolFrame::ControlFrame(_) => {
1208            Err(ProtocolCodecError::SerializeFailure(String::from(
1209                "compatibility sidecar response converted to non-sidecar-response wire frame",
1210            )))
1211        }
1212    }
1213}
1214
1215pub fn dispatch_result_from_compat(
1216    result: CompatDispatchResult,
1217) -> Result<WireDispatchResult, ProtocolCodecError> {
1218    let response = match crate::protocol::to_generated_protocol_frame(
1219        &crate::protocol::ProtocolFrame::Response(result.response),
1220    )? {
1221        ProtocolFrame::ResponseFrame(response) => response,
1222        ProtocolFrame::RequestFrame(_)
1223        | ProtocolFrame::EventFrame(_)
1224        | ProtocolFrame::SidecarRequestFrame(_)
1225        | ProtocolFrame::SidecarResponseFrame(_)
1226        | ProtocolFrame::ControlFrame(_) => {
1227            return Err(ProtocolCodecError::SerializeFailure(String::from(
1228                "compatibility dispatch response converted to non-response wire frame",
1229            )));
1230        }
1231    };
1232
1233    let events = result
1234        .events
1235        .into_iter()
1236        .map(|event| {
1237            match crate::protocol::to_generated_protocol_frame(
1238                &crate::protocol::ProtocolFrame::Event(event),
1239            )? {
1240                ProtocolFrame::EventFrame(event) => Ok(event),
1241                ProtocolFrame::RequestFrame(_)
1242                | ProtocolFrame::ResponseFrame(_)
1243                | ProtocolFrame::SidecarRequestFrame(_)
1244                | ProtocolFrame::SidecarResponseFrame(_)
1245                | ProtocolFrame::ControlFrame(_) => Err(ProtocolCodecError::SerializeFailure(
1246                    String::from("compatibility dispatch event converted to non-event wire frame"),
1247                )),
1248            }
1249        })
1250        .collect::<Result<Vec<_>, _>>()?;
1251
1252    Ok(WireDispatchResult { response, events })
1253}
1254
1255fn validate_frame(frame: &ProtocolFrame) -> Result<(), ProtocolCodecError> {
1256    match frame {
1257        ProtocolFrame::RequestFrame(frame) => {
1258            validate_schema(&frame.schema)?;
1259            validate_request_id(frame.request_id)
1260        }
1261        ProtocolFrame::ResponseFrame(frame) => {
1262            validate_schema(&frame.schema)?;
1263            validate_request_id(frame.request_id)
1264        }
1265        ProtocolFrame::EventFrame(frame) => validate_schema(&frame.schema),
1266        ProtocolFrame::SidecarRequestFrame(frame) => {
1267            validate_schema(&frame.schema)?;
1268            validate_request_id(frame.request_id)
1269        }
1270        ProtocolFrame::SidecarResponseFrame(frame) => {
1271            validate_schema(&frame.schema)?;
1272            validate_request_id(frame.request_id)
1273        }
1274        ProtocolFrame::ControlFrame(frame) => validate_schema(&frame.schema),
1275    }
1276}
1277
1278fn validate_schema(schema: &ProtocolSchema) -> Result<(), ProtocolCodecError> {
1279    if schema.name != PROTOCOL_NAME || schema.version != PROTOCOL_VERSION {
1280        return Err(ProtocolCodecError::UnsupportedSchema {
1281            name: schema.name.clone(),
1282            version: schema.version,
1283        });
1284    }
1285    Ok(())
1286}
1287
1288fn validate_request_id(request_id: RequestId) -> Result<(), ProtocolCodecError> {
1289    if request_id == 0 {
1290        return Err(ProtocolCodecError::InvalidRequestId);
1291    }
1292    Ok(())
1293}
1294
1295#[cfg(test)]
1296mod tests {
1297    use super::*;
1298    use crate::generated_protocol::v1::{
1299        FsPermissionScope, PatternPermissionScope, PermissionMode,
1300    };
1301    use std::collections::BTreeMap;
1302
1303    #[test]
1304    fn legacy_metadata_preserves_js_runtime_limits_with_only_new_fields() {
1305        let metadata = BTreeMap::from([(
1306            String::from("limits.js_runtime.cpu_time_limit_ms"),
1307            String::from("123"),
1308        )]);
1309
1310        let config = legacy_limits_config(&metadata).expect("limits config");
1311        let js_runtime = config.js_runtime.expect("js runtime limits");
1312
1313        assert_eq!(js_runtime.cpu_time_limit_ms, Some(123));
1314    }
1315
1316    #[test]
1317    fn legacy_metadata_preserves_wasm_limits_with_only_new_fields() {
1318        let metadata = BTreeMap::from([(
1319            String::from("limits.wasm.prewarm_timeout_ms"),
1320            String::from("456"),
1321        )]);
1322
1323        let config = legacy_limits_config(&metadata).expect("limits config");
1324        let wasm = config.wasm.expect("wasm limits");
1325
1326        assert_eq!(wasm.prewarm_timeout_ms, Some(456));
1327    }
1328
1329    #[test]
1330    fn legacy_metadata_preserves_wasm_runner_heap_limit_as_only_new_field() {
1331        let metadata = BTreeMap::from([(
1332            String::from("limits.wasm.runner_heap_limit_mb"),
1333            String::from("789"),
1334        )]);
1335
1336        let config = legacy_limits_config(&metadata).expect("limits config");
1337        let wasm = config.wasm.expect("wasm limits");
1338
1339        assert_eq!(wasm.runner_heap_limit_mb, Some(789));
1340    }
1341
1342    #[test]
1343    fn legacy_metadata_preserves_wasm_runner_cpu_limit_as_only_new_field() {
1344        let metadata = BTreeMap::from([(
1345            String::from("limits.wasm.runner_cpu_time_limit_ms"),
1346            String::from("987"),
1347        )]);
1348
1349        let config = legacy_limits_config(&metadata).expect("limits config");
1350        let wasm = config.wasm.expect("wasm limits");
1351
1352        assert_eq!(wasm.runner_cpu_time_limit_ms, Some(987));
1353    }
1354
1355    #[test]
1356    fn permissions_policy_default_matches_no_policy_deny_all() {
1357        let policy = PermissionsPolicy::default();
1358
1359        assert!(matches!(
1360            policy.fs,
1361            Some(FsPermissionScope::PermissionMode(PermissionMode::Deny))
1362        ));
1363        for scope in [
1364            policy.network,
1365            policy.child_process,
1366            policy.process,
1367            policy.env,
1368            policy.binding,
1369        ] {
1370            assert!(matches!(
1371                scope,
1372                Some(PatternPermissionScope::PermissionMode(PermissionMode::Deny))
1373            ));
1374        }
1375    }
1376
1377    #[test]
1378    fn permissions_policy_allow_all_remains_explicit() {
1379        let policy = PermissionsPolicy::allow_all();
1380
1381        assert!(matches!(
1382            policy.fs,
1383            Some(FsPermissionScope::PermissionMode(PermissionMode::Allow))
1384        ));
1385        for scope in [
1386            policy.network,
1387            policy.child_process,
1388            policy.process,
1389            policy.env,
1390            policy.binding,
1391        ] {
1392            assert!(matches!(
1393                scope,
1394                Some(PatternPermissionScope::PermissionMode(
1395                    PermissionMode::Allow
1396                ))
1397            ));
1398        }
1399    }
1400}