1use secure_exec_kernel::resource_accounting::ResourceLimits;
9use secure_exec_vm_config::{ResourceLimitsConfig, VmLimitsConfig};
10
11use crate::SidecarCoreError;
12
13pub 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;
40pub 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#[derive(Debug, Clone, PartialEq, Eq, Default)]
52pub struct VmLimits {
53 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 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 pub max_read_line_bytes: usize,
108 pub stdout_buffer_byte_limit: usize,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct JsRuntimeLimits {
114 pub v8_heap_limit_mb: Option<u32>,
118 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 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 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 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 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
517pub 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}