Skip to main content

agentos_vm_config/
lib.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use ts_rs::TS;
5
6/// Canonical Rust-side VM config. Unknown fields must stay rejected here and in
7/// the TS preflight schema at
8/// `packages/core/src/node-runtime-options-schema.ts`; update both when a
9/// public `NodeRuntime.create(...)` option changes the generated VM config.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
11#[serde(rename_all = "camelCase", deny_unknown_fields)]
12#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
13#[derive(Default)]
14pub struct CreateVmConfig {
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    #[ts(optional)]
17    pub cwd: Option<String>,
18    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
19    #[ts(type = "Record<string, string>")]
20    pub env: BTreeMap<String, String>,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    #[ts(optional)]
23    pub database: Option<VmSqliteDescriptor>,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    #[ts(optional)]
26    pub user: Option<VmUserConfig>,
27    #[serde(default, rename = "rootFilesystem")]
28    pub root_filesystem: RootFilesystemConfig,
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    #[ts(optional)]
31    pub permissions: Option<PermissionsPolicy>,
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    #[ts(optional)]
34    pub limits: Option<VmLimitsConfig>,
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    #[ts(optional)]
37    pub dns: Option<VmDnsConfig>,
38    #[serde(
39        default,
40        rename = "nativeRoot",
41        skip_serializing_if = "Option::is_none"
42    )]
43    #[ts(optional)]
44    pub native_root: Option<NativeRootFilesystemConfig>,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    #[ts(optional)]
47    pub listen: Option<VmListenPolicyConfig>,
48    #[serde(
49        default,
50        rename = "loopbackExemptPorts",
51        skip_serializing_if = "Vec::is_empty"
52    )]
53    pub loopback_exempt_ports: Vec<u16>,
54    #[serde(default, rename = "jsRuntime", skip_serializing_if = "Option::is_none")]
55    #[ts(optional)]
56    pub js_runtime: Option<JsRuntimeConfig>,
57    #[serde(
58        default,
59        rename = "bootstrapCommands",
60        skip_serializing_if = "Option::is_none"
61    )]
62    #[ts(optional)]
63    pub bootstrap_commands: Option<Vec<String>>,
64}
65
66impl CreateVmConfig {
67    pub fn validate(&self, max_frame_bytes: usize) -> Result<(), VmConfigError> {
68        if let Some(cwd) = self.cwd.as_deref() {
69            validate_guest_path("cwd", cwd)?;
70        }
71        if let Some(database) = &self.database {
72            database.validate()?;
73        }
74        if let Some(user) = &self.user {
75            user.validate()?;
76        }
77        self.root_filesystem.validate()?;
78        if let Some(native_root) = &self.native_root {
79            native_root.validate()?;
80        }
81        if self.native_root.is_some() && !self.root_filesystem.bootstrap_entries.is_empty() {
82            return Err(VmConfigError::new(
83                "nativeRoot does not support rootFilesystem.bootstrapEntries",
84            ));
85        }
86        if let Some(dns) = &self.dns {
87            dns.validate()?;
88        }
89        if let Some(listen) = &self.listen {
90            listen.validate()?;
91        }
92        if let Some(limits) = &self.limits {
93            limits.validate(max_frame_bytes)?;
94        }
95        if let Some(js_runtime) = &self.js_runtime {
96            js_runtime.validate()?;
97        }
98        if let Some(bootstrap_commands) = &self.bootstrap_commands {
99            validate_command_names("bootstrapCommands", bootstrap_commands)?;
100        }
101        Ok(())
102    }
103}
104
105/// Transport used by the VM-scoped SQLite substrate.
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
107#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
108#[ts(tag = "type", rename_all = "snake_case")]
109#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
110pub enum VmSqliteDescriptor {
111    /// Rivet actor SQLite reached through the actor's local runtime socket.
112    ActorUds { path: String },
113    /// A SQLite database file owned by the native sidecar host.
114    SqliteFile { path: String },
115}
116
117impl VmSqliteDescriptor {
118    fn validate(&self) -> Result<(), VmConfigError> {
119        match self {
120            Self::ActorUds { path } => {
121                validate_absolute_host_path("database.path", path)?;
122            }
123            Self::SqliteFile { path } => validate_absolute_host_path("database.path", path)?,
124        }
125        Ok(())
126    }
127}
128
129fn validate_absolute_host_path(field: &str, path: &str) -> Result<(), VmConfigError> {
130    if path.is_empty() || !path.starts_with('/') || path.as_bytes().contains(&0) {
131        return Err(VmConfigError::new(format!(
132            "{field} must be a non-empty absolute path without NUL bytes"
133        )));
134    }
135    Ok(())
136}
137
138/// Initial Linux-style credentials and account record for processes in a VM.
139#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, TS)]
140#[serde(rename_all = "camelCase", deny_unknown_fields)]
141#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
142pub struct VmUserConfig {
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    #[ts(optional)]
145    pub uid: Option<u32>,
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    #[ts(optional)]
148    pub gid: Option<u32>,
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    #[ts(optional)]
151    pub euid: Option<u32>,
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    #[ts(optional)]
154    pub egid: Option<u32>,
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    #[ts(optional)]
157    pub username: Option<String>,
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    #[ts(optional)]
160    pub homedir: Option<String>,
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    #[ts(optional)]
163    pub shell: Option<String>,
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    #[ts(optional)]
166    pub gecos: Option<String>,
167    #[serde(default, rename = "groupName", skip_serializing_if = "Option::is_none")]
168    #[ts(optional)]
169    pub group_name: Option<String>,
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    #[ts(optional)]
172    pub supplementary_gids: Option<Vec<u32>>,
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    #[ts(optional)]
175    pub accounts: Option<Vec<VmUserAccountConfig>>,
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    #[ts(optional)]
178    pub groups: Option<Vec<VmGroupConfig>>,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
182#[serde(rename_all = "camelCase", deny_unknown_fields)]
183#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
184pub struct VmUserAccountConfig {
185    pub uid: u32,
186    pub gid: u32,
187    pub username: String,
188    pub homedir: String,
189    pub shell: String,
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    #[ts(optional)]
192    pub gecos: Option<String>,
193    #[serde(default, skip_serializing_if = "Vec::is_empty")]
194    pub supplementary_gids: Vec<u32>,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
198#[serde(rename_all = "camelCase", deny_unknown_fields)]
199#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
200pub struct VmGroupConfig {
201    pub gid: u32,
202    pub name: String,
203    #[serde(default, skip_serializing_if = "Vec::is_empty")]
204    pub members: Vec<String>,
205}
206
207impl VmUserConfig {
208    fn validate(&self) -> Result<(), VmConfigError> {
209        const MAX_SUPPLEMENTARY_GIDS: usize = 64;
210        const MAX_ACCOUNTS: usize = 64;
211        const MAX_GROUPS: usize = 128;
212        if self
213            .supplementary_gids
214            .as_ref()
215            .is_some_and(|groups| groups.len() > MAX_SUPPLEMENTARY_GIDS)
216        {
217            return Err(VmConfigError::new(format!(
218                "user.supplementaryGids exceeds limit of {MAX_SUPPLEMENTARY_GIDS}"
219            )));
220        }
221        for (label, value) in [
222            ("user.username", self.username.as_deref()),
223            ("user.groupName", self.group_name.as_deref()),
224        ] {
225            if value.is_some_and(|value| {
226                value.is_empty()
227                    || value.contains([':', '\n', '\r', '\0'])
228                    || value.chars().any(char::is_whitespace)
229            }) {
230                return Err(VmConfigError::new(format!("{label} is invalid")));
231            }
232        }
233        if self
234            .gecos
235            .as_deref()
236            .is_some_and(|value| value.contains([':', '\n', '\r', '\0']))
237        {
238            return Err(VmConfigError::new("user.gecos is invalid"));
239        }
240        let accounts = self.accounts.as_deref().unwrap_or_default();
241        let groups = self.groups.as_deref().unwrap_or_default();
242        if accounts.len() > MAX_ACCOUNTS {
243            return Err(VmConfigError::new(format!(
244                "user.accounts exceeds limit of {MAX_ACCOUNTS}"
245            )));
246        }
247        if groups.len() > MAX_GROUPS {
248            return Err(VmConfigError::new(format!(
249                "user.groups exceeds limit of {MAX_GROUPS}"
250            )));
251        }
252        let mut account_uids = std::collections::BTreeSet::new();
253        let mut account_names = std::collections::BTreeSet::new();
254        for account in accounts {
255            validate_account_name("user.accounts[].username", &account.username)?;
256            validate_guest_path("user.accounts[].homedir", &account.homedir)?;
257            validate_guest_path("user.accounts[].shell", &account.shell)?;
258            if account
259                .gecos
260                .as_deref()
261                .is_some_and(|value| value.contains([':', '\n', '\r', '\0']))
262            {
263                return Err(VmConfigError::new("user.accounts[].gecos is invalid"));
264            }
265            if account.supplementary_gids.len() > MAX_SUPPLEMENTARY_GIDS {
266                return Err(VmConfigError::new(format!(
267                    "user.accounts[].supplementaryGids exceeds limit of {MAX_SUPPLEMENTARY_GIDS}"
268                )));
269            }
270            if !account_uids.insert(account.uid) {
271                return Err(VmConfigError::new(format!(
272                    "duplicate user account uid {}",
273                    account.uid
274                )));
275            }
276            if !account_names.insert(account.username.as_str()) {
277                return Err(VmConfigError::new(format!(
278                    "duplicate user account name {}",
279                    account.username
280                )));
281            }
282        }
283        let mut group_gids = std::collections::BTreeSet::new();
284        let mut group_names = std::collections::BTreeSet::new();
285        for group in groups {
286            validate_account_name("user.groups[].name", &group.name)?;
287            for member in &group.members {
288                validate_account_name("user.groups[].members[]", member)?;
289            }
290            if !group_gids.insert(group.gid) {
291                return Err(VmConfigError::new(format!(
292                    "duplicate user group gid {}",
293                    group.gid
294                )));
295            }
296            if !group_names.insert(group.name.as_str()) {
297                return Err(VmConfigError::new(format!(
298                    "duplicate user group name {}",
299                    group.name
300                )));
301            }
302        }
303        if let Some(homedir) = self.homedir.as_deref() {
304            validate_guest_path("user.homedir", homedir)?;
305        }
306        if let Some(shell) = self.shell.as_deref() {
307            validate_guest_path("user.shell", shell)?;
308        }
309        Ok(())
310    }
311}
312
313fn validate_account_name(label: &str, value: &str) -> Result<(), VmConfigError> {
314    if value.is_empty()
315        || value.contains([':', '\n', '\r', '\0'])
316        || value.chars().any(char::is_whitespace)
317    {
318        return Err(VmConfigError::new(format!("{label} is invalid")));
319    }
320    Ok(())
321}
322
323/// Guest JavaScript host-environment configuration.
324///
325/// Selects which globals/builtins/module-resolution surface guest JS sees,
326/// modeled on esbuild's `platform`. Omitting this preserves full Node.js
327/// emulation (`platform = node`).
328#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, TS)]
329#[serde(rename_all = "camelCase", deny_unknown_fields)]
330#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
331pub struct JsRuntimeConfig {
332    /// Which host environment to emulate for guest JS. Default `node`.
333    #[serde(default)]
334    pub platform: JsRuntimePlatform,
335    /// How bare import specifiers resolve. Independent of `platform`.
336    /// Default `node`.
337    #[serde(default, rename = "moduleResolution")]
338    pub module_resolution: JsModuleResolution,
339    /// Node builtin-module allow-list. Only valid when `platform = node`.
340    /// `None` => engine default allow-list. `Some([])` => deny all builtins.
341    /// `Some([..])` => exactly those.
342    #[serde(
343        default,
344        rename = "allowedBuiltins",
345        skip_serializing_if = "Option::is_none"
346    )]
347    #[ts(optional)]
348    pub allowed_builtins: Option<Vec<String>>,
349    /// Opt in to a high-resolution monotonic guest clock. Default false keeps
350    /// the security-oriented 1ms timer resolution.
351    #[serde(
352        default,
353        rename = "highResolutionTime",
354        skip_serializing_if = "Option::is_none"
355    )]
356    #[ts(optional)]
357    pub high_resolution_time: Option<bool>,
358}
359
360impl JsRuntimeConfig {
361    fn validate(&self) -> Result<(), VmConfigError> {
362        if let Some(allowed) = &self.allowed_builtins {
363            if self.platform != JsRuntimePlatform::Node {
364                return Err(VmConfigError::new(
365                    "jsRuntime.allowedBuiltins is only valid when jsRuntime.platform is \"node\"",
366                ));
367            }
368            for name in allowed {
369                if !is_known_node_builtin(name) {
370                    return Err(VmConfigError::new(format!(
371                        "jsRuntime.allowedBuiltins contains unknown builtin {name:?}"
372                    )));
373                }
374            }
375        }
376        Ok(())
377    }
378}
379
380#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
381#[serde(rename_all = "lowercase")]
382#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
383#[derive(Default)]
384pub enum JsRuntimePlatform {
385    /// Full Node.js host surface (process/Buffer/require, `node:*`, npm
386    /// resolution, virtual Node identity). Default.
387    #[default]
388    Node,
389    /// Web-platform globals (fetch/URL/WebCrypto/...), no Node surface.
390    Browser,
391    /// Universal primitives only (console, timers, queueMicrotask) — no web
392    /// platform, no Node surface.
393    Neutral,
394    /// Language-only: ECMAScript spec globals + WebAssembly. Nothing host-provided.
395    Bare,
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
399#[serde(rename_all = "lowercase")]
400#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
401#[derive(Default)]
402pub enum JsModuleResolution {
403    /// node_modules ancestor-walk + exports/imports/conditions + realpath. Default.
404    #[default]
405    Node,
406    /// Relative/absolute ESM from the VFS only; bare specifiers do not resolve.
407    Relative,
408    /// No resolution: any import/require (even relative) fails.
409    None,
410}
411
412/// Canonical set of recognized Node builtin module names (without the `node:`
413/// prefix), kept in sync with `normalize_builtin_specifier` in
414/// `crates/execution/src/javascript.rs`. Used to validate
415/// `jsRuntime.allowedBuiltins` entries.
416const KNOWN_NODE_BUILTINS: &[&str] = &[
417    "assert",
418    "async_hooks",
419    "buffer",
420    "child_process",
421    "cluster",
422    "console",
423    "constants",
424    "crypto",
425    "dgram",
426    "diagnostics_channel",
427    "dns",
428    "dns/promises",
429    "domain",
430    "events",
431    "fs",
432    "fs/promises",
433    "http",
434    "http2",
435    "https",
436    "inspector",
437    "module",
438    "net",
439    "os",
440    "path",
441    "path/posix",
442    "path/win32",
443    "perf_hooks",
444    "process",
445    "punycode",
446    "querystring",
447    "readline",
448    "repl",
449    "sqlite",
450    "stream",
451    "stream/consumers",
452    "stream/promises",
453    "stream/web",
454    "string_decoder",
455    "sys",
456    "timers",
457    "timers/promises",
458    "tls",
459    "trace_events",
460    "tty",
461    "url",
462    "util",
463    "util/types",
464    "v8",
465    "vm",
466    "wasi",
467    "worker_threads",
468    "zlib",
469];
470
471fn is_known_node_builtin(name: &str) -> bool {
472    let bare = name.strip_prefix("node:").unwrap_or(name);
473    KNOWN_NODE_BUILTINS.contains(&bare)
474}
475
476#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
477#[serde(rename_all = "camelCase", deny_unknown_fields)]
478#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
479pub struct RootFilesystemConfig {
480    #[serde(default)]
481    pub mode: RootFilesystemMode,
482    #[serde(default, rename = "disableDefaultBaseLayer")]
483    pub disable_default_base_layer: bool,
484    #[serde(default, skip_serializing_if = "Vec::is_empty")]
485    pub lowers: Vec<RootFilesystemLowerDescriptor>,
486    #[serde(
487        default,
488        rename = "bootstrapEntries",
489        skip_serializing_if = "Vec::is_empty"
490    )]
491    pub bootstrap_entries: Vec<RootFilesystemEntry>,
492}
493
494impl Default for RootFilesystemConfig {
495    fn default() -> Self {
496        Self {
497            mode: RootFilesystemMode::Ephemeral,
498            disable_default_base_layer: false,
499            lowers: Vec::new(),
500            bootstrap_entries: Vec::new(),
501        }
502    }
503}
504
505impl RootFilesystemConfig {
506    fn validate(&self) -> Result<(), VmConfigError> {
507        for lower in &self.lowers {
508            if let RootFilesystemLowerDescriptor::Snapshot { entries } = lower {
509                for entry in entries {
510                    entry.validate()?;
511                }
512            }
513        }
514        for entry in &self.bootstrap_entries {
515            entry.validate()?;
516        }
517        Ok(())
518    }
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
522#[serde(rename_all = "kebab-case")]
523#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
524#[derive(Default)]
525pub enum RootFilesystemMode {
526    #[default]
527    Ephemeral,
528    ReadOnly,
529}
530
531#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
532#[serde(tag = "kind", rename_all = "camelCase")]
533#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
534pub enum RootFilesystemLowerDescriptor {
535    Snapshot {
536        #[serde(default)]
537        entries: Vec<RootFilesystemEntry>,
538    },
539    BundledBaseFilesystem,
540}
541
542#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
543#[serde(rename_all = "camelCase", deny_unknown_fields)]
544#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
545pub struct RootFilesystemEntry {
546    pub path: String,
547    pub kind: RootFilesystemEntryKind,
548    #[serde(default, skip_serializing_if = "Option::is_none")]
549    #[ts(optional)]
550    pub mode: Option<u32>,
551    #[serde(default, skip_serializing_if = "Option::is_none")]
552    #[ts(optional)]
553    pub uid: Option<u32>,
554    #[serde(default, skip_serializing_if = "Option::is_none")]
555    #[ts(optional)]
556    pub gid: Option<u32>,
557    #[serde(default, skip_serializing_if = "Option::is_none")]
558    #[ts(optional)]
559    pub content: Option<String>,
560    #[serde(default, skip_serializing_if = "Option::is_none")]
561    #[ts(optional)]
562    pub encoding: Option<RootFilesystemEntryEncoding>,
563    #[serde(default, skip_serializing_if = "Option::is_none")]
564    #[ts(optional)]
565    pub target: Option<String>,
566    #[serde(default)]
567    pub executable: bool,
568}
569
570impl RootFilesystemEntry {
571    fn validate(&self) -> Result<(), VmConfigError> {
572        validate_guest_path("root filesystem entry path", &self.path)?;
573        match self.kind {
574            RootFilesystemEntryKind::File => {
575                if self.target.is_some() {
576                    return Err(VmConfigError::new(format!(
577                        "file entry {} must not include target",
578                        self.path
579                    )));
580                }
581            }
582            RootFilesystemEntryKind::Directory => {
583                if self.content.is_some() || self.encoding.is_some() || self.target.is_some() {
584                    return Err(VmConfigError::new(format!(
585                        "directory entry {} must not include content, encoding, or target",
586                        self.path
587                    )));
588                }
589            }
590            RootFilesystemEntryKind::Symlink => {
591                if self.target.as_deref().unwrap_or("").is_empty() {
592                    return Err(VmConfigError::new(format!(
593                        "symlink entry {} requires target",
594                        self.path
595                    )));
596                }
597                if self.content.is_some() || self.encoding.is_some() {
598                    return Err(VmConfigError::new(format!(
599                        "symlink entry {} must not include content or encoding",
600                        self.path
601                    )));
602                }
603            }
604        }
605        Ok(())
606    }
607}
608
609#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
610#[serde(rename_all = "lowercase")]
611#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
612pub enum RootFilesystemEntryKind {
613    File,
614    Directory,
615    Symlink,
616}
617
618#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
619#[serde(rename_all = "lowercase")]
620#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
621pub enum RootFilesystemEntryEncoding {
622    Utf8,
623    Base64,
624}
625
626#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
627#[serde(rename_all = "camelCase", deny_unknown_fields)]
628#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
629pub struct NativeRootFilesystemConfig {
630    pub plugin: MountPluginDescriptor,
631    #[serde(default, rename = "readOnly")]
632    pub read_only: bool,
633}
634
635impl NativeRootFilesystemConfig {
636    fn validate(&self) -> Result<(), VmConfigError> {
637        if self.plugin.id.trim().is_empty() {
638            return Err(VmConfigError::new("nativeRoot.plugin.id is required"));
639        }
640        Ok(())
641    }
642}
643
644#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
645#[serde(rename_all = "camelCase", deny_unknown_fields)]
646#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
647pub struct MountPluginDescriptor {
648    pub id: String,
649    #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
650    #[ts(type = "import(\"@rivet-dev/agentos-runtime-core/descriptors\").MountConfigJsonValue")]
651    pub config: serde_json::Value,
652}
653
654#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
655#[serde(rename_all = "lowercase")]
656#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
657pub enum PermissionMode {
658    Allow,
659    Ask,
660    Deny,
661}
662
663#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
664#[serde(untagged)]
665#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
666pub enum FsPermissionScope {
667    Mode(PermissionMode),
668    Rules(FsPermissionRuleSet),
669}
670
671#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
672#[serde(untagged)]
673#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
674pub enum PatternPermissionScope {
675    Mode(PermissionMode),
676    Rules(PatternPermissionRuleSet),
677}
678
679#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
680#[serde(rename_all = "camelCase", deny_unknown_fields)]
681#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
682pub struct FsPermissionRuleSet {
683    #[serde(default, skip_serializing_if = "Option::is_none")]
684    #[ts(optional)]
685    pub default: Option<PermissionMode>,
686    #[serde(default)]
687    pub rules: Vec<FsPermissionRule>,
688}
689
690#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
691#[serde(rename_all = "camelCase", deny_unknown_fields)]
692#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
693pub struct PatternPermissionRuleSet {
694    #[serde(default, skip_serializing_if = "Option::is_none")]
695    #[ts(optional)]
696    pub default: Option<PermissionMode>,
697    #[serde(default)]
698    pub rules: Vec<PatternPermissionRule>,
699}
700
701#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
702#[serde(rename_all = "camelCase", deny_unknown_fields)]
703#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
704pub struct FsPermissionRule {
705    pub mode: PermissionMode,
706    #[serde(default, skip_serializing_if = "Vec::is_empty")]
707    pub operations: Vec<String>,
708    #[serde(default, skip_serializing_if = "Vec::is_empty")]
709    pub paths: Vec<String>,
710}
711
712#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
713#[serde(rename_all = "camelCase", deny_unknown_fields)]
714#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
715pub struct PatternPermissionRule {
716    pub mode: PermissionMode,
717    #[serde(default, skip_serializing_if = "Vec::is_empty")]
718    pub operations: Vec<String>,
719    #[serde(default, skip_serializing_if = "Vec::is_empty")]
720    pub patterns: Vec<String>,
721}
722
723#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
724#[serde(rename_all = "camelCase", deny_unknown_fields)]
725#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
726pub struct PermissionsPolicy {
727    #[serde(default, skip_serializing_if = "Option::is_none")]
728    #[ts(optional)]
729    pub fs: Option<FsPermissionScope>,
730    #[serde(default, skip_serializing_if = "Option::is_none")]
731    #[ts(optional)]
732    pub network: Option<PatternPermissionScope>,
733    #[serde(
734        default,
735        rename = "childProcess",
736        skip_serializing_if = "Option::is_none"
737    )]
738    #[ts(optional)]
739    pub child_process: Option<PatternPermissionScope>,
740    #[serde(default, skip_serializing_if = "Option::is_none")]
741    #[ts(optional)]
742    pub process: Option<PatternPermissionScope>,
743    #[serde(default, skip_serializing_if = "Option::is_none")]
744    #[ts(optional)]
745    pub env: Option<PatternPermissionScope>,
746    #[serde(default, skip_serializing_if = "Option::is_none")]
747    #[ts(optional)]
748    pub binding: Option<PatternPermissionScope>,
749}
750
751#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
752#[serde(rename_all = "camelCase", deny_unknown_fields)]
753#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
754pub struct VmLimitsConfig {
755    #[serde(default, skip_serializing_if = "Option::is_none")]
756    #[ts(optional)]
757    pub reactor: Option<ReactorLimitsConfig>,
758    #[serde(default, skip_serializing_if = "Option::is_none")]
759    #[ts(optional)]
760    pub resources: Option<ResourceLimitsConfig>,
761    #[serde(default, skip_serializing_if = "Option::is_none")]
762    #[ts(optional)]
763    pub http: Option<HttpLimitsConfig>,
764    #[serde(default, skip_serializing_if = "Option::is_none")]
765    #[ts(optional)]
766    pub udp: Option<UdpLimitsConfig>,
767    #[serde(default, skip_serializing_if = "Option::is_none")]
768    #[ts(optional)]
769    pub tls: Option<TlsLimitsConfig>,
770    #[serde(default, skip_serializing_if = "Option::is_none")]
771    #[ts(optional)]
772    pub http2: Option<Http2LimitsConfig>,
773    #[serde(default, skip_serializing_if = "Option::is_none")]
774    #[ts(optional)]
775    pub bindings: Option<BindingLimitsConfig>,
776    #[serde(default, skip_serializing_if = "Option::is_none")]
777    #[ts(optional)]
778    pub plugins: Option<PluginLimitsConfig>,
779    #[serde(default, skip_serializing_if = "Option::is_none")]
780    #[ts(optional)]
781    pub acp: Option<AcpLimitsConfig>,
782    #[serde(default, skip_serializing_if = "Option::is_none")]
783    #[ts(optional)]
784    pub sqlite: Option<SqliteLimitsConfig>,
785    #[serde(default, rename = "jsRuntime", skip_serializing_if = "Option::is_none")]
786    #[ts(optional)]
787    pub js_runtime: Option<JsRuntimeLimitsConfig>,
788    #[serde(default, skip_serializing_if = "Option::is_none")]
789    #[ts(optional)]
790    pub python: Option<PythonLimitsConfig>,
791    #[serde(default, skip_serializing_if = "Option::is_none")]
792    #[ts(optional)]
793    pub wasm: Option<WasmLimitsConfig>,
794    #[serde(default, skip_serializing_if = "Option::is_none")]
795    #[ts(optional)]
796    pub execution: Option<ExecutionLimitsConfig>,
797    #[serde(default, skip_serializing_if = "Option::is_none")]
798    #[ts(optional)]
799    pub process: Option<ProcessLimitsConfig>,
800}
801
802impl VmLimitsConfig {
803    fn validate(&self, max_frame_bytes: usize) -> Result<(), VmConfigError> {
804        if let Some(reactor) = &self.reactor {
805            validate_nonzero_options([
806                ("limits.reactor.maxCapabilities", reactor.max_capabilities),
807                ("limits.reactor.maxReadyHandles", reactor.max_ready_handles),
808                ("limits.reactor.maxTasks", reactor.max_tasks),
809                ("limits.reactor.workQuantum", reactor.work_quantum),
810                ("limits.reactor.byteQuantum", reactor.byte_quantum),
811                (
812                    "limits.reactor.maxHandleCommands",
813                    reactor.max_handle_commands,
814                ),
815                (
816                    "limits.reactor.maxHandleCommandBytes",
817                    reactor.max_handle_command_bytes,
818                ),
819                ("limits.reactor.maxBridgeCalls", reactor.max_bridge_calls),
820                (
821                    "limits.reactor.maxBridgeRequestBytes",
822                    reactor.max_bridge_request_bytes,
823                ),
824                (
825                    "limits.reactor.maxBridgeResponseBytes",
826                    reactor.max_bridge_response_bytes,
827                ),
828                (
829                    "limits.reactor.maxAsyncCompletions",
830                    reactor.max_async_completions,
831                ),
832                (
833                    "limits.reactor.maxAsyncCompletionBytes",
834                    reactor.max_async_completion_bytes,
835                ),
836                ("limits.reactor.maxBlockingJobs", reactor.max_blocking_jobs),
837                (
838                    "limits.reactor.maxBlockingBytes",
839                    reactor.max_blocking_bytes,
840                ),
841                (
842                    "limits.reactor.perHandleOperationQuantum",
843                    reactor.per_handle_operation_quantum,
844                ),
845                ("limits.reactor.acceptQuantum", reactor.accept_quantum),
846                ("limits.reactor.datagramQuantum", reactor.datagram_quantum),
847                (
848                    "limits.reactor.completionQuantum",
849                    reactor.completion_quantum,
850                ),
851                ("limits.reactor.signalQuantum", reactor.signal_quantum),
852                (
853                    "limits.reactor.shutdownDeadlineMs",
854                    reactor.shutdown_deadline_ms,
855                ),
856                (
857                    "limits.reactor.operationDeadlineMs",
858                    reactor.operation_deadline_ms,
859                ),
860            ])?;
861            validate_optional_parent(
862                "limits.reactor.maxCapabilities",
863                reactor.max_capabilities,
864                "limits.reactor.maxReadyHandles",
865                reactor.max_ready_handles,
866            )?;
867            validate_optional_parent(
868                "limits.reactor.perHandleOperationQuantum",
869                reactor.per_handle_operation_quantum,
870                "limits.reactor.maxHandleCommands",
871                reactor.max_handle_commands,
872            )?;
873            validate_optional_parent(
874                "limits.reactor.acceptQuantum",
875                reactor.accept_quantum,
876                "limits.reactor.maxCapabilities",
877                reactor.max_capabilities,
878            )?;
879            validate_optional_parent(
880                "limits.reactor.completionQuantum",
881                reactor.completion_quantum,
882                "limits.reactor.maxAsyncCompletions",
883                reactor.max_async_completions,
884            )?;
885            if let Some(max_bridge_request_bytes) = reactor.max_bridge_request_bytes {
886                if max_bridge_request_bytes > max_frame_bytes as u64 {
887                    return Err(VmConfigError::new(format!(
888                        "limits.reactor.maxBridgeRequestBytes ({max_bridge_request_bytes}) must \
889                         be <= the sidecar wire frame cap ({max_frame_bytes})"
890                    )));
891                }
892            }
893            if let Some(max_bridge_response_bytes) = reactor.max_bridge_response_bytes {
894                if max_bridge_response_bytes > max_frame_bytes as u64 {
895                    return Err(VmConfigError::new(format!(
896                        "limits.reactor.maxBridgeResponseBytes ({max_bridge_response_bytes}) must \
897                         be <= the sidecar wire frame cap ({max_frame_bytes})"
898                    )));
899                }
900            }
901        }
902        if let Some(http) = &self.http {
903            if let Some(max_fetch_response_bytes) = http.max_fetch_response_bytes {
904                if max_fetch_response_bytes == 0 {
905                    return Err(VmConfigError::new(
906                        "limits.http.maxFetchResponseBytes must be greater than zero",
907                    ));
908                }
909                if max_fetch_response_bytes as usize > max_frame_bytes {
910                    return Err(VmConfigError::new(format!(
911                        "limits.http.maxFetchResponseBytes ({max_fetch_response_bytes}) must be <= the sidecar wire frame cap ({max_frame_bytes})"
912                    )));
913                }
914            }
915        }
916        if let Some(udp) = &self.udp {
917            validate_nonzero_options([
918                (
919                    "limits.udp.maxBufferedDatagrams",
920                    udp.max_buffered_datagrams,
921                ),
922                ("limits.udp.maxBufferedBytes", udp.max_buffered_bytes),
923            ])?;
924        }
925        if let Some(tls) = &self.tls {
926            validate_nonzero_options([("limits.tls.maxBufferedBytes", tls.max_buffered_bytes)])?;
927        }
928        if let Some(http2) = &self.http2 {
929            validate_nonzero_options([
930                ("limits.http2.maxConnections", http2.max_connections),
931                ("limits.http2.maxStreams", http2.max_streams),
932                (
933                    "limits.http2.maxStreamsPerConnection",
934                    http2.max_streams_per_connection,
935                ),
936                ("limits.http2.maxBufferedBytes", http2.max_buffered_bytes),
937                ("limits.http2.maxHeaderBytes", http2.max_header_bytes),
938                ("limits.http2.maxDataBytes", http2.max_data_bytes),
939                (
940                    "limits.http2.maxPendingCommands",
941                    http2.max_pending_commands,
942                ),
943                (
944                    "limits.http2.maxPendingCommandBytes",
945                    http2.max_pending_command_bytes,
946                ),
947                ("limits.http2.maxPendingEvents", http2.max_pending_events),
948                (
949                    "limits.http2.maxPendingEventBytes",
950                    http2.max_pending_event_bytes,
951                ),
952            ])?;
953            validate_optional_parent(
954                "limits.http2.maxStreamsPerConnection",
955                http2.max_streams_per_connection,
956                "limits.http2.maxStreams",
957                http2.max_streams,
958            )?;
959            for (path, value) in [
960                ("limits.http2.maxHeaderBytes", http2.max_header_bytes),
961                ("limits.http2.maxDataBytes", http2.max_data_bytes),
962                (
963                    "limits.http2.maxPendingCommandBytes",
964                    http2.max_pending_command_bytes,
965                ),
966                (
967                    "limits.http2.maxPendingEventBytes",
968                    http2.max_pending_event_bytes,
969                ),
970            ] {
971                validate_optional_parent(
972                    path,
973                    value,
974                    "limits.http2.maxBufferedBytes",
975                    http2.max_buffered_bytes,
976                )?;
977            }
978        }
979        if let Some(resources) = &self.resources {
980            let aggregate_socket_bytes = resources.max_socket_buffered_bytes;
981            for (path, value) in [
982                (
983                    "limits.reactor.maxHandleCommandBytes",
984                    self.reactor
985                        .as_ref()
986                        .and_then(|limits| limits.max_handle_command_bytes),
987                ),
988                (
989                    "limits.http.maxFetchResponseBytes",
990                    self.http
991                        .as_ref()
992                        .and_then(|limits| limits.max_fetch_response_bytes),
993                ),
994                (
995                    "limits.udp.maxBufferedBytes",
996                    self.udp
997                        .as_ref()
998                        .and_then(|limits| limits.max_buffered_bytes),
999                ),
1000                (
1001                    "limits.tls.maxBufferedBytes",
1002                    self.tls
1003                        .as_ref()
1004                        .and_then(|limits| limits.max_buffered_bytes),
1005                ),
1006                (
1007                    "limits.http2.maxBufferedBytes",
1008                    self.http2
1009                        .as_ref()
1010                        .and_then(|limits| limits.max_buffered_bytes),
1011                ),
1012            ] {
1013                validate_optional_parent(
1014                    path,
1015                    value,
1016                    "limits.resources.maxSocketBufferedBytes",
1017                    aggregate_socket_bytes,
1018                )?;
1019            }
1020            validate_optional_parent(
1021                "limits.udp.maxBufferedDatagrams",
1022                self.udp
1023                    .as_ref()
1024                    .and_then(|limits| limits.max_buffered_datagrams),
1025                "limits.resources.maxSocketDatagramQueueLen",
1026                resources.max_socket_datagram_queue_len,
1027            )?;
1028            validate_optional_parent(
1029                "limits.http2.maxConnections",
1030                self.http2
1031                    .as_ref()
1032                    .and_then(|limits| limits.max_connections),
1033                "limits.resources.maxConnections",
1034                resources.max_connections,
1035            )?;
1036        }
1037        if let (Some(reactor), Some(udp)) = (&self.reactor, &self.udp) {
1038            validate_optional_parent(
1039                "limits.reactor.datagramQuantum",
1040                reactor.datagram_quantum,
1041                "limits.udp.maxBufferedDatagrams",
1042                udp.max_buffered_datagrams,
1043            )?;
1044        }
1045        if let Some(bindings) = &self.bindings {
1046            if let (Some(default), Some(max)) = (
1047                bindings.default_binding_timeout_ms,
1048                bindings.max_binding_timeout_ms,
1049            ) {
1050                if default > max {
1051                    return Err(VmConfigError::new(
1052                        "limits.bindings.defaultBindingTimeoutMs must be <= limits.bindings.maxBindingTimeoutMs",
1053                    ));
1054                }
1055            }
1056        }
1057        if let Some(js_runtime) = &self.js_runtime {
1058            validate_nonzero_options([("limits.jsRuntime.maxTimers", js_runtime.max_timers)])?;
1059        }
1060        if let Some(execution) = &self.execution {
1061            validate_nonzero_options([
1062                (
1063                    "limits.execution.completedTtlMs",
1064                    execution.completed_ttl_ms,
1065                ),
1066                (
1067                    "limits.execution.maxCompletedExecutions",
1068                    execution.max_completed_executions,
1069                ),
1070                (
1071                    "limits.execution.liveExecutionWarningThreshold",
1072                    execution.live_execution_warning_threshold,
1073                ),
1074            ])?;
1075            const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
1076            for (path, value) in [
1077                (
1078                    "limits.execution.completedTtlMs",
1079                    execution.completed_ttl_ms,
1080                ),
1081                (
1082                    "limits.execution.maxCompletedExecutions",
1083                    execution.max_completed_executions,
1084                ),
1085                (
1086                    "limits.execution.liveExecutionWarningThreshold",
1087                    execution.live_execution_warning_threshold,
1088                ),
1089            ] {
1090                if value.is_some_and(|value| value > MAX_SAFE_INTEGER) {
1091                    return Err(VmConfigError::new(format!("{path} must be a safe integer")));
1092                }
1093            }
1094        }
1095        Ok(())
1096    }
1097}
1098
1099fn validate_nonzero_options<const N: usize>(
1100    values: [(&str, Option<u64>); N],
1101) -> Result<(), VmConfigError> {
1102    for (path, value) in values {
1103        if value == Some(0) {
1104            return Err(VmConfigError::new(format!(
1105                "{path} must be greater than zero"
1106            )));
1107        }
1108    }
1109    Ok(())
1110}
1111
1112fn validate_optional_parent(
1113    child_path: &str,
1114    child: Option<u64>,
1115    parent_path: &str,
1116    parent: Option<u64>,
1117) -> Result<(), VmConfigError> {
1118    if let (Some(child), Some(parent)) = (child, parent) {
1119        if child > parent {
1120            return Err(VmConfigError::new(format!(
1121                "{child_path} ({child}) must be <= {parent_path} ({parent})"
1122            )));
1123        }
1124    }
1125    Ok(())
1126}
1127
1128macro_rules! limits_struct {
1129    ($name:ident { $($field:ident),* $(,)? }) => {
1130        #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
1131        #[serde(rename_all = "camelCase", deny_unknown_fields)]
1132        #[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
1133        pub struct $name {
1134            $(
1135                #[serde(default, skip_serializing_if = "Option::is_none")]
1136                #[ts(optional)]
1137                #[ts(type = "number")]
1138                pub $field: Option<u64>,
1139            )*
1140        }
1141    };
1142}
1143
1144limits_struct!(ResourceLimitsConfig {
1145    cpu_count,
1146    max_processes,
1147    max_open_fds,
1148    max_pipes,
1149    max_ptys,
1150    max_sockets,
1151    max_connections,
1152    max_socket_buffered_bytes,
1153    max_socket_datagram_queue_len,
1154    max_filesystem_bytes,
1155    max_inode_count,
1156    max_blocking_read_ms,
1157    max_pread_bytes,
1158    max_fd_write_bytes,
1159    max_process_argv_bytes,
1160    max_process_env_bytes,
1161    max_readdir_entries,
1162    max_recursive_fs_depth,
1163    max_recursive_fs_entries,
1164    max_wasm_fuel,
1165    max_wasm_memory_bytes,
1166    max_wasm_stack_bytes,
1167});
1168
1169limits_struct!(ReactorLimitsConfig {
1170    max_capabilities,
1171    max_ready_handles,
1172    max_tasks,
1173    work_quantum,
1174    byte_quantum,
1175    max_handle_commands,
1176    max_handle_command_bytes,
1177    max_bridge_calls,
1178    max_bridge_request_bytes,
1179    max_bridge_response_bytes,
1180    max_async_completions,
1181    max_async_completion_bytes,
1182    max_blocking_jobs,
1183    max_blocking_bytes,
1184    per_handle_operation_quantum,
1185    accept_quantum,
1186    datagram_quantum,
1187    completion_quantum,
1188    signal_quantum,
1189    shutdown_deadline_ms,
1190    operation_deadline_ms,
1191});
1192
1193limits_struct!(HttpLimitsConfig {
1194    max_fetch_response_bytes,
1195});
1196
1197limits_struct!(UdpLimitsConfig {
1198    max_buffered_datagrams,
1199    max_buffered_bytes,
1200});
1201
1202limits_struct!(TlsLimitsConfig { max_buffered_bytes });
1203
1204limits_struct!(Http2LimitsConfig {
1205    max_connections,
1206    max_streams,
1207    max_streams_per_connection,
1208    max_buffered_bytes,
1209    max_header_bytes,
1210    max_data_bytes,
1211    max_pending_commands,
1212    max_pending_command_bytes,
1213    max_pending_events,
1214    max_pending_event_bytes,
1215});
1216
1217limits_struct!(BindingLimitsConfig {
1218    default_binding_timeout_ms,
1219    max_binding_timeout_ms,
1220    max_registered_collections,
1221    max_registered_bindings_per_vm,
1222    max_bindings_per_collection,
1223    max_binding_schema_bytes,
1224    max_examples_per_binding,
1225    max_binding_example_input_bytes,
1226});
1227
1228limits_struct!(PluginLimitsConfig {
1229    max_persisted_manifest_bytes,
1230    max_persisted_manifest_file_bytes,
1231});
1232
1233limits_struct!(AcpLimitsConfig {
1234    max_read_line_bytes,
1235    stdout_buffer_byte_limit,
1236    max_completed_message_bytes,
1237    max_turn_output_bytes,
1238    max_prompt_bytes,
1239    max_prompt_blocks,
1240    max_fallback_continuation_bytes,
1241    max_session_history_bytes,
1242    max_session_history_events,
1243    max_history_page_entries,
1244    max_session_list_entries,
1245    max_sessions_per_vm,
1246    max_prompts_per_session,
1247    max_prompts_per_vm,
1248    max_pending_permissions_per_session,
1249    max_pending_permissions_per_vm,
1250    max_permission_outcomes_per_session,
1251    max_permission_outcomes_per_vm,
1252});
1253
1254limits_struct!(SqliteLimitsConfig { max_result_bytes });
1255
1256limits_struct!(JsRuntimeLimitsConfig {
1257    v8_heap_limit_mb,
1258    sync_rpc_wait_timeout_ms,
1259    cpu_time_limit_ms,
1260    wall_clock_limit_ms,
1261    import_cache_materialize_timeout_ms,
1262    captured_output_limit_bytes,
1263    stdin_buffer_limit_bytes,
1264    event_payload_limit_bytes,
1265    max_timers,
1266    v8_ipc_max_frame_bytes,
1267});
1268
1269limits_struct!(PythonLimitsConfig {
1270    output_buffer_max_bytes,
1271    execution_timeout_ms,
1272    max_old_space_mb,
1273    vfs_rpc_timeout_ms,
1274});
1275
1276limits_struct!(WasmLimitsConfig {
1277    max_module_file_bytes,
1278    captured_output_limit_bytes,
1279    sync_read_limit_bytes,
1280    prewarm_timeout_ms,
1281    runner_heap_limit_mb,
1282    runner_cpu_time_limit_ms,
1283});
1284
1285limits_struct!(ExecutionLimitsConfig {
1286    completed_ttl_ms,
1287    max_completed_executions,
1288    live_execution_warning_threshold,
1289});
1290
1291limits_struct!(ProcessLimitsConfig {
1292    max_spawn_file_actions,
1293    max_spawn_file_action_bytes,
1294    pending_stdin_bytes,
1295    pending_event_count,
1296    pending_event_bytes,
1297});
1298
1299#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
1300#[serde(rename_all = "camelCase", deny_unknown_fields)]
1301#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
1302pub struct VmDnsConfig {
1303    #[serde(default, rename = "nameServers", skip_serializing_if = "Vec::is_empty")]
1304    pub name_servers: Vec<String>,
1305    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1306    pub overrides: BTreeMap<String, Vec<String>>,
1307}
1308
1309impl VmDnsConfig {
1310    fn validate(&self) -> Result<(), VmConfigError> {
1311        for entry in &self.name_servers {
1312            if entry.trim().is_empty() {
1313                return Err(VmConfigError::new(
1314                    "dns.nameServers entries must not be empty",
1315                ));
1316            }
1317        }
1318        for (host, addresses) in &self.overrides {
1319            if host.trim().is_empty() {
1320                return Err(VmConfigError::new("dns.overrides keys must not be empty"));
1321            }
1322            if addresses.is_empty() {
1323                return Err(VmConfigError::new(format!(
1324                    "dns.overrides.{host} must contain at least one address"
1325                )));
1326            }
1327        }
1328        Ok(())
1329    }
1330}
1331
1332#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
1333#[serde(rename_all = "camelCase", deny_unknown_fields)]
1334#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")]
1335pub struct VmListenPolicyConfig {
1336    #[serde(default, rename = "portMin", skip_serializing_if = "Option::is_none")]
1337    #[ts(optional)]
1338    pub port_min: Option<u16>,
1339    #[serde(default, rename = "portMax", skip_serializing_if = "Option::is_none")]
1340    #[ts(optional)]
1341    pub port_max: Option<u16>,
1342    #[serde(
1343        default,
1344        rename = "allowPrivileged",
1345        skip_serializing_if = "Option::is_none"
1346    )]
1347    #[ts(optional)]
1348    pub allow_privileged: Option<bool>,
1349}
1350
1351impl VmListenPolicyConfig {
1352    fn validate(&self) -> Result<(), VmConfigError> {
1353        if self.port_min == Some(0) {
1354            return Err(VmConfigError::new(
1355                "listen.portMin must be between 1 and 65535",
1356            ));
1357        }
1358        if self.port_max == Some(0) {
1359            return Err(VmConfigError::new(
1360                "listen.portMax must be between 1 and 65535",
1361            ));
1362        }
1363        if let (Some(min), Some(max)) = (self.port_min, self.port_max) {
1364            if min > max {
1365                return Err(VmConfigError::new(
1366                    "listen.portMin must be <= listen.portMax",
1367                ));
1368            }
1369        }
1370        Ok(())
1371    }
1372}
1373
1374#[derive(Debug, Clone, PartialEq, Eq)]
1375pub struct VmConfigError {
1376    message: String,
1377}
1378
1379impl VmConfigError {
1380    pub fn new(message: impl Into<String>) -> Self {
1381        Self {
1382            message: message.into(),
1383        }
1384    }
1385}
1386
1387impl std::fmt::Display for VmConfigError {
1388    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1389        f.write_str(&self.message)
1390    }
1391}
1392
1393impl std::error::Error for VmConfigError {}
1394
1395fn validate_guest_path(label: &str, path: &str) -> Result<(), VmConfigError> {
1396    if !path.starts_with('/') {
1397        return Err(VmConfigError::new(format!("{label} must be absolute")));
1398    }
1399    if path.split('/').any(|part| part == "..") {
1400        return Err(VmConfigError::new(format!("{label} must not contain '..'")));
1401    }
1402    Ok(())
1403}
1404
1405fn validate_command_names(label: &str, commands: &[String]) -> Result<(), VmConfigError> {
1406    for command in commands {
1407        if command.is_empty()
1408            || command == "."
1409            || command == ".."
1410            || command.contains('/')
1411            || command.contains('\0')
1412        {
1413            return Err(VmConfigError::new(format!(
1414                "{label} contains invalid command name {command:?}"
1415            )));
1416        }
1417    }
1418
1419    Ok(())
1420}
1421
1422#[cfg(test)]
1423mod tests {
1424    use super::*;
1425
1426    #[test]
1427    fn default_config_round_trips() {
1428        let config = CreateVmConfig::default();
1429        let json = serde_json::to_string(&config).expect("serialize config");
1430        let decoded: CreateVmConfig = serde_json::from_str(&json).expect("decode config");
1431        assert_eq!(decoded, config);
1432    }
1433
1434    #[test]
1435    fn unknown_fields_are_rejected() {
1436        let error =
1437            serde_json::from_str::<CreateVmConfig>(r#"{"rootFilesystem":{},"surprise":true}"#)
1438                .expect_err("unknown fields should fail");
1439        assert!(error.to_string().contains("unknown field"));
1440    }
1441
1442    #[test]
1443    fn root_user_config_round_trips_and_validates() {
1444        let config = CreateVmConfig {
1445            user: Some(VmUserConfig {
1446                uid: Some(0),
1447                gid: Some(0),
1448                username: Some(String::from("root")),
1449                homedir: Some(String::from("/root")),
1450                shell: Some(String::from("/bin/sh")),
1451                supplementary_gids: Some(vec![0, 10]),
1452                ..VmUserConfig::default()
1453            }),
1454            ..CreateVmConfig::default()
1455        };
1456        config.validate(usize::MAX).expect("valid root account");
1457        let json = serde_json::to_string(&config).expect("serialize root user");
1458        let decoded: CreateVmConfig = serde_json::from_str(&json).expect("decode root user");
1459        assert_eq!(decoded, config);
1460    }
1461
1462    #[test]
1463    fn user_config_rejects_unbounded_groups_and_invalid_records() {
1464        let too_many_groups = CreateVmConfig {
1465            user: Some(VmUserConfig {
1466                supplementary_gids: Some((0..65).collect()),
1467                ..VmUserConfig::default()
1468            }),
1469            ..CreateVmConfig::default()
1470        };
1471        assert!(too_many_groups.validate(usize::MAX).is_err());
1472
1473        let invalid_name = CreateVmConfig {
1474            user: Some(VmUserConfig {
1475                username: Some(String::from("bad:name")),
1476                ..VmUserConfig::default()
1477            }),
1478            ..CreateVmConfig::default()
1479        };
1480        assert!(invalid_name.validate(usize::MAX).is_err());
1481    }
1482
1483    #[test]
1484    fn validate_rejects_fetch_limit_above_frame_cap() {
1485        let config = CreateVmConfig {
1486            limits: Some(VmLimitsConfig {
1487                http: Some(HttpLimitsConfig {
1488                    max_fetch_response_bytes: Some(2048),
1489                }),
1490                ..VmLimitsConfig::default()
1491            }),
1492            ..CreateVmConfig::default()
1493        };
1494        assert!(config.validate(1024).is_err());
1495    }
1496
1497    #[test]
1498    fn canonical_reactor_and_protocol_limits_round_trip() {
1499        let config: CreateVmConfig = serde_json::from_value(serde_json::json!({
1500            "limits": {
1501                "resources": {
1502                    "maxConnections": 16,
1503                    "maxSocketBufferedBytes": 8192,
1504                    "maxSocketDatagramQueueLen": 128
1505                },
1506                "reactor": {
1507                    "maxCapabilities": 128,
1508                    "maxReadyHandles": 256,
1509                    "maxTasks": 512,
1510                    "workQuantum": 32,
1511                    "byteQuantum": 4096,
1512                    "maxHandleCommands": 64,
1513                    "maxHandleCommandBytes": 1024,
1514                    "maxBridgeCalls": 32,
1515                    "maxBridgeResponseBytes": 4096,
1516                    "maxAsyncCompletions": 64,
1517                    "maxAsyncCompletionBytes": 2048,
1518                    "maxBlockingJobs": 32,
1519                    "maxBlockingBytes": 4096,
1520                    "perHandleOperationQuantum": 8,
1521                    "acceptQuantum": 16,
1522                    "datagramQuantum": 8,
1523                    "completionQuantum": 16,
1524                    "signalQuantum": 16,
1525                    "shutdownDeadlineMs": 5000,
1526                    "operationDeadlineMs": 30000
1527                },
1528                "http": { "maxFetchResponseBytes": 4096 },
1529                "udp": {
1530                    "maxBufferedDatagrams": 64,
1531                    "maxBufferedBytes": 4096
1532                },
1533                "tls": { "maxBufferedBytes": 2048 },
1534                "http2": {
1535                    "maxConnections": 8,
1536                    "maxStreams": 64,
1537                    "maxStreamsPerConnection": 16,
1538                    "maxBufferedBytes": 8192,
1539                    "maxHeaderBytes": 1024,
1540                    "maxDataBytes": 4096,
1541                    "maxPendingCommands": 32,
1542                    "maxPendingCommandBytes": 1024,
1543                    "maxPendingEvents": 32,
1544                    "maxPendingEventBytes": 2048
1545                }
1546            }
1547        }))
1548        .expect("decode canonical limits");
1549        config.validate(16 * 1024).expect("valid relationships");
1550
1551        let json = serde_json::to_string(&config).expect("serialize canonical limits");
1552        let decoded: CreateVmConfig = serde_json::from_str(&json).expect("decode round trip");
1553        assert_eq!(decoded, config);
1554        assert!(json.contains("maxHandleCommandBytes"));
1555        assert!(json.contains("maxBufferedDatagrams"));
1556        assert!(json.contains("maxStreamsPerConnection"));
1557        assert!(json.contains("shutdownDeadlineMs"));
1558    }
1559
1560    #[test]
1561    fn canonical_limits_reject_zero_and_invalid_parent_relationships() {
1562        let cases = [
1563            (
1564                serde_json::json!({
1565                    "reactor": { "maxHandleCommands": 0 }
1566                }),
1567                "limits.reactor.maxHandleCommands",
1568            ),
1569            (
1570                serde_json::json!({
1571                    "reactor": { "maxBlockingJobs": 0 }
1572                }),
1573                "limits.reactor.maxBlockingJobs",
1574            ),
1575            (
1576                serde_json::json!({
1577                    "udp": { "maxBufferedBytes": 0 }
1578                }),
1579                "limits.udp.maxBufferedBytes",
1580            ),
1581            (
1582                serde_json::json!({
1583                    "reactor": { "maxCapabilities": 8, "maxReadyHandles": 4 }
1584                }),
1585                "limits.reactor.maxCapabilities",
1586            ),
1587            (
1588                serde_json::json!({
1589                    "resources": { "maxSocketBufferedBytes": 1024 },
1590                    "tls": { "maxBufferedBytes": 2048 }
1591                }),
1592                "limits.tls.maxBufferedBytes",
1593            ),
1594            (
1595                serde_json::json!({
1596                    "http2": {
1597                        "maxBufferedBytes": 1024,
1598                        "maxPendingEventBytes": 2048
1599                    }
1600                }),
1601                "limits.http2.maxPendingEventBytes",
1602            ),
1603            (
1604                serde_json::json!({
1605                    "resources": { "maxSocketDatagramQueueLen": 16 },
1606                    "udp": { "maxBufferedDatagrams": 32 }
1607                }),
1608                "limits.udp.maxBufferedDatagrams",
1609            ),
1610        ];
1611
1612        for (limits, expected_path) in cases {
1613            let config: CreateVmConfig = serde_json::from_value(serde_json::json!({
1614                "limits": limits
1615            }))
1616            .expect("decode invalid relationship fixture");
1617            let error = config
1618                .validate(16 * 1024)
1619                .expect_err("invalid relationship must fail");
1620            assert!(
1621                error.to_string().contains(expected_path),
1622                "expected {expected_path} in {error}"
1623            );
1624        }
1625    }
1626
1627    fn js_runtime_config(value: serde_json::Value) -> Result<CreateVmConfig, serde_json::Error> {
1628        serde_json::from_value(serde_json::json!({ "jsRuntime": value }))
1629    }
1630
1631    #[test]
1632    fn js_runtime_defaults_to_node() {
1633        let config: CreateVmConfig =
1634            serde_json::from_value(serde_json::json!({ "jsRuntime": {} })).expect("decode");
1635        let js = config.js_runtime.expect("jsRuntime present");
1636        assert_eq!(js.platform, JsRuntimePlatform::Node);
1637        assert_eq!(js.module_resolution, JsModuleResolution::Node);
1638        assert!(js.allowed_builtins.is_none());
1639        assert!(js.high_resolution_time.is_none());
1640    }
1641
1642    #[test]
1643    fn js_runtime_high_resolution_time_defaults_off_and_round_trips() {
1644        let defaulted = js_runtime_config(serde_json::json!({})).unwrap();
1645        assert!(defaulted.js_runtime.unwrap().high_resolution_time.is_none());
1646
1647        let enabled = js_runtime_config(serde_json::json!({
1648            "highResolutionTime": true,
1649        }))
1650        .unwrap();
1651        assert_eq!(
1652            enabled.js_runtime.as_ref().unwrap().high_resolution_time,
1653            Some(true)
1654        );
1655        let json = serde_json::to_string(&enabled).expect("serialize");
1656        assert!(json.contains("highResolutionTime"));
1657        let decoded: CreateVmConfig = serde_json::from_str(&json).expect("re-decode");
1658        assert_eq!(decoded, enabled);
1659    }
1660
1661    #[test]
1662    fn js_runtime_all_platform_resolution_combos_round_trip() {
1663        for platform in ["node", "browser", "neutral", "bare"] {
1664            for resolution in ["node", "relative", "none"] {
1665                let config = js_runtime_config(serde_json::json!({
1666                    "platform": platform,
1667                    "moduleResolution": resolution,
1668                }))
1669                .unwrap_or_else(|err| panic!("decode {platform}/{resolution}: {err}"));
1670                let json = serde_json::to_string(&config).expect("serialize");
1671                let decoded: CreateVmConfig = serde_json::from_str(&json).expect("re-decode");
1672                assert_eq!(decoded, config);
1673                assert!(config.validate(usize::MAX).is_ok());
1674            }
1675        }
1676    }
1677
1678    #[test]
1679    fn js_runtime_allowed_builtins_tri_state() {
1680        // None => omitted.
1681        let none = js_runtime_config(serde_json::json!({ "platform": "node" })).unwrap();
1682        assert!(none.js_runtime.unwrap().allowed_builtins.is_none());
1683        // Some([]) => deny all (representable, distinct from None).
1684        let empty = js_runtime_config(serde_json::json!({ "allowedBuiltins": [] })).unwrap();
1685        assert_eq!(empty.js_runtime.unwrap().allowed_builtins, Some(Vec::new()));
1686        // Some([..]) => explicit.
1687        let some = js_runtime_config(serde_json::json!({ "allowedBuiltins": ["path", "node:fs"] }))
1688            .unwrap();
1689        assert_eq!(
1690            some.js_runtime.unwrap().allowed_builtins,
1691            Some(vec!["path".to_owned(), "node:fs".to_owned()])
1692        );
1693    }
1694
1695    #[test]
1696    fn js_runtime_rejects_allowed_builtins_under_non_node_platform() {
1697        for platform in ["browser", "neutral", "bare"] {
1698            let config = js_runtime_config(serde_json::json!({
1699                "platform": platform,
1700                "allowedBuiltins": ["path"],
1701            }))
1702            .unwrap();
1703            let error = config
1704                .validate(usize::MAX)
1705                .expect_err("allowedBuiltins under non-node must reject");
1706            assert!(error.to_string().contains("allowedBuiltins"));
1707        }
1708    }
1709
1710    #[test]
1711    fn js_runtime_rejects_unknown_builtin_names() {
1712        let config = js_runtime_config(serde_json::json!({
1713            "platform": "node",
1714            "allowedBuiltins": ["path", "totally_not_a_builtin"],
1715        }))
1716        .unwrap();
1717        let error = config
1718            .validate(usize::MAX)
1719            .expect_err("unknown builtin must reject");
1720        assert!(error.to_string().contains("unknown builtin"));
1721    }
1722
1723    #[test]
1724    fn js_runtime_accepts_empty_allow_list_under_node() {
1725        let config =
1726            js_runtime_config(serde_json::json!({ "platform": "node", "allowedBuiltins": [] }))
1727                .unwrap();
1728        assert!(config.validate(usize::MAX).is_ok());
1729    }
1730
1731    #[test]
1732    fn js_runtime_rejects_unknown_fields() {
1733        let error = js_runtime_config(serde_json::json!({ "surprise": true }))
1734            .expect_err("unknown jsRuntime field should fail");
1735        assert!(error.to_string().contains("unknown field"));
1736    }
1737}