Skip to main content

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