Skip to main content

secure_exec_sidecar_protocol/
wire.rs

1//! Generated Secure Exec sidecar wire protocol surface.
2//!
3//! This module is the public generated protocol entrypoint. The hand-written
4//! `protocol` module remains an internal compatibility layer while callers move
5//! to generated wire frames.
6
7use std::error::Error;
8use std::fmt;
9
10pub use crate::generated_protocol::v1::*;
11
12// The generated BARE types intentionally omit `Copy`/`Default`; restore them on the
13// crate-local generated types so the wider sidecar keeps the ergonomics it relies on
14// after the hand-written protocol types were replaced with these aliases. These live in
15// `wire` (not `protocol`) because `protocol.rs` is `#[path]`-included by integration
16// tests, where the generated types would be foreign and the impls would break the orphan rule.
17impl 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// `derive(Default)` cannot be added: these are foreign generated types, so the
22// `Default` impl must be written by hand here (orphan rule).
23#[allow(clippy::derivable_impls)]
24impl Default for crate::generated_protocol::v1::RootFilesystemEntryKind {
25    fn default() -> Self {
26        Self::File
27    }
28}
29
30impl Default for crate::generated_protocol::v1::RootFilesystemEntry {
31    fn default() -> Self {
32        Self {
33            path: String::new(),
34            kind: crate::generated_protocol::v1::RootFilesystemEntryKind::File,
35            mode: None,
36            uid: None,
37            gid: None,
38            content: None,
39            encoding: None,
40            target: None,
41            executable: false,
42        }
43    }
44}
45
46#[allow(clippy::derivable_impls)]
47impl Default for crate::generated_protocol::v1::RootFilesystemMode {
48    fn default() -> Self {
49        Self::Ephemeral
50    }
51}
52
53#[allow(clippy::derivable_impls)]
54impl Default for crate::generated_protocol::v1::RootFilesystemDescriptor {
55    fn default() -> Self {
56        Self {
57            mode: crate::generated_protocol::v1::RootFilesystemMode::default(),
58            disable_default_base_layer: false,
59            lowers: Vec::new(),
60            bootstrap_entries: Vec::new(),
61        }
62    }
63}
64
65impl crate::generated_protocol::v1::PermissionsPolicy {
66    pub fn deny_all() -> Self {
67        use crate::generated_protocol::v1::{
68            FsPermissionScope, PatternPermissionScope, PermissionMode,
69        };
70        Self {
71            fs: Some(FsPermissionScope::PermissionMode(PermissionMode::Deny)),
72            network: Some(PatternPermissionScope::PermissionMode(PermissionMode::Deny)),
73            child_process: Some(PatternPermissionScope::PermissionMode(PermissionMode::Deny)),
74            process: Some(PatternPermissionScope::PermissionMode(PermissionMode::Deny)),
75            env: Some(PatternPermissionScope::PermissionMode(PermissionMode::Deny)),
76            binding: Some(PatternPermissionScope::PermissionMode(PermissionMode::Deny)),
77        }
78    }
79
80    pub fn allow_all() -> Self {
81        use crate::generated_protocol::v1::{
82            FsPermissionScope, PatternPermissionScope, PermissionMode,
83        };
84        Self {
85            fs: Some(FsPermissionScope::PermissionMode(PermissionMode::Allow)),
86            network: Some(PatternPermissionScope::PermissionMode(
87                PermissionMode::Allow,
88            )),
89            child_process: Some(PatternPermissionScope::PermissionMode(
90                PermissionMode::Allow,
91            )),
92            process: Some(PatternPermissionScope::PermissionMode(
93                PermissionMode::Allow,
94            )),
95            env: Some(PatternPermissionScope::PermissionMode(
96                PermissionMode::Allow,
97            )),
98            binding: Some(PatternPermissionScope::PermissionMode(
99                PermissionMode::Allow,
100            )),
101        }
102    }
103}
104
105impl Default for crate::generated_protocol::v1::PermissionsPolicy {
106    fn default() -> Self {
107        Self::deny_all()
108    }
109}
110
111impl crate::generated_protocol::v1::CreateVmRequest {
112    pub fn json_config(
113        runtime: crate::generated_protocol::v1::GuestRuntimeKind,
114        config: 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(permissions_policy_config_from_wire),
134            limits: legacy_limits_config(&metadata),
135            dns: legacy_dns_config(&metadata),
136            native_root: legacy_native_root_config(&metadata),
137            listen: legacy_listen_config(&metadata),
138            ..Default::default()
139        };
140        config.loopback_exempt_ports = legacy_loopback_exempt_ports(&config.env);
141        Self::json_config(runtime, config)
142    }
143}
144
145fn legacy_env_config(
146    metadata: &std::collections::BTreeMap<String, String>,
147) -> std::collections::BTreeMap<String, String> {
148    metadata
149        .iter()
150        .filter_map(|(key, value)| {
151            key.strip_prefix("env.")
152                .map(|name| (name.to_string(), value.clone()))
153        })
154        .collect()
155}
156
157fn legacy_root_filesystem_config(
158    descriptor: crate::generated_protocol::v1::RootFilesystemDescriptor,
159) -> 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
235pub fn permissions_policy_config_from_wire(
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
614// Ownership-scope constructor ergonomics. The generated BARE union exposes only the
615// tuple-wrapped variants (`ConnectionOwnership`/`SessionOwnership`/`VmOwnership`); restore
616// the hand-written `connection`/`session`/`vm` helpers the sidecar relies on. These live in
617// `wire` (not `protocol`) for the same orphan-rule reason as the impls above: `protocol.rs`
618// is `#[path]`-included by integration tests where the generated type is foreign.
619impl 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;
648// 16 MiB: large enough to carry a trusted-client CreateVm config that inlines an
649// entire base-filesystem snapshot, while still bounding a single frame.
650pub const DEFAULT_MAX_FRAME_BYTES: usize = 16 * 1024 * 1024;
651
652#[derive(Debug, Clone, PartialEq, Eq)]
653pub enum ProtocolCodecError {
654    TruncatedFrame {
655        actual: usize,
656    },
657    LengthPrefixMismatch {
658        declared: usize,
659        actual: usize,
660    },
661    FrameTooLarge {
662        size: usize,
663        max: usize,
664    },
665    UnsupportedSchema {
666        name: String,
667        version: u16,
668    },
669    InvalidRequestId,
670    InvalidRequestDirection {
671        request_id: RequestId,
672        expected: RequestDirection,
673    },
674    EmptyOwnershipField {
675        field: &'static str,
676    },
677    EmptyAuthToken,
678    InvalidOwnershipScope {
679        required: OwnershipRequirement,
680        actual: OwnershipRequirement,
681    },
682    SerializeFailure(String),
683    DeserializeFailure(String),
684}
685
686impl fmt::Display for ProtocolCodecError {
687    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
688        match self {
689            Self::TruncatedFrame { actual } => {
690                write!(
691                    f,
692                    "protocol frame is truncated: only {actual} bytes provided"
693                )
694            }
695            Self::LengthPrefixMismatch { declared, actual } => write!(
696                f,
697                "protocol frame length prefix mismatch: declared {declared} bytes, got {actual}",
698            ),
699            Self::FrameTooLarge { size, max } => {
700                write!(f, "protocol frame is {size} bytes, limit is {max}")
701            }
702            Self::UnsupportedSchema { name, version } => write!(
703                f,
704                "unsupported protocol schema {name}@{version}; expected {PROTOCOL_NAME}@{PROTOCOL_VERSION}",
705            ),
706            Self::InvalidRequestId => write!(f, "protocol request identifiers must be non-zero"),
707            Self::InvalidRequestDirection {
708                request_id,
709                expected,
710            } => write!(f, "protocol request id {request_id} must be {expected}",),
711            Self::EmptyOwnershipField { field } => {
712                write!(f, "protocol ownership field `{field}` cannot be empty")
713            }
714            Self::EmptyAuthToken => {
715                write!(f, "authenticate requests require a non-empty auth token")
716            }
717            Self::InvalidOwnershipScope { required, actual } => write!(
718                f,
719                "protocol frame requires {required} ownership but carried {actual}",
720            ),
721            Self::SerializeFailure(message) => {
722                write!(f, "protocol frame serialization failed: {message}")
723            }
724            Self::DeserializeFailure(message) => {
725                write!(f, "protocol frame deserialization failed: {message}")
726            }
727        }
728    }
729}
730
731impl Error for ProtocolCodecError {}
732
733#[derive(Debug, Clone, Copy, PartialEq, Eq)]
734pub enum OwnershipRequirement {
735    Any,
736    Connection,
737    Session,
738    Vm,
739    SessionOrVm,
740}
741
742impl fmt::Display for OwnershipRequirement {
743    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
744        match self {
745            Self::Any => write!(f, "any"),
746            Self::Connection => write!(f, "connection"),
747            Self::Session => write!(f, "session"),
748            Self::Vm => write!(f, "vm"),
749            Self::SessionOrVm => write!(f, "session-or-vm"),
750        }
751    }
752}
753
754#[derive(Debug, Clone, Copy, PartialEq, Eq)]
755pub enum RequestDirection {
756    Host,
757    Sidecar,
758}
759
760impl fmt::Display for RequestDirection {
761    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
762        match self {
763            Self::Host => write!(f, "positive"),
764            Self::Sidecar => write!(f, "negative"),
765        }
766    }
767}
768
769#[derive(Debug, Clone, PartialEq, Eq)]
770pub struct WireDispatchResult {
771    pub response: ResponseFrame,
772    pub events: Vec<EventFrame>,
773}
774
775#[derive(Debug, Clone, PartialEq, Eq)]
776pub struct CompatDispatchResult {
777    pub response: crate::protocol::ResponseFrame,
778    pub events: Vec<crate::protocol::EventFrame>,
779}
780
781#[derive(Debug, Clone)]
782pub struct WireFrameCodec {
783    max_frame_bytes: usize,
784}
785
786impl WireFrameCodec {
787    pub fn new(max_frame_bytes: usize) -> Self {
788        Self { max_frame_bytes }
789    }
790
791    pub fn max_frame_bytes(&self) -> usize {
792        self.max_frame_bytes
793    }
794
795    pub fn encode(&self, frame: &ProtocolFrame) -> Result<Vec<u8>, ProtocolCodecError> {
796        validate_frame(frame)?;
797
798        let payload = serde_bare::to_vec(frame)
799            .map_err(|error| ProtocolCodecError::SerializeFailure(error.to_string()))?;
800        if payload.len() > self.max_frame_bytes {
801            return Err(ProtocolCodecError::FrameTooLarge {
802                size: payload.len(),
803                max: self.max_frame_bytes,
804            });
805        }
806
807        let length =
808            u32::try_from(payload.len()).map_err(|_| ProtocolCodecError::FrameTooLarge {
809                size: payload.len(),
810                max: u32::MAX as usize,
811            })?;
812
813        let mut encoded = Vec::with_capacity(4 + payload.len());
814        encoded.extend_from_slice(&length.to_be_bytes());
815        encoded.extend_from_slice(&payload);
816        Ok(encoded)
817    }
818
819    pub fn decode(&self, bytes: &[u8]) -> Result<ProtocolFrame, ProtocolCodecError> {
820        let payload = self.checked_payload(bytes)?;
821        let frame = serde_bare::from_slice(payload)
822            .map_err(|error| ProtocolCodecError::DeserializeFailure(error.to_string()))?;
823        validate_frame(&frame)?;
824        Ok(frame)
825    }
826
827    /// Encode a frame as a bare message WITHOUT the 4-byte length prefix.
828    ///
829    /// Stream transports (stdio) use [`encode`] so frames can be delimited in a
830    /// byte stream. Message transports where the boundary is the call itself
831    /// (the browser `pushFrame` / `postMessage` path) use this so the on-wire
832    /// bytes match the TypeScript `encodeProtocolFramePayload(frame, "bare")`,
833    /// which emits the raw bare frame with no prefix.
834    pub fn encode_message(&self, frame: &ProtocolFrame) -> Result<Vec<u8>, ProtocolCodecError> {
835        validate_frame(frame)?;
836        let payload = serde_bare::to_vec(frame)
837            .map_err(|error| ProtocolCodecError::SerializeFailure(error.to_string()))?;
838        if payload.len() > self.max_frame_bytes {
839            return Err(ProtocolCodecError::FrameTooLarge {
840                size: payload.len(),
841                max: self.max_frame_bytes,
842            });
843        }
844        Ok(payload)
845    }
846
847    /// Decode a bare message produced by [`encode_message`] (no length prefix).
848    pub fn decode_message(&self, bytes: &[u8]) -> Result<ProtocolFrame, ProtocolCodecError> {
849        if bytes.len() > self.max_frame_bytes {
850            return Err(ProtocolCodecError::FrameTooLarge {
851                size: bytes.len(),
852                max: self.max_frame_bytes,
853            });
854        }
855        let frame = serde_bare::from_slice(bytes)
856            .map_err(|error| ProtocolCodecError::DeserializeFailure(error.to_string()))?;
857        validate_frame(&frame)?;
858        Ok(frame)
859    }
860
861    fn checked_payload<'a>(&self, bytes: &'a [u8]) -> Result<&'a [u8], ProtocolCodecError> {
862        if bytes.len() < 4 {
863            return Err(ProtocolCodecError::TruncatedFrame {
864                actual: bytes.len(),
865            });
866        }
867
868        let declared =
869            u32::from_be_bytes(bytes[..4].try_into().expect("length prefix is four bytes"))
870                as usize;
871        if declared > self.max_frame_bytes {
872            return Err(ProtocolCodecError::FrameTooLarge {
873                size: declared,
874                max: self.max_frame_bytes,
875            });
876        }
877
878        let actual = bytes.len() - 4;
879        if declared != actual {
880            return Err(ProtocolCodecError::LengthPrefixMismatch { declared, actual });
881        }
882
883        Ok(&bytes[4..])
884    }
885}
886
887impl Default for WireFrameCodec {
888    fn default() -> Self {
889        Self::new(DEFAULT_MAX_FRAME_BYTES)
890    }
891}
892
893pub fn protocol_schema() -> ProtocolSchema {
894    ProtocolSchema::current()
895}
896
897impl ProtocolSchema {
898    pub fn current() -> Self {
899        Self {
900            name: PROTOCOL_NAME.to_string(),
901            version: PROTOCOL_VERSION,
902        }
903    }
904}
905
906impl Default for ProtocolSchema {
907    fn default() -> Self {
908        Self::current()
909    }
910}
911
912pub fn request_frame_to_compat(
913    request: RequestFrame,
914) -> Result<crate::protocol::RequestFrame, ProtocolCodecError> {
915    match crate::protocol::from_generated_protocol_frame(ProtocolFrame::RequestFrame(request))? {
916        crate::protocol::ProtocolFrame::Request(request) => Ok(request),
917        crate::protocol::ProtocolFrame::Response(_)
918        | crate::protocol::ProtocolFrame::Event(_)
919        | crate::protocol::ProtocolFrame::SidecarRequest(_)
920        | crate::protocol::ProtocolFrame::SidecarResponse(_) => {
921            Err(ProtocolCodecError::DeserializeFailure(String::from(
922                "wire request frame converted to non-request compatibility frame",
923            )))
924        }
925    }
926}
927
928pub fn ownership_scope_to_compat(ownership: OwnershipScope) -> crate::protocol::OwnershipScope {
929    crate::protocol::from_generated_ownership_scope(ownership)
930}
931
932pub fn request_payload_to_compat(
933    ownership: &crate::protocol::OwnershipScope,
934    payload: RequestPayload,
935) -> Result<crate::protocol::RequestPayload, ProtocolCodecError> {
936    match crate::protocol::from_generated_protocol_frame(ProtocolFrame::RequestFrame(
937        RequestFrame {
938            schema: protocol_schema(),
939            request_id: 1,
940            ownership: crate::protocol::to_generated_ownership_scope(ownership),
941            payload,
942        },
943    ))? {
944        crate::protocol::ProtocolFrame::Request(request) => Ok(request.payload),
945        crate::protocol::ProtocolFrame::Response(_)
946        | crate::protocol::ProtocolFrame::Event(_)
947        | crate::protocol::ProtocolFrame::SidecarRequest(_)
948        | crate::protocol::ProtocolFrame::SidecarResponse(_) => {
949            Err(ProtocolCodecError::DeserializeFailure(String::from(
950                "wire request payload converted to non-request compatibility frame",
951            )))
952        }
953    }
954}
955
956pub fn response_payload_from_compat(
957    ownership: &crate::protocol::OwnershipScope,
958    payload: crate::protocol::ResponsePayload,
959) -> Result<ResponsePayload, ProtocolCodecError> {
960    match crate::protocol::to_generated_protocol_frame(&crate::protocol::ProtocolFrame::Response(
961        crate::protocol::ResponseFrame::new(1, ownership.clone(), payload),
962    ))? {
963        ProtocolFrame::ResponseFrame(response) => Ok(response.payload),
964        ProtocolFrame::RequestFrame(_)
965        | ProtocolFrame::EventFrame(_)
966        | ProtocolFrame::SidecarRequestFrame(_)
967        | ProtocolFrame::SidecarResponseFrame(_) => Err(ProtocolCodecError::SerializeFailure(
968            String::from("compatibility response payload converted to non-response wire frame"),
969        )),
970    }
971}
972
973pub fn event_frame_from_compat(
974    event: crate::protocol::EventFrame,
975) -> Result<EventFrame, ProtocolCodecError> {
976    match crate::protocol::to_generated_protocol_frame(&crate::protocol::ProtocolFrame::Event(
977        event,
978    ))? {
979        ProtocolFrame::EventFrame(event) => Ok(event),
980        ProtocolFrame::RequestFrame(_)
981        | ProtocolFrame::ResponseFrame(_)
982        | ProtocolFrame::SidecarRequestFrame(_)
983        | ProtocolFrame::SidecarResponseFrame(_) => Err(ProtocolCodecError::SerializeFailure(
984            String::from("compatibility event converted to non-event wire frame"),
985        )),
986    }
987}
988
989pub fn event_frame_to_compat(
990    event: EventFrame,
991) -> Result<crate::protocol::EventFrame, ProtocolCodecError> {
992    match crate::protocol::from_generated_protocol_frame(ProtocolFrame::EventFrame(event))? {
993        crate::protocol::ProtocolFrame::Event(event) => Ok(event),
994        crate::protocol::ProtocolFrame::Request(_)
995        | crate::protocol::ProtocolFrame::Response(_)
996        | crate::protocol::ProtocolFrame::SidecarRequest(_)
997        | crate::protocol::ProtocolFrame::SidecarResponse(_) => {
998            Err(ProtocolCodecError::DeserializeFailure(String::from(
999                "wire event converted to non-event compatibility frame",
1000            )))
1001        }
1002    }
1003}
1004
1005pub fn sidecar_request_frame_from_compat(
1006    request: crate::protocol::SidecarRequestFrame,
1007) -> Result<SidecarRequestFrame, ProtocolCodecError> {
1008    match crate::protocol::to_generated_protocol_frame(
1009        &crate::protocol::ProtocolFrame::SidecarRequest(request),
1010    )? {
1011        ProtocolFrame::SidecarRequestFrame(request) => Ok(request),
1012        ProtocolFrame::RequestFrame(_)
1013        | ProtocolFrame::ResponseFrame(_)
1014        | ProtocolFrame::EventFrame(_)
1015        | ProtocolFrame::SidecarResponseFrame(_) => {
1016            Err(ProtocolCodecError::SerializeFailure(String::from(
1017                "compatibility sidecar request converted to non-sidecar-request wire frame",
1018            )))
1019        }
1020    }
1021}
1022
1023pub fn sidecar_request_payload_to_compat(
1024    ownership: &crate::protocol::OwnershipScope,
1025    payload: SidecarRequestPayload,
1026) -> Result<crate::protocol::SidecarRequestPayload, ProtocolCodecError> {
1027    match crate::protocol::from_generated_protocol_frame(ProtocolFrame::SidecarRequestFrame(
1028        SidecarRequestFrame {
1029            schema: protocol_schema(),
1030            request_id: -1,
1031            ownership: crate::protocol::to_generated_ownership_scope(ownership),
1032            payload,
1033        },
1034    ))? {
1035        crate::protocol::ProtocolFrame::SidecarRequest(request) => Ok(request.payload),
1036        crate::protocol::ProtocolFrame::Request(_)
1037        | crate::protocol::ProtocolFrame::Response(_)
1038        | crate::protocol::ProtocolFrame::Event(_)
1039        | crate::protocol::ProtocolFrame::SidecarResponse(_) => {
1040            Err(ProtocolCodecError::DeserializeFailure(String::from(
1041                "wire sidecar request payload converted to non-sidecar-request compatibility frame",
1042            )))
1043        }
1044    }
1045}
1046
1047pub fn sidecar_response_frame_to_compat(
1048    response: SidecarResponseFrame,
1049) -> Result<crate::protocol::SidecarResponseFrame, ProtocolCodecError> {
1050    match crate::protocol::from_generated_protocol_frame(ProtocolFrame::SidecarResponseFrame(
1051        response,
1052    ))? {
1053        crate::protocol::ProtocolFrame::SidecarResponse(response) => Ok(response),
1054        crate::protocol::ProtocolFrame::Request(_)
1055        | crate::protocol::ProtocolFrame::Response(_)
1056        | crate::protocol::ProtocolFrame::Event(_)
1057        | crate::protocol::ProtocolFrame::SidecarRequest(_) => {
1058            Err(ProtocolCodecError::DeserializeFailure(String::from(
1059                "wire sidecar response converted to non-sidecar-response compatibility frame",
1060            )))
1061        }
1062    }
1063}
1064
1065pub fn sidecar_response_frame_from_compat(
1066    response: crate::protocol::SidecarResponseFrame,
1067) -> Result<SidecarResponseFrame, ProtocolCodecError> {
1068    match crate::protocol::to_generated_protocol_frame(
1069        &crate::protocol::ProtocolFrame::SidecarResponse(response),
1070    )? {
1071        ProtocolFrame::SidecarResponseFrame(response) => Ok(response),
1072        ProtocolFrame::RequestFrame(_)
1073        | ProtocolFrame::ResponseFrame(_)
1074        | ProtocolFrame::EventFrame(_)
1075        | ProtocolFrame::SidecarRequestFrame(_) => {
1076            Err(ProtocolCodecError::SerializeFailure(String::from(
1077                "compatibility sidecar response converted to non-sidecar-response wire frame",
1078            )))
1079        }
1080    }
1081}
1082
1083pub fn dispatch_result_from_compat(
1084    result: CompatDispatchResult,
1085) -> Result<WireDispatchResult, ProtocolCodecError> {
1086    let response = match crate::protocol::to_generated_protocol_frame(
1087        &crate::protocol::ProtocolFrame::Response(result.response),
1088    )? {
1089        ProtocolFrame::ResponseFrame(response) => response,
1090        ProtocolFrame::RequestFrame(_)
1091        | ProtocolFrame::EventFrame(_)
1092        | ProtocolFrame::SidecarRequestFrame(_)
1093        | ProtocolFrame::SidecarResponseFrame(_) => {
1094            return Err(ProtocolCodecError::SerializeFailure(String::from(
1095                "compatibility dispatch response converted to non-response wire frame",
1096            )));
1097        }
1098    };
1099
1100    let events = result
1101        .events
1102        .into_iter()
1103        .map(|event| {
1104            match crate::protocol::to_generated_protocol_frame(
1105                &crate::protocol::ProtocolFrame::Event(event),
1106            )? {
1107                ProtocolFrame::EventFrame(event) => Ok(event),
1108                ProtocolFrame::RequestFrame(_)
1109                | ProtocolFrame::ResponseFrame(_)
1110                | ProtocolFrame::SidecarRequestFrame(_)
1111                | ProtocolFrame::SidecarResponseFrame(_) => {
1112                    Err(ProtocolCodecError::SerializeFailure(String::from(
1113                        "compatibility dispatch event converted to non-event wire frame",
1114                    )))
1115                }
1116            }
1117        })
1118        .collect::<Result<Vec<_>, _>>()?;
1119
1120    Ok(WireDispatchResult { response, events })
1121}
1122
1123fn validate_frame(frame: &ProtocolFrame) -> Result<(), ProtocolCodecError> {
1124    match frame {
1125        ProtocolFrame::RequestFrame(frame) => {
1126            validate_schema(&frame.schema)?;
1127            validate_request_id(frame.request_id)
1128        }
1129        ProtocolFrame::ResponseFrame(frame) => {
1130            validate_schema(&frame.schema)?;
1131            validate_request_id(frame.request_id)
1132        }
1133        ProtocolFrame::EventFrame(frame) => validate_schema(&frame.schema),
1134        ProtocolFrame::SidecarRequestFrame(frame) => {
1135            validate_schema(&frame.schema)?;
1136            validate_request_id(frame.request_id)
1137        }
1138        ProtocolFrame::SidecarResponseFrame(frame) => {
1139            validate_schema(&frame.schema)?;
1140            validate_request_id(frame.request_id)
1141        }
1142    }
1143}
1144
1145fn validate_schema(schema: &ProtocolSchema) -> Result<(), ProtocolCodecError> {
1146    if schema.name != PROTOCOL_NAME || schema.version != PROTOCOL_VERSION {
1147        return Err(ProtocolCodecError::UnsupportedSchema {
1148            name: schema.name.clone(),
1149            version: schema.version,
1150        });
1151    }
1152    Ok(())
1153}
1154
1155fn validate_request_id(request_id: RequestId) -> Result<(), ProtocolCodecError> {
1156    if request_id == 0 {
1157        return Err(ProtocolCodecError::InvalidRequestId);
1158    }
1159    Ok(())
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164    use super::*;
1165    use crate::generated_protocol::v1::{
1166        FsPermissionScope, PatternPermissionScope, PermissionMode,
1167    };
1168
1169    #[test]
1170    fn permissions_policy_default_matches_no_policy_deny_all() {
1171        let policy = PermissionsPolicy::default();
1172
1173        assert!(matches!(
1174            policy.fs,
1175            Some(FsPermissionScope::PermissionMode(PermissionMode::Deny))
1176        ));
1177        for scope in [
1178            policy.network,
1179            policy.child_process,
1180            policy.process,
1181            policy.env,
1182            policy.binding,
1183        ] {
1184            assert!(matches!(
1185                scope,
1186                Some(PatternPermissionScope::PermissionMode(PermissionMode::Deny))
1187            ));
1188        }
1189    }
1190
1191    #[test]
1192    fn permissions_policy_allow_all_remains_explicit() {
1193        let policy = PermissionsPolicy::allow_all();
1194
1195        assert!(matches!(
1196            policy.fs,
1197            Some(FsPermissionScope::PermissionMode(PermissionMode::Allow))
1198        ));
1199        for scope in [
1200            policy.network,
1201            policy.child_process,
1202            policy.process,
1203            policy.env,
1204            policy.binding,
1205        ] {
1206            assert!(matches!(
1207                scope,
1208                Some(PatternPermissionScope::PermissionMode(
1209                    PermissionMode::Allow
1210                ))
1211            ));
1212        }
1213    }
1214}