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