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 tool: 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 tool: 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 tool: permissions.tool.map(legacy_pattern_permission_scope_config),
251 }
252}
253
254fn legacy_permission_mode_config(
255 mode: crate::generated_protocol::v1::PermissionMode,
256) -> secure_exec_vm_config::PermissionMode {
257 match mode {
258 crate::generated_protocol::v1::PermissionMode::Allow => {
259 secure_exec_vm_config::PermissionMode::Allow
260 }
261 crate::generated_protocol::v1::PermissionMode::Ask => {
262 secure_exec_vm_config::PermissionMode::Ask
263 }
264 crate::generated_protocol::v1::PermissionMode::Deny => {
265 secure_exec_vm_config::PermissionMode::Deny
266 }
267 }
268}
269
270fn legacy_fs_permission_scope_config(
271 scope: crate::generated_protocol::v1::FsPermissionScope,
272) -> secure_exec_vm_config::FsPermissionScope {
273 match scope {
274 crate::generated_protocol::v1::FsPermissionScope::PermissionMode(mode) => {
275 secure_exec_vm_config::FsPermissionScope::Mode(legacy_permission_mode_config(mode))
276 }
277 crate::generated_protocol::v1::FsPermissionScope::FsPermissionRuleSet(rules) => {
278 secure_exec_vm_config::FsPermissionScope::Rules(
279 secure_exec_vm_config::FsPermissionRuleSet {
280 default: rules.default.map(legacy_permission_mode_config),
281 rules: rules
282 .rules
283 .into_iter()
284 .map(|rule| secure_exec_vm_config::FsPermissionRule {
285 mode: legacy_permission_mode_config(rule.mode),
286 operations: rule.operations,
287 paths: rule.paths,
288 })
289 .collect(),
290 },
291 )
292 }
293 }
294}
295
296fn legacy_pattern_permission_scope_config(
297 scope: crate::generated_protocol::v1::PatternPermissionScope,
298) -> secure_exec_vm_config::PatternPermissionScope {
299 match scope {
300 crate::generated_protocol::v1::PatternPermissionScope::PermissionMode(mode) => {
301 secure_exec_vm_config::PatternPermissionScope::Mode(legacy_permission_mode_config(mode))
302 }
303 crate::generated_protocol::v1::PatternPermissionScope::PatternPermissionRuleSet(rules) => {
304 secure_exec_vm_config::PatternPermissionScope::Rules(
305 secure_exec_vm_config::PatternPermissionRuleSet {
306 default: rules.default.map(legacy_permission_mode_config),
307 rules: rules
308 .rules
309 .into_iter()
310 .map(|rule| secure_exec_vm_config::PatternPermissionRule {
311 mode: legacy_permission_mode_config(rule.mode),
312 operations: rule.operations,
313 patterns: rule.patterns,
314 })
315 .collect(),
316 },
317 )
318 }
319 }
320}
321
322fn legacy_dns_config(
323 metadata: &std::collections::BTreeMap<String, String>,
324) -> Option<secure_exec_vm_config::VmDnsConfig> {
325 let mut dns = secure_exec_vm_config::VmDnsConfig::default();
326 if let Some(value) = metadata.get("network.dns.servers") {
327 dns.name_servers = value
328 .split(',')
329 .map(str::trim)
330 .filter(|entry| !entry.is_empty())
331 .map(str::to_string)
332 .collect();
333 }
334 for (key, value) in metadata {
335 let Some(hostname) = key.strip_prefix("network.dns.override.") else {
336 continue;
337 };
338 dns.overrides.insert(
339 hostname.to_string(),
340 value
341 .split(',')
342 .map(str::trim)
343 .filter(|entry| !entry.is_empty())
344 .map(str::to_string)
345 .collect(),
346 );
347 }
348 if dns.name_servers.is_empty() && dns.overrides.is_empty() {
349 None
350 } else {
351 Some(dns)
352 }
353}
354
355fn legacy_native_root_config(
356 metadata: &std::collections::BTreeMap<String, String>,
357) -> Option<secure_exec_vm_config::NativeRootFilesystemConfig> {
358 let id = metadata.get("rootFilesystem.nativePlugin.id")?;
359 let config = metadata
360 .get("rootFilesystem.nativePlugin.config")
361 .map(|value| serde_json::from_str(value).expect("parse native root plugin config"))
362 .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
363 let read_only = metadata
364 .get("rootFilesystem.nativePlugin.readOnly")
365 .map(|value| value.parse::<bool>().expect("parse native root readOnly"))
366 .unwrap_or(false);
367 Some(secure_exec_vm_config::NativeRootFilesystemConfig {
368 plugin: secure_exec_vm_config::MountPluginDescriptor {
369 id: id.clone(),
370 config,
371 },
372 read_only,
373 })
374}
375
376fn legacy_listen_config(
377 metadata: &std::collections::BTreeMap<String, String>,
378) -> Option<secure_exec_vm_config::VmListenPolicyConfig> {
379 let listen = secure_exec_vm_config::VmListenPolicyConfig {
380 port_min: metadata
381 .get("network.listen.port_min")
382 .map(|value| value.parse::<u16>().expect("parse network.listen.port_min")),
383 port_max: metadata
384 .get("network.listen.port_max")
385 .map(|value| value.parse::<u16>().expect("parse network.listen.port_max")),
386 allow_privileged: metadata
387 .get("network.listen.allow_privileged")
388 .map(|value| {
389 value
390 .parse::<bool>()
391 .expect("parse network.listen.allow_privileged")
392 }),
393 };
394 if listen.port_min.is_none() && listen.port_max.is_none() && listen.allow_privileged.is_none() {
395 None
396 } else {
397 Some(listen)
398 }
399}
400
401fn legacy_loopback_exempt_ports(env: &std::collections::BTreeMap<String, String>) -> Vec<u16> {
402 let Some(value) = env.get("AGENT_OS_LOOPBACK_EXEMPT_PORTS") else {
403 return Vec::new();
404 };
405 serde_json::from_str::<Vec<u16>>(value).unwrap_or_default()
406}
407
408fn legacy_limits_config(
409 metadata: &std::collections::BTreeMap<String, String>,
410) -> Option<secure_exec_vm_config::VmLimitsConfig> {
411 let resources = secure_exec_vm_config::ResourceLimitsConfig {
412 cpu_count: legacy_u64(metadata, "resource.cpu_count"),
413 max_processes: legacy_u64(metadata, "resource.max_processes"),
414 max_open_fds: legacy_u64(metadata, "resource.max_open_fds"),
415 max_pipes: legacy_u64(metadata, "resource.max_pipes"),
416 max_ptys: legacy_u64(metadata, "resource.max_ptys"),
417 max_sockets: legacy_u64(metadata, "resource.max_sockets"),
418 max_connections: legacy_u64(metadata, "resource.max_connections"),
419 max_socket_buffered_bytes: legacy_u64(metadata, "resource.max_socket_buffered_bytes"),
420 max_socket_datagram_queue_len: legacy_u64(
421 metadata,
422 "resource.max_socket_datagram_queue_len",
423 ),
424 max_filesystem_bytes: legacy_u64(metadata, "resource.max_filesystem_bytes"),
425 max_inode_count: legacy_u64(metadata, "resource.max_inode_count"),
426 max_blocking_read_ms: legacy_u64(metadata, "resource.max_blocking_read_ms"),
427 max_pread_bytes: legacy_u64(metadata, "resource.max_pread_bytes"),
428 max_fd_write_bytes: legacy_u64(metadata, "resource.max_fd_write_bytes"),
429 max_process_argv_bytes: legacy_u64(metadata, "resource.max_process_argv_bytes"),
430 max_process_env_bytes: legacy_u64(metadata, "resource.max_process_env_bytes"),
431 max_readdir_entries: legacy_u64(metadata, "resource.max_readdir_entries"),
432 max_wasm_fuel: legacy_u64(metadata, "resource.max_wasm_fuel"),
433 max_wasm_memory_bytes: legacy_u64(metadata, "resource.max_wasm_memory_bytes"),
434 max_wasm_stack_bytes: legacy_u64(metadata, "resource.max_wasm_stack_bytes"),
435 };
436 let http = secure_exec_vm_config::HttpLimitsConfig {
437 max_fetch_response_bytes: legacy_u64(metadata, "limits.http.max_fetch_response_bytes"),
438 };
439 let tools = secure_exec_vm_config::ToolLimitsConfig {
440 default_tool_timeout_ms: legacy_u64(metadata, "limits.tools.default_tool_timeout_ms"),
441 max_tool_timeout_ms: legacy_u64(metadata, "limits.tools.max_tool_timeout_ms"),
442 max_registered_toolkits: legacy_u64(metadata, "limits.tools.max_registered_toolkits"),
443 max_registered_tools_per_vm: legacy_u64(
444 metadata,
445 "limits.tools.max_registered_tools_per_vm",
446 ),
447 max_tools_per_toolkit: legacy_u64(metadata, "limits.tools.max_tools_per_toolkit"),
448 max_tool_schema_bytes: legacy_u64(metadata, "limits.tools.max_tool_schema_bytes"),
449 max_tool_examples_per_tool: legacy_u64(metadata, "limits.tools.max_tool_examples_per_tool"),
450 max_tool_example_input_bytes: legacy_u64(
451 metadata,
452 "limits.tools.max_tool_example_input_bytes",
453 ),
454 };
455 let plugins = secure_exec_vm_config::PluginLimitsConfig {
456 max_persisted_manifest_bytes: legacy_u64(
457 metadata,
458 "limits.plugins.max_persisted_manifest_bytes",
459 ),
460 max_persisted_manifest_file_bytes: legacy_u64(
461 metadata,
462 "limits.plugins.max_persisted_manifest_file_bytes",
463 ),
464 };
465 let acp = secure_exec_vm_config::AcpLimitsConfig {
466 max_read_line_bytes: legacy_u64(metadata, "limits.acp.max_read_line_bytes"),
467 stdout_buffer_byte_limit: legacy_u64(metadata, "limits.acp.stdout_buffer_byte_limit"),
468 };
469 let js_runtime = secure_exec_vm_config::JsRuntimeLimitsConfig {
470 v8_heap_limit_mb: legacy_u64(metadata, "limits.js_runtime.v8_heap_limit_mb"),
471 captured_output_limit_bytes: legacy_u64(
472 metadata,
473 "limits.js_runtime.captured_output_limit_bytes",
474 ),
475 stdin_buffer_limit_bytes: legacy_u64(
476 metadata,
477 "limits.js_runtime.stdin_buffer_limit_bytes",
478 ),
479 event_payload_limit_bytes: legacy_u64(
480 metadata,
481 "limits.js_runtime.event_payload_limit_bytes",
482 ),
483 v8_ipc_max_frame_bytes: legacy_u64(metadata, "limits.js_runtime.v8_ipc_max_frame_bytes"),
484 };
485 let python = secure_exec_vm_config::PythonLimitsConfig {
486 output_buffer_max_bytes: legacy_u64(metadata, "limits.python.output_buffer_max_bytes"),
487 execution_timeout_ms: legacy_u64(metadata, "limits.python.execution_timeout_ms"),
488 vfs_rpc_timeout_ms: legacy_u64(metadata, "limits.python.vfs_rpc_timeout_ms"),
489 };
490 let wasm = secure_exec_vm_config::WasmLimitsConfig {
491 max_module_file_bytes: legacy_u64(metadata, "limits.wasm.max_module_file_bytes"),
492 captured_output_limit_bytes: legacy_u64(
493 metadata,
494 "limits.wasm.captured_output_limit_bytes",
495 ),
496 sync_read_limit_bytes: legacy_u64(metadata, "limits.wasm.sync_read_limit_bytes"),
497 };
498
499 let config = secure_exec_vm_config::VmLimitsConfig {
500 resources: legacy_has_resource_limits(&resources).then_some(resources),
501 http: http.max_fetch_response_bytes.is_some().then_some(http),
502 tools: legacy_has_tool_limits(&tools).then_some(tools),
503 plugins: legacy_has_plugin_limits(&plugins).then_some(plugins),
504 acp: legacy_has_acp_limits(&acp).then_some(acp),
505 js_runtime: legacy_has_js_runtime_limits(&js_runtime).then_some(js_runtime),
506 python: legacy_has_python_limits(&python).then_some(python),
507 wasm: legacy_has_wasm_limits(&wasm).then_some(wasm),
508 };
509
510 if config.resources.is_none()
511 && config.http.is_none()
512 && config.tools.is_none()
513 && config.plugins.is_none()
514 && config.acp.is_none()
515 && config.js_runtime.is_none()
516 && config.python.is_none()
517 && config.wasm.is_none()
518 {
519 None
520 } else {
521 Some(config)
522 }
523}
524
525fn legacy_u64(metadata: &std::collections::BTreeMap<String, String>, key: &str) -> Option<u64> {
526 metadata.get(key).map(|value| {
527 value
528 .parse::<u64>()
529 .unwrap_or_else(|error| panic!("parse {key}: {error}"))
530 })
531}
532
533fn legacy_has_resource_limits(config: &secure_exec_vm_config::ResourceLimitsConfig) -> bool {
534 config.cpu_count.is_some()
535 || config.max_processes.is_some()
536 || config.max_open_fds.is_some()
537 || config.max_pipes.is_some()
538 || config.max_ptys.is_some()
539 || config.max_sockets.is_some()
540 || config.max_connections.is_some()
541 || config.max_socket_buffered_bytes.is_some()
542 || config.max_socket_datagram_queue_len.is_some()
543 || config.max_filesystem_bytes.is_some()
544 || config.max_inode_count.is_some()
545 || config.max_blocking_read_ms.is_some()
546 || config.max_pread_bytes.is_some()
547 || config.max_fd_write_bytes.is_some()
548 || config.max_process_argv_bytes.is_some()
549 || config.max_process_env_bytes.is_some()
550 || config.max_readdir_entries.is_some()
551 || config.max_wasm_fuel.is_some()
552 || config.max_wasm_memory_bytes.is_some()
553 || config.max_wasm_stack_bytes.is_some()
554}
555
556fn legacy_has_tool_limits(config: &secure_exec_vm_config::ToolLimitsConfig) -> bool {
557 config.default_tool_timeout_ms.is_some()
558 || config.max_tool_timeout_ms.is_some()
559 || config.max_registered_toolkits.is_some()
560 || config.max_registered_tools_per_vm.is_some()
561 || config.max_tools_per_toolkit.is_some()
562 || config.max_tool_schema_bytes.is_some()
563 || config.max_tool_examples_per_tool.is_some()
564 || config.max_tool_example_input_bytes.is_some()
565}
566
567fn legacy_has_plugin_limits(config: &secure_exec_vm_config::PluginLimitsConfig) -> bool {
568 config.max_persisted_manifest_bytes.is_some()
569 || config.max_persisted_manifest_file_bytes.is_some()
570}
571
572fn legacy_has_acp_limits(config: &secure_exec_vm_config::AcpLimitsConfig) -> bool {
573 config.max_read_line_bytes.is_some() || config.stdout_buffer_byte_limit.is_some()
574}
575
576fn legacy_has_js_runtime_limits(config: &secure_exec_vm_config::JsRuntimeLimitsConfig) -> bool {
577 config.v8_heap_limit_mb.is_some()
578 || config.captured_output_limit_bytes.is_some()
579 || config.stdin_buffer_limit_bytes.is_some()
580 || config.event_payload_limit_bytes.is_some()
581 || config.v8_ipc_max_frame_bytes.is_some()
582}
583
584fn legacy_has_python_limits(config: &secure_exec_vm_config::PythonLimitsConfig) -> bool {
585 config.output_buffer_max_bytes.is_some()
586 || config.execution_timeout_ms.is_some()
587 || config.vfs_rpc_timeout_ms.is_some()
588}
589
590fn legacy_has_wasm_limits(config: &secure_exec_vm_config::WasmLimitsConfig) -> bool {
591 config.max_module_file_bytes.is_some()
592 || config.captured_output_limit_bytes.is_some()
593 || config.sync_read_limit_bytes.is_some()
594}
595
596impl crate::generated_protocol::v1::OwnershipScope {
602 pub fn connection(connection_id: impl Into<String>) -> Self {
603 Self::ConnectionOwnership(crate::generated_protocol::v1::ConnectionOwnership {
604 connection_id: connection_id.into(),
605 })
606 }
607
608 pub fn session(connection_id: impl Into<String>, session_id: impl Into<String>) -> Self {
609 Self::SessionOwnership(crate::generated_protocol::v1::SessionOwnership {
610 connection_id: connection_id.into(),
611 session_id: session_id.into(),
612 })
613 }
614
615 pub fn vm(
616 connection_id: impl Into<String>,
617 session_id: impl Into<String>,
618 vm_id: impl Into<String>,
619 ) -> Self {
620 Self::VmOwnership(crate::generated_protocol::v1::VmOwnership {
621 connection_id: connection_id.into(),
622 session_id: session_id.into(),
623 vm_id: vm_id.into(),
624 })
625 }
626}
627
628pub const PROTOCOL_NAME: &str = "secure-exec-sidecar";
629pub const PROTOCOL_VERSION: u16 = 7;
630pub const DEFAULT_MAX_FRAME_BYTES: usize = 1024 * 1024;
631
632#[derive(Debug, Clone, PartialEq, Eq)]
633pub enum ProtocolCodecError {
634 TruncatedFrame {
635 actual: usize,
636 },
637 LengthPrefixMismatch {
638 declared: usize,
639 actual: usize,
640 },
641 FrameTooLarge {
642 size: usize,
643 max: usize,
644 },
645 UnsupportedSchema {
646 name: String,
647 version: u16,
648 },
649 InvalidRequestId,
650 InvalidRequestDirection {
651 request_id: RequestId,
652 expected: RequestDirection,
653 },
654 EmptyOwnershipField {
655 field: &'static str,
656 },
657 EmptyAuthToken,
658 InvalidOwnershipScope {
659 required: OwnershipRequirement,
660 actual: OwnershipRequirement,
661 },
662 SerializeFailure(String),
663 DeserializeFailure(String),
664}
665
666impl fmt::Display for ProtocolCodecError {
667 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
668 match self {
669 Self::TruncatedFrame { actual } => {
670 write!(
671 f,
672 "protocol frame is truncated: only {actual} bytes provided"
673 )
674 }
675 Self::LengthPrefixMismatch { declared, actual } => write!(
676 f,
677 "protocol frame length prefix mismatch: declared {declared} bytes, got {actual}",
678 ),
679 Self::FrameTooLarge { size, max } => {
680 write!(f, "protocol frame is {size} bytes, limit is {max}")
681 }
682 Self::UnsupportedSchema { name, version } => write!(
683 f,
684 "unsupported protocol schema {name}@{version}; expected {PROTOCOL_NAME}@{PROTOCOL_VERSION}",
685 ),
686 Self::InvalidRequestId => write!(f, "protocol request identifiers must be non-zero"),
687 Self::InvalidRequestDirection {
688 request_id,
689 expected,
690 } => write!(f, "protocol request id {request_id} must be {expected}",),
691 Self::EmptyOwnershipField { field } => {
692 write!(f, "protocol ownership field `{field}` cannot be empty")
693 }
694 Self::EmptyAuthToken => {
695 write!(f, "authenticate requests require a non-empty auth token")
696 }
697 Self::InvalidOwnershipScope { required, actual } => write!(
698 f,
699 "protocol frame requires {required} ownership but carried {actual}",
700 ),
701 Self::SerializeFailure(message) => {
702 write!(f, "protocol frame serialization failed: {message}")
703 }
704 Self::DeserializeFailure(message) => {
705 write!(f, "protocol frame deserialization failed: {message}")
706 }
707 }
708 }
709}
710
711impl Error for ProtocolCodecError {}
712
713#[derive(Debug, Clone, Copy, PartialEq, Eq)]
714pub enum OwnershipRequirement {
715 Any,
716 Connection,
717 Session,
718 Vm,
719 SessionOrVm,
720}
721
722impl fmt::Display for OwnershipRequirement {
723 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
724 match self {
725 Self::Any => write!(f, "any"),
726 Self::Connection => write!(f, "connection"),
727 Self::Session => write!(f, "session"),
728 Self::Vm => write!(f, "vm"),
729 Self::SessionOrVm => write!(f, "session-or-vm"),
730 }
731 }
732}
733
734#[derive(Debug, Clone, Copy, PartialEq, Eq)]
735pub enum RequestDirection {
736 Host,
737 Sidecar,
738}
739
740impl fmt::Display for RequestDirection {
741 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
742 match self {
743 Self::Host => write!(f, "positive"),
744 Self::Sidecar => write!(f, "negative"),
745 }
746 }
747}
748
749#[derive(Debug, Clone, PartialEq, Eq)]
750pub struct WireDispatchResult {
751 pub response: ResponseFrame,
752 pub events: Vec<EventFrame>,
753}
754
755#[derive(Debug, Clone)]
756pub struct WireFrameCodec {
757 max_frame_bytes: usize,
758}
759
760impl WireFrameCodec {
761 pub fn new(max_frame_bytes: usize) -> Self {
762 Self { max_frame_bytes }
763 }
764
765 pub fn max_frame_bytes(&self) -> usize {
766 self.max_frame_bytes
767 }
768
769 pub fn encode(&self, frame: &ProtocolFrame) -> Result<Vec<u8>, ProtocolCodecError> {
770 validate_frame(frame)?;
771
772 let payload = serde_bare::to_vec(frame)
773 .map_err(|error| ProtocolCodecError::SerializeFailure(error.to_string()))?;
774 if payload.len() > self.max_frame_bytes {
775 return Err(ProtocolCodecError::FrameTooLarge {
776 size: payload.len(),
777 max: self.max_frame_bytes,
778 });
779 }
780
781 let length =
782 u32::try_from(payload.len()).map_err(|_| ProtocolCodecError::FrameTooLarge {
783 size: payload.len(),
784 max: u32::MAX as usize,
785 })?;
786
787 let mut encoded = Vec::with_capacity(4 + payload.len());
788 encoded.extend_from_slice(&length.to_be_bytes());
789 encoded.extend_from_slice(&payload);
790 Ok(encoded)
791 }
792
793 pub fn decode(&self, bytes: &[u8]) -> Result<ProtocolFrame, ProtocolCodecError> {
794 let payload = self.checked_payload(bytes)?;
795 let frame = serde_bare::from_slice(payload)
796 .map_err(|error| ProtocolCodecError::DeserializeFailure(error.to_string()))?;
797 validate_frame(&frame)?;
798 Ok(frame)
799 }
800
801 fn checked_payload<'a>(&self, bytes: &'a [u8]) -> Result<&'a [u8], ProtocolCodecError> {
802 if bytes.len() < 4 {
803 return Err(ProtocolCodecError::TruncatedFrame {
804 actual: bytes.len(),
805 });
806 }
807
808 let declared =
809 u32::from_be_bytes(bytes[..4].try_into().expect("length prefix is four bytes"))
810 as usize;
811 if declared > self.max_frame_bytes {
812 return Err(ProtocolCodecError::FrameTooLarge {
813 size: declared,
814 max: self.max_frame_bytes,
815 });
816 }
817
818 let actual = bytes.len() - 4;
819 if declared != actual {
820 return Err(ProtocolCodecError::LengthPrefixMismatch { declared, actual });
821 }
822
823 Ok(&bytes[4..])
824 }
825}
826
827impl Default for WireFrameCodec {
828 fn default() -> Self {
829 Self::new(DEFAULT_MAX_FRAME_BYTES)
830 }
831}
832
833pub fn protocol_schema() -> ProtocolSchema {
834 ProtocolSchema::current()
835}
836
837impl ProtocolSchema {
838 pub fn current() -> Self {
839 Self {
840 name: PROTOCOL_NAME.to_string(),
841 version: PROTOCOL_VERSION,
842 }
843 }
844}
845
846impl Default for ProtocolSchema {
847 fn default() -> Self {
848 Self::current()
849 }
850}
851
852pub(crate) fn request_frame_to_compat(
853 request: RequestFrame,
854) -> Result<crate::protocol::RequestFrame, ProtocolCodecError> {
855 match crate::protocol::from_generated_protocol_frame(ProtocolFrame::RequestFrame(request))? {
856 crate::protocol::ProtocolFrame::Request(request) => Ok(request),
857 crate::protocol::ProtocolFrame::Response(_)
858 | crate::protocol::ProtocolFrame::Event(_)
859 | crate::protocol::ProtocolFrame::SidecarRequest(_)
860 | crate::protocol::ProtocolFrame::SidecarResponse(_) => {
861 Err(ProtocolCodecError::DeserializeFailure(String::from(
862 "wire request frame converted to non-request compatibility frame",
863 )))
864 }
865 }
866}
867
868pub(crate) fn ownership_scope_to_compat(
869 ownership: OwnershipScope,
870) -> crate::protocol::OwnershipScope {
871 crate::protocol::from_generated_ownership_scope(ownership)
872}
873
874pub(crate) fn request_payload_to_compat(
875 ownership: &crate::protocol::OwnershipScope,
876 payload: RequestPayload,
877) -> Result<crate::protocol::RequestPayload, ProtocolCodecError> {
878 match crate::protocol::from_generated_protocol_frame(ProtocolFrame::RequestFrame(
879 RequestFrame {
880 schema: protocol_schema(),
881 request_id: 1,
882 ownership: crate::protocol::to_generated_ownership_scope(ownership),
883 payload,
884 },
885 ))? {
886 crate::protocol::ProtocolFrame::Request(request) => Ok(request.payload),
887 crate::protocol::ProtocolFrame::Response(_)
888 | crate::protocol::ProtocolFrame::Event(_)
889 | crate::protocol::ProtocolFrame::SidecarRequest(_)
890 | crate::protocol::ProtocolFrame::SidecarResponse(_) => {
891 Err(ProtocolCodecError::DeserializeFailure(String::from(
892 "wire request payload converted to non-request compatibility frame",
893 )))
894 }
895 }
896}
897
898pub(crate) fn response_payload_from_compat(
899 ownership: &crate::protocol::OwnershipScope,
900 payload: crate::protocol::ResponsePayload,
901) -> Result<ResponsePayload, ProtocolCodecError> {
902 match crate::protocol::to_generated_protocol_frame(&crate::protocol::ProtocolFrame::Response(
903 crate::protocol::ResponseFrame::new(1, ownership.clone(), payload),
904 ))? {
905 ProtocolFrame::ResponseFrame(response) => Ok(response.payload),
906 ProtocolFrame::RequestFrame(_)
907 | ProtocolFrame::EventFrame(_)
908 | ProtocolFrame::SidecarRequestFrame(_)
909 | ProtocolFrame::SidecarResponseFrame(_) => Err(ProtocolCodecError::SerializeFailure(
910 String::from("compatibility response payload converted to non-response wire frame"),
911 )),
912 }
913}
914
915pub(crate) fn event_frame_from_compat(
916 event: crate::protocol::EventFrame,
917) -> Result<EventFrame, ProtocolCodecError> {
918 match crate::protocol::to_generated_protocol_frame(&crate::protocol::ProtocolFrame::Event(
919 event,
920 ))? {
921 ProtocolFrame::EventFrame(event) => Ok(event),
922 ProtocolFrame::RequestFrame(_)
923 | ProtocolFrame::ResponseFrame(_)
924 | ProtocolFrame::SidecarRequestFrame(_)
925 | ProtocolFrame::SidecarResponseFrame(_) => Err(ProtocolCodecError::SerializeFailure(
926 String::from("compatibility event converted to non-event wire frame"),
927 )),
928 }
929}
930
931pub(crate) fn event_frame_to_compat(
932 event: EventFrame,
933) -> Result<crate::protocol::EventFrame, ProtocolCodecError> {
934 match crate::protocol::from_generated_protocol_frame(ProtocolFrame::EventFrame(event))? {
935 crate::protocol::ProtocolFrame::Event(event) => Ok(event),
936 crate::protocol::ProtocolFrame::Request(_)
937 | crate::protocol::ProtocolFrame::Response(_)
938 | crate::protocol::ProtocolFrame::SidecarRequest(_)
939 | crate::protocol::ProtocolFrame::SidecarResponse(_) => {
940 Err(ProtocolCodecError::DeserializeFailure(String::from(
941 "wire event converted to non-event compatibility frame",
942 )))
943 }
944 }
945}
946
947pub(crate) fn sidecar_request_frame_from_compat(
948 request: crate::protocol::SidecarRequestFrame,
949) -> Result<SidecarRequestFrame, ProtocolCodecError> {
950 match crate::protocol::to_generated_protocol_frame(
951 &crate::protocol::ProtocolFrame::SidecarRequest(request),
952 )? {
953 ProtocolFrame::SidecarRequestFrame(request) => Ok(request),
954 ProtocolFrame::RequestFrame(_)
955 | ProtocolFrame::ResponseFrame(_)
956 | ProtocolFrame::EventFrame(_)
957 | ProtocolFrame::SidecarResponseFrame(_) => {
958 Err(ProtocolCodecError::SerializeFailure(String::from(
959 "compatibility sidecar request converted to non-sidecar-request wire frame",
960 )))
961 }
962 }
963}
964
965pub(crate) fn sidecar_request_payload_to_compat(
966 ownership: &crate::protocol::OwnershipScope,
967 payload: SidecarRequestPayload,
968) -> Result<crate::protocol::SidecarRequestPayload, ProtocolCodecError> {
969 match crate::protocol::from_generated_protocol_frame(ProtocolFrame::SidecarRequestFrame(
970 SidecarRequestFrame {
971 schema: protocol_schema(),
972 request_id: -1,
973 ownership: crate::protocol::to_generated_ownership_scope(ownership),
974 payload,
975 },
976 ))? {
977 crate::protocol::ProtocolFrame::SidecarRequest(request) => Ok(request.payload),
978 crate::protocol::ProtocolFrame::Request(_)
979 | crate::protocol::ProtocolFrame::Response(_)
980 | crate::protocol::ProtocolFrame::Event(_)
981 | crate::protocol::ProtocolFrame::SidecarResponse(_) => {
982 Err(ProtocolCodecError::DeserializeFailure(String::from(
983 "wire sidecar request payload converted to non-sidecar-request compatibility frame",
984 )))
985 }
986 }
987}
988
989pub(crate) fn sidecar_response_frame_to_compat(
990 response: SidecarResponseFrame,
991) -> Result<crate::protocol::SidecarResponseFrame, ProtocolCodecError> {
992 match crate::protocol::from_generated_protocol_frame(ProtocolFrame::SidecarResponseFrame(
993 response,
994 ))? {
995 crate::protocol::ProtocolFrame::SidecarResponse(response) => Ok(response),
996 crate::protocol::ProtocolFrame::Request(_)
997 | crate::protocol::ProtocolFrame::Response(_)
998 | crate::protocol::ProtocolFrame::Event(_)
999 | crate::protocol::ProtocolFrame::SidecarRequest(_) => {
1000 Err(ProtocolCodecError::DeserializeFailure(String::from(
1001 "wire sidecar response converted to non-sidecar-response compatibility frame",
1002 )))
1003 }
1004 }
1005}
1006
1007pub(crate) fn sidecar_response_frame_from_compat(
1008 response: crate::protocol::SidecarResponseFrame,
1009) -> Result<SidecarResponseFrame, ProtocolCodecError> {
1010 match crate::protocol::to_generated_protocol_frame(
1011 &crate::protocol::ProtocolFrame::SidecarResponse(response),
1012 )? {
1013 ProtocolFrame::SidecarResponseFrame(response) => Ok(response),
1014 ProtocolFrame::RequestFrame(_)
1015 | ProtocolFrame::ResponseFrame(_)
1016 | ProtocolFrame::EventFrame(_)
1017 | ProtocolFrame::SidecarRequestFrame(_) => {
1018 Err(ProtocolCodecError::SerializeFailure(String::from(
1019 "compatibility sidecar response converted to non-sidecar-response wire frame",
1020 )))
1021 }
1022 }
1023}
1024
1025pub(crate) fn dispatch_result_from_compat(
1026 result: crate::state::DispatchResult,
1027) -> Result<WireDispatchResult, ProtocolCodecError> {
1028 let response = match crate::protocol::to_generated_protocol_frame(
1029 &crate::protocol::ProtocolFrame::Response(result.response),
1030 )? {
1031 ProtocolFrame::ResponseFrame(response) => response,
1032 ProtocolFrame::RequestFrame(_)
1033 | ProtocolFrame::EventFrame(_)
1034 | ProtocolFrame::SidecarRequestFrame(_)
1035 | ProtocolFrame::SidecarResponseFrame(_) => {
1036 return Err(ProtocolCodecError::SerializeFailure(String::from(
1037 "compatibility dispatch response converted to non-response wire frame",
1038 )));
1039 }
1040 };
1041
1042 let events = result
1043 .events
1044 .into_iter()
1045 .map(|event| {
1046 match crate::protocol::to_generated_protocol_frame(
1047 &crate::protocol::ProtocolFrame::Event(event),
1048 )? {
1049 ProtocolFrame::EventFrame(event) => Ok(event),
1050 ProtocolFrame::RequestFrame(_)
1051 | ProtocolFrame::ResponseFrame(_)
1052 | ProtocolFrame::SidecarRequestFrame(_)
1053 | ProtocolFrame::SidecarResponseFrame(_) => {
1054 Err(ProtocolCodecError::SerializeFailure(String::from(
1055 "compatibility dispatch event converted to non-event wire frame",
1056 )))
1057 }
1058 }
1059 })
1060 .collect::<Result<Vec<_>, _>>()?;
1061
1062 Ok(WireDispatchResult { response, events })
1063}
1064
1065fn validate_frame(frame: &ProtocolFrame) -> Result<(), ProtocolCodecError> {
1066 match frame {
1067 ProtocolFrame::RequestFrame(frame) => {
1068 validate_schema(&frame.schema)?;
1069 validate_request_id(frame.request_id)
1070 }
1071 ProtocolFrame::ResponseFrame(frame) => {
1072 validate_schema(&frame.schema)?;
1073 validate_request_id(frame.request_id)
1074 }
1075 ProtocolFrame::EventFrame(frame) => validate_schema(&frame.schema),
1076 ProtocolFrame::SidecarRequestFrame(frame) => {
1077 validate_schema(&frame.schema)?;
1078 validate_request_id(frame.request_id)
1079 }
1080 ProtocolFrame::SidecarResponseFrame(frame) => {
1081 validate_schema(&frame.schema)?;
1082 validate_request_id(frame.request_id)
1083 }
1084 }
1085}
1086
1087fn validate_schema(schema: &ProtocolSchema) -> Result<(), ProtocolCodecError> {
1088 if schema.name != PROTOCOL_NAME || schema.version != PROTOCOL_VERSION {
1089 return Err(ProtocolCodecError::UnsupportedSchema {
1090 name: schema.name.clone(),
1091 version: schema.version,
1092 });
1093 }
1094 Ok(())
1095}
1096
1097fn validate_request_id(request_id: RequestId) -> Result<(), ProtocolCodecError> {
1098 if request_id == 0 {
1099 return Err(ProtocolCodecError::InvalidRequestId);
1100 }
1101 Ok(())
1102}