Skip to main content

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