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 host_function: 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 host_function: 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: Some(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 =
141 legacy_loopback_exempt_ports(config.env.as_ref().expect("legacy env is present"));
142 Self::json_config(runtime, config)
143 }
144}
145
146fn legacy_env_config(
147 metadata: &std::collections::BTreeMap<String, String>,
148) -> std::collections::BTreeMap<String, String> {
149 metadata
150 .iter()
151 .filter_map(|(key, value)| {
152 key.strip_prefix("env.")
153 .map(|name| (name.to_string(), value.clone()))
154 })
155 .collect()
156}
157
158fn legacy_root_filesystem_config(
159 descriptor: crate::generated_protocol::v1::RootFilesystemDescriptor,
160) -> agentos_vm_config::RootFilesystemConfig {
161 agentos_vm_config::RootFilesystemConfig {
162 mode: match descriptor.mode {
163 crate::generated_protocol::v1::RootFilesystemMode::Ephemeral => {
164 agentos_vm_config::RootFilesystemMode::Ephemeral
165 }
166 crate::generated_protocol::v1::RootFilesystemMode::ReadOnly => {
167 agentos_vm_config::RootFilesystemMode::ReadOnly
168 }
169 },
170 disable_default_base_layer: descriptor.disable_default_base_layer,
171 lowers: descriptor
172 .lowers
173 .into_iter()
174 .map(legacy_root_lower_config)
175 .collect(),
176 bootstrap_entries: descriptor
177 .bootstrap_entries
178 .into_iter()
179 .map(legacy_root_entry_config)
180 .collect(),
181 }
182}
183
184fn legacy_root_lower_config(
185 lower: crate::generated_protocol::v1::RootFilesystemLowerDescriptor,
186) -> agentos_vm_config::RootFilesystemLowerDescriptor {
187 match lower {
188 crate::generated_protocol::v1::RootFilesystemLowerDescriptor::SnapshotRootFilesystemLower(
189 snapshot,
190 ) => agentos_vm_config::RootFilesystemLowerDescriptor::Snapshot {
191 entries: snapshot
192 .entries
193 .into_iter()
194 .map(legacy_root_entry_config)
195 .collect(),
196 },
197 crate::generated_protocol::v1::RootFilesystemLowerDescriptor::BundledBaseFilesystemLower => {
198 agentos_vm_config::RootFilesystemLowerDescriptor::BundledBaseFilesystem
199 }
200 }
201}
202
203fn legacy_root_entry_config(
204 entry: crate::generated_protocol::v1::RootFilesystemEntry,
205) -> agentos_vm_config::RootFilesystemEntry {
206 agentos_vm_config::RootFilesystemEntry {
207 path: entry.path,
208 kind: match entry.kind {
209 crate::generated_protocol::v1::RootFilesystemEntryKind::File => {
210 agentos_vm_config::RootFilesystemEntryKind::File
211 }
212 crate::generated_protocol::v1::RootFilesystemEntryKind::Directory => {
213 agentos_vm_config::RootFilesystemEntryKind::Directory
214 }
215 crate::generated_protocol::v1::RootFilesystemEntryKind::Symlink => {
216 agentos_vm_config::RootFilesystemEntryKind::Symlink
217 }
218 },
219 mode: entry.mode,
220 uid: entry.uid,
221 gid: entry.gid,
222 content: entry.content,
223 encoding: entry.encoding.map(|encoding| match encoding {
224 crate::generated_protocol::v1::RootFilesystemEntryEncoding::Utf8 => {
225 agentos_vm_config::RootFilesystemEntryEncoding::Utf8
226 }
227 crate::generated_protocol::v1::RootFilesystemEntryEncoding::Base64 => {
228 agentos_vm_config::RootFilesystemEntryEncoding::Base64
229 }
230 }),
231 target: entry.target,
232 executable: entry.executable,
233 }
234}
235
236pub fn permissions_policy_config_from_wire(
237 permissions: crate::generated_protocol::v1::PermissionsPolicy,
238) -> agentos_vm_config::PermissionsPolicy {
239 agentos_vm_config::PermissionsPolicy {
240 fs: permissions.fs.map(legacy_fs_permission_scope_config),
241 network: permissions
242 .network
243 .map(legacy_pattern_permission_scope_config),
244 child_process: permissions
245 .child_process
246 .map(legacy_pattern_permission_scope_config),
247 process: permissions
248 .process
249 .map(legacy_pattern_permission_scope_config),
250 env: permissions.env.map(legacy_pattern_permission_scope_config),
251 host_function: permissions
252 .host_function
253 .map(legacy_pattern_permission_scope_config),
254 }
255}
256
257fn legacy_permission_mode_config(
258 mode: crate::generated_protocol::v1::PermissionMode,
259) -> agentos_vm_config::PermissionMode {
260 match mode {
261 crate::generated_protocol::v1::PermissionMode::Allow => {
262 agentos_vm_config::PermissionMode::Allow
263 }
264 crate::generated_protocol::v1::PermissionMode::Ask => {
265 agentos_vm_config::PermissionMode::Ask
266 }
267 crate::generated_protocol::v1::PermissionMode::Deny => {
268 agentos_vm_config::PermissionMode::Deny
269 }
270 }
271}
272
273fn legacy_fs_permission_scope_config(
274 scope: crate::generated_protocol::v1::FsPermissionScope,
275) -> agentos_vm_config::FsPermissionScope {
276 match scope {
277 crate::generated_protocol::v1::FsPermissionScope::PermissionMode(mode) => {
278 agentos_vm_config::FsPermissionScope::Mode(legacy_permission_mode_config(mode))
279 }
280 crate::generated_protocol::v1::FsPermissionScope::FsPermissionRuleSet(rules) => {
281 agentos_vm_config::FsPermissionScope::Rules(agentos_vm_config::FsPermissionRuleSet {
282 default: rules.default.map(legacy_permission_mode_config),
283 rules: rules
284 .rules
285 .into_iter()
286 .map(|rule| agentos_vm_config::FsPermissionRule {
287 mode: legacy_permission_mode_config(rule.mode),
288 operations: rule.operations,
289 paths: rule.paths,
290 })
291 .collect(),
292 })
293 }
294 }
295}
296
297fn legacy_pattern_permission_scope_config(
298 scope: crate::generated_protocol::v1::PatternPermissionScope,
299) -> agentos_vm_config::PatternPermissionScope {
300 match scope {
301 crate::generated_protocol::v1::PatternPermissionScope::PermissionMode(mode) => {
302 agentos_vm_config::PatternPermissionScope::Mode(legacy_permission_mode_config(mode))
303 }
304 crate::generated_protocol::v1::PatternPermissionScope::PatternPermissionRuleSet(rules) => {
305 agentos_vm_config::PatternPermissionScope::Rules(
306 agentos_vm_config::PatternPermissionRuleSet {
307 default: rules.default.map(legacy_permission_mode_config),
308 rules: rules
309 .rules
310 .into_iter()
311 .map(|rule| agentos_vm_config::PatternPermissionRule {
312 mode: legacy_permission_mode_config(rule.mode),
313 operations: rule.operations,
314 patterns: rule.patterns,
315 })
316 .collect(),
317 },
318 )
319 }
320 }
321}
322
323fn legacy_dns_config(
324 metadata: &std::collections::BTreeMap<String, String>,
325) -> Option<agentos_vm_config::VmDnsConfig> {
326 let mut dns = agentos_vm_config::VmDnsConfig::default();
327 if let Some(value) = metadata.get("network.dns.servers") {
328 dns.name_servers = value
329 .split(',')
330 .map(str::trim)
331 .filter(|entry| !entry.is_empty())
332 .map(str::to_string)
333 .collect();
334 }
335 for (key, value) in metadata {
336 let Some(hostname) = key.strip_prefix("network.dns.override.") else {
337 continue;
338 };
339 dns.overrides.insert(
340 hostname.to_string(),
341 value
342 .split(',')
343 .map(str::trim)
344 .filter(|entry| !entry.is_empty())
345 .map(str::to_string)
346 .collect(),
347 );
348 }
349 if dns.name_servers.is_empty() && dns.overrides.is_empty() {
350 None
351 } else {
352 Some(dns)
353 }
354}
355
356fn legacy_native_root_config(
357 metadata: &std::collections::BTreeMap<String, String>,
358) -> Option<agentos_vm_config::NativeRootFilesystemConfig> {
359 let id = metadata.get("rootFilesystem.nativePlugin.id")?;
360 let config = metadata
361 .get("rootFilesystem.nativePlugin.config")
362 .map(|value| serde_json::from_str(value).expect("parse native root plugin config"))
363 .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
364 let read_only = metadata
365 .get("rootFilesystem.nativePlugin.readOnly")
366 .map(|value| value.parse::<bool>().expect("parse native root readOnly"))
367 .unwrap_or(false);
368 Some(agentos_vm_config::NativeRootFilesystemConfig {
369 plugin: agentos_vm_config::MountPluginDescriptor {
370 id: id.clone(),
371 config,
372 },
373 read_only,
374 })
375}
376
377fn legacy_listen_config(
378 metadata: &std::collections::BTreeMap<String, String>,
379) -> Option<agentos_vm_config::VmListenPolicyConfig> {
380 let listen = agentos_vm_config::VmListenPolicyConfig {
381 port_min: metadata
382 .get("network.listen.port_min")
383 .map(|value| value.parse::<u16>().expect("parse network.listen.port_min")),
384 port_max: metadata
385 .get("network.listen.port_max")
386 .map(|value| value.parse::<u16>().expect("parse network.listen.port_max")),
387 allow_privileged: metadata
388 .get("network.listen.allow_privileged")
389 .map(|value| {
390 value
391 .parse::<bool>()
392 .expect("parse network.listen.allow_privileged")
393 }),
394 };
395 if listen.port_min.is_none() && listen.port_max.is_none() && listen.allow_privileged.is_none() {
396 None
397 } else {
398 Some(listen)
399 }
400}
401
402fn legacy_loopback_exempt_ports(env: &std::collections::BTreeMap<String, String>) -> Vec<u16> {
403 let Some(value) = env.get("AGENTOS_LOOPBACK_EXEMPT_PORTS") else {
404 return Vec::new();
405 };
406 serde_json::from_str::<Vec<serde_json::Value>>(value)
407 .unwrap_or_default()
408 .into_iter()
409 .filter_map(|value| match value {
410 serde_json::Value::Number(number) => number.as_u64(),
411 serde_json::Value::String(value) => value.parse::<u64>().ok(),
412 _ => None,
413 })
414 .filter_map(|port| u16::try_from(port).ok())
415 .collect()
416}
417
418fn legacy_limits_config(
419 metadata: &std::collections::BTreeMap<String, String>,
420) -> Option<agentos_vm_config::VmLimitsConfig> {
421 let resources = agentos_vm_config::ResourceLimitsConfig {
422 cpu_count: legacy_u64(metadata, "resource.cpu_count"),
423 max_processes: legacy_u64(metadata, "resource.max_processes"),
424 max_open_fds: legacy_u64(metadata, "resource.max_open_fds"),
425 max_pipes: legacy_u64(metadata, "resource.max_pipes"),
426 max_ptys: legacy_u64(metadata, "resource.max_ptys"),
427 max_sockets: legacy_u64(metadata, "resource.max_sockets"),
428 max_connections: legacy_u64(metadata, "resource.max_connections"),
429 max_socket_buffered_bytes: legacy_u64(metadata, "resource.max_socket_buffered_bytes"),
430 max_socket_datagram_queue_len: legacy_u64(
431 metadata,
432 "resource.max_socket_datagram_queue_len",
433 ),
434 max_filesystem_bytes: legacy_u64(metadata, "resource.max_filesystem_bytes"),
435 max_inode_count: legacy_u64(metadata, "resource.max_inode_count"),
436 max_blocking_read_ms: legacy_u64(metadata, "resource.max_blocking_read_ms"),
437 max_pread_bytes: legacy_u64(metadata, "resource.max_pread_bytes"),
438 max_fd_write_bytes: legacy_u64(metadata, "resource.max_fd_write_bytes"),
439 max_process_argv_bytes: legacy_u64(metadata, "resource.max_process_argv_bytes"),
440 max_process_env_bytes: legacy_u64(metadata, "resource.max_process_env_bytes"),
441 max_readdir_entries: legacy_u64(metadata, "resource.max_readdir_entries"),
442 max_recursive_fs_depth: legacy_u64(metadata, "resource.max_recursive_fs_depth"),
443 max_recursive_fs_entries: legacy_u64(metadata, "resource.max_recursive_fs_entries"),
444 max_wasm_fuel: legacy_u64(metadata, "resource.max_wasm_fuel"),
445 max_wasm_memory_bytes: legacy_u64(metadata, "resource.max_wasm_memory_bytes"),
446 max_wasm_stack_bytes: legacy_u64(metadata, "resource.max_wasm_stack_bytes"),
447 };
448 let http = agentos_vm_config::HttpLimitsConfig {
449 max_fetch_response_bytes: legacy_u64(metadata, "limits.http.max_fetch_response_bytes"),
450 };
451 let host_functions = agentos_vm_config::HostFunctionLimitsConfig {
452 default_timeout_ms: legacy_u64(metadata, "limits.host_functions.default_timeout_ms"),
453 max_timeout_ms: legacy_u64(metadata, "limits.host_functions.max_timeout_ms"),
454 max_registered_collections: legacy_u64(
455 metadata,
456 "limits.host_functions.max_registered_collections",
457 ),
458 max_registered_functions_per_vm: legacy_u64(
459 metadata,
460 "limits.host_functions.max_registered_functions_per_vm",
461 ),
462 max_functions_per_collection: legacy_u64(
463 metadata,
464 "limits.host_functions.max_functions_per_collection",
465 ),
466 max_schema_bytes: legacy_u64(metadata, "limits.host_functions.max_schema_bytes"),
467 max_examples_per_function: legacy_u64(
468 metadata,
469 "limits.host_functions.max_examples_per_function",
470 ),
471 max_example_input_bytes: legacy_u64(
472 metadata,
473 "limits.host_functions.max_example_input_bytes",
474 ),
475 };
476 let plugins = agentos_vm_config::PluginLimitsConfig {
477 max_persisted_manifest_bytes: legacy_u64(
478 metadata,
479 "limits.plugins.max_persisted_manifest_bytes",
480 ),
481 max_persisted_manifest_file_bytes: legacy_u64(
482 metadata,
483 "limits.plugins.max_persisted_manifest_file_bytes",
484 ),
485 };
486 let sqlite = agentos_vm_config::SqliteLimitsConfig {
487 max_result_bytes: legacy_u64(metadata, "limits.sqlite.max_result_bytes"),
488 };
489 let js_runtime = agentos_vm_config::JsRuntimeLimitsConfig {
490 v8_heap_limit_mb: legacy_u64(metadata, "limits.js_runtime.v8_heap_limit_mb"),
491 sync_rpc_wait_timeout_ms: legacy_u64(
492 metadata,
493 "limits.js_runtime.sync_rpc_wait_timeout_ms",
494 ),
495 cpu_time_limit_ms: legacy_u64(metadata, "limits.js_runtime.cpu_time_limit_ms"),
496 wall_clock_limit_ms: legacy_u64(metadata, "limits.js_runtime.wall_clock_limit_ms"),
497 import_cache_materialize_timeout_ms: legacy_u64(
498 metadata,
499 "limits.js_runtime.import_cache_materialize_timeout_ms",
500 ),
501 captured_output_limit_bytes: legacy_u64(
502 metadata,
503 "limits.js_runtime.captured_output_limit_bytes",
504 ),
505 stdin_buffer_limit_bytes: legacy_u64(
506 metadata,
507 "limits.js_runtime.stdin_buffer_limit_bytes",
508 ),
509 event_payload_limit_bytes: legacy_u64(
510 metadata,
511 "limits.js_runtime.event_payload_limit_bytes",
512 ),
513 max_timers: legacy_u64(metadata, "limits.js_runtime.max_timers"),
514 v8_ipc_max_frame_bytes: legacy_u64(metadata, "limits.js_runtime.v8_ipc_max_frame_bytes"),
515 };
516 let python = agentos_vm_config::PythonLimitsConfig {
517 output_buffer_max_bytes: legacy_u64(metadata, "limits.python.output_buffer_max_bytes"),
518 execution_timeout_ms: legacy_u64(metadata, "limits.python.execution_timeout_ms"),
519 max_old_space_mb: legacy_u64(metadata, "limits.python.max_old_space_mb"),
520 vfs_rpc_timeout_ms: legacy_u64(metadata, "limits.python.vfs_rpc_timeout_ms"),
521 };
522 let wasm = agentos_vm_config::WasmLimitsConfig {
523 max_module_file_bytes: legacy_u64(metadata, "limits.wasm.max_module_file_bytes"),
524 captured_output_limit_bytes: legacy_u64(
525 metadata,
526 "limits.wasm.captured_output_limit_bytes",
527 ),
528 sync_read_limit_bytes: legacy_u64(metadata, "limits.wasm.sync_read_limit_bytes"),
529 prewarm_timeout_ms: legacy_u64(metadata, "limits.wasm.prewarm_timeout_ms"),
530 runner_heap_limit_mb: legacy_u64(metadata, "limits.wasm.runner_heap_limit_mb"),
531 runner_cpu_time_limit_ms: legacy_u64(metadata, "limits.wasm.runner_cpu_time_limit_ms"),
532 };
533 let execution = agentos_vm_config::ExecutionLimitsConfig {
534 completed_ttl_ms: legacy_u64(metadata, "limits.execution.completed_ttl_ms"),
535 max_completed_executions: legacy_u64(metadata, "limits.execution.max_completed_executions"),
536 live_execution_warning_threshold: legacy_u64(
537 metadata,
538 "limits.execution.live_execution_warning_threshold",
539 ),
540 };
541 let process = agentos_vm_config::ProcessLimitsConfig {
542 max_spawn_file_actions: legacy_u64(metadata, "limits.process.max_spawn_file_actions")
543 .or_else(|| legacy_u64(metadata, "limits.wasm.max_spawn_file_actions")),
544 max_spawn_file_action_bytes: legacy_u64(
545 metadata,
546 "limits.process.max_spawn_file_action_bytes",
547 )
548 .or_else(|| legacy_u64(metadata, "limits.wasm.max_spawn_file_action_bytes")),
549 pending_stdin_bytes: legacy_u64(metadata, "limits.process.pending_stdin_bytes"),
550 pending_event_count: legacy_u64(metadata, "limits.process.pending_event_count"),
551 pending_event_bytes: legacy_u64(metadata, "limits.process.pending_event_bytes"),
552 output_replay_events: legacy_u64(metadata, "limits.process.output_replay_events"),
553 output_replay_bytes: legacy_u64(metadata, "limits.process.output_replay_bytes"),
554 output_replay_page_events: legacy_u64(metadata, "limits.process.output_replay_page_events"),
555 output_replay_page_bytes: legacy_u64(metadata, "limits.process.output_replay_page_bytes"),
556 max_output_replays: legacy_u64(metadata, "limits.process.max_output_replays"),
557 };
558
559 let config = agentos_vm_config::VmLimitsConfig {
560 reactor: None,
561 resources: legacy_has_resource_limits(&resources).then_some(resources),
562 http: http.max_fetch_response_bytes.is_some().then_some(http),
563 udp: None,
564 tls: None,
565 http2: None,
566 host_functions: legacy_has_host_function_limits(&host_functions).then_some(host_functions),
567 plugins: legacy_has_plugin_limits(&plugins).then_some(plugins),
568 sqlite: sqlite.max_result_bytes.is_some().then_some(sqlite),
569 js_runtime: legacy_has_js_runtime_limits(&js_runtime).then_some(js_runtime),
570 python: legacy_has_python_limits(&python).then_some(python),
571 wasm: legacy_has_wasm_limits(&wasm).then_some(wasm),
572 execution: (execution.completed_ttl_ms.is_some()
573 || execution.max_completed_executions.is_some()
574 || execution.live_execution_warning_threshold.is_some())
575 .then_some(execution),
576 process: legacy_has_process_limits(&process).then_some(process),
577 agentos_packages: None,
578 };
579
580 if config.resources.is_none()
581 && config.http.is_none()
582 && config.host_functions.is_none()
583 && config.plugins.is_none()
584 && config.sqlite.is_none()
585 && config.js_runtime.is_none()
586 && config.python.is_none()
587 && config.wasm.is_none()
588 && config.execution.is_none()
589 && config.process.is_none()
590 {
591 None
592 } else {
593 Some(config)
594 }
595}
596
597fn legacy_u64(metadata: &std::collections::BTreeMap<String, String>, key: &str) -> Option<u64> {
598 metadata.get(key).map(|value| {
599 value
600 .parse::<u64>()
601 .unwrap_or_else(|error| panic!("parse {key}: {error}"))
602 })
603}
604
605fn legacy_has_resource_limits(config: &agentos_vm_config::ResourceLimitsConfig) -> bool {
606 config.cpu_count.is_some()
607 || config.max_processes.is_some()
608 || config.max_open_fds.is_some()
609 || config.max_pipes.is_some()
610 || config.max_ptys.is_some()
611 || config.max_sockets.is_some()
612 || config.max_connections.is_some()
613 || config.max_socket_buffered_bytes.is_some()
614 || config.max_socket_datagram_queue_len.is_some()
615 || config.max_filesystem_bytes.is_some()
616 || config.max_inode_count.is_some()
617 || config.max_blocking_read_ms.is_some()
618 || config.max_pread_bytes.is_some()
619 || config.max_fd_write_bytes.is_some()
620 || config.max_process_argv_bytes.is_some()
621 || config.max_process_env_bytes.is_some()
622 || config.max_readdir_entries.is_some()
623 || config.max_wasm_fuel.is_some()
624 || config.max_wasm_memory_bytes.is_some()
625 || config.max_wasm_stack_bytes.is_some()
626}
627
628fn legacy_has_host_function_limits(config: &agentos_vm_config::HostFunctionLimitsConfig) -> bool {
629 config.default_timeout_ms.is_some()
630 || config.max_timeout_ms.is_some()
631 || config.max_registered_collections.is_some()
632 || config.max_registered_functions_per_vm.is_some()
633 || config.max_functions_per_collection.is_some()
634 || config.max_schema_bytes.is_some()
635 || config.max_examples_per_function.is_some()
636 || config.max_example_input_bytes.is_some()
637}
638
639fn legacy_has_plugin_limits(config: &agentos_vm_config::PluginLimitsConfig) -> bool {
640 config.max_persisted_manifest_bytes.is_some()
641 || config.max_persisted_manifest_file_bytes.is_some()
642}
643
644fn legacy_has_js_runtime_limits(config: &agentos_vm_config::JsRuntimeLimitsConfig) -> bool {
645 config.v8_heap_limit_mb.is_some()
646 || config.sync_rpc_wait_timeout_ms.is_some()
647 || config.cpu_time_limit_ms.is_some()
648 || config.wall_clock_limit_ms.is_some()
649 || config.import_cache_materialize_timeout_ms.is_some()
650 || config.captured_output_limit_bytes.is_some()
651 || config.stdin_buffer_limit_bytes.is_some()
652 || config.event_payload_limit_bytes.is_some()
653 || config.max_timers.is_some()
654 || config.v8_ipc_max_frame_bytes.is_some()
655}
656
657fn legacy_has_python_limits(config: &agentos_vm_config::PythonLimitsConfig) -> bool {
658 config.output_buffer_max_bytes.is_some()
659 || config.execution_timeout_ms.is_some()
660 || config.max_old_space_mb.is_some()
661 || config.vfs_rpc_timeout_ms.is_some()
662}
663
664fn legacy_has_wasm_limits(config: &agentos_vm_config::WasmLimitsConfig) -> bool {
665 config.max_module_file_bytes.is_some()
666 || config.captured_output_limit_bytes.is_some()
667 || config.sync_read_limit_bytes.is_some()
668 || config.prewarm_timeout_ms.is_some()
669 || config.runner_heap_limit_mb.is_some()
670 || config.runner_cpu_time_limit_ms.is_some()
671}
672
673fn legacy_has_process_limits(config: &agentos_vm_config::ProcessLimitsConfig) -> bool {
674 config.max_spawn_file_actions.is_some()
675 || config.max_spawn_file_action_bytes.is_some()
676 || config.pending_stdin_bytes.is_some()
677 || config.pending_event_count.is_some()
678 || config.pending_event_bytes.is_some()
679}
680
681impl crate::generated_protocol::v1::OwnershipScope {
687 pub fn connection(connection_id: impl Into<String>) -> Self {
688 Self::ConnectionOwnership(crate::generated_protocol::v1::ConnectionOwnership {
689 connection_id: connection_id.into(),
690 })
691 }
692
693 pub fn session(connection_id: impl Into<String>, session_id: impl Into<String>) -> Self {
694 Self::SessionOwnership(crate::generated_protocol::v1::SessionOwnership {
695 connection_id: connection_id.into(),
696 session_id: session_id.into(),
697 })
698 }
699
700 pub fn vm(
701 connection_id: impl Into<String>,
702 session_id: impl Into<String>,
703 vm_id: impl Into<String>,
704 ) -> Self {
705 Self::VmOwnership(crate::generated_protocol::v1::VmOwnership {
706 connection_id: connection_id.into(),
707 session_id: session_id.into(),
708 vm_id: vm_id.into(),
709 })
710 }
711}
712
713pub const PROTOCOL_NAME: &str = "agentos-native-sidecar";
714pub const PROTOCOL_VERSION: u16 = 11;
715pub const DEFAULT_MAX_FRAME_BYTES: usize = 16 * 1024 * 1024;
718
719pub const MAX_GUEST_FILESYSTEM_WRITE_FILES_ENTRIES: usize = 16;
725
726#[derive(Debug, Clone, PartialEq, Eq)]
727pub enum ProtocolCodecError {
728 TruncatedFrame {
729 actual: usize,
730 },
731 LengthPrefixMismatch {
732 declared: usize,
733 actual: usize,
734 },
735 FrameTooLarge {
736 size: usize,
737 max: usize,
738 },
739 UnsupportedSchema {
740 name: String,
741 version: u16,
742 },
743 InvalidRequestId,
744 InvalidRequestDirection {
745 request_id: RequestId,
746 expected: RequestDirection,
747 },
748 EmptyOwnershipField {
749 field: &'static str,
750 },
751 EmptyAuthToken,
752 InvalidOwnershipScope {
753 required: OwnershipRequirement,
754 actual: OwnershipRequirement,
755 },
756 SerializeFailure(String),
757 DeserializeFailure(String),
758}
759
760impl fmt::Display for ProtocolCodecError {
761 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
762 match self {
763 Self::TruncatedFrame { actual } => {
764 write!(
765 f,
766 "protocol frame is truncated: only {actual} bytes provided"
767 )
768 }
769 Self::LengthPrefixMismatch { declared, actual } => write!(
770 f,
771 "protocol frame length prefix mismatch: declared {declared} bytes, got {actual}",
772 ),
773 Self::FrameTooLarge { size, max } => {
774 write!(f, "protocol frame is {size} bytes, limit is {max}")
775 }
776 Self::UnsupportedSchema { name, version } => write!(
777 f,
778 "unsupported protocol schema {name}@{version}; expected {PROTOCOL_NAME}@{PROTOCOL_VERSION}",
779 ),
780 Self::InvalidRequestId => write!(f, "protocol request identifiers must be non-zero"),
781 Self::InvalidRequestDirection {
782 request_id,
783 expected,
784 } => write!(f, "protocol request id {request_id} must be {expected}",),
785 Self::EmptyOwnershipField { field } => {
786 write!(f, "protocol ownership field `{field}` cannot be empty")
787 }
788 Self::EmptyAuthToken => {
789 write!(f, "authenticate requests require a non-empty auth token")
790 }
791 Self::InvalidOwnershipScope { required, actual } => write!(
792 f,
793 "protocol frame requires {required} ownership but carried {actual}",
794 ),
795 Self::SerializeFailure(message) => {
796 write!(f, "protocol frame serialization failed: {message}")
797 }
798 Self::DeserializeFailure(message) => {
799 write!(f, "protocol frame deserialization failed: {message}")
800 }
801 }
802 }
803}
804
805impl Error for ProtocolCodecError {}
806
807#[derive(Debug, Clone, Copy, PartialEq, Eq)]
808pub enum OwnershipRequirement {
809 Any,
810 Connection,
811 Session,
812 Vm,
813 SessionOrVm,
814}
815
816impl fmt::Display for OwnershipRequirement {
817 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
818 match self {
819 Self::Any => write!(f, "any"),
820 Self::Connection => write!(f, "connection"),
821 Self::Session => write!(f, "session"),
822 Self::Vm => write!(f, "vm"),
823 Self::SessionOrVm => write!(f, "session-or-vm"),
824 }
825 }
826}
827
828#[derive(Debug, Clone, Copy, PartialEq, Eq)]
829pub enum RequestDirection {
830 Host,
831 Sidecar,
832}
833
834impl fmt::Display for RequestDirection {
835 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
836 match self {
837 Self::Host => write!(f, "positive"),
838 Self::Sidecar => write!(f, "negative"),
839 }
840 }
841}
842
843#[derive(Debug, Clone, PartialEq, Eq)]
844pub struct WireDispatchResult {
845 pub response: ResponseFrame,
846 pub events: Vec<EventFrame>,
847}
848
849#[derive(Debug, Clone, PartialEq, Eq)]
850pub struct CompatDispatchResult {
851 pub response: crate::protocol::ResponseFrame,
852 pub events: Vec<crate::protocol::EventFrame>,
853}
854
855#[derive(Debug, Clone)]
856pub struct WireFrameCodec {
857 max_frame_bytes: usize,
858}
859
860impl WireFrameCodec {
861 pub fn new(max_frame_bytes: usize) -> Self {
862 Self { max_frame_bytes }
863 }
864
865 pub fn max_frame_bytes(&self) -> usize {
866 self.max_frame_bytes
867 }
868
869 pub fn encode(&self, frame: &ProtocolFrame) -> Result<Vec<u8>, ProtocolCodecError> {
870 validate_frame(frame)?;
871
872 let payload = serde_bare::to_vec(frame)
873 .map_err(|error| ProtocolCodecError::SerializeFailure(error.to_string()))?;
874 if payload.len() > self.max_frame_bytes {
875 return Err(ProtocolCodecError::FrameTooLarge {
876 size: payload.len(),
877 max: self.max_frame_bytes,
878 });
879 }
880
881 let length =
882 u32::try_from(payload.len()).map_err(|_| ProtocolCodecError::FrameTooLarge {
883 size: payload.len(),
884 max: u32::MAX as usize,
885 })?;
886
887 let mut encoded = Vec::with_capacity(4 + payload.len());
888 encoded.extend_from_slice(&length.to_be_bytes());
889 encoded.extend_from_slice(&payload);
890 Ok(encoded)
891 }
892
893 pub fn decode(&self, bytes: &[u8]) -> Result<ProtocolFrame, ProtocolCodecError> {
894 let payload = self.checked_payload(bytes)?;
895 let frame = serde_bare::from_slice(payload)
896 .map_err(|error| ProtocolCodecError::DeserializeFailure(error.to_string()))?;
897 validate_frame(&frame)?;
898 Ok(frame)
899 }
900
901 pub fn encode_message(&self, frame: &ProtocolFrame) -> Result<Vec<u8>, ProtocolCodecError> {
909 validate_frame(frame)?;
910 let payload = serde_bare::to_vec(frame)
911 .map_err(|error| ProtocolCodecError::SerializeFailure(error.to_string()))?;
912 if payload.len() > self.max_frame_bytes {
913 return Err(ProtocolCodecError::FrameTooLarge {
914 size: payload.len(),
915 max: self.max_frame_bytes,
916 });
917 }
918 Ok(payload)
919 }
920
921 pub fn decode_message(&self, bytes: &[u8]) -> Result<ProtocolFrame, ProtocolCodecError> {
923 if bytes.len() > self.max_frame_bytes {
924 return Err(ProtocolCodecError::FrameTooLarge {
925 size: bytes.len(),
926 max: self.max_frame_bytes,
927 });
928 }
929 let frame = serde_bare::from_slice(bytes)
930 .map_err(|error| ProtocolCodecError::DeserializeFailure(error.to_string()))?;
931 validate_frame(&frame)?;
932 Ok(frame)
933 }
934
935 fn checked_payload<'a>(&self, bytes: &'a [u8]) -> Result<&'a [u8], ProtocolCodecError> {
936 if bytes.len() < 4 {
937 return Err(ProtocolCodecError::TruncatedFrame {
938 actual: bytes.len(),
939 });
940 }
941
942 let declared =
943 u32::from_be_bytes(bytes[..4].try_into().expect("length prefix is four bytes"))
944 as usize;
945 if declared > self.max_frame_bytes {
946 return Err(ProtocolCodecError::FrameTooLarge {
947 size: declared,
948 max: self.max_frame_bytes,
949 });
950 }
951
952 let actual = bytes.len() - 4;
953 if declared != actual {
954 return Err(ProtocolCodecError::LengthPrefixMismatch { declared, actual });
955 }
956
957 Ok(&bytes[4..])
958 }
959}
960
961impl Default for WireFrameCodec {
962 fn default() -> Self {
963 Self::new(DEFAULT_MAX_FRAME_BYTES)
964 }
965}
966
967pub fn protocol_schema() -> ProtocolSchema {
968 ProtocolSchema::current()
969}
970
971impl ProtocolSchema {
972 pub fn current() -> Self {
973 Self {
974 name: PROTOCOL_NAME.to_string(),
975 version: PROTOCOL_VERSION,
976 }
977 }
978}
979
980impl Default for ProtocolSchema {
981 fn default() -> Self {
982 Self::current()
983 }
984}
985
986pub fn request_frame_to_compat(
987 request: RequestFrame,
988) -> Result<crate::protocol::RequestFrame, ProtocolCodecError> {
989 match crate::protocol::from_generated_protocol_frame(ProtocolFrame::RequestFrame(request))? {
990 crate::protocol::ProtocolFrame::Request(request) => Ok(request),
991 crate::protocol::ProtocolFrame::Response(_)
992 | crate::protocol::ProtocolFrame::Event(_)
993 | crate::protocol::ProtocolFrame::SidecarRequest(_)
994 | crate::protocol::ProtocolFrame::SidecarResponse(_)
995 | crate::protocol::ProtocolFrame::Control(_) => {
996 Err(ProtocolCodecError::DeserializeFailure(String::from(
997 "wire request frame converted to non-request compatibility frame",
998 )))
999 }
1000 }
1001}
1002
1003pub fn ownership_scope_to_compat(ownership: OwnershipScope) -> crate::protocol::OwnershipScope {
1004 crate::protocol::from_generated_ownership_scope(ownership)
1005}
1006
1007pub fn request_payload_to_compat(
1008 ownership: &crate::protocol::OwnershipScope,
1009 payload: RequestPayload,
1010) -> Result<crate::protocol::RequestPayload, ProtocolCodecError> {
1011 match crate::protocol::from_generated_protocol_frame(ProtocolFrame::RequestFrame(
1012 RequestFrame {
1013 schema: protocol_schema(),
1014 request_id: 1,
1015 ownership: crate::protocol::to_generated_ownership_scope(ownership),
1016 payload,
1017 },
1018 ))? {
1019 crate::protocol::ProtocolFrame::Request(request) => Ok(request.payload),
1020 crate::protocol::ProtocolFrame::Response(_)
1021 | crate::protocol::ProtocolFrame::Event(_)
1022 | crate::protocol::ProtocolFrame::SidecarRequest(_)
1023 | crate::protocol::ProtocolFrame::SidecarResponse(_)
1024 | crate::protocol::ProtocolFrame::Control(_) => {
1025 Err(ProtocolCodecError::DeserializeFailure(String::from(
1026 "wire request payload converted to non-request compatibility frame",
1027 )))
1028 }
1029 }
1030}
1031
1032pub fn response_payload_from_compat(
1033 ownership: &crate::protocol::OwnershipScope,
1034 payload: crate::protocol::ResponsePayload,
1035) -> Result<ResponsePayload, ProtocolCodecError> {
1036 match crate::protocol::to_generated_protocol_frame(&crate::protocol::ProtocolFrame::Response(
1037 crate::protocol::ResponseFrame::new(1, ownership.clone(), payload),
1038 ))? {
1039 ProtocolFrame::ResponseFrame(response) => Ok(response.payload),
1040 ProtocolFrame::RequestFrame(_)
1041 | ProtocolFrame::EventFrame(_)
1042 | ProtocolFrame::SidecarRequestFrame(_)
1043 | ProtocolFrame::SidecarResponseFrame(_)
1044 | ProtocolFrame::ControlFrame(_) => Err(ProtocolCodecError::SerializeFailure(
1045 String::from("compatibility response payload converted to non-response wire frame"),
1046 )),
1047 }
1048}
1049
1050pub fn event_frame_from_compat(
1051 event: crate::protocol::EventFrame,
1052) -> Result<EventFrame, ProtocolCodecError> {
1053 match crate::protocol::to_generated_protocol_frame(&crate::protocol::ProtocolFrame::Event(
1054 event,
1055 ))? {
1056 ProtocolFrame::EventFrame(event) => Ok(event),
1057 ProtocolFrame::RequestFrame(_)
1058 | ProtocolFrame::ResponseFrame(_)
1059 | ProtocolFrame::SidecarRequestFrame(_)
1060 | ProtocolFrame::SidecarResponseFrame(_)
1061 | ProtocolFrame::ControlFrame(_) => Err(ProtocolCodecError::SerializeFailure(
1062 String::from("compatibility event converted to non-event wire frame"),
1063 )),
1064 }
1065}
1066
1067pub fn event_frame_to_compat(
1068 event: EventFrame,
1069) -> Result<crate::protocol::EventFrame, ProtocolCodecError> {
1070 match crate::protocol::from_generated_protocol_frame(ProtocolFrame::EventFrame(event))? {
1071 crate::protocol::ProtocolFrame::Event(event) => Ok(event),
1072 crate::protocol::ProtocolFrame::Request(_)
1073 | crate::protocol::ProtocolFrame::Response(_)
1074 | crate::protocol::ProtocolFrame::SidecarRequest(_)
1075 | crate::protocol::ProtocolFrame::SidecarResponse(_)
1076 | crate::protocol::ProtocolFrame::Control(_) => {
1077 Err(ProtocolCodecError::DeserializeFailure(String::from(
1078 "wire event converted to non-event compatibility frame",
1079 )))
1080 }
1081 }
1082}
1083
1084pub fn sidecar_request_frame_from_compat(
1085 request: crate::protocol::SidecarRequestFrame,
1086) -> Result<SidecarRequestFrame, ProtocolCodecError> {
1087 match crate::protocol::to_generated_protocol_frame(
1088 &crate::protocol::ProtocolFrame::SidecarRequest(request),
1089 )? {
1090 ProtocolFrame::SidecarRequestFrame(request) => Ok(request),
1091 ProtocolFrame::RequestFrame(_)
1092 | ProtocolFrame::ResponseFrame(_)
1093 | ProtocolFrame::EventFrame(_)
1094 | ProtocolFrame::SidecarResponseFrame(_)
1095 | ProtocolFrame::ControlFrame(_) => {
1096 Err(ProtocolCodecError::SerializeFailure(String::from(
1097 "compatibility sidecar request converted to non-sidecar-request wire frame",
1098 )))
1099 }
1100 }
1101}
1102
1103pub fn sidecar_request_payload_to_compat(
1104 ownership: &crate::protocol::OwnershipScope,
1105 payload: SidecarRequestPayload,
1106) -> Result<crate::protocol::SidecarRequestPayload, ProtocolCodecError> {
1107 match crate::protocol::from_generated_protocol_frame(ProtocolFrame::SidecarRequestFrame(
1108 SidecarRequestFrame {
1109 schema: protocol_schema(),
1110 request_id: -1,
1111 ownership: crate::protocol::to_generated_ownership_scope(ownership),
1112 payload,
1113 },
1114 ))? {
1115 crate::protocol::ProtocolFrame::SidecarRequest(request) => Ok(request.payload),
1116 crate::protocol::ProtocolFrame::Request(_)
1117 | crate::protocol::ProtocolFrame::Response(_)
1118 | crate::protocol::ProtocolFrame::Event(_)
1119 | crate::protocol::ProtocolFrame::SidecarResponse(_)
1120 | crate::protocol::ProtocolFrame::Control(_) => {
1121 Err(ProtocolCodecError::DeserializeFailure(String::from(
1122 "wire sidecar request payload converted to non-sidecar-request compatibility frame",
1123 )))
1124 }
1125 }
1126}
1127
1128pub fn sidecar_response_frame_to_compat(
1129 response: SidecarResponseFrame,
1130) -> Result<crate::protocol::SidecarResponseFrame, ProtocolCodecError> {
1131 match crate::protocol::from_generated_protocol_frame(ProtocolFrame::SidecarResponseFrame(
1132 response,
1133 ))? {
1134 crate::protocol::ProtocolFrame::SidecarResponse(response) => Ok(response),
1135 crate::protocol::ProtocolFrame::Request(_)
1136 | crate::protocol::ProtocolFrame::Response(_)
1137 | crate::protocol::ProtocolFrame::Event(_)
1138 | crate::protocol::ProtocolFrame::SidecarRequest(_)
1139 | crate::protocol::ProtocolFrame::Control(_) => {
1140 Err(ProtocolCodecError::DeserializeFailure(String::from(
1141 "wire sidecar response converted to non-sidecar-response compatibility frame",
1142 )))
1143 }
1144 }
1145}
1146
1147pub fn sidecar_response_frame_from_compat(
1148 response: crate::protocol::SidecarResponseFrame,
1149) -> Result<SidecarResponseFrame, ProtocolCodecError> {
1150 match crate::protocol::to_generated_protocol_frame(
1151 &crate::protocol::ProtocolFrame::SidecarResponse(response),
1152 )? {
1153 ProtocolFrame::SidecarResponseFrame(response) => Ok(response),
1154 ProtocolFrame::RequestFrame(_)
1155 | ProtocolFrame::ResponseFrame(_)
1156 | ProtocolFrame::EventFrame(_)
1157 | ProtocolFrame::SidecarRequestFrame(_)
1158 | ProtocolFrame::ControlFrame(_) => {
1159 Err(ProtocolCodecError::SerializeFailure(String::from(
1160 "compatibility sidecar response converted to non-sidecar-response wire frame",
1161 )))
1162 }
1163 }
1164}
1165
1166pub fn dispatch_result_from_compat(
1167 result: CompatDispatchResult,
1168) -> Result<WireDispatchResult, ProtocolCodecError> {
1169 let response = match crate::protocol::to_generated_protocol_frame(
1170 &crate::protocol::ProtocolFrame::Response(result.response),
1171 )? {
1172 ProtocolFrame::ResponseFrame(response) => response,
1173 ProtocolFrame::RequestFrame(_)
1174 | ProtocolFrame::EventFrame(_)
1175 | ProtocolFrame::SidecarRequestFrame(_)
1176 | ProtocolFrame::SidecarResponseFrame(_)
1177 | ProtocolFrame::ControlFrame(_) => {
1178 return Err(ProtocolCodecError::SerializeFailure(String::from(
1179 "compatibility dispatch response converted to non-response wire frame",
1180 )));
1181 }
1182 };
1183
1184 let events = result
1185 .events
1186 .into_iter()
1187 .map(|event| {
1188 match crate::protocol::to_generated_protocol_frame(
1189 &crate::protocol::ProtocolFrame::Event(event),
1190 )? {
1191 ProtocolFrame::EventFrame(event) => Ok(event),
1192 ProtocolFrame::RequestFrame(_)
1193 | ProtocolFrame::ResponseFrame(_)
1194 | ProtocolFrame::SidecarRequestFrame(_)
1195 | ProtocolFrame::SidecarResponseFrame(_)
1196 | ProtocolFrame::ControlFrame(_) => Err(ProtocolCodecError::SerializeFailure(
1197 String::from("compatibility dispatch event converted to non-event wire frame"),
1198 )),
1199 }
1200 })
1201 .collect::<Result<Vec<_>, _>>()?;
1202
1203 Ok(WireDispatchResult { response, events })
1204}
1205
1206fn validate_frame(frame: &ProtocolFrame) -> Result<(), ProtocolCodecError> {
1207 match frame {
1208 ProtocolFrame::RequestFrame(frame) => {
1209 validate_schema(&frame.schema)?;
1210 validate_request_id(frame.request_id)
1211 }
1212 ProtocolFrame::ResponseFrame(frame) => {
1213 validate_schema(&frame.schema)?;
1214 validate_request_id(frame.request_id)
1215 }
1216 ProtocolFrame::EventFrame(frame) => validate_schema(&frame.schema),
1217 ProtocolFrame::SidecarRequestFrame(frame) => {
1218 validate_schema(&frame.schema)?;
1219 validate_request_id(frame.request_id)
1220 }
1221 ProtocolFrame::SidecarResponseFrame(frame) => {
1222 validate_schema(&frame.schema)?;
1223 validate_request_id(frame.request_id)
1224 }
1225 ProtocolFrame::ControlFrame(frame) => validate_schema(&frame.schema),
1226 }
1227}
1228
1229fn validate_schema(schema: &ProtocolSchema) -> Result<(), ProtocolCodecError> {
1230 if schema.name != PROTOCOL_NAME || schema.version != PROTOCOL_VERSION {
1231 return Err(ProtocolCodecError::UnsupportedSchema {
1232 name: schema.name.clone(),
1233 version: schema.version,
1234 });
1235 }
1236 Ok(())
1237}
1238
1239fn validate_request_id(request_id: RequestId) -> Result<(), ProtocolCodecError> {
1240 if request_id == 0 {
1241 return Err(ProtocolCodecError::InvalidRequestId);
1242 }
1243 Ok(())
1244}
1245
1246#[cfg(test)]
1247mod tests {
1248 use super::*;
1249 use crate::generated_protocol::v1::{
1250 FsPermissionScope, PatternPermissionScope, PermissionMode,
1251 };
1252 use std::collections::BTreeMap;
1253
1254 #[test]
1255 fn legacy_metadata_preserves_js_runtime_limits_with_only_new_fields() {
1256 let metadata = BTreeMap::from([(
1257 String::from("limits.js_runtime.cpu_time_limit_ms"),
1258 String::from("123"),
1259 )]);
1260
1261 let config = legacy_limits_config(&metadata).expect("limits config");
1262 let js_runtime = config.js_runtime.expect("js runtime limits");
1263
1264 assert_eq!(js_runtime.cpu_time_limit_ms, Some(123));
1265 }
1266
1267 #[test]
1268 fn legacy_metadata_preserves_wasm_limits_with_only_new_fields() {
1269 let metadata = BTreeMap::from([(
1270 String::from("limits.wasm.prewarm_timeout_ms"),
1271 String::from("456"),
1272 )]);
1273
1274 let config = legacy_limits_config(&metadata).expect("limits config");
1275 let wasm = config.wasm.expect("wasm limits");
1276
1277 assert_eq!(wasm.prewarm_timeout_ms, Some(456));
1278 }
1279
1280 #[test]
1281 fn legacy_metadata_preserves_wasm_runner_heap_limit_as_only_new_field() {
1282 let metadata = BTreeMap::from([(
1283 String::from("limits.wasm.runner_heap_limit_mb"),
1284 String::from("789"),
1285 )]);
1286
1287 let config = legacy_limits_config(&metadata).expect("limits config");
1288 let wasm = config.wasm.expect("wasm limits");
1289
1290 assert_eq!(wasm.runner_heap_limit_mb, Some(789));
1291 }
1292
1293 #[test]
1294 fn legacy_metadata_preserves_wasm_runner_cpu_limit_as_only_new_field() {
1295 let metadata = BTreeMap::from([(
1296 String::from("limits.wasm.runner_cpu_time_limit_ms"),
1297 String::from("987"),
1298 )]);
1299
1300 let config = legacy_limits_config(&metadata).expect("limits config");
1301 let wasm = config.wasm.expect("wasm limits");
1302
1303 assert_eq!(wasm.runner_cpu_time_limit_ms, Some(987));
1304 }
1305
1306 #[test]
1307 fn permissions_policy_default_matches_no_policy_deny_all() {
1308 let policy = PermissionsPolicy::default();
1309
1310 assert!(matches!(
1311 policy.fs,
1312 Some(FsPermissionScope::PermissionMode(PermissionMode::Deny))
1313 ));
1314 for scope in [
1315 policy.network,
1316 policy.child_process,
1317 policy.process,
1318 policy.env,
1319 policy.host_function,
1320 ] {
1321 assert!(matches!(
1322 scope,
1323 Some(PatternPermissionScope::PermissionMode(PermissionMode::Deny))
1324 ));
1325 }
1326 }
1327
1328 #[test]
1329 fn permissions_policy_allow_all_remains_explicit() {
1330 let policy = PermissionsPolicy::allow_all();
1331
1332 assert!(matches!(
1333 policy.fs,
1334 Some(FsPermissionScope::PermissionMode(PermissionMode::Allow))
1335 ));
1336 for scope in [
1337 policy.network,
1338 policy.child_process,
1339 policy.process,
1340 policy.env,
1341 policy.host_function,
1342 ] {
1343 assert!(matches!(
1344 scope,
1345 Some(PatternPermissionScope::PermissionMode(
1346 PermissionMode::Allow
1347 ))
1348 ));
1349 }
1350 }
1351
1352 #[test]
1353 fn package_unlink_frames_round_trip() {
1354 let codec = WireFrameCodec::default();
1355 let ownership = OwnershipScope::VmOwnership(VmOwnership {
1356 connection_id: String::from("connection"),
1357 session_id: String::from("session"),
1358 vm_id: String::from("vm"),
1359 });
1360 let request = ProtocolFrame::RequestFrame(RequestFrame {
1361 schema: protocol_schema(),
1362 request_id: 1,
1363 ownership: ownership.clone(),
1364 payload: RequestPayload::UnlinkPackageRequest(UnlinkPackageRequest {
1365 package_id: String::from("sha256:package"),
1366 }),
1367 });
1368 let response = ProtocolFrame::ResponseFrame(ResponseFrame {
1369 schema: protocol_schema(),
1370 request_id: 1,
1371 ownership,
1372 payload: ResponsePayload::PackageUnlinkedResponse(PackageUnlinkedResponse {
1373 removed_commands: vec![String::from("tool")],
1374 }),
1375 });
1376
1377 for frame in [request, response] {
1378 let encoded = codec.encode(&frame).expect("encode frame");
1379 assert_eq!(codec.decode(&encoded).expect("decode frame"), frame);
1380 }
1381 }
1382
1383 #[test]
1384 fn session_package_acquisition_frames_round_trip_without_host_path() {
1385 let codec = WireFrameCodec::default();
1386 let ownership = OwnershipScope::SessionOwnership(SessionOwnership {
1387 connection_id: String::from("connection"),
1388 session_id: String::from("session"),
1389 });
1390 let request = ProtocolFrame::RequestFrame(RequestFrame {
1391 schema: protocol_schema(),
1392 request_id: 2,
1393 ownership: ownership.clone(),
1394 payload: RequestPayload::AcquirePackageRequest(AcquirePackageRequest {
1395 source: PackageAcquisitionSource::PackageUrlSource(PackageUrlSource {
1396 url: String::from("https://packages.example/tool.aospkg"),
1397 expected_digest: Some(String::from("sha256:abc")),
1398 }),
1399 advisory: true,
1400 timeout_ms: Some(250),
1401 max_package_bytes: None,
1402 download_timeout_ms: None,
1403 connect_timeout_ms: None,
1404 max_redirects: None,
1405 allow_insecure_local_http: false,
1406 }),
1407 });
1408 let response = ProtocolFrame::ResponseFrame(ResponseFrame {
1409 schema: protocol_schema(),
1410 request_id: 2,
1411 ownership,
1412 payload: ResponsePayload::PackageAcquiredResponse(PackageAcquiredResponse {
1413 package_id: String::from("sha256:abc"),
1414 digest: String::from("sha256:abc"),
1415 size: 123,
1416 package_name: String::from("tool"),
1417 version: String::from("1"),
1418 commands: vec![String::from("tool")],
1419 }),
1420 });
1421 for frame in [request, response] {
1422 let encoded = codec.encode(&frame).expect("encode frame");
1423 assert_eq!(codec.decode(&encoded).expect("decode frame"), frame);
1424 }
1425 }
1426
1427 #[test]
1428 fn vm_package_install_frames_round_trip_without_host_path_in_response() {
1429 let codec = WireFrameCodec::default();
1430 let ownership = OwnershipScope::VmOwnership(VmOwnership {
1431 connection_id: "connection".into(),
1432 session_id: "session".into(),
1433 vm_id: "vm".into(),
1434 });
1435 let package = PackageAcquiredResponse {
1436 package_id: "sha256:abc".into(),
1437 digest: "sha256:abc".into(),
1438 size: 123,
1439 package_name: "tool".into(),
1440 version: "1".into(),
1441 commands: vec!["tool".into()],
1442 };
1443 let frames = [
1444 ProtocolFrame::RequestFrame(RequestFrame {
1445 schema: protocol_schema(),
1446 request_id: 3,
1447 ownership: ownership.clone(),
1448 payload: RequestPayload::InstallPackageRequest(InstallPackageRequest {
1449 acquisition: AcquirePackageRequest {
1450 source: PackageAcquisitionSource::PackageUrlSource(PackageUrlSource {
1451 url: "https://packages.example/tool.aospkg".into(),
1452 expected_digest: Some("sha256:abc".into()),
1453 }),
1454 advisory: false,
1455 timeout_ms: Some(250),
1456 max_package_bytes: None,
1457 download_timeout_ms: None,
1458 connect_timeout_ms: None,
1459 max_redirects: None,
1460 allow_insecure_local_http: false,
1461 },
1462 }),
1463 }),
1464 ProtocolFrame::ResponseFrame(ResponseFrame {
1465 schema: protocol_schema(),
1466 request_id: 3,
1467 ownership,
1468 payload: ResponsePayload::PackageInstalledResponse(PackageInstalledResponse {
1469 package,
1470 projected_commands: vec![ProjectedCommand {
1471 name: "tool".into(),
1472 guest_path: "/opt/agentos/bin/tool".into(),
1473 }],
1474 }),
1475 }),
1476 ];
1477 for frame in frames {
1478 let encoded = codec.encode(&frame).expect("encode frame");
1479 assert_eq!(codec.decode(&encoded).expect("decode frame"), frame);
1480 }
1481 }
1482}