1use std::error::Error;
8use std::fmt;
9
10pub use crate::generated_protocol::v1::*;
11
12impl 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#[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
737impl 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;
771pub 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 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 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}