use secure_exec_kernel::resource_accounting::ResourceLimits;
use secure_exec_vm_config::{ResourceLimitsConfig, VmLimitsConfig};
use crate::state::SidecarError;
use crate::wire::DEFAULT_MAX_FRAME_BYTES;
pub const DEFAULT_MAX_FETCH_RESPONSE_BYTES: usize = DEFAULT_MAX_FRAME_BYTES;
pub const DEFAULT_TOOL_TIMEOUT_MS: u64 = 30_000;
pub const MAX_TOOL_TIMEOUT_MS: u64 = 300_000;
pub const MAX_REGISTERED_TOOLKITS: usize = 64;
pub const MAX_REGISTERED_TOOLS_PER_VM: usize = 256;
pub const MAX_TOOLS_PER_TOOLKIT: usize = 64;
pub const MAX_TOOL_SCHEMA_BYTES: usize = 16 * 1024;
pub const MAX_TOOL_EXAMPLES_PER_TOOL: usize = 16;
pub const MAX_TOOL_EXAMPLE_INPUT_BYTES: usize = 4 * 1024;
pub const MAX_PERSISTED_MANIFEST_BYTES: usize = 64 * 1024 * 1024;
pub const MAX_PERSISTED_MANIFEST_FILE_BYTES: u64 = 1024 * 1024 * 1024;
pub const DEFAULT_ACP_MAX_READ_LINE_BYTES: usize = 16 * 1024 * 1024;
pub const DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT: usize = 1024 * 1024;
pub const DEFAULT_JS_CAPTURED_OUTPUT_LIMIT_BYTES: usize = 16 * 1024 * 1024;
pub const DEFAULT_JS_STDIN_BUFFER_LIMIT_BYTES: usize = 16 * 1024 * 1024;
pub const DEFAULT_JS_EVENT_PAYLOAD_LIMIT_BYTES: usize = 1024 * 1024;
pub const DEFAULT_V8_IPC_MAX_FRAME_BYTES: u32 = 64 * 1024 * 1024;
pub const DEFAULT_PYTHON_OUTPUT_BUFFER_MAX_BYTES: usize = 1024 * 1024;
pub const DEFAULT_PYTHON_EXECUTION_TIMEOUT_MS: u64 = 5 * 60 * 1000;
pub const DEFAULT_PYTHON_VFS_RPC_TIMEOUT_MS: u64 = 30 * 1000;
pub const DEFAULT_WASM_MAX_MODULE_FILE_BYTES: u64 = 256 * 1024 * 1024;
pub const DEFAULT_WASM_CAPTURED_OUTPUT_LIMIT_BYTES: usize = 16 * 1024 * 1024;
pub const DEFAULT_WASM_SYNC_READ_LIMIT_BYTES: usize = 16 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct VmLimits {
pub resources: ResourceLimits,
pub http: HttpLimits,
pub tools: ToolLimits,
pub plugins: PluginLimits,
pub acp: AcpLimits,
pub js_runtime: JsRuntimeLimits,
pub python: PythonLimits,
pub wasm: WasmLimits,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpLimits {
pub max_fetch_response_bytes: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolLimits {
pub default_tool_timeout_ms: u64,
pub max_tool_timeout_ms: u64,
pub max_registered_toolkits: usize,
pub max_registered_tools_per_vm: usize,
pub max_tools_per_toolkit: usize,
pub max_tool_schema_bytes: usize,
pub max_tool_examples_per_tool: usize,
pub max_tool_example_input_bytes: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginLimits {
pub max_persisted_manifest_bytes: usize,
pub max_persisted_manifest_file_bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AcpLimits {
pub max_read_line_bytes: usize,
pub stdout_buffer_byte_limit: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsRuntimeLimits {
pub v8_heap_limit_mb: Option<u32>,
pub captured_output_limit_bytes: usize,
pub stdin_buffer_limit_bytes: usize,
pub event_payload_limit_bytes: usize,
pub v8_ipc_max_frame_bytes: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PythonLimits {
pub output_buffer_max_bytes: usize,
pub execution_timeout_ms: u64,
pub vfs_rpc_timeout_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WasmLimits {
pub max_module_file_bytes: u64,
pub captured_output_limit_bytes: usize,
pub sync_read_limit_bytes: usize,
}
impl Default for HttpLimits {
fn default() -> Self {
Self {
max_fetch_response_bytes: DEFAULT_MAX_FETCH_RESPONSE_BYTES,
}
}
}
impl Default for ToolLimits {
fn default() -> Self {
Self {
default_tool_timeout_ms: DEFAULT_TOOL_TIMEOUT_MS,
max_tool_timeout_ms: MAX_TOOL_TIMEOUT_MS,
max_registered_toolkits: MAX_REGISTERED_TOOLKITS,
max_registered_tools_per_vm: MAX_REGISTERED_TOOLS_PER_VM,
max_tools_per_toolkit: MAX_TOOLS_PER_TOOLKIT,
max_tool_schema_bytes: MAX_TOOL_SCHEMA_BYTES,
max_tool_examples_per_tool: MAX_TOOL_EXAMPLES_PER_TOOL,
max_tool_example_input_bytes: MAX_TOOL_EXAMPLE_INPUT_BYTES,
}
}
}
impl Default for PluginLimits {
fn default() -> Self {
Self {
max_persisted_manifest_bytes: MAX_PERSISTED_MANIFEST_BYTES,
max_persisted_manifest_file_bytes: MAX_PERSISTED_MANIFEST_FILE_BYTES,
}
}
}
impl Default for AcpLimits {
fn default() -> Self {
Self {
max_read_line_bytes: DEFAULT_ACP_MAX_READ_LINE_BYTES,
stdout_buffer_byte_limit: DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT,
}
}
}
impl Default for JsRuntimeLimits {
fn default() -> Self {
Self {
v8_heap_limit_mb: None,
captured_output_limit_bytes: DEFAULT_JS_CAPTURED_OUTPUT_LIMIT_BYTES,
stdin_buffer_limit_bytes: DEFAULT_JS_STDIN_BUFFER_LIMIT_BYTES,
event_payload_limit_bytes: DEFAULT_JS_EVENT_PAYLOAD_LIMIT_BYTES,
v8_ipc_max_frame_bytes: DEFAULT_V8_IPC_MAX_FRAME_BYTES,
}
}
}
impl Default for PythonLimits {
fn default() -> Self {
Self {
output_buffer_max_bytes: DEFAULT_PYTHON_OUTPUT_BUFFER_MAX_BYTES,
execution_timeout_ms: DEFAULT_PYTHON_EXECUTION_TIMEOUT_MS,
vfs_rpc_timeout_ms: DEFAULT_PYTHON_VFS_RPC_TIMEOUT_MS,
}
}
}
impl Default for WasmLimits {
fn default() -> Self {
Self {
max_module_file_bytes: DEFAULT_WASM_MAX_MODULE_FILE_BYTES,
captured_output_limit_bytes: DEFAULT_WASM_CAPTURED_OUTPUT_LIMIT_BYTES,
sync_read_limit_bytes: DEFAULT_WASM_SYNC_READ_LIMIT_BYTES,
}
}
}
pub fn vm_limits_from_config(
config: Option<&VmLimitsConfig>,
sidecar_max_frame_bytes: usize,
) -> Result<VmLimits, SidecarError> {
let mut limits = VmLimits::default();
let Some(config) = config else {
validate_vm_limits(&limits, sidecar_max_frame_bytes)?;
return Ok(limits);
};
if let Some(resources) = config.resources.as_ref() {
apply_resource_limits_config(&mut limits.resources, resources)?;
}
if let Some(http) = config.http.as_ref() {
set_usize(
&mut limits.http.max_fetch_response_bytes,
http.max_fetch_response_bytes,
"limits.http.maxFetchResponseBytes",
)?;
}
if let Some(tools) = config.tools.as_ref() {
set_u64(
&mut limits.tools.default_tool_timeout_ms,
tools.default_tool_timeout_ms,
"limits.tools.defaultToolTimeoutMs",
)?;
set_u64(
&mut limits.tools.max_tool_timeout_ms,
tools.max_tool_timeout_ms,
"limits.tools.maxToolTimeoutMs",
)?;
set_usize(
&mut limits.tools.max_registered_toolkits,
tools.max_registered_toolkits,
"limits.tools.maxRegisteredToolkits",
)?;
set_usize(
&mut limits.tools.max_registered_tools_per_vm,
tools.max_registered_tools_per_vm,
"limits.tools.maxRegisteredToolsPerVm",
)?;
set_usize(
&mut limits.tools.max_tools_per_toolkit,
tools.max_tools_per_toolkit,
"limits.tools.maxToolsPerToolkit",
)?;
set_usize(
&mut limits.tools.max_tool_schema_bytes,
tools.max_tool_schema_bytes,
"limits.tools.maxToolSchemaBytes",
)?;
set_usize(
&mut limits.tools.max_tool_examples_per_tool,
tools.max_tool_examples_per_tool,
"limits.tools.maxToolExamplesPerTool",
)?;
set_usize(
&mut limits.tools.max_tool_example_input_bytes,
tools.max_tool_example_input_bytes,
"limits.tools.maxToolExampleInputBytes",
)?;
}
if let Some(plugins) = config.plugins.as_ref() {
set_usize(
&mut limits.plugins.max_persisted_manifest_bytes,
plugins.max_persisted_manifest_bytes,
"limits.plugins.maxPersistedManifestBytes",
)?;
set_u64(
&mut limits.plugins.max_persisted_manifest_file_bytes,
plugins.max_persisted_manifest_file_bytes,
"limits.plugins.maxPersistedManifestFileBytes",
)?;
}
if let Some(acp) = config.acp.as_ref() {
set_usize(
&mut limits.acp.max_read_line_bytes,
acp.max_read_line_bytes,
"limits.acp.maxReadLineBytes",
)?;
set_usize(
&mut limits.acp.stdout_buffer_byte_limit,
acp.stdout_buffer_byte_limit,
"limits.acp.stdoutBufferByteLimit",
)?;
}
if let Some(js_runtime) = config.js_runtime.as_ref() {
if let Some(value) = js_runtime.v8_heap_limit_mb {
limits.js_runtime.v8_heap_limit_mb = Some(
u32::try_from(value)
.map_err(|_| integer_too_large("limits.jsRuntime.v8HeapLimitMb", value))?,
);
}
set_usize(
&mut limits.js_runtime.captured_output_limit_bytes,
js_runtime.captured_output_limit_bytes,
"limits.jsRuntime.capturedOutputLimitBytes",
)?;
set_usize(
&mut limits.js_runtime.stdin_buffer_limit_bytes,
js_runtime.stdin_buffer_limit_bytes,
"limits.jsRuntime.stdinBufferLimitBytes",
)?;
set_usize(
&mut limits.js_runtime.event_payload_limit_bytes,
js_runtime.event_payload_limit_bytes,
"limits.jsRuntime.eventPayloadLimitBytes",
)?;
if let Some(value) = js_runtime.v8_ipc_max_frame_bytes {
limits.js_runtime.v8_ipc_max_frame_bytes = u32::try_from(value)
.map_err(|_| integer_too_large("limits.jsRuntime.v8IpcMaxFrameBytes", value))?;
}
}
if let Some(python) = config.python.as_ref() {
set_usize(
&mut limits.python.output_buffer_max_bytes,
python.output_buffer_max_bytes,
"limits.python.outputBufferMaxBytes",
)?;
set_u64(
&mut limits.python.execution_timeout_ms,
python.execution_timeout_ms,
"limits.python.executionTimeoutMs",
)?;
set_u64(
&mut limits.python.vfs_rpc_timeout_ms,
python.vfs_rpc_timeout_ms,
"limits.python.vfsRpcTimeoutMs",
)?;
}
if let Some(wasm) = config.wasm.as_ref() {
set_u64(
&mut limits.wasm.max_module_file_bytes,
wasm.max_module_file_bytes,
"limits.wasm.maxModuleFileBytes",
)?;
set_usize(
&mut limits.wasm.captured_output_limit_bytes,
wasm.captured_output_limit_bytes,
"limits.wasm.capturedOutputLimitBytes",
)?;
set_usize(
&mut limits.wasm.sync_read_limit_bytes,
wasm.sync_read_limit_bytes,
"limits.wasm.syncReadLimitBytes",
)?;
}
validate_vm_limits(&limits, sidecar_max_frame_bytes)?;
Ok(limits)
}
fn apply_resource_limits_config(
limits: &mut ResourceLimits,
config: &ResourceLimitsConfig,
) -> Result<(), SidecarError> {
set_optional_usize(
&mut limits.virtual_cpu_count,
config.cpu_count,
"limits.resources.cpuCount",
)?;
set_optional_usize(
&mut limits.max_processes,
config.max_processes,
"limits.resources.maxProcesses",
)?;
set_optional_usize(
&mut limits.max_open_fds,
config.max_open_fds,
"limits.resources.maxOpenFds",
)?;
set_optional_usize(
&mut limits.max_pipes,
config.max_pipes,
"limits.resources.maxPipes",
)?;
set_optional_usize(
&mut limits.max_ptys,
config.max_ptys,
"limits.resources.maxPtys",
)?;
set_optional_usize(
&mut limits.max_sockets,
config.max_sockets,
"limits.resources.maxSockets",
)?;
set_optional_usize(
&mut limits.max_connections,
config.max_connections,
"limits.resources.maxConnections",
)?;
set_optional_usize(
&mut limits.max_socket_buffered_bytes,
config.max_socket_buffered_bytes,
"limits.resources.maxSocketBufferedBytes",
)?;
set_optional_usize(
&mut limits.max_socket_datagram_queue_len,
config.max_socket_datagram_queue_len,
"limits.resources.maxSocketDatagramQueueLen",
)?;
set_optional_u64(
&mut limits.max_filesystem_bytes,
config.max_filesystem_bytes,
);
set_optional_usize(
&mut limits.max_inode_count,
config.max_inode_count,
"limits.resources.maxInodeCount",
)?;
set_optional_u64(
&mut limits.max_blocking_read_ms,
config.max_blocking_read_ms,
);
set_optional_usize(
&mut limits.max_pread_bytes,
config.max_pread_bytes,
"limits.resources.maxPreadBytes",
)?;
set_optional_usize(
&mut limits.max_fd_write_bytes,
config.max_fd_write_bytes,
"limits.resources.maxFdWriteBytes",
)?;
set_optional_usize(
&mut limits.max_process_argv_bytes,
config.max_process_argv_bytes,
"limits.resources.maxProcessArgvBytes",
)?;
set_optional_usize(
&mut limits.max_process_env_bytes,
config.max_process_env_bytes,
"limits.resources.maxProcessEnvBytes",
)?;
set_optional_usize(
&mut limits.max_readdir_entries,
config.max_readdir_entries,
"limits.resources.maxReaddirEntries",
)?;
set_optional_u64(&mut limits.max_wasm_fuel, config.max_wasm_fuel);
set_optional_u64(
&mut limits.max_wasm_memory_bytes,
config.max_wasm_memory_bytes,
);
set_optional_usize(
&mut limits.max_wasm_stack_bytes,
config.max_wasm_stack_bytes,
"limits.resources.maxWasmStackBytes",
)?;
Ok(())
}
fn set_usize(target: &mut usize, value: Option<u64>, key: &str) -> Result<(), SidecarError> {
if let Some(value) = value {
*target = usize::try_from(value).map_err(|_| integer_too_large(key, value))?;
}
Ok(())
}
fn set_u64(target: &mut u64, value: Option<u64>, _key: &str) -> Result<(), SidecarError> {
if let Some(value) = value {
*target = value;
}
Ok(())
}
fn set_optional_usize(
target: &mut Option<usize>,
value: Option<u64>,
key: &str,
) -> Result<(), SidecarError> {
if let Some(value) = value {
*target = Some(usize::try_from(value).map_err(|_| integer_too_large(key, value))?);
}
Ok(())
}
fn set_optional_u64(target: &mut Option<u64>, value: Option<u64>) {
if let Some(value) = value {
*target = Some(value);
}
}
fn integer_too_large(key: &str, value: u64) -> SidecarError {
SidecarError::InvalidState(format!("{key} value {value} does not fit this platform"))
}
pub(crate) fn validate_vm_limits(
limits: &VmLimits,
sidecar_max_frame_bytes: usize,
) -> Result<(), SidecarError> {
if limits.http.max_fetch_response_bytes == 0 {
return Err(SidecarError::InvalidState(
"limits.http.max_fetch_response_bytes must be greater than zero".to_string(),
));
}
if limits.http.max_fetch_response_bytes > sidecar_max_frame_bytes {
return Err(SidecarError::InvalidState(format!(
"limits.http.max_fetch_response_bytes ({}) must be <= the sidecar wire frame cap ({})",
limits.http.max_fetch_response_bytes, sidecar_max_frame_bytes
)));
}
if limits.tools.default_tool_timeout_ms > limits.tools.max_tool_timeout_ms {
return Err(SidecarError::InvalidState(format!(
"limits.tools.default_tool_timeout_ms ({}) must be <= limits.tools.max_tool_timeout_ms ({})",
limits.tools.default_tool_timeout_ms, limits.tools.max_tool_timeout_ms
)));
}
let nonzero_usize: [(&str, usize); 13] = [
(
"limits.tools.max_registered_toolkits",
limits.tools.max_registered_toolkits,
),
(
"limits.tools.max_registered_tools_per_vm",
limits.tools.max_registered_tools_per_vm,
),
(
"limits.tools.max_tools_per_toolkit",
limits.tools.max_tools_per_toolkit,
),
(
"limits.tools.max_tool_schema_bytes",
limits.tools.max_tool_schema_bytes,
),
(
"limits.tools.max_tool_example_input_bytes",
limits.tools.max_tool_example_input_bytes,
),
(
"limits.plugins.max_persisted_manifest_bytes",
limits.plugins.max_persisted_manifest_bytes,
),
(
"limits.acp.max_read_line_bytes",
limits.acp.max_read_line_bytes,
),
(
"limits.acp.stdout_buffer_byte_limit",
limits.acp.stdout_buffer_byte_limit,
),
(
"limits.js_runtime.captured_output_limit_bytes",
limits.js_runtime.captured_output_limit_bytes,
),
(
"limits.js_runtime.stdin_buffer_limit_bytes",
limits.js_runtime.stdin_buffer_limit_bytes,
),
(
"limits.js_runtime.event_payload_limit_bytes",
limits.js_runtime.event_payload_limit_bytes,
),
(
"limits.python.output_buffer_max_bytes",
limits.python.output_buffer_max_bytes,
),
(
"limits.wasm.captured_output_limit_bytes",
limits.wasm.captured_output_limit_bytes,
),
];
for (key, value) in nonzero_usize {
if value == 0 {
return Err(SidecarError::InvalidState(format!(
"{key} must be greater than zero"
)));
}
}
if limits.wasm.sync_read_limit_bytes == 0 {
return Err(SidecarError::InvalidState(
"limits.wasm.sync_read_limit_bytes must be greater than zero".to_string(),
));
}
if limits.wasm.max_module_file_bytes == 0 {
return Err(SidecarError::InvalidState(
"limits.wasm.max_module_file_bytes must be greater than zero".to_string(),
));
}
if limits.js_runtime.v8_ipc_max_frame_bytes == 0 {
return Err(SidecarError::InvalidState(
"limits.js_runtime.v8_ipc_max_frame_bytes must be greater than zero".to_string(),
));
}
if limits.python.execution_timeout_ms == 0 {
return Err(SidecarError::InvalidState(
"limits.python.execution_timeout_ms must be greater than zero".to_string(),
));
}
if limits.python.vfs_rpc_timeout_ms == 0 {
return Err(SidecarError::InvalidState(
"limits.python.vfs_rpc_timeout_ms must be greater than zero".to_string(),
));
}
if let Some(0) = limits.js_runtime.v8_heap_limit_mb {
return Err(SidecarError::InvalidState(
"limits.js_runtime.v8_heap_limit_mb must be greater than zero".to_string(),
));
}
Ok(())
}