Skip to main content

secure_exec_sidecar_core/
limits.rs

1//! Typed, operator-tunable VM-scoped runtime limits.
2//!
3//! `VmLimits` is the single home for runtime bounds that operators may tune through the typed
4//! create-VM JSON config. Every field is a concrete value (not `Option`): the `Default` impls own
5//! the numbers and they are byte-identical to the historical hardcoded constants, so behavior is
6//! unchanged unless an operator overrides a config field.
7
8use secure_exec_kernel::resource_accounting::ResourceLimits;
9use secure_exec_vm_config::{ResourceLimitsConfig, VmLimitsConfig};
10
11use crate::SidecarCoreError;
12
13/// Default cap on `vm.fetch()` buffered response bodies. Historically aliased to the wire frame
14/// cap; decoupled here but still validated to stay within the negotiated frame budget.
15pub const DEFAULT_MAX_FETCH_RESPONSE_BYTES: usize = 1024 * 1024;
16
17pub const DEFAULT_TOOL_TIMEOUT_MS: u64 = 30_000;
18pub const MAX_TOOL_TIMEOUT_MS: u64 = 300_000;
19pub const MAX_REGISTERED_TOOLKITS: usize = 64;
20pub const MAX_REGISTERED_TOOLS_PER_VM: usize = 256;
21pub const MAX_TOOLS_PER_TOOLKIT: usize = 64;
22pub const MAX_TOOL_SCHEMA_BYTES: usize = 16 * 1024;
23pub const MAX_TOOL_EXAMPLES_PER_TOOL: usize = 16;
24pub const MAX_TOOL_EXAMPLE_INPUT_BYTES: usize = 4 * 1024;
25
26pub const MAX_PERSISTED_MANIFEST_BYTES: usize = 64 * 1024 * 1024;
27pub const MAX_PERSISTED_MANIFEST_FILE_BYTES: u64 = 1024 * 1024 * 1024;
28
29pub const DEFAULT_ACP_MAX_READ_LINE_BYTES: usize = 16 * 1024 * 1024;
30pub const DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT: usize = 1024 * 1024;
31
32pub const DEFAULT_JS_CAPTURED_OUTPUT_LIMIT_BYTES: usize = 16 * 1024 * 1024;
33pub const DEFAULT_JS_STDIN_BUFFER_LIMIT_BYTES: usize = 16 * 1024 * 1024;
34pub const DEFAULT_JS_EVENT_PAYLOAD_LIMIT_BYTES: usize = 1024 * 1024;
35pub const DEFAULT_V8_IPC_MAX_FRAME_BYTES: u32 = 64 * 1024 * 1024;
36pub const DEFAULT_V8_HEAP_LIMIT_MB: u32 = 128;
37
38pub const DEFAULT_PYTHON_OUTPUT_BUFFER_MAX_BYTES: usize = 1024 * 1024;
39pub const DEFAULT_PYTHON_EXECUTION_TIMEOUT_MS: u64 = 5 * 60 * 1000;
40/// `0` keeps the Pyodide runner's V8 old-space at the engine default.
41pub const DEFAULT_PYTHON_MAX_OLD_SPACE_MB: usize = 0;
42pub const DEFAULT_PYTHON_VFS_RPC_TIMEOUT_MS: u64 = 30 * 1000;
43
44pub const DEFAULT_WASM_MAX_MODULE_FILE_BYTES: u64 = 256 * 1024 * 1024;
45pub const DEFAULT_WASM_CAPTURED_OUTPUT_LIMIT_BYTES: usize = 16 * 1024 * 1024;
46pub const DEFAULT_WASM_SYNC_READ_LIMIT_BYTES: usize = 16 * 1024 * 1024;
47
48/// All operator-tunable VM-scoped limits. Fields are concrete values; the `Default` impls own the
49/// numbers and equal today's hardcoded constants, so unset operator config leaves behavior
50/// unchanged.
51#[derive(Debug, Clone, PartialEq, Eq, Default)]
52pub struct VmLimits {
53    /// Kernel resource limits (existing type, existing `resource.*` keys).
54    pub resources: ResourceLimits,
55    pub http: HttpLimits,
56    pub tools: ToolLimits,
57    pub plugins: PluginLimits,
58    pub acp: AcpLimits,
59    pub js_runtime: JsRuntimeLimits,
60    pub python: PythonLimits,
61    pub wasm: WasmLimits,
62}
63
64pub fn virtual_os_cpu_count(resource_limits: &ResourceLimits) -> usize {
65    resource_limits.virtual_cpu_count.unwrap_or(1).max(1)
66}
67
68pub fn virtual_os_totalmem_bytes(resource_limits: &ResourceLimits) -> u64 {
69    resource_limits
70        .max_wasm_memory_bytes
71        .unwrap_or(1024 * 1024 * 1024)
72}
73
74pub fn virtual_os_freemem_bytes(resource_limits: &ResourceLimits) -> u64 {
75    resource_limits
76        .max_wasm_memory_bytes
77        .unwrap_or(512 * 1024 * 1024)
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct HttpLimits {
82    /// Cap on `vm.fetch()` buffered response bodies. Must be `<=` the sidecar wire frame cap.
83    pub max_fetch_response_bytes: usize,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct ToolLimits {
88    pub default_tool_timeout_ms: u64,
89    pub max_tool_timeout_ms: u64,
90    pub max_registered_toolkits: usize,
91    pub max_registered_tools_per_vm: usize,
92    pub max_tools_per_toolkit: usize,
93    pub max_tool_schema_bytes: usize,
94    pub max_tool_examples_per_tool: usize,
95    pub max_tool_example_input_bytes: usize,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct PluginLimits {
100    pub max_persisted_manifest_bytes: usize,
101    pub max_persisted_manifest_file_bytes: u64,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct AcpLimits {
106    /// Maximum length of a single ACP adapter stdout line. Threaded into `AcpClientOptions`.
107    pub max_read_line_bytes: usize,
108    /// Pre-session ACP adapter stdout buffer cap.
109    pub stdout_buffer_byte_limit: usize,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct JsRuntimeLimits {
114    /// `None` keeps the V8 engine default heap. Carried as the typed
115    /// `JavascriptExecutionLimits.v8_heap_limit_mb` on the execution request
116    /// (no longer the `AGENTOS_V8_HEAP_LIMIT_MB` env knob).
117    pub v8_heap_limit_mb: Option<u32>,
118    /// Sync-RPC blocking-wait ceiling in ms. `None` keeps the engine default.
119    pub sync_rpc_wait_timeout_ms: Option<u64>,
120    pub captured_output_limit_bytes: usize,
121    pub stdin_buffer_limit_bytes: usize,
122    pub event_payload_limit_bytes: usize,
123    /// V8 IPC codec frame cap. Must feed both codec sides (`crates/execution/src/v8_ipc.rs` and
124    /// `crates/v8-runtime/src/ipc_binary.rs`).
125    pub v8_ipc_max_frame_bytes: u32,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct PythonLimits {
130    pub output_buffer_max_bytes: usize,
131    pub execution_timeout_ms: u64,
132    /// Pyodide V8 old-space cap in MB (`0` keeps the V8 default).
133    pub max_old_space_mb: usize,
134    pub vfs_rpc_timeout_ms: u64,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct WasmLimits {
139    pub max_module_file_bytes: u64,
140    pub captured_output_limit_bytes: usize,
141    /// WASM sync read cap. Also templated into the JS runner shim, so it must flow from one field.
142    pub sync_read_limit_bytes: usize,
143}
144
145impl Default for HttpLimits {
146    fn default() -> Self {
147        Self {
148            max_fetch_response_bytes: DEFAULT_MAX_FETCH_RESPONSE_BYTES,
149        }
150    }
151}
152
153impl Default for ToolLimits {
154    fn default() -> Self {
155        Self {
156            default_tool_timeout_ms: DEFAULT_TOOL_TIMEOUT_MS,
157            max_tool_timeout_ms: MAX_TOOL_TIMEOUT_MS,
158            max_registered_toolkits: MAX_REGISTERED_TOOLKITS,
159            max_registered_tools_per_vm: MAX_REGISTERED_TOOLS_PER_VM,
160            max_tools_per_toolkit: MAX_TOOLS_PER_TOOLKIT,
161            max_tool_schema_bytes: MAX_TOOL_SCHEMA_BYTES,
162            max_tool_examples_per_tool: MAX_TOOL_EXAMPLES_PER_TOOL,
163            max_tool_example_input_bytes: MAX_TOOL_EXAMPLE_INPUT_BYTES,
164        }
165    }
166}
167
168impl Default for PluginLimits {
169    fn default() -> Self {
170        Self {
171            max_persisted_manifest_bytes: MAX_PERSISTED_MANIFEST_BYTES,
172            max_persisted_manifest_file_bytes: MAX_PERSISTED_MANIFEST_FILE_BYTES,
173        }
174    }
175}
176
177impl Default for AcpLimits {
178    fn default() -> Self {
179        Self {
180            max_read_line_bytes: DEFAULT_ACP_MAX_READ_LINE_BYTES,
181            stdout_buffer_byte_limit: DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT,
182        }
183    }
184}
185
186impl Default for JsRuntimeLimits {
187    fn default() -> Self {
188        Self {
189            // Workers-style 128 MiB heap cap by default. Operators can raise or
190            // clear this through trusted VM config when a VM needs more room.
191            v8_heap_limit_mb: Some(DEFAULT_V8_HEAP_LIMIT_MB),
192            sync_rpc_wait_timeout_ms: None,
193            captured_output_limit_bytes: DEFAULT_JS_CAPTURED_OUTPUT_LIMIT_BYTES,
194            stdin_buffer_limit_bytes: DEFAULT_JS_STDIN_BUFFER_LIMIT_BYTES,
195            event_payload_limit_bytes: DEFAULT_JS_EVENT_PAYLOAD_LIMIT_BYTES,
196            v8_ipc_max_frame_bytes: DEFAULT_V8_IPC_MAX_FRAME_BYTES,
197        }
198    }
199}
200
201impl Default for PythonLimits {
202    fn default() -> Self {
203        Self {
204            output_buffer_max_bytes: DEFAULT_PYTHON_OUTPUT_BUFFER_MAX_BYTES,
205            execution_timeout_ms: DEFAULT_PYTHON_EXECUTION_TIMEOUT_MS,
206            max_old_space_mb: DEFAULT_PYTHON_MAX_OLD_SPACE_MB,
207            vfs_rpc_timeout_ms: DEFAULT_PYTHON_VFS_RPC_TIMEOUT_MS,
208        }
209    }
210}
211
212impl Default for WasmLimits {
213    fn default() -> Self {
214        Self {
215            max_module_file_bytes: DEFAULT_WASM_MAX_MODULE_FILE_BYTES,
216            captured_output_limit_bytes: DEFAULT_WASM_CAPTURED_OUTPUT_LIMIT_BYTES,
217            sync_read_limit_bytes: DEFAULT_WASM_SYNC_READ_LIMIT_BYTES,
218        }
219    }
220}
221
222pub fn vm_limits_from_config(
223    config: Option<&VmLimitsConfig>,
224    sidecar_max_frame_bytes: usize,
225) -> Result<VmLimits, SidecarCoreError> {
226    let mut limits = VmLimits::default();
227    let Some(config) = config else {
228        validate_vm_limits(&limits, sidecar_max_frame_bytes)?;
229        return Ok(limits);
230    };
231
232    if let Some(resources) = config.resources.as_ref() {
233        apply_resource_limits_config(&mut limits.resources, resources)?;
234    }
235    if let Some(http) = config.http.as_ref() {
236        set_usize(
237            &mut limits.http.max_fetch_response_bytes,
238            http.max_fetch_response_bytes,
239            "limits.http.maxFetchResponseBytes",
240        )?;
241    }
242    if let Some(tools) = config.tools.as_ref() {
243        set_u64(
244            &mut limits.tools.default_tool_timeout_ms,
245            tools.default_tool_timeout_ms,
246            "limits.tools.defaultToolTimeoutMs",
247        )?;
248        set_u64(
249            &mut limits.tools.max_tool_timeout_ms,
250            tools.max_tool_timeout_ms,
251            "limits.tools.maxToolTimeoutMs",
252        )?;
253        set_usize(
254            &mut limits.tools.max_registered_toolkits,
255            tools.max_registered_toolkits,
256            "limits.tools.maxRegisteredToolkits",
257        )?;
258        set_usize(
259            &mut limits.tools.max_registered_tools_per_vm,
260            tools.max_registered_tools_per_vm,
261            "limits.tools.maxRegisteredToolsPerVm",
262        )?;
263        set_usize(
264            &mut limits.tools.max_tools_per_toolkit,
265            tools.max_tools_per_toolkit,
266            "limits.tools.maxToolsPerToolkit",
267        )?;
268        set_usize(
269            &mut limits.tools.max_tool_schema_bytes,
270            tools.max_tool_schema_bytes,
271            "limits.tools.maxToolSchemaBytes",
272        )?;
273        set_usize(
274            &mut limits.tools.max_tool_examples_per_tool,
275            tools.max_tool_examples_per_tool,
276            "limits.tools.maxToolExamplesPerTool",
277        )?;
278        set_usize(
279            &mut limits.tools.max_tool_example_input_bytes,
280            tools.max_tool_example_input_bytes,
281            "limits.tools.maxToolExampleInputBytes",
282        )?;
283    }
284    if let Some(plugins) = config.plugins.as_ref() {
285        set_usize(
286            &mut limits.plugins.max_persisted_manifest_bytes,
287            plugins.max_persisted_manifest_bytes,
288            "limits.plugins.maxPersistedManifestBytes",
289        )?;
290        set_u64(
291            &mut limits.plugins.max_persisted_manifest_file_bytes,
292            plugins.max_persisted_manifest_file_bytes,
293            "limits.plugins.maxPersistedManifestFileBytes",
294        )?;
295    }
296    if let Some(acp) = config.acp.as_ref() {
297        set_usize(
298            &mut limits.acp.max_read_line_bytes,
299            acp.max_read_line_bytes,
300            "limits.acp.maxReadLineBytes",
301        )?;
302        set_usize(
303            &mut limits.acp.stdout_buffer_byte_limit,
304            acp.stdout_buffer_byte_limit,
305            "limits.acp.stdoutBufferByteLimit",
306        )?;
307    }
308    if let Some(js_runtime) = config.js_runtime.as_ref() {
309        if let Some(value) = js_runtime.v8_heap_limit_mb {
310            limits.js_runtime.v8_heap_limit_mb = Some(
311                u32::try_from(value)
312                    .map_err(|_| integer_too_large("limits.jsRuntime.v8HeapLimitMb", value))?,
313            );
314        }
315        set_usize(
316            &mut limits.js_runtime.captured_output_limit_bytes,
317            js_runtime.captured_output_limit_bytes,
318            "limits.jsRuntime.capturedOutputLimitBytes",
319        )?;
320        set_usize(
321            &mut limits.js_runtime.stdin_buffer_limit_bytes,
322            js_runtime.stdin_buffer_limit_bytes,
323            "limits.jsRuntime.stdinBufferLimitBytes",
324        )?;
325        set_usize(
326            &mut limits.js_runtime.event_payload_limit_bytes,
327            js_runtime.event_payload_limit_bytes,
328            "limits.jsRuntime.eventPayloadLimitBytes",
329        )?;
330        if let Some(value) = js_runtime.v8_ipc_max_frame_bytes {
331            limits.js_runtime.v8_ipc_max_frame_bytes = u32::try_from(value)
332                .map_err(|_| integer_too_large("limits.jsRuntime.v8IpcMaxFrameBytes", value))?;
333        }
334        if let Some(value) = js_runtime.sync_rpc_wait_timeout_ms {
335            limits.js_runtime.sync_rpc_wait_timeout_ms = Some(value);
336        }
337    }
338    if let Some(python) = config.python.as_ref() {
339        set_usize(
340            &mut limits.python.output_buffer_max_bytes,
341            python.output_buffer_max_bytes,
342            "limits.python.outputBufferMaxBytes",
343        )?;
344        set_u64(
345            &mut limits.python.execution_timeout_ms,
346            python.execution_timeout_ms,
347            "limits.python.executionTimeoutMs",
348        )?;
349        set_usize(
350            &mut limits.python.max_old_space_mb,
351            python.max_old_space_mb,
352            "limits.python.maxOldSpaceMb",
353        )?;
354        set_u64(
355            &mut limits.python.vfs_rpc_timeout_ms,
356            python.vfs_rpc_timeout_ms,
357            "limits.python.vfsRpcTimeoutMs",
358        )?;
359    }
360    if let Some(wasm) = config.wasm.as_ref() {
361        set_u64(
362            &mut limits.wasm.max_module_file_bytes,
363            wasm.max_module_file_bytes,
364            "limits.wasm.maxModuleFileBytes",
365        )?;
366        set_usize(
367            &mut limits.wasm.captured_output_limit_bytes,
368            wasm.captured_output_limit_bytes,
369            "limits.wasm.capturedOutputLimitBytes",
370        )?;
371        set_usize(
372            &mut limits.wasm.sync_read_limit_bytes,
373            wasm.sync_read_limit_bytes,
374            "limits.wasm.syncReadLimitBytes",
375        )?;
376    }
377
378    validate_vm_limits(&limits, sidecar_max_frame_bytes)?;
379    Ok(limits)
380}
381
382fn apply_resource_limits_config(
383    limits: &mut ResourceLimits,
384    config: &ResourceLimitsConfig,
385) -> Result<(), SidecarCoreError> {
386    set_optional_usize(
387        &mut limits.virtual_cpu_count,
388        config.cpu_count,
389        "limits.resources.cpuCount",
390    )?;
391    set_optional_usize(
392        &mut limits.max_processes,
393        config.max_processes,
394        "limits.resources.maxProcesses",
395    )?;
396    set_optional_usize(
397        &mut limits.max_open_fds,
398        config.max_open_fds,
399        "limits.resources.maxOpenFds",
400    )?;
401    set_optional_usize(
402        &mut limits.max_pipes,
403        config.max_pipes,
404        "limits.resources.maxPipes",
405    )?;
406    set_optional_usize(
407        &mut limits.max_ptys,
408        config.max_ptys,
409        "limits.resources.maxPtys",
410    )?;
411    set_optional_usize(
412        &mut limits.max_sockets,
413        config.max_sockets,
414        "limits.resources.maxSockets",
415    )?;
416    set_optional_usize(
417        &mut limits.max_connections,
418        config.max_connections,
419        "limits.resources.maxConnections",
420    )?;
421    set_optional_usize(
422        &mut limits.max_socket_buffered_bytes,
423        config.max_socket_buffered_bytes,
424        "limits.resources.maxSocketBufferedBytes",
425    )?;
426    set_optional_usize(
427        &mut limits.max_socket_datagram_queue_len,
428        config.max_socket_datagram_queue_len,
429        "limits.resources.maxSocketDatagramQueueLen",
430    )?;
431    set_optional_u64(
432        &mut limits.max_filesystem_bytes,
433        config.max_filesystem_bytes,
434    );
435    set_optional_usize(
436        &mut limits.max_inode_count,
437        config.max_inode_count,
438        "limits.resources.maxInodeCount",
439    )?;
440    set_optional_u64(
441        &mut limits.max_blocking_read_ms,
442        config.max_blocking_read_ms,
443    );
444    set_optional_usize(
445        &mut limits.max_pread_bytes,
446        config.max_pread_bytes,
447        "limits.resources.maxPreadBytes",
448    )?;
449    set_optional_usize(
450        &mut limits.max_fd_write_bytes,
451        config.max_fd_write_bytes,
452        "limits.resources.maxFdWriteBytes",
453    )?;
454    set_optional_usize(
455        &mut limits.max_process_argv_bytes,
456        config.max_process_argv_bytes,
457        "limits.resources.maxProcessArgvBytes",
458    )?;
459    set_optional_usize(
460        &mut limits.max_process_env_bytes,
461        config.max_process_env_bytes,
462        "limits.resources.maxProcessEnvBytes",
463    )?;
464    set_optional_usize(
465        &mut limits.max_readdir_entries,
466        config.max_readdir_entries,
467        "limits.resources.maxReaddirEntries",
468    )?;
469    set_optional_u64(&mut limits.max_wasm_fuel, config.max_wasm_fuel);
470    set_optional_u64(
471        &mut limits.max_wasm_memory_bytes,
472        config.max_wasm_memory_bytes,
473    );
474    set_optional_usize(
475        &mut limits.max_wasm_stack_bytes,
476        config.max_wasm_stack_bytes,
477        "limits.resources.maxWasmStackBytes",
478    )?;
479    Ok(())
480}
481
482fn set_usize(target: &mut usize, value: Option<u64>, key: &str) -> Result<(), SidecarCoreError> {
483    if let Some(value) = value {
484        *target = usize::try_from(value).map_err(|_| integer_too_large(key, value))?;
485    }
486    Ok(())
487}
488
489fn set_u64(target: &mut u64, value: Option<u64>, _key: &str) -> Result<(), SidecarCoreError> {
490    if let Some(value) = value {
491        *target = value;
492    }
493    Ok(())
494}
495
496fn set_optional_usize(
497    target: &mut Option<usize>,
498    value: Option<u64>,
499    key: &str,
500) -> Result<(), SidecarCoreError> {
501    if let Some(value) = value {
502        *target = Some(usize::try_from(value).map_err(|_| integer_too_large(key, value))?);
503    }
504    Ok(())
505}
506
507fn set_optional_u64(target: &mut Option<u64>, value: Option<u64>) {
508    if let Some(value) = value {
509        *target = Some(value);
510    }
511}
512
513fn integer_too_large(key: &str, value: u64) -> SidecarCoreError {
514    SidecarCoreError::new(format!("{key} value {value} does not fit this platform"))
515}
516
517/// Cross-field validation. Fail-by-default: reject any configuration that would deadlock or
518/// violate the wire frame budget with an explicit, actionable message.
519pub fn validate_vm_limits(
520    limits: &VmLimits,
521    sidecar_max_frame_bytes: usize,
522) -> Result<(), SidecarCoreError> {
523    if limits.http.max_fetch_response_bytes == 0 {
524        return Err(SidecarCoreError::new(
525            "limits.http.max_fetch_response_bytes must be greater than zero".to_string(),
526        ));
527    }
528    if limits.http.max_fetch_response_bytes > sidecar_max_frame_bytes {
529        return Err(SidecarCoreError::new(format!(
530            "limits.http.max_fetch_response_bytes ({}) must be <= the sidecar wire frame cap ({})",
531            limits.http.max_fetch_response_bytes, sidecar_max_frame_bytes
532        )));
533    }
534
535    if limits.tools.default_tool_timeout_ms > limits.tools.max_tool_timeout_ms {
536        return Err(SidecarCoreError::new(format!(
537            "limits.tools.default_tool_timeout_ms ({}) must be <= limits.tools.max_tool_timeout_ms ({})",
538            limits.tools.default_tool_timeout_ms, limits.tools.max_tool_timeout_ms
539        )));
540    }
541
542    let nonzero_usize: [(&str, usize); 13] = [
543        (
544            "limits.tools.max_registered_toolkits",
545            limits.tools.max_registered_toolkits,
546        ),
547        (
548            "limits.tools.max_registered_tools_per_vm",
549            limits.tools.max_registered_tools_per_vm,
550        ),
551        (
552            "limits.tools.max_tools_per_toolkit",
553            limits.tools.max_tools_per_toolkit,
554        ),
555        (
556            "limits.tools.max_tool_schema_bytes",
557            limits.tools.max_tool_schema_bytes,
558        ),
559        (
560            "limits.tools.max_tool_example_input_bytes",
561            limits.tools.max_tool_example_input_bytes,
562        ),
563        (
564            "limits.plugins.max_persisted_manifest_bytes",
565            limits.plugins.max_persisted_manifest_bytes,
566        ),
567        (
568            "limits.acp.max_read_line_bytes",
569            limits.acp.max_read_line_bytes,
570        ),
571        (
572            "limits.acp.stdout_buffer_byte_limit",
573            limits.acp.stdout_buffer_byte_limit,
574        ),
575        (
576            "limits.js_runtime.captured_output_limit_bytes",
577            limits.js_runtime.captured_output_limit_bytes,
578        ),
579        (
580            "limits.js_runtime.stdin_buffer_limit_bytes",
581            limits.js_runtime.stdin_buffer_limit_bytes,
582        ),
583        (
584            "limits.js_runtime.event_payload_limit_bytes",
585            limits.js_runtime.event_payload_limit_bytes,
586        ),
587        (
588            "limits.python.output_buffer_max_bytes",
589            limits.python.output_buffer_max_bytes,
590        ),
591        (
592            "limits.wasm.captured_output_limit_bytes",
593            limits.wasm.captured_output_limit_bytes,
594        ),
595    ];
596    for (key, value) in nonzero_usize {
597        if value == 0 {
598            return Err(SidecarCoreError::new(format!(
599                "{key} must be greater than zero"
600            )));
601        }
602    }
603
604    if limits.wasm.sync_read_limit_bytes == 0 {
605        return Err(SidecarCoreError::new(
606            "limits.wasm.sync_read_limit_bytes must be greater than zero".to_string(),
607        ));
608    }
609    if limits.wasm.max_module_file_bytes == 0 {
610        return Err(SidecarCoreError::new(
611            "limits.wasm.max_module_file_bytes must be greater than zero".to_string(),
612        ));
613    }
614    if limits.js_runtime.v8_ipc_max_frame_bytes == 0 {
615        return Err(SidecarCoreError::new(
616            "limits.js_runtime.v8_ipc_max_frame_bytes must be greater than zero".to_string(),
617        ));
618    }
619    if limits.python.execution_timeout_ms == 0 {
620        return Err(SidecarCoreError::new(
621            "limits.python.execution_timeout_ms must be greater than zero".to_string(),
622        ));
623    }
624    if limits.python.vfs_rpc_timeout_ms == 0 {
625        return Err(SidecarCoreError::new(
626            "limits.python.vfs_rpc_timeout_ms must be greater than zero".to_string(),
627        ));
628    }
629    if let Some(0) = limits.js_runtime.v8_heap_limit_mb {
630        return Err(SidecarCoreError::new(
631            "limits.js_runtime.v8_heap_limit_mb must be greater than zero".to_string(),
632        ));
633    }
634
635    Ok(())
636}