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
719#[derive(Debug, Clone, PartialEq, Eq)]
720pub enum ProtocolCodecError {
721 TruncatedFrame {
722 actual: usize,
723 },
724 LengthPrefixMismatch {
725 declared: usize,
726 actual: usize,
727 },
728 FrameTooLarge {
729 size: usize,
730 max: usize,
731 },
732 UnsupportedSchema {
733 name: String,
734 version: u16,
735 },
736 InvalidRequestId,
737 InvalidRequestDirection {
738 request_id: RequestId,
739 expected: RequestDirection,
740 },
741 EmptyOwnershipField {
742 field: &'static str,
743 },
744 EmptyAuthToken,
745 InvalidOwnershipScope {
746 required: OwnershipRequirement,
747 actual: OwnershipRequirement,
748 },
749 SerializeFailure(String),
750 DeserializeFailure(String),
751}
752
753impl fmt::Display for ProtocolCodecError {
754 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
755 match self {
756 Self::TruncatedFrame { actual } => {
757 write!(
758 f,
759 "protocol frame is truncated: only {actual} bytes provided"
760 )
761 }
762 Self::LengthPrefixMismatch { declared, actual } => write!(
763 f,
764 "protocol frame length prefix mismatch: declared {declared} bytes, got {actual}",
765 ),
766 Self::FrameTooLarge { size, max } => {
767 write!(f, "protocol frame is {size} bytes, limit is {max}")
768 }
769 Self::UnsupportedSchema { name, version } => write!(
770 f,
771 "unsupported protocol schema {name}@{version}; expected {PROTOCOL_NAME}@{PROTOCOL_VERSION}",
772 ),
773 Self::InvalidRequestId => write!(f, "protocol request identifiers must be non-zero"),
774 Self::InvalidRequestDirection {
775 request_id,
776 expected,
777 } => write!(f, "protocol request id {request_id} must be {expected}",),
778 Self::EmptyOwnershipField { field } => {
779 write!(f, "protocol ownership field `{field}` cannot be empty")
780 }
781 Self::EmptyAuthToken => {
782 write!(f, "authenticate requests require a non-empty auth token")
783 }
784 Self::InvalidOwnershipScope { required, actual } => write!(
785 f,
786 "protocol frame requires {required} ownership but carried {actual}",
787 ),
788 Self::SerializeFailure(message) => {
789 write!(f, "protocol frame serialization failed: {message}")
790 }
791 Self::DeserializeFailure(message) => {
792 write!(f, "protocol frame deserialization failed: {message}")
793 }
794 }
795 }
796}
797
798impl Error for ProtocolCodecError {}
799
800#[derive(Debug, Clone, Copy, PartialEq, Eq)]
801pub enum OwnershipRequirement {
802 Any,
803 Connection,
804 Session,
805 Vm,
806 SessionOrVm,
807}
808
809impl fmt::Display for OwnershipRequirement {
810 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
811 match self {
812 Self::Any => write!(f, "any"),
813 Self::Connection => write!(f, "connection"),
814 Self::Session => write!(f, "session"),
815 Self::Vm => write!(f, "vm"),
816 Self::SessionOrVm => write!(f, "session-or-vm"),
817 }
818 }
819}
820
821#[derive(Debug, Clone, Copy, PartialEq, Eq)]
822pub enum RequestDirection {
823 Host,
824 Sidecar,
825}
826
827impl fmt::Display for RequestDirection {
828 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
829 match self {
830 Self::Host => write!(f, "positive"),
831 Self::Sidecar => write!(f, "negative"),
832 }
833 }
834}
835
836#[derive(Debug, Clone, PartialEq, Eq)]
837pub struct WireDispatchResult {
838 pub response: ResponseFrame,
839 pub events: Vec<EventFrame>,
840}
841
842#[derive(Debug, Clone, PartialEq, Eq)]
843pub struct CompatDispatchResult {
844 pub response: crate::protocol::ResponseFrame,
845 pub events: Vec<crate::protocol::EventFrame>,
846}
847
848#[derive(Debug, Clone)]
849pub struct WireFrameCodec {
850 max_frame_bytes: usize,
851}
852
853impl WireFrameCodec {
854 pub fn new(max_frame_bytes: usize) -> Self {
855 Self { max_frame_bytes }
856 }
857
858 pub fn max_frame_bytes(&self) -> usize {
859 self.max_frame_bytes
860 }
861
862 pub fn encode(&self, frame: &ProtocolFrame) -> Result<Vec<u8>, ProtocolCodecError> {
863 validate_frame(frame)?;
864
865 let payload = serde_bare::to_vec(frame)
866 .map_err(|error| ProtocolCodecError::SerializeFailure(error.to_string()))?;
867 if payload.len() > self.max_frame_bytes {
868 return Err(ProtocolCodecError::FrameTooLarge {
869 size: payload.len(),
870 max: self.max_frame_bytes,
871 });
872 }
873
874 let length =
875 u32::try_from(payload.len()).map_err(|_| ProtocolCodecError::FrameTooLarge {
876 size: payload.len(),
877 max: u32::MAX as usize,
878 })?;
879
880 let mut encoded = Vec::with_capacity(4 + payload.len());
881 encoded.extend_from_slice(&length.to_be_bytes());
882 encoded.extend_from_slice(&payload);
883 Ok(encoded)
884 }
885
886 pub fn decode(&self, bytes: &[u8]) -> Result<ProtocolFrame, ProtocolCodecError> {
887 let payload = self.checked_payload(bytes)?;
888 let frame = serde_bare::from_slice(payload)
889 .map_err(|error| ProtocolCodecError::DeserializeFailure(error.to_string()))?;
890 validate_frame(&frame)?;
891 Ok(frame)
892 }
893
894 pub fn encode_message(&self, frame: &ProtocolFrame) -> Result<Vec<u8>, ProtocolCodecError> {
902 validate_frame(frame)?;
903 let payload = serde_bare::to_vec(frame)
904 .map_err(|error| ProtocolCodecError::SerializeFailure(error.to_string()))?;
905 if payload.len() > self.max_frame_bytes {
906 return Err(ProtocolCodecError::FrameTooLarge {
907 size: payload.len(),
908 max: self.max_frame_bytes,
909 });
910 }
911 Ok(payload)
912 }
913
914 pub fn decode_message(&self, bytes: &[u8]) -> Result<ProtocolFrame, ProtocolCodecError> {
916 if bytes.len() > self.max_frame_bytes {
917 return Err(ProtocolCodecError::FrameTooLarge {
918 size: bytes.len(),
919 max: self.max_frame_bytes,
920 });
921 }
922 let frame = serde_bare::from_slice(bytes)
923 .map_err(|error| ProtocolCodecError::DeserializeFailure(error.to_string()))?;
924 validate_frame(&frame)?;
925 Ok(frame)
926 }
927
928 fn checked_payload<'a>(&self, bytes: &'a [u8]) -> Result<&'a [u8], ProtocolCodecError> {
929 if bytes.len() < 4 {
930 return Err(ProtocolCodecError::TruncatedFrame {
931 actual: bytes.len(),
932 });
933 }
934
935 let declared =
936 u32::from_be_bytes(bytes[..4].try_into().expect("length prefix is four bytes"))
937 as usize;
938 if declared > self.max_frame_bytes {
939 return Err(ProtocolCodecError::FrameTooLarge {
940 size: declared,
941 max: self.max_frame_bytes,
942 });
943 }
944
945 let actual = bytes.len() - 4;
946 if declared != actual {
947 return Err(ProtocolCodecError::LengthPrefixMismatch { declared, actual });
948 }
949
950 Ok(&bytes[4..])
951 }
952}
953
954impl Default for WireFrameCodec {
955 fn default() -> Self {
956 Self::new(DEFAULT_MAX_FRAME_BYTES)
957 }
958}
959
960pub fn protocol_schema() -> ProtocolSchema {
961 ProtocolSchema::current()
962}
963
964impl ProtocolSchema {
965 pub fn current() -> Self {
966 Self {
967 name: PROTOCOL_NAME.to_string(),
968 version: PROTOCOL_VERSION,
969 }
970 }
971}
972
973impl Default for ProtocolSchema {
974 fn default() -> Self {
975 Self::current()
976 }
977}
978
979pub fn request_frame_to_compat(
980 request: RequestFrame,
981) -> Result<crate::protocol::RequestFrame, ProtocolCodecError> {
982 match crate::protocol::from_generated_protocol_frame(ProtocolFrame::RequestFrame(request))? {
983 crate::protocol::ProtocolFrame::Request(request) => Ok(request),
984 crate::protocol::ProtocolFrame::Response(_)
985 | crate::protocol::ProtocolFrame::Event(_)
986 | crate::protocol::ProtocolFrame::SidecarRequest(_)
987 | crate::protocol::ProtocolFrame::SidecarResponse(_)
988 | crate::protocol::ProtocolFrame::Control(_) => {
989 Err(ProtocolCodecError::DeserializeFailure(String::from(
990 "wire request frame converted to non-request compatibility frame",
991 )))
992 }
993 }
994}
995
996pub fn ownership_scope_to_compat(ownership: OwnershipScope) -> crate::protocol::OwnershipScope {
997 crate::protocol::from_generated_ownership_scope(ownership)
998}
999
1000pub fn request_payload_to_compat(
1001 ownership: &crate::protocol::OwnershipScope,
1002 payload: RequestPayload,
1003) -> Result<crate::protocol::RequestPayload, ProtocolCodecError> {
1004 match crate::protocol::from_generated_protocol_frame(ProtocolFrame::RequestFrame(
1005 RequestFrame {
1006 schema: protocol_schema(),
1007 request_id: 1,
1008 ownership: crate::protocol::to_generated_ownership_scope(ownership),
1009 payload,
1010 },
1011 ))? {
1012 crate::protocol::ProtocolFrame::Request(request) => Ok(request.payload),
1013 crate::protocol::ProtocolFrame::Response(_)
1014 | crate::protocol::ProtocolFrame::Event(_)
1015 | crate::protocol::ProtocolFrame::SidecarRequest(_)
1016 | crate::protocol::ProtocolFrame::SidecarResponse(_)
1017 | crate::protocol::ProtocolFrame::Control(_) => {
1018 Err(ProtocolCodecError::DeserializeFailure(String::from(
1019 "wire request payload converted to non-request compatibility frame",
1020 )))
1021 }
1022 }
1023}
1024
1025pub fn response_payload_from_compat(
1026 ownership: &crate::protocol::OwnershipScope,
1027 payload: crate::protocol::ResponsePayload,
1028) -> Result<ResponsePayload, ProtocolCodecError> {
1029 match crate::protocol::to_generated_protocol_frame(&crate::protocol::ProtocolFrame::Response(
1030 crate::protocol::ResponseFrame::new(1, ownership.clone(), payload),
1031 ))? {
1032 ProtocolFrame::ResponseFrame(response) => Ok(response.payload),
1033 ProtocolFrame::RequestFrame(_)
1034 | ProtocolFrame::EventFrame(_)
1035 | ProtocolFrame::SidecarRequestFrame(_)
1036 | ProtocolFrame::SidecarResponseFrame(_)
1037 | ProtocolFrame::ControlFrame(_) => Err(ProtocolCodecError::SerializeFailure(
1038 String::from("compatibility response payload converted to non-response wire frame"),
1039 )),
1040 }
1041}
1042
1043pub fn event_frame_from_compat(
1044 event: crate::protocol::EventFrame,
1045) -> Result<EventFrame, ProtocolCodecError> {
1046 match crate::protocol::to_generated_protocol_frame(&crate::protocol::ProtocolFrame::Event(
1047 event,
1048 ))? {
1049 ProtocolFrame::EventFrame(event) => Ok(event),
1050 ProtocolFrame::RequestFrame(_)
1051 | ProtocolFrame::ResponseFrame(_)
1052 | ProtocolFrame::SidecarRequestFrame(_)
1053 | ProtocolFrame::SidecarResponseFrame(_)
1054 | ProtocolFrame::ControlFrame(_) => Err(ProtocolCodecError::SerializeFailure(
1055 String::from("compatibility event converted to non-event wire frame"),
1056 )),
1057 }
1058}
1059
1060pub fn event_frame_to_compat(
1061 event: EventFrame,
1062) -> Result<crate::protocol::EventFrame, ProtocolCodecError> {
1063 match crate::protocol::from_generated_protocol_frame(ProtocolFrame::EventFrame(event))? {
1064 crate::protocol::ProtocolFrame::Event(event) => Ok(event),
1065 crate::protocol::ProtocolFrame::Request(_)
1066 | crate::protocol::ProtocolFrame::Response(_)
1067 | crate::protocol::ProtocolFrame::SidecarRequest(_)
1068 | crate::protocol::ProtocolFrame::SidecarResponse(_)
1069 | crate::protocol::ProtocolFrame::Control(_) => {
1070 Err(ProtocolCodecError::DeserializeFailure(String::from(
1071 "wire event converted to non-event compatibility frame",
1072 )))
1073 }
1074 }
1075}
1076
1077pub fn sidecar_request_frame_from_compat(
1078 request: crate::protocol::SidecarRequestFrame,
1079) -> Result<SidecarRequestFrame, ProtocolCodecError> {
1080 match crate::protocol::to_generated_protocol_frame(
1081 &crate::protocol::ProtocolFrame::SidecarRequest(request),
1082 )? {
1083 ProtocolFrame::SidecarRequestFrame(request) => Ok(request),
1084 ProtocolFrame::RequestFrame(_)
1085 | ProtocolFrame::ResponseFrame(_)
1086 | ProtocolFrame::EventFrame(_)
1087 | ProtocolFrame::SidecarResponseFrame(_)
1088 | ProtocolFrame::ControlFrame(_) => {
1089 Err(ProtocolCodecError::SerializeFailure(String::from(
1090 "compatibility sidecar request converted to non-sidecar-request wire frame",
1091 )))
1092 }
1093 }
1094}
1095
1096pub fn sidecar_request_payload_to_compat(
1097 ownership: &crate::protocol::OwnershipScope,
1098 payload: SidecarRequestPayload,
1099) -> Result<crate::protocol::SidecarRequestPayload, ProtocolCodecError> {
1100 match crate::protocol::from_generated_protocol_frame(ProtocolFrame::SidecarRequestFrame(
1101 SidecarRequestFrame {
1102 schema: protocol_schema(),
1103 request_id: -1,
1104 ownership: crate::protocol::to_generated_ownership_scope(ownership),
1105 payload,
1106 },
1107 ))? {
1108 crate::protocol::ProtocolFrame::SidecarRequest(request) => Ok(request.payload),
1109 crate::protocol::ProtocolFrame::Request(_)
1110 | crate::protocol::ProtocolFrame::Response(_)
1111 | crate::protocol::ProtocolFrame::Event(_)
1112 | crate::protocol::ProtocolFrame::SidecarResponse(_)
1113 | crate::protocol::ProtocolFrame::Control(_) => {
1114 Err(ProtocolCodecError::DeserializeFailure(String::from(
1115 "wire sidecar request payload converted to non-sidecar-request compatibility frame",
1116 )))
1117 }
1118 }
1119}
1120
1121pub fn sidecar_response_frame_to_compat(
1122 response: SidecarResponseFrame,
1123) -> Result<crate::protocol::SidecarResponseFrame, ProtocolCodecError> {
1124 match crate::protocol::from_generated_protocol_frame(ProtocolFrame::SidecarResponseFrame(
1125 response,
1126 ))? {
1127 crate::protocol::ProtocolFrame::SidecarResponse(response) => Ok(response),
1128 crate::protocol::ProtocolFrame::Request(_)
1129 | crate::protocol::ProtocolFrame::Response(_)
1130 | crate::protocol::ProtocolFrame::Event(_)
1131 | crate::protocol::ProtocolFrame::SidecarRequest(_)
1132 | crate::protocol::ProtocolFrame::Control(_) => {
1133 Err(ProtocolCodecError::DeserializeFailure(String::from(
1134 "wire sidecar response converted to non-sidecar-response compatibility frame",
1135 )))
1136 }
1137 }
1138}
1139
1140pub fn sidecar_response_frame_from_compat(
1141 response: crate::protocol::SidecarResponseFrame,
1142) -> Result<SidecarResponseFrame, ProtocolCodecError> {
1143 match crate::protocol::to_generated_protocol_frame(
1144 &crate::protocol::ProtocolFrame::SidecarResponse(response),
1145 )? {
1146 ProtocolFrame::SidecarResponseFrame(response) => Ok(response),
1147 ProtocolFrame::RequestFrame(_)
1148 | ProtocolFrame::ResponseFrame(_)
1149 | ProtocolFrame::EventFrame(_)
1150 | ProtocolFrame::SidecarRequestFrame(_)
1151 | ProtocolFrame::ControlFrame(_) => {
1152 Err(ProtocolCodecError::SerializeFailure(String::from(
1153 "compatibility sidecar response converted to non-sidecar-response wire frame",
1154 )))
1155 }
1156 }
1157}
1158
1159pub fn dispatch_result_from_compat(
1160 result: CompatDispatchResult,
1161) -> Result<WireDispatchResult, ProtocolCodecError> {
1162 let response = match crate::protocol::to_generated_protocol_frame(
1163 &crate::protocol::ProtocolFrame::Response(result.response),
1164 )? {
1165 ProtocolFrame::ResponseFrame(response) => response,
1166 ProtocolFrame::RequestFrame(_)
1167 | ProtocolFrame::EventFrame(_)
1168 | ProtocolFrame::SidecarRequestFrame(_)
1169 | ProtocolFrame::SidecarResponseFrame(_)
1170 | ProtocolFrame::ControlFrame(_) => {
1171 return Err(ProtocolCodecError::SerializeFailure(String::from(
1172 "compatibility dispatch response converted to non-response wire frame",
1173 )));
1174 }
1175 };
1176
1177 let events = result
1178 .events
1179 .into_iter()
1180 .map(|event| {
1181 match crate::protocol::to_generated_protocol_frame(
1182 &crate::protocol::ProtocolFrame::Event(event),
1183 )? {
1184 ProtocolFrame::EventFrame(event) => Ok(event),
1185 ProtocolFrame::RequestFrame(_)
1186 | ProtocolFrame::ResponseFrame(_)
1187 | ProtocolFrame::SidecarRequestFrame(_)
1188 | ProtocolFrame::SidecarResponseFrame(_)
1189 | ProtocolFrame::ControlFrame(_) => Err(ProtocolCodecError::SerializeFailure(
1190 String::from("compatibility dispatch event converted to non-event wire frame"),
1191 )),
1192 }
1193 })
1194 .collect::<Result<Vec<_>, _>>()?;
1195
1196 Ok(WireDispatchResult { response, events })
1197}
1198
1199fn validate_frame(frame: &ProtocolFrame) -> Result<(), ProtocolCodecError> {
1200 match frame {
1201 ProtocolFrame::RequestFrame(frame) => {
1202 validate_schema(&frame.schema)?;
1203 validate_request_id(frame.request_id)
1204 }
1205 ProtocolFrame::ResponseFrame(frame) => {
1206 validate_schema(&frame.schema)?;
1207 validate_request_id(frame.request_id)
1208 }
1209 ProtocolFrame::EventFrame(frame) => validate_schema(&frame.schema),
1210 ProtocolFrame::SidecarRequestFrame(frame) => {
1211 validate_schema(&frame.schema)?;
1212 validate_request_id(frame.request_id)
1213 }
1214 ProtocolFrame::SidecarResponseFrame(frame) => {
1215 validate_schema(&frame.schema)?;
1216 validate_request_id(frame.request_id)
1217 }
1218 ProtocolFrame::ControlFrame(frame) => validate_schema(&frame.schema),
1219 }
1220}
1221
1222fn validate_schema(schema: &ProtocolSchema) -> Result<(), ProtocolCodecError> {
1223 if schema.name != PROTOCOL_NAME || schema.version != PROTOCOL_VERSION {
1224 return Err(ProtocolCodecError::UnsupportedSchema {
1225 name: schema.name.clone(),
1226 version: schema.version,
1227 });
1228 }
1229 Ok(())
1230}
1231
1232fn validate_request_id(request_id: RequestId) -> Result<(), ProtocolCodecError> {
1233 if request_id == 0 {
1234 return Err(ProtocolCodecError::InvalidRequestId);
1235 }
1236 Ok(())
1237}
1238
1239#[cfg(test)]
1240mod tests {
1241 use super::*;
1242 use crate::generated_protocol::v1::{
1243 FsPermissionScope, PatternPermissionScope, PermissionMode,
1244 };
1245 use std::collections::BTreeMap;
1246
1247 #[test]
1248 fn legacy_metadata_preserves_js_runtime_limits_with_only_new_fields() {
1249 let metadata = BTreeMap::from([(
1250 String::from("limits.js_runtime.cpu_time_limit_ms"),
1251 String::from("123"),
1252 )]);
1253
1254 let config = legacy_limits_config(&metadata).expect("limits config");
1255 let js_runtime = config.js_runtime.expect("js runtime limits");
1256
1257 assert_eq!(js_runtime.cpu_time_limit_ms, Some(123));
1258 }
1259
1260 #[test]
1261 fn legacy_metadata_preserves_wasm_limits_with_only_new_fields() {
1262 let metadata = BTreeMap::from([(
1263 String::from("limits.wasm.prewarm_timeout_ms"),
1264 String::from("456"),
1265 )]);
1266
1267 let config = legacy_limits_config(&metadata).expect("limits config");
1268 let wasm = config.wasm.expect("wasm limits");
1269
1270 assert_eq!(wasm.prewarm_timeout_ms, Some(456));
1271 }
1272
1273 #[test]
1274 fn legacy_metadata_preserves_wasm_runner_heap_limit_as_only_new_field() {
1275 let metadata = BTreeMap::from([(
1276 String::from("limits.wasm.runner_heap_limit_mb"),
1277 String::from("789"),
1278 )]);
1279
1280 let config = legacy_limits_config(&metadata).expect("limits config");
1281 let wasm = config.wasm.expect("wasm limits");
1282
1283 assert_eq!(wasm.runner_heap_limit_mb, Some(789));
1284 }
1285
1286 #[test]
1287 fn legacy_metadata_preserves_wasm_runner_cpu_limit_as_only_new_field() {
1288 let metadata = BTreeMap::from([(
1289 String::from("limits.wasm.runner_cpu_time_limit_ms"),
1290 String::from("987"),
1291 )]);
1292
1293 let config = legacy_limits_config(&metadata).expect("limits config");
1294 let wasm = config.wasm.expect("wasm limits");
1295
1296 assert_eq!(wasm.runner_cpu_time_limit_ms, Some(987));
1297 }
1298
1299 #[test]
1300 fn permissions_policy_default_matches_no_policy_deny_all() {
1301 let policy = PermissionsPolicy::default();
1302
1303 assert!(matches!(
1304 policy.fs,
1305 Some(FsPermissionScope::PermissionMode(PermissionMode::Deny))
1306 ));
1307 for scope in [
1308 policy.network,
1309 policy.child_process,
1310 policy.process,
1311 policy.env,
1312 policy.host_function,
1313 ] {
1314 assert!(matches!(
1315 scope,
1316 Some(PatternPermissionScope::PermissionMode(PermissionMode::Deny))
1317 ));
1318 }
1319 }
1320
1321 #[test]
1322 fn permissions_policy_allow_all_remains_explicit() {
1323 let policy = PermissionsPolicy::allow_all();
1324
1325 assert!(matches!(
1326 policy.fs,
1327 Some(FsPermissionScope::PermissionMode(PermissionMode::Allow))
1328 ));
1329 for scope in [
1330 policy.network,
1331 policy.child_process,
1332 policy.process,
1333 policy.env,
1334 policy.host_function,
1335 ] {
1336 assert!(matches!(
1337 scope,
1338 Some(PatternPermissionScope::PermissionMode(
1339 PermissionMode::Allow
1340 ))
1341 ));
1342 }
1343 }
1344
1345 #[test]
1346 fn package_unlink_frames_round_trip() {
1347 let codec = WireFrameCodec::default();
1348 let ownership = OwnershipScope::VmOwnership(VmOwnership {
1349 connection_id: String::from("connection"),
1350 session_id: String::from("session"),
1351 vm_id: String::from("vm"),
1352 });
1353 let request = ProtocolFrame::RequestFrame(RequestFrame {
1354 schema: protocol_schema(),
1355 request_id: 1,
1356 ownership: ownership.clone(),
1357 payload: RequestPayload::UnlinkPackageRequest(UnlinkPackageRequest {
1358 package_id: String::from("sha256:package"),
1359 }),
1360 });
1361 let response = ProtocolFrame::ResponseFrame(ResponseFrame {
1362 schema: protocol_schema(),
1363 request_id: 1,
1364 ownership,
1365 payload: ResponsePayload::PackageUnlinkedResponse(PackageUnlinkedResponse {
1366 removed_commands: vec![String::from("tool")],
1367 }),
1368 });
1369
1370 for frame in [request, response] {
1371 let encoded = codec.encode(&frame).expect("encode frame");
1372 assert_eq!(codec.decode(&encoded).expect("decode frame"), frame);
1373 }
1374 }
1375
1376 #[test]
1377 fn session_package_acquisition_frames_round_trip_without_host_path() {
1378 let codec = WireFrameCodec::default();
1379 let ownership = OwnershipScope::SessionOwnership(SessionOwnership {
1380 connection_id: String::from("connection"),
1381 session_id: String::from("session"),
1382 });
1383 let request = ProtocolFrame::RequestFrame(RequestFrame {
1384 schema: protocol_schema(),
1385 request_id: 2,
1386 ownership: ownership.clone(),
1387 payload: RequestPayload::AcquirePackageRequest(AcquirePackageRequest {
1388 source: PackageAcquisitionSource::PackageUrlSource(PackageUrlSource {
1389 url: String::from("https://packages.example/tool.aospkg"),
1390 expected_digest: Some(String::from("sha256:abc")),
1391 }),
1392 advisory: true,
1393 timeout_ms: Some(250),
1394 max_package_bytes: None,
1395 download_timeout_ms: None,
1396 connect_timeout_ms: None,
1397 max_redirects: None,
1398 allow_insecure_local_http: false,
1399 }),
1400 });
1401 let response = ProtocolFrame::ResponseFrame(ResponseFrame {
1402 schema: protocol_schema(),
1403 request_id: 2,
1404 ownership,
1405 payload: ResponsePayload::PackageAcquiredResponse(PackageAcquiredResponse {
1406 package_id: String::from("sha256:abc"),
1407 digest: String::from("sha256:abc"),
1408 size: 123,
1409 package_name: String::from("tool"),
1410 version: String::from("1"),
1411 commands: vec![String::from("tool")],
1412 }),
1413 });
1414 for frame in [request, response] {
1415 let encoded = codec.encode(&frame).expect("encode frame");
1416 assert_eq!(codec.decode(&encoded).expect("decode frame"), frame);
1417 }
1418 }
1419
1420 #[test]
1421 fn vm_package_install_frames_round_trip_without_host_path_in_response() {
1422 let codec = WireFrameCodec::default();
1423 let ownership = OwnershipScope::VmOwnership(VmOwnership {
1424 connection_id: "connection".into(),
1425 session_id: "session".into(),
1426 vm_id: "vm".into(),
1427 });
1428 let package = PackageAcquiredResponse {
1429 package_id: "sha256:abc".into(),
1430 digest: "sha256:abc".into(),
1431 size: 123,
1432 package_name: "tool".into(),
1433 version: "1".into(),
1434 commands: vec!["tool".into()],
1435 };
1436 let frames = [
1437 ProtocolFrame::RequestFrame(RequestFrame {
1438 schema: protocol_schema(),
1439 request_id: 3,
1440 ownership: ownership.clone(),
1441 payload: RequestPayload::InstallPackageRequest(InstallPackageRequest {
1442 acquisition: AcquirePackageRequest {
1443 source: PackageAcquisitionSource::PackageUrlSource(PackageUrlSource {
1444 url: "https://packages.example/tool.aospkg".into(),
1445 expected_digest: Some("sha256:abc".into()),
1446 }),
1447 advisory: false,
1448 timeout_ms: Some(250),
1449 max_package_bytes: None,
1450 download_timeout_ms: None,
1451 connect_timeout_ms: None,
1452 max_redirects: None,
1453 allow_insecure_local_http: false,
1454 },
1455 }),
1456 }),
1457 ProtocolFrame::ResponseFrame(ResponseFrame {
1458 schema: protocol_schema(),
1459 request_id: 3,
1460 ownership,
1461 payload: ResponsePayload::PackageInstalledResponse(PackageInstalledResponse {
1462 package,
1463 projected_commands: vec![ProjectedCommand {
1464 name: "tool".into(),
1465 guest_path: "/opt/agentos/bin/tool".into(),
1466 }],
1467 }),
1468 }),
1469 ];
1470 for frame in frames {
1471 let encoded = codec.encode(&frame).expect("encode frame");
1472 assert_eq!(codec.decode(&encoded).expect("decode frame"), frame);
1473 }
1474 }
1475}