1use std::collections::BTreeMap;
12use std::sync::Arc;
13
14use serde::{Deserialize, Serialize};
15
16use crate::fs::VirtualFileSystem;
17pub use agentos_vm_config::{
18 VmGroupConfig, VmSqliteCallbackRequest, VmSqliteCallbackResponse, VmSqliteDescriptor,
19 VmSqliteQueryResult, VmSqliteStatement, VmSqliteValue, VmUserAccountConfig, VmUserConfig,
20 VM_SQLITE_CALLBACK_NAMESPACE,
21};
22
23pub type SidecarSqliteCallback = Arc<
24 dyn Fn(
25 VmSqliteCallbackRequest,
26 ) -> futures::future::BoxFuture<'static, Result<VmSqliteCallbackResponse, String>>
27 + Send
28 + Sync,
29>;
30
31#[derive(Default)]
36pub struct AgentOsConfig {
37 pub database: Option<agentos_vm_config::VmSqliteDescriptor>,
40 pub sidecar_sqlite_callback: Option<SidecarSqliteCallback>,
43 pub user: Option<VmUserConfig>,
45 pub environment: Option<BTreeMap<String, String>>,
48 pub software: Vec<SoftwareInput>,
50 pub packages: Vec<PackageRef>,
54 pub packages_mount_at: Option<String>,
57 pub package_resolver: Option<crate::software::PackageResolverOptions>,
60 pub software_sources: Vec<crate::software_registry::SoftwareSource>,
64 pub software_registry: Option<crate::software_registry::SoftwareRegistryOptions>,
67 pub loopback_exempt_ports: Vec<u16>,
69 pub allowed_node_builtins: Option<Vec<String>>,
71 pub high_resolution_time: Option<bool>,
74 pub root_filesystem: RootFilesystemConfig,
76 pub mounts: Vec<MountConfig>,
78 pub schedule_driver: Option<Arc<dyn ScheduleDriver>>,
80 pub host_functions: HostFunctionCollections,
82 pub sidecar_js_bridge_callback: Option<SidecarJsBridgeCallback>,
84 pub permissions: Option<Permissions>,
86 pub limits: Option<AgentOsLimits>,
88 pub sidecar: Option<AgentOsSidecarConfig>,
90 pub sidecar_binary_path: Option<String>,
95}
96
97#[derive(Default)]
99pub struct AgentOsConfigBuilder {
100 config: AgentOsConfig,
101}
102
103impl AgentOsConfigBuilder {
104 pub fn new() -> Self {
105 Self::default()
106 }
107
108 pub fn database(mut self, database: agentos_vm_config::VmSqliteDescriptor) -> Self {
109 self.config.database = Some(database);
110 self
111 }
112
113 pub fn sidecar_sqlite_callback(mut self, callback: SidecarSqliteCallback) -> Self {
114 self.config.sidecar_sqlite_callback = Some(callback);
115 self
116 }
117
118 pub fn packages(mut self, packages: Vec<PackageRef>) -> Self {
119 self.config.packages = packages;
120 self
121 }
122
123 pub fn packages_mount_at(mut self, mount_at: impl Into<String>) -> Self {
124 self.config.packages_mount_at = Some(mount_at.into());
125 self
126 }
127
128 pub fn package_resolver(mut self, options: crate::software::PackageResolverOptions) -> Self {
129 self.config.package_resolver = Some(options);
130 self
131 }
132
133 pub fn software_sources(
134 mut self,
135 sources: Vec<crate::software_registry::SoftwareSource>,
136 ) -> Self {
137 self.config.software_sources = sources;
138 self
139 }
140
141 pub fn software_registry(
142 mut self,
143 options: crate::software_registry::SoftwareRegistryOptions,
144 ) -> Self {
145 self.config.software_registry = Some(options);
146 self
147 }
148
149 pub fn loopback_exempt_ports(mut self, ports: Vec<u16>) -> Self {
150 self.config.loopback_exempt_ports = ports;
151 self
152 }
153
154 pub fn allowed_node_builtins(mut self, builtins: Vec<String>) -> Self {
155 self.config.allowed_node_builtins = Some(builtins);
156 self
157 }
158
159 pub fn user(mut self, user: VmUserConfig) -> Self {
160 self.config.user = Some(user);
161 self
162 }
163
164 pub fn environment(mut self, environment: BTreeMap<String, String>) -> Self {
165 self.config.environment = Some(environment);
166 self
167 }
168
169 pub fn high_resolution_time(mut self, enabled: bool) -> Self {
170 self.config.high_resolution_time = Some(enabled);
171 self
172 }
173
174 pub fn root_filesystem(mut self, root: RootFilesystemConfig) -> Self {
175 self.config.root_filesystem = root;
176 self
177 }
178
179 pub fn mounts(mut self, mounts: Vec<MountConfig>) -> Self {
180 self.config.mounts = mounts;
181 self
182 }
183
184 pub fn schedule_driver(mut self, driver: Arc<dyn ScheduleDriver>) -> Self {
185 self.config.schedule_driver = Some(driver);
186 self
187 }
188
189 pub fn host_functions(mut self, host_functions: HostFunctionCollections) -> Self {
190 self.config.host_functions = host_functions;
191 self
192 }
193
194 pub fn sidecar_js_bridge_callback(mut self, callback: SidecarJsBridgeCallback) -> Self {
195 self.config.sidecar_js_bridge_callback = Some(callback);
196 self
197 }
198
199 pub fn permissions(mut self, permissions: Permissions) -> Self {
200 self.config.permissions = Some(permissions);
201 self
202 }
203
204 pub fn limits(mut self, limits: AgentOsLimits) -> Self {
205 self.config.limits = Some(limits);
206 self
207 }
208
209 pub fn sidecar(mut self, sidecar: AgentOsSidecarConfig) -> Self {
210 self.config.sidecar = Some(sidecar);
211 self
212 }
213
214 pub fn sidecar_binary_path(mut self, path: impl Into<String>) -> Self {
215 self.config.sidecar_binary_path = Some(path.into());
216 self
217 }
218
219 pub fn build(self) -> AgentOsConfig {
220 self.config
221 }
222}
223
224impl AgentOsConfig {
225 pub fn validate(&self) -> Result<(), crate::ClientError> {
231 crate::agent_os::validate_config(self)
232 }
233}
234
235#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
239#[serde(rename_all = "kebab-case")]
240pub enum SoftwareKind {
241 #[default]
244 WasmCommands,
245 HostFunction,
247}
248
249#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub struct SoftwareInput {
253 pub package: String,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub version: Option<String>,
256 #[serde(default)]
258 pub kind: SoftwareKind,
259}
260
261#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266pub struct PackageRef {
267 #[serde(rename = "packagePath")]
270 pub path: String,
271}
272
273pub type HostFunctionCallback = Arc<
277 dyn Fn(
278 serde_json::Value,
279 ) -> futures::future::BoxFuture<'static, Result<serde_json::Value, String>>
280 + Send
281 + Sync,
282>;
283
284#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct SidecarJsBridgeCall {
287 pub call_id: String,
288 pub mount_id: String,
289 pub operation: String,
290 pub args: serde_json::Value,
291}
292
293pub type SidecarJsBridgeCallback = Arc<
299 dyn Fn(
300 SidecarJsBridgeCall,
301 )
302 -> futures::future::BoxFuture<'static, Result<Option<serde_json::Value>, String>>
303 + Send
304 + Sync,
305>;
306
307#[derive(Clone)]
310pub struct HostFunction {
311 pub input_schema: serde_json::Value,
313 pub timeout_ms: Option<u64>,
314 pub execute: HostFunctionCallback,
316}
317
318pub type HostFunctionCollection = BTreeMap<String, HostFunction>;
321
322pub type HostFunctionCollections = BTreeMap<String, HostFunctionCollection>;
328
329#[derive(Clone)]
331pub struct ResolvedHostFunction {
332 pub name: String,
334 pub description: String,
336 pub input_schema: serde_json::Value,
337 pub timeout_ms: Option<u64>,
338 pub execute: HostFunctionCallback,
339}
340
341#[derive(Clone)]
345pub struct ResolvedHostFunctions {
346 pub name: String,
348 pub functions: Vec<ResolvedHostFunction>,
349}
350
351pub fn host_function_command_name(key: &str) -> String {
356 let mut out = String::with_capacity(key.len() + 4);
357 let chars: Vec<char> = key.chars().collect();
358 for (index, character) in chars.iter().enumerate() {
359 if character.is_ascii_uppercase() && index > 0 {
360 let previous = chars[index - 1];
361 let next_is_lower = chars
362 .get(index + 1)
363 .is_some_and(|next| next.is_ascii_lowercase());
364 if previous.is_ascii_lowercase()
365 || previous.is_ascii_digit()
366 || (previous.is_ascii_uppercase() && next_is_lower)
367 {
368 out.push('-');
369 }
370 }
371 out.extend(character.to_lowercase());
372 }
373 out
374}
375
376fn is_command_name(name: &str) -> bool {
377 !name.is_empty()
378 && !name.starts_with('-')
379 && !name.ends_with('-')
380 && !name.contains("--")
381 && name.chars().all(|character| {
382 character.is_ascii_lowercase() || character.is_ascii_digit() || character == '-'
383 })
384}
385
386fn to_command_name(kind: &str, key: &str) -> Result<String, String> {
387 let name = host_function_command_name(key);
388 if is_command_name(&name) {
389 Ok(name)
390 } else {
391 Err(format!(
392 "{kind} name \"{key}\" must be alphanumeric, written in camelCase or with single hyphen separators"
393 ))
394 }
395}
396
397fn schema_description(input_schema: &serde_json::Value) -> String {
399 input_schema
400 .get("description")
401 .and_then(serde_json::Value::as_str)
402 .unwrap_or_default()
403 .to_string()
404}
405
406pub fn resolve_host_functions(
410 collections: &HostFunctionCollections,
411) -> Result<Vec<ResolvedHostFunctions>, String> {
412 let mut resolved = Vec::with_capacity(collections.len());
413 let mut seen_collections: BTreeMap<String, String> = BTreeMap::new();
414
415 for (collection_key, collection) in collections {
416 let name = to_command_name("Host function collection", collection_key)?;
417 if let Some(collided) = seen_collections.get(&name) {
418 return Err(format!(
419 "Host function collections \"{collided}\" and \"{collection_key}\" both resolve to the command name \"{name}\""
420 ));
421 }
422 seen_collections.insert(name.clone(), collection_key.clone());
423
424 let mut functions = Vec::with_capacity(collection.len());
425 let mut seen_functions: BTreeMap<String, String> = BTreeMap::new();
426 for (function_key, definition) in collection {
427 let function_name = to_command_name("Host function", function_key)?;
428 if let Some(collided) = seen_functions.get(&function_name) {
429 return Err(format!(
430 "Host functions \"{collided}\" and \"{function_key}\" in collection \"{collection_key}\" both resolve to the command name \"{function_name}\""
431 ));
432 }
433 seen_functions.insert(function_name.clone(), function_key.clone());
434 functions.push(ResolvedHostFunction {
435 name: function_name,
436 description: schema_description(&definition.input_schema),
437 input_schema: definition.input_schema.clone(),
438 timeout_ms: definition.timeout_ms,
439 execute: definition.execute.clone(),
440 });
441 }
442
443 resolved.push(ResolvedHostFunctions { name, functions });
444 }
445
446 Ok(resolved)
447}
448
449#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
456#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
457#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
458#[serde(deny_unknown_fields)]
459pub struct AgentOsLimits {
460 #[serde(
461 default,
462 rename = "agentosPackages",
463 skip_serializing_if = "Option::is_none"
464 )]
465 #[cfg_attr(feature = "contract", ts(optional))]
466 pub agentos_packages: Option<AgentOsPackageLimits>,
467 #[serde(default, skip_serializing_if = "Option::is_none")]
468 #[cfg_attr(feature = "contract", ts(optional))]
469 pub reactor: Option<ReactorLimits>,
470 #[serde(default, skip_serializing_if = "Option::is_none")]
471 pub resources: Option<ResourceLimits>,
472 #[serde(default, skip_serializing_if = "Option::is_none")]
473 pub http: Option<HttpLimits>,
474 #[serde(default, skip_serializing_if = "Option::is_none")]
475 #[cfg_attr(feature = "contract", ts(optional))]
476 pub tls: Option<TlsLimits>,
477 #[serde(
478 default,
479 rename = "hostFunctions",
480 skip_serializing_if = "Option::is_none"
481 )]
482 pub host_functions: Option<HostFunctionLimits>,
483 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub plugins: Option<PluginLimits>,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
486 pub sqlite: Option<SqliteLimits>,
487 #[serde(default, rename = "jsRuntime", skip_serializing_if = "Option::is_none")]
488 pub js_runtime: Option<JsRuntimeLimits>,
489 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub python: Option<PythonLimits>,
491 #[serde(default, skip_serializing_if = "Option::is_none")]
492 pub wasm: Option<WasmLimits>,
493 #[serde(default, skip_serializing_if = "Option::is_none")]
494 #[cfg_attr(feature = "contract", ts(optional))]
495 pub execution: Option<ExecutionLimits>,
496 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub process: Option<ProcessLimits>,
498}
499
500#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
501#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
502#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
503#[serde(rename_all = "camelCase", deny_unknown_fields)]
504pub struct AgentOsPackageLimits {
505 #[serde(default, skip_serializing_if = "Option::is_none")]
506 #[cfg_attr(feature = "contract", ts(optional))]
507 pub max_mounts: Option<u64>,
508}
509
510#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
512#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
513#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
514#[serde(rename_all = "camelCase", deny_unknown_fields)]
515pub struct ReactorLimits {
516 #[serde(default, skip_serializing_if = "Option::is_none")]
517 #[cfg_attr(feature = "contract", ts(optional))]
518 pub max_capabilities: Option<u64>,
519 #[serde(default, skip_serializing_if = "Option::is_none")]
520 #[cfg_attr(feature = "contract", ts(optional))]
521 pub max_ready_handles: Option<u64>,
522 #[serde(default, skip_serializing_if = "Option::is_none")]
523 #[cfg_attr(feature = "contract", ts(optional))]
524 pub max_tasks: Option<u64>,
525 #[serde(default, skip_serializing_if = "Option::is_none")]
526 #[cfg_attr(feature = "contract", ts(optional))]
527 pub work_quantum: Option<u64>,
528 #[serde(default, skip_serializing_if = "Option::is_none")]
529 #[cfg_attr(feature = "contract", ts(optional))]
530 pub byte_quantum: Option<u64>,
531 #[serde(default, skip_serializing_if = "Option::is_none")]
532 #[cfg_attr(feature = "contract", ts(optional))]
533 pub max_handle_commands: Option<u64>,
534 #[serde(default, skip_serializing_if = "Option::is_none")]
535 #[cfg_attr(feature = "contract", ts(optional))]
536 pub max_handle_command_bytes: Option<u64>,
537 #[serde(default, skip_serializing_if = "Option::is_none")]
538 #[cfg_attr(feature = "contract", ts(optional))]
539 pub max_bridge_calls: Option<u64>,
540 #[serde(default, skip_serializing_if = "Option::is_none")]
541 #[cfg_attr(feature = "contract", ts(optional))]
542 pub max_bridge_request_bytes: Option<u64>,
543 #[serde(default, skip_serializing_if = "Option::is_none")]
544 #[cfg_attr(feature = "contract", ts(optional))]
545 pub max_bridge_response_bytes: Option<u64>,
546 #[serde(default, skip_serializing_if = "Option::is_none")]
547 #[cfg_attr(feature = "contract", ts(optional))]
548 pub max_async_completions: Option<u64>,
549 #[serde(default, skip_serializing_if = "Option::is_none")]
550 #[cfg_attr(feature = "contract", ts(optional))]
551 pub max_async_completion_bytes: Option<u64>,
552 #[serde(default, skip_serializing_if = "Option::is_none")]
553 #[cfg_attr(feature = "contract", ts(optional))]
554 pub max_blocking_jobs: Option<u64>,
555 #[serde(default, skip_serializing_if = "Option::is_none")]
556 #[cfg_attr(feature = "contract", ts(optional))]
557 pub max_blocking_bytes: Option<u64>,
558 #[serde(default, skip_serializing_if = "Option::is_none")]
559 #[cfg_attr(feature = "contract", ts(optional))]
560 pub per_handle_operation_quantum: Option<u64>,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
562 #[cfg_attr(feature = "contract", ts(optional))]
563 pub accept_quantum: Option<u64>,
564 #[serde(default, skip_serializing_if = "Option::is_none")]
565 #[cfg_attr(feature = "contract", ts(optional))]
566 pub datagram_quantum: Option<u64>,
567 #[serde(default, skip_serializing_if = "Option::is_none")]
568 #[cfg_attr(feature = "contract", ts(optional))]
569 pub completion_quantum: Option<u64>,
570 #[serde(default, skip_serializing_if = "Option::is_none")]
571 #[cfg_attr(feature = "contract", ts(optional))]
572 pub signal_quantum: Option<u64>,
573 #[serde(default, skip_serializing_if = "Option::is_none")]
574 #[cfg_attr(feature = "contract", ts(optional))]
575 pub shutdown_deadline_ms: Option<u64>,
576 #[serde(default, skip_serializing_if = "Option::is_none")]
577 #[cfg_attr(feature = "contract", ts(optional))]
578 pub operation_deadline_ms: Option<u64>,
579}
580
581#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
582#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
583#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
584#[serde(deny_unknown_fields)]
585pub struct ResourceLimits {
586 #[serde(default, rename = "cpuCount", skip_serializing_if = "Option::is_none")]
587 pub cpu_count: Option<u64>,
588 #[serde(
589 default,
590 rename = "maxProcesses",
591 skip_serializing_if = "Option::is_none"
592 )]
593 pub max_processes: Option<u64>,
594 #[serde(
595 default,
596 rename = "maxOpenFds",
597 skip_serializing_if = "Option::is_none"
598 )]
599 pub max_open_fds: Option<u64>,
600 #[serde(default, rename = "maxPipes", skip_serializing_if = "Option::is_none")]
601 pub max_pipes: Option<u64>,
602 #[serde(default, rename = "maxPtys", skip_serializing_if = "Option::is_none")]
603 pub max_ptys: Option<u64>,
604 #[serde(
605 default,
606 rename = "maxSockets",
607 skip_serializing_if = "Option::is_none"
608 )]
609 pub max_sockets: Option<u64>,
610 #[serde(
611 default,
612 rename = "maxConnections",
613 skip_serializing_if = "Option::is_none"
614 )]
615 pub max_connections: Option<u64>,
616 #[serde(
617 default,
618 rename = "maxSocketBufferedBytes",
619 skip_serializing_if = "Option::is_none"
620 )]
621 pub max_socket_buffered_bytes: Option<u64>,
622 #[serde(
623 default,
624 rename = "maxSocketDatagramQueueLen",
625 skip_serializing_if = "Option::is_none"
626 )]
627 pub max_socket_datagram_queue_len: Option<u64>,
628 #[serde(
629 default,
630 rename = "maxFilesystemBytes",
631 skip_serializing_if = "Option::is_none"
632 )]
633 pub max_filesystem_bytes: Option<u64>,
634 #[serde(
635 default,
636 rename = "maxInodeCount",
637 skip_serializing_if = "Option::is_none"
638 )]
639 pub max_inode_count: Option<u64>,
640 #[serde(
641 default,
642 rename = "maxBlockingReadMs",
643 skip_serializing_if = "Option::is_none"
644 )]
645 pub max_blocking_read_ms: Option<u64>,
646 #[serde(
647 default,
648 rename = "maxPreadBytes",
649 skip_serializing_if = "Option::is_none"
650 )]
651 pub max_pread_bytes: Option<u64>,
652 #[serde(
653 default,
654 rename = "maxFdWriteBytes",
655 skip_serializing_if = "Option::is_none"
656 )]
657 pub max_fd_write_bytes: Option<u64>,
658 #[serde(
659 default,
660 rename = "maxProcessArgvBytes",
661 skip_serializing_if = "Option::is_none"
662 )]
663 pub max_process_argv_bytes: Option<u64>,
664 #[serde(
665 default,
666 rename = "maxProcessEnvBytes",
667 skip_serializing_if = "Option::is_none"
668 )]
669 pub max_process_env_bytes: Option<u64>,
670 #[serde(
671 default,
672 rename = "maxReaddirEntries",
673 skip_serializing_if = "Option::is_none"
674 )]
675 pub max_readdir_entries: Option<u64>,
676 #[serde(
677 default,
678 rename = "maxWasmFuel",
679 skip_serializing_if = "Option::is_none"
680 )]
681 pub max_wasm_fuel: Option<u64>,
682 #[serde(
683 default,
684 rename = "maxWasmMemoryBytes",
685 skip_serializing_if = "Option::is_none"
686 )]
687 pub max_wasm_memory_bytes: Option<u64>,
688 #[serde(
689 default,
690 rename = "maxWasmStackBytes",
691 skip_serializing_if = "Option::is_none"
692 )]
693 pub max_wasm_stack_bytes: Option<u64>,
694}
695
696#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
697#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
698#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
699#[serde(deny_unknown_fields)]
700pub struct HttpLimits {
701 #[serde(
702 default,
703 rename = "maxFetchResponseBytes",
704 skip_serializing_if = "Option::is_none"
705 )]
706 pub max_fetch_response_bytes: Option<u64>,
707}
708
709#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
711#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
712#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
713#[serde(rename_all = "camelCase", deny_unknown_fields)]
714pub struct TlsLimits {
715 #[serde(default, skip_serializing_if = "Option::is_none")]
716 #[cfg_attr(feature = "contract", ts(optional))]
717 pub max_buffered_bytes: Option<u64>,
718}
719
720#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
722#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
723#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
724#[serde(rename_all = "camelCase", deny_unknown_fields)]
725pub struct ExecutionLimits {
726 #[serde(default, skip_serializing_if = "Option::is_none")]
727 #[cfg_attr(feature = "contract", ts(optional))]
728 pub completed_ttl_ms: Option<u64>,
729 #[serde(default, skip_serializing_if = "Option::is_none")]
730 #[cfg_attr(feature = "contract", ts(optional))]
731 pub max_completed_executions: Option<u64>,
732 #[serde(default, skip_serializing_if = "Option::is_none")]
733 #[cfg_attr(feature = "contract", ts(optional))]
734 pub live_execution_warning_threshold: Option<u64>,
735}
736
737#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
738#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
739#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
740#[serde(deny_unknown_fields)]
741pub struct HostFunctionLimits {
742 #[serde(
743 default,
744 rename = "defaultTimeoutMs",
745 skip_serializing_if = "Option::is_none"
746 )]
747 pub default_timeout_ms: Option<u64>,
748 #[serde(
749 default,
750 rename = "maxTimeoutMs",
751 skip_serializing_if = "Option::is_none"
752 )]
753 pub max_timeout_ms: Option<u64>,
754 #[serde(
755 default,
756 rename = "maxRegisteredCollections",
757 skip_serializing_if = "Option::is_none"
758 )]
759 pub max_registered_collections: Option<u64>,
760 #[serde(
761 default,
762 rename = "maxRegisteredFunctionsPerVm",
763 skip_serializing_if = "Option::is_none"
764 )]
765 pub max_registered_functions_per_vm: Option<u64>,
766 #[serde(
767 default,
768 rename = "maxFunctionsPerCollection",
769 skip_serializing_if = "Option::is_none"
770 )]
771 pub max_functions_per_collection: Option<u64>,
772 #[serde(
773 default,
774 rename = "maxSchemaBytes",
775 skip_serializing_if = "Option::is_none"
776 )]
777 pub max_schema_bytes: Option<u64>,
778 #[serde(
779 default,
780 rename = "maxExamplesPerFunction",
781 skip_serializing_if = "Option::is_none"
782 )]
783 pub max_examples_per_function: Option<u64>,
784 #[serde(
785 default,
786 rename = "maxExampleInputBytes",
787 skip_serializing_if = "Option::is_none"
788 )]
789 pub max_example_input_bytes: Option<u64>,
790}
791
792#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
793#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
794#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
795#[serde(deny_unknown_fields)]
796pub struct PluginLimits {
797 #[serde(
798 default,
799 rename = "maxPersistedManifestBytes",
800 skip_serializing_if = "Option::is_none"
801 )]
802 pub max_persisted_manifest_bytes: Option<u64>,
803 #[serde(
804 default,
805 rename = "maxPersistedManifestFileBytes",
806 skip_serializing_if = "Option::is_none"
807 )]
808 pub max_persisted_manifest_file_bytes: Option<u64>,
809}
810
811#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
812#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
813#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
814#[serde(deny_unknown_fields)]
815pub struct SqliteLimits {
816 #[serde(
817 default,
818 rename = "maxResultBytes",
819 skip_serializing_if = "Option::is_none"
820 )]
821 pub max_result_bytes: Option<u64>,
822}
823
824#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
825#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
826#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
827#[serde(deny_unknown_fields)]
828pub struct JsRuntimeLimits {
829 #[serde(
830 default,
831 rename = "v8HeapLimitMb",
832 skip_serializing_if = "Option::is_none"
833 )]
834 pub v8_heap_limit_mb: Option<u64>,
835 #[serde(
836 default,
837 rename = "syncRpcWaitTimeoutMs",
838 skip_serializing_if = "Option::is_none"
839 )]
840 pub sync_rpc_wait_timeout_ms: Option<u64>,
841 #[serde(
842 default,
843 rename = "cpuTimeLimitMs",
844 skip_serializing_if = "Option::is_none"
845 )]
846 pub cpu_time_limit_ms: Option<u64>,
847 #[serde(
848 default,
849 rename = "wallClockLimitMs",
850 skip_serializing_if = "Option::is_none"
851 )]
852 pub wall_clock_limit_ms: Option<u64>,
853 #[serde(
854 default,
855 rename = "importCacheMaterializeTimeoutMs",
856 skip_serializing_if = "Option::is_none"
857 )]
858 pub import_cache_materialize_timeout_ms: Option<u64>,
859 #[serde(
860 default,
861 rename = "capturedOutputLimitBytes",
862 skip_serializing_if = "Option::is_none"
863 )]
864 pub captured_output_limit_bytes: Option<u64>,
865 #[serde(
866 default,
867 rename = "stdinBufferLimitBytes",
868 skip_serializing_if = "Option::is_none"
869 )]
870 pub stdin_buffer_limit_bytes: Option<u64>,
871 #[serde(
872 default,
873 rename = "eventPayloadLimitBytes",
874 skip_serializing_if = "Option::is_none"
875 )]
876 pub event_payload_limit_bytes: Option<u64>,
877 #[serde(
878 default,
879 rename = "v8IpcMaxFrameBytes",
880 skip_serializing_if = "Option::is_none"
881 )]
882 pub v8_ipc_max_frame_bytes: Option<u64>,
883}
884
885#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
886#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
887#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
888#[serde(deny_unknown_fields)]
889pub struct PythonLimits {
890 #[serde(
891 default,
892 rename = "outputBufferMaxBytes",
893 skip_serializing_if = "Option::is_none"
894 )]
895 pub output_buffer_max_bytes: Option<u64>,
896 #[serde(
897 default,
898 rename = "executionTimeoutMs",
899 skip_serializing_if = "Option::is_none"
900 )]
901 pub execution_timeout_ms: Option<u64>,
902 #[serde(
903 default,
904 rename = "maxOldSpaceMb",
905 skip_serializing_if = "Option::is_none"
906 )]
907 pub max_old_space_mb: Option<u64>,
908 #[serde(
909 default,
910 rename = "vfsRpcTimeoutMs",
911 skip_serializing_if = "Option::is_none"
912 )]
913 pub vfs_rpc_timeout_ms: Option<u64>,
914}
915
916#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
917#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
918#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
919#[serde(deny_unknown_fields)]
920pub struct WasmLimits {
921 #[serde(
922 default,
923 rename = "maxModuleFileBytes",
924 skip_serializing_if = "Option::is_none"
925 )]
926 pub max_module_file_bytes: Option<u64>,
927 #[serde(
928 default,
929 rename = "capturedOutputLimitBytes",
930 skip_serializing_if = "Option::is_none"
931 )]
932 pub captured_output_limit_bytes: Option<u64>,
933 #[serde(
934 default,
935 rename = "syncReadLimitBytes",
936 skip_serializing_if = "Option::is_none"
937 )]
938 pub sync_read_limit_bytes: Option<u64>,
939 #[serde(
940 default,
941 rename = "prewarmTimeoutMs",
942 skip_serializing_if = "Option::is_none"
943 )]
944 pub prewarm_timeout_ms: Option<u64>,
945 #[serde(
946 default,
947 rename = "runnerHeapLimitMb",
948 skip_serializing_if = "Option::is_none"
949 )]
950 pub runner_heap_limit_mb: Option<u64>,
951 #[serde(
952 default,
953 rename = "runnerCpuTimeLimitMs",
954 skip_serializing_if = "Option::is_none"
955 )]
956 pub runner_cpu_time_limit_ms: Option<u64>,
957}
958
959#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
960#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
961#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
962#[serde(deny_unknown_fields)]
963pub struct ProcessLimits {
964 #[serde(
965 default,
966 rename = "maxSpawnFileActions",
967 skip_serializing_if = "Option::is_none"
968 )]
969 pub max_spawn_file_actions: Option<u64>,
970 #[serde(
971 default,
972 rename = "maxSpawnFileActionBytes",
973 skip_serializing_if = "Option::is_none"
974 )]
975 pub max_spawn_file_action_bytes: Option<u64>,
976 #[serde(
977 default,
978 rename = "pendingStdinBytes",
979 skip_serializing_if = "Option::is_none"
980 )]
981 pub pending_stdin_bytes: Option<u64>,
982 #[serde(
983 default,
984 rename = "pendingEventCount",
985 skip_serializing_if = "Option::is_none"
986 )]
987 pub pending_event_count: Option<u64>,
988 #[serde(
989 default,
990 rename = "pendingEventBytes",
991 skip_serializing_if = "Option::is_none"
992 )]
993 pub pending_event_bytes: Option<u64>,
994}
995
996#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1002#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
1003#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1004#[serde(deny_unknown_fields)]
1005pub struct Permissions {
1006 #[serde(default, skip_serializing_if = "Option::is_none")]
1007 pub fs: Option<FsPermissions>,
1008 #[serde(default, skip_serializing_if = "Option::is_none")]
1009 pub network: Option<PatternPermissions>,
1010 #[serde(
1011 default,
1012 rename = "childProcess",
1013 skip_serializing_if = "Option::is_none"
1014 )]
1015 pub child_process: Option<PatternPermissions>,
1016 #[serde(default, skip_serializing_if = "Option::is_none")]
1017 pub process: Option<PatternPermissions>,
1018 #[serde(default, skip_serializing_if = "Option::is_none")]
1019 pub env: Option<PatternPermissions>,
1020 #[serde(
1021 default,
1022 rename = "hostFunction",
1023 skip_serializing_if = "Option::is_none"
1024 )]
1025 pub host_function: Option<PatternPermissions>,
1026}
1027
1028#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1030#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1031#[serde(rename_all = "lowercase")]
1032pub enum PermissionMode {
1033 Allow,
1034 Deny,
1035}
1036
1037#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1039#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1040#[serde(untagged)]
1041pub enum FsPermissions {
1042 Mode(PermissionMode),
1043 Rules(RulePermissions<FsPermissionRule>),
1044}
1045
1046#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1048#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1049#[serde(untagged)]
1050pub enum PatternPermissions {
1051 Mode(PermissionMode),
1052 Rules(RulePermissions<PatternPermissionRule>),
1053}
1054
1055#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1057#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
1058#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1059#[serde(deny_unknown_fields)]
1060pub struct RulePermissions<T> {
1061 #[serde(default, skip_serializing_if = "Option::is_none")]
1062 pub default: Option<PermissionMode>,
1063 pub rules: Vec<T>,
1064}
1065
1066#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1068#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
1069#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1070#[serde(deny_unknown_fields)]
1071pub struct FsPermissionRule {
1072 pub mode: PermissionMode,
1073 #[serde(default, skip_serializing_if = "Option::is_none")]
1074 pub operations: Option<Vec<String>>,
1075 #[serde(default, skip_serializing_if = "Option::is_none")]
1076 pub paths: Option<Vec<String>>,
1077}
1078
1079#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1081#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
1082#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1083#[serde(deny_unknown_fields)]
1084pub struct PatternPermissionRule {
1085 pub mode: PermissionMode,
1086 #[serde(default, skip_serializing_if = "Option::is_none")]
1087 pub operations: Option<Vec<String>>,
1088 #[serde(default, skip_serializing_if = "Option::is_none")]
1089 pub patterns: Option<Vec<String>>,
1090}
1091
1092#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1098#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1099pub struct RootFilesystemConfig {
1100 #[serde(default, rename = "type")]
1101 pub kind: RootFilesystemKind,
1102 #[serde(default, skip_serializing_if = "Option::is_none")]
1103 pub mode: Option<RootFilesystemMode>,
1104 #[serde(
1105 default,
1106 rename = "nativePlugin",
1107 skip_serializing_if = "Option::is_none"
1108 )]
1109 pub native_plugin: Option<MountPlugin>,
1110 #[serde(default, rename = "disableDefaultBaseLayer")]
1111 pub disable_default_base_layer: bool,
1112 #[serde(default)]
1113 pub lowers: Vec<RootLowerInput>,
1114}
1115
1116impl Default for RootFilesystemConfig {
1117 fn default() -> Self {
1118 Self {
1119 kind: RootFilesystemKind::Overlay,
1120 mode: None,
1121 native_plugin: None,
1122 disable_default_base_layer: false,
1123 lowers: Vec::new(),
1124 }
1125 }
1126}
1127
1128#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1130#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1131#[serde(rename_all = "lowercase")]
1132pub enum RootFilesystemKind {
1133 #[default]
1134 Overlay,
1135 Native,
1136}
1137
1138#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1141#[serde(rename_all = "kebab-case")]
1142pub enum RootFilesystemMode {
1143 Ephemeral,
1144 ReadOnly,
1145}
1146
1147#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1150#[serde(tag = "kind", rename_all = "kebab-case")]
1151pub enum RootLowerInput {
1152 BundledBaseFilesystem,
1154 #[serde(untagged)]
1156 SnapshotExport(crate::fs::RootSnapshotExport),
1157}
1158
1159pub enum MountConfig {
1165 Plain {
1167 path: String,
1168 driver: Arc<dyn VirtualFileSystem>,
1169 guest_source: Option<String>,
1170 guest_fstype: Option<String>,
1171 read_only: bool,
1172 },
1173 Native {
1175 path: String,
1176 plugin: MountPlugin,
1177 guest_source: Option<String>,
1178 guest_fstype: Option<String>,
1179 read_only: bool,
1180 },
1181 Overlay {
1183 path: String,
1184 filesystem: OverlayMountConfig,
1185 },
1186}
1187
1188#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1191pub struct MountPlugin {
1192 pub id: String,
1193 #[serde(default, skip_serializing_if = "Option::is_none")]
1194 pub config: Option<serde_json::Value>,
1195}
1196
1197pub fn node_modules_mount(host_node_modules_dir: impl Into<String>) -> MountConfig {
1205 MountConfig::Native {
1206 path: "/root/node_modules".to_string(),
1207 plugin: MountPlugin {
1208 id: "host_dir".to_string(),
1209 config: Some(serde_json::json!({
1210 "hostPath": host_node_modules_dir.into(),
1211 "readOnly": true,
1212 })),
1213 },
1214 guest_source: Some(String::from("host_dir")),
1215 guest_fstype: Some(String::from("host_dir")),
1216 read_only: true,
1217 }
1218}
1219
1220#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1223pub struct OverlayMountConfig {
1224 #[serde(rename = "type")]
1225 pub kind: String,
1226 pub store: serde_json::Value,
1227 #[serde(default, skip_serializing_if = "Option::is_none")]
1228 pub mode: Option<RootFilesystemMode>,
1229 pub lowers: Vec<RootLowerInput>,
1230}
1231
1232pub enum AgentOsSidecarConfig {
1238 Shared { pool: Option<String> },
1240 Explicit {
1242 handle: Arc<crate::sidecar::AgentOsSidecar>,
1243 },
1244}
1245
1246pub type ScheduleCallback = Arc<dyn Fn() -> futures::future::BoxFuture<'static, ()> + Send + Sync>;
1256
1257#[derive(Clone)]
1260pub struct ScheduleEntry {
1261 pub id: String,
1263 pub schedule: String,
1265 pub callback: ScheduleCallback,
1267}
1268
1269pub trait ScheduleDriver: Send + Sync {
1275 fn schedule(&self, entry: ScheduleEntry) -> ScheduleHandle;
1278
1279 fn cancel(&self, handle: &ScheduleHandle);
1281
1282 fn dispose(&self);
1284}
1285
1286#[derive(Clone)]
1289pub struct ScheduleHandle {
1290 pub id: String,
1291}
1292
1293#[derive(Default)]
1300pub struct TimerScheduleDriver {
1301 timers: Arc<scc::HashMap<String, tokio_util::sync::CancellationToken>>,
1302}
1303
1304impl TimerScheduleDriver {
1305 pub fn new() -> Self {
1306 Self {
1307 timers: Arc::new(scc::HashMap::new()),
1308 }
1309 }
1310
1311 fn schedule_next(
1315 timers: Arc<scc::HashMap<String, tokio_util::sync::CancellationToken>>,
1316 entry: ScheduleEntry,
1317 cancel: tokio_util::sync::CancellationToken,
1318 ) {
1319 let now = chrono::Utc::now();
1320 let parsed = match crate::cron::parse_schedule(&entry.schedule) {
1321 Ok(parsed) => parsed,
1322 Err(_) => {
1323 let _ = timers.remove(&entry.id);
1324 return;
1325 }
1326 };
1327 let is_cron = parsed.is_cron();
1328 let next = match crate::cron::resolve_next_run(&parsed, now) {
1329 Some(next) => next,
1330 None => {
1331 let _ = timers.remove(&entry.id);
1333 return;
1334 }
1335 };
1336
1337 let delay = (next - now).to_std().unwrap_or(std::time::Duration::ZERO);
1338
1339 tokio::spawn(async move {
1340 tokio::select! {
1341 _ = cancel.cancelled() => {
1342 return;
1343 }
1344 _ = tokio::time::sleep(delay) => {}
1345 }
1346 if cancel.is_cancelled() {
1347 return;
1348 }
1349 (entry.callback)().await;
1351
1352 if is_cron && timers.contains(&entry.id) {
1353 Self::schedule_next(Arc::clone(&timers), entry, cancel);
1354 } else {
1355 let _ = timers.remove(&entry.id);
1356 }
1357 });
1358 }
1359}
1360
1361impl ScheduleDriver for TimerScheduleDriver {
1362 fn schedule(&self, entry: ScheduleEntry) -> ScheduleHandle {
1363 let id = entry.id.clone();
1364 let cancel = tokio_util::sync::CancellationToken::new();
1365 if let Some((_, old)) = self.timers.remove(&id) {
1367 old.cancel();
1368 }
1369 let _ = self.timers.insert(id.clone(), cancel.clone());
1370
1371 Self::schedule_next(Arc::clone(&self.timers), entry, cancel);
1372
1373 ScheduleHandle { id }
1374 }
1375
1376 fn cancel(&self, handle: &ScheduleHandle) {
1377 if let Some((_, cancel)) = self.timers.remove(&handle.id) {
1378 cancel.cancel();
1379 }
1380 }
1381
1382 fn dispose(&self) {
1383 self.timers.scan(|_, cancel| cancel.cancel());
1384 self.timers.clear();
1385 }
1386}