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 loopback_exempt_ports: Vec<u16>,
62 pub allowed_node_builtins: Option<Vec<String>>,
64 pub high_resolution_time: Option<bool>,
67 pub root_filesystem: RootFilesystemConfig,
69 pub mounts: Vec<MountConfig>,
71 pub schedule_driver: Option<Arc<dyn ScheduleDriver>>,
73 pub host_functions: HostFunctionCollections,
75 pub sidecar_js_bridge_callback: Option<SidecarJsBridgeCallback>,
77 pub permissions: Option<Permissions>,
79 pub limits: Option<AgentOsLimits>,
81 pub sidecar: Option<AgentOsSidecarConfig>,
83 pub sidecar_binary_path: Option<String>,
88}
89
90#[derive(Default)]
92pub struct AgentOsConfigBuilder {
93 config: AgentOsConfig,
94}
95
96impl AgentOsConfigBuilder {
97 pub fn new() -> Self {
98 Self::default()
99 }
100
101 pub fn database(mut self, database: agentos_vm_config::VmSqliteDescriptor) -> Self {
102 self.config.database = Some(database);
103 self
104 }
105
106 pub fn sidecar_sqlite_callback(mut self, callback: SidecarSqliteCallback) -> Self {
107 self.config.sidecar_sqlite_callback = Some(callback);
108 self
109 }
110
111 pub fn packages(mut self, packages: Vec<PackageRef>) -> Self {
112 self.config.packages = packages;
113 self
114 }
115
116 pub fn packages_mount_at(mut self, mount_at: impl Into<String>) -> Self {
117 self.config.packages_mount_at = Some(mount_at.into());
118 self
119 }
120
121 pub fn package_resolver(mut self, options: crate::software::PackageResolverOptions) -> Self {
122 self.config.package_resolver = Some(options);
123 self
124 }
125
126 pub fn loopback_exempt_ports(mut self, ports: Vec<u16>) -> Self {
127 self.config.loopback_exempt_ports = ports;
128 self
129 }
130
131 pub fn allowed_node_builtins(mut self, builtins: Vec<String>) -> Self {
132 self.config.allowed_node_builtins = Some(builtins);
133 self
134 }
135
136 pub fn user(mut self, user: VmUserConfig) -> Self {
137 self.config.user = Some(user);
138 self
139 }
140
141 pub fn environment(mut self, environment: BTreeMap<String, String>) -> Self {
142 self.config.environment = Some(environment);
143 self
144 }
145
146 pub fn high_resolution_time(mut self, enabled: bool) -> Self {
147 self.config.high_resolution_time = Some(enabled);
148 self
149 }
150
151 pub fn root_filesystem(mut self, root: RootFilesystemConfig) -> Self {
152 self.config.root_filesystem = root;
153 self
154 }
155
156 pub fn mounts(mut self, mounts: Vec<MountConfig>) -> Self {
157 self.config.mounts = mounts;
158 self
159 }
160
161 pub fn schedule_driver(mut self, driver: Arc<dyn ScheduleDriver>) -> Self {
162 self.config.schedule_driver = Some(driver);
163 self
164 }
165
166 pub fn host_functions(mut self, host_functions: HostFunctionCollections) -> Self {
167 self.config.host_functions = host_functions;
168 self
169 }
170
171 pub fn sidecar_js_bridge_callback(mut self, callback: SidecarJsBridgeCallback) -> Self {
172 self.config.sidecar_js_bridge_callback = Some(callback);
173 self
174 }
175
176 pub fn permissions(mut self, permissions: Permissions) -> Self {
177 self.config.permissions = Some(permissions);
178 self
179 }
180
181 pub fn limits(mut self, limits: AgentOsLimits) -> Self {
182 self.config.limits = Some(limits);
183 self
184 }
185
186 pub fn sidecar(mut self, sidecar: AgentOsSidecarConfig) -> Self {
187 self.config.sidecar = Some(sidecar);
188 self
189 }
190
191 pub fn sidecar_binary_path(mut self, path: impl Into<String>) -> Self {
192 self.config.sidecar_binary_path = Some(path.into());
193 self
194 }
195
196 pub fn build(self) -> AgentOsConfig {
197 self.config
198 }
199}
200
201impl AgentOsConfig {
202 pub fn validate(&self) -> Result<(), crate::ClientError> {
208 crate::agent_os::validate_config(self)
209 }
210}
211
212#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
216#[serde(rename_all = "kebab-case")]
217pub enum SoftwareKind {
218 #[default]
221 WasmCommands,
222 HostFunction,
224}
225
226#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
228#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229pub struct SoftwareInput {
230 pub package: String,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub version: Option<String>,
233 #[serde(default)]
235 pub kind: SoftwareKind,
236}
237
238#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
243pub struct PackageRef {
244 #[serde(rename = "packagePath")]
247 pub path: String,
248}
249
250pub type HostFunctionCallback = Arc<
254 dyn Fn(
255 serde_json::Value,
256 ) -> futures::future::BoxFuture<'static, Result<serde_json::Value, String>>
257 + Send
258 + Sync,
259>;
260
261#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct SidecarJsBridgeCall {
264 pub call_id: String,
265 pub mount_id: String,
266 pub operation: String,
267 pub args: serde_json::Value,
268}
269
270pub type SidecarJsBridgeCallback = Arc<
276 dyn Fn(
277 SidecarJsBridgeCall,
278 )
279 -> futures::future::BoxFuture<'static, Result<Option<serde_json::Value>, String>>
280 + Send
281 + Sync,
282>;
283
284#[derive(Clone)]
287pub struct HostFunction {
288 pub input_schema: serde_json::Value,
290 pub timeout_ms: Option<u64>,
291 pub execute: HostFunctionCallback,
293}
294
295pub type HostFunctionCollection = BTreeMap<String, HostFunction>;
298
299pub type HostFunctionCollections = BTreeMap<String, HostFunctionCollection>;
305
306#[derive(Clone)]
308pub struct ResolvedHostFunction {
309 pub name: String,
311 pub description: String,
313 pub input_schema: serde_json::Value,
314 pub timeout_ms: Option<u64>,
315 pub execute: HostFunctionCallback,
316}
317
318#[derive(Clone)]
322pub struct ResolvedHostFunctions {
323 pub name: String,
325 pub functions: Vec<ResolvedHostFunction>,
326}
327
328pub fn host_function_command_name(key: &str) -> String {
333 let mut out = String::with_capacity(key.len() + 4);
334 let chars: Vec<char> = key.chars().collect();
335 for (index, character) in chars.iter().enumerate() {
336 if character.is_ascii_uppercase() && index > 0 {
337 let previous = chars[index - 1];
338 let next_is_lower = chars
339 .get(index + 1)
340 .is_some_and(|next| next.is_ascii_lowercase());
341 if previous.is_ascii_lowercase()
342 || previous.is_ascii_digit()
343 || (previous.is_ascii_uppercase() && next_is_lower)
344 {
345 out.push('-');
346 }
347 }
348 out.extend(character.to_lowercase());
349 }
350 out
351}
352
353fn is_command_name(name: &str) -> bool {
354 !name.is_empty()
355 && !name.starts_with('-')
356 && !name.ends_with('-')
357 && !name.contains("--")
358 && name.chars().all(|character| {
359 character.is_ascii_lowercase() || character.is_ascii_digit() || character == '-'
360 })
361}
362
363fn to_command_name(kind: &str, key: &str) -> Result<String, String> {
364 let name = host_function_command_name(key);
365 if is_command_name(&name) {
366 Ok(name)
367 } else {
368 Err(format!(
369 "{kind} name \"{key}\" must be alphanumeric, written in camelCase or with single hyphen separators"
370 ))
371 }
372}
373
374fn schema_description(input_schema: &serde_json::Value) -> String {
376 input_schema
377 .get("description")
378 .and_then(serde_json::Value::as_str)
379 .unwrap_or_default()
380 .to_string()
381}
382
383pub fn resolve_host_functions(
387 collections: &HostFunctionCollections,
388) -> Result<Vec<ResolvedHostFunctions>, String> {
389 let mut resolved = Vec::with_capacity(collections.len());
390 let mut seen_collections: BTreeMap<String, String> = BTreeMap::new();
391
392 for (collection_key, collection) in collections {
393 let name = to_command_name("Host function collection", collection_key)?;
394 if let Some(collided) = seen_collections.get(&name) {
395 return Err(format!(
396 "Host function collections \"{collided}\" and \"{collection_key}\" both resolve to the command name \"{name}\""
397 ));
398 }
399 seen_collections.insert(name.clone(), collection_key.clone());
400
401 let mut functions = Vec::with_capacity(collection.len());
402 let mut seen_functions: BTreeMap<String, String> = BTreeMap::new();
403 for (function_key, definition) in collection {
404 let function_name = to_command_name("Host function", function_key)?;
405 if let Some(collided) = seen_functions.get(&function_name) {
406 return Err(format!(
407 "Host functions \"{collided}\" and \"{function_key}\" in collection \"{collection_key}\" both resolve to the command name \"{function_name}\""
408 ));
409 }
410 seen_functions.insert(function_name.clone(), function_key.clone());
411 functions.push(ResolvedHostFunction {
412 name: function_name,
413 description: schema_description(&definition.input_schema),
414 input_schema: definition.input_schema.clone(),
415 timeout_ms: definition.timeout_ms,
416 execute: definition.execute.clone(),
417 });
418 }
419
420 resolved.push(ResolvedHostFunctions { name, functions });
421 }
422
423 Ok(resolved)
424}
425
426#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
433#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
434#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
435#[serde(deny_unknown_fields)]
436pub struct AgentOsLimits {
437 #[serde(
438 default,
439 rename = "agentosPackages",
440 skip_serializing_if = "Option::is_none"
441 )]
442 #[cfg_attr(feature = "contract", ts(optional))]
443 pub agentos_packages: Option<AgentOsPackageLimits>,
444 #[serde(default, skip_serializing_if = "Option::is_none")]
445 pub resources: Option<ResourceLimits>,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
447 pub http: Option<HttpLimits>,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
449 #[cfg_attr(feature = "contract", ts(optional))]
450 pub tls: Option<TlsLimits>,
451 #[serde(
452 default,
453 rename = "hostFunctions",
454 skip_serializing_if = "Option::is_none"
455 )]
456 pub host_functions: Option<HostFunctionLimits>,
457 #[serde(default, skip_serializing_if = "Option::is_none")]
458 pub plugins: Option<PluginLimits>,
459 #[serde(default, skip_serializing_if = "Option::is_none")]
460 pub sqlite: Option<SqliteLimits>,
461 #[serde(default, rename = "jsRuntime", skip_serializing_if = "Option::is_none")]
462 pub js_runtime: Option<JsRuntimeLimits>,
463 #[serde(default, skip_serializing_if = "Option::is_none")]
464 pub python: Option<PythonLimits>,
465 #[serde(default, skip_serializing_if = "Option::is_none")]
466 pub wasm: Option<WasmLimits>,
467 #[serde(default, skip_serializing_if = "Option::is_none")]
468 #[cfg_attr(feature = "contract", ts(optional))]
469 pub execution: Option<ExecutionLimits>,
470 #[serde(default, skip_serializing_if = "Option::is_none")]
471 pub process: Option<ProcessLimits>,
472}
473
474#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
475#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
476#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
477#[serde(rename_all = "camelCase", deny_unknown_fields)]
478pub struct AgentOsPackageLimits {
479 #[serde(default, skip_serializing_if = "Option::is_none")]
480 #[cfg_attr(feature = "contract", ts(optional))]
481 pub max_mounts: Option<u64>,
482}
483
484#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
485#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
486#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
487#[serde(deny_unknown_fields)]
488pub struct ResourceLimits {
489 #[serde(default, rename = "cpuCount", skip_serializing_if = "Option::is_none")]
490 pub cpu_count: Option<u64>,
491 #[serde(
492 default,
493 rename = "maxProcesses",
494 skip_serializing_if = "Option::is_none"
495 )]
496 pub max_processes: Option<u64>,
497 #[serde(
498 default,
499 rename = "maxOpenFds",
500 skip_serializing_if = "Option::is_none"
501 )]
502 pub max_open_fds: Option<u64>,
503 #[serde(default, rename = "maxPipes", skip_serializing_if = "Option::is_none")]
504 pub max_pipes: Option<u64>,
505 #[serde(default, rename = "maxPtys", skip_serializing_if = "Option::is_none")]
506 pub max_ptys: Option<u64>,
507 #[serde(
508 default,
509 rename = "maxSockets",
510 skip_serializing_if = "Option::is_none"
511 )]
512 pub max_sockets: Option<u64>,
513 #[serde(
514 default,
515 rename = "maxConnections",
516 skip_serializing_if = "Option::is_none"
517 )]
518 pub max_connections: Option<u64>,
519 #[serde(
520 default,
521 rename = "maxSocketBufferedBytes",
522 skip_serializing_if = "Option::is_none"
523 )]
524 pub max_socket_buffered_bytes: Option<u64>,
525 #[serde(
526 default,
527 rename = "maxSocketDatagramQueueLen",
528 skip_serializing_if = "Option::is_none"
529 )]
530 pub max_socket_datagram_queue_len: Option<u64>,
531 #[serde(
532 default,
533 rename = "maxFilesystemBytes",
534 skip_serializing_if = "Option::is_none"
535 )]
536 pub max_filesystem_bytes: Option<u64>,
537 #[serde(
538 default,
539 rename = "maxInodeCount",
540 skip_serializing_if = "Option::is_none"
541 )]
542 pub max_inode_count: Option<u64>,
543 #[serde(
544 default,
545 rename = "maxBlockingReadMs",
546 skip_serializing_if = "Option::is_none"
547 )]
548 pub max_blocking_read_ms: Option<u64>,
549 #[serde(
550 default,
551 rename = "maxPreadBytes",
552 skip_serializing_if = "Option::is_none"
553 )]
554 pub max_pread_bytes: Option<u64>,
555 #[serde(
556 default,
557 rename = "maxFdWriteBytes",
558 skip_serializing_if = "Option::is_none"
559 )]
560 pub max_fd_write_bytes: Option<u64>,
561 #[serde(
562 default,
563 rename = "maxProcessArgvBytes",
564 skip_serializing_if = "Option::is_none"
565 )]
566 pub max_process_argv_bytes: Option<u64>,
567 #[serde(
568 default,
569 rename = "maxProcessEnvBytes",
570 skip_serializing_if = "Option::is_none"
571 )]
572 pub max_process_env_bytes: Option<u64>,
573 #[serde(
574 default,
575 rename = "maxReaddirEntries",
576 skip_serializing_if = "Option::is_none"
577 )]
578 pub max_readdir_entries: Option<u64>,
579 #[serde(
580 default,
581 rename = "maxWasmFuel",
582 skip_serializing_if = "Option::is_none"
583 )]
584 pub max_wasm_fuel: Option<u64>,
585 #[serde(
586 default,
587 rename = "maxWasmMemoryBytes",
588 skip_serializing_if = "Option::is_none"
589 )]
590 pub max_wasm_memory_bytes: Option<u64>,
591 #[serde(
592 default,
593 rename = "maxWasmStackBytes",
594 skip_serializing_if = "Option::is_none"
595 )]
596 pub max_wasm_stack_bytes: Option<u64>,
597}
598
599#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
600#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
601#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
602#[serde(deny_unknown_fields)]
603pub struct HttpLimits {
604 #[serde(
605 default,
606 rename = "maxFetchResponseBytes",
607 skip_serializing_if = "Option::is_none"
608 )]
609 pub max_fetch_response_bytes: Option<u64>,
610}
611
612#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
614#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
615#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
616#[serde(rename_all = "camelCase", deny_unknown_fields)]
617pub struct TlsLimits {
618 #[serde(default, skip_serializing_if = "Option::is_none")]
619 #[cfg_attr(feature = "contract", ts(optional))]
620 pub max_buffered_bytes: Option<u64>,
621}
622
623#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
625#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
626#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
627#[serde(rename_all = "camelCase", deny_unknown_fields)]
628pub struct ExecutionLimits {
629 #[serde(default, skip_serializing_if = "Option::is_none")]
630 #[cfg_attr(feature = "contract", ts(optional))]
631 pub completed_ttl_ms: Option<u64>,
632 #[serde(default, skip_serializing_if = "Option::is_none")]
633 #[cfg_attr(feature = "contract", ts(optional))]
634 pub max_completed_executions: Option<u64>,
635 #[serde(default, skip_serializing_if = "Option::is_none")]
636 #[cfg_attr(feature = "contract", ts(optional))]
637 pub live_execution_warning_threshold: Option<u64>,
638}
639
640#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
641#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
642#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
643#[serde(deny_unknown_fields)]
644pub struct HostFunctionLimits {
645 #[serde(
646 default,
647 rename = "defaultTimeoutMs",
648 skip_serializing_if = "Option::is_none"
649 )]
650 pub default_timeout_ms: Option<u64>,
651 #[serde(
652 default,
653 rename = "maxTimeoutMs",
654 skip_serializing_if = "Option::is_none"
655 )]
656 pub max_timeout_ms: Option<u64>,
657 #[serde(
658 default,
659 rename = "maxRegisteredCollections",
660 skip_serializing_if = "Option::is_none"
661 )]
662 pub max_registered_collections: Option<u64>,
663 #[serde(
664 default,
665 rename = "maxRegisteredFunctionsPerVm",
666 skip_serializing_if = "Option::is_none"
667 )]
668 pub max_registered_functions_per_vm: Option<u64>,
669 #[serde(
670 default,
671 rename = "maxFunctionsPerCollection",
672 skip_serializing_if = "Option::is_none"
673 )]
674 pub max_functions_per_collection: Option<u64>,
675 #[serde(
676 default,
677 rename = "maxSchemaBytes",
678 skip_serializing_if = "Option::is_none"
679 )]
680 pub max_schema_bytes: Option<u64>,
681 #[serde(
682 default,
683 rename = "maxExamplesPerFunction",
684 skip_serializing_if = "Option::is_none"
685 )]
686 pub max_examples_per_function: Option<u64>,
687 #[serde(
688 default,
689 rename = "maxExampleInputBytes",
690 skip_serializing_if = "Option::is_none"
691 )]
692 pub max_example_input_bytes: Option<u64>,
693}
694
695#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
696#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
697#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
698#[serde(deny_unknown_fields)]
699pub struct PluginLimits {
700 #[serde(
701 default,
702 rename = "maxPersistedManifestBytes",
703 skip_serializing_if = "Option::is_none"
704 )]
705 pub max_persisted_manifest_bytes: Option<u64>,
706 #[serde(
707 default,
708 rename = "maxPersistedManifestFileBytes",
709 skip_serializing_if = "Option::is_none"
710 )]
711 pub max_persisted_manifest_file_bytes: Option<u64>,
712}
713
714#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
715#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
716#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
717#[serde(deny_unknown_fields)]
718pub struct SqliteLimits {
719 #[serde(
720 default,
721 rename = "maxResultBytes",
722 skip_serializing_if = "Option::is_none"
723 )]
724 pub max_result_bytes: Option<u64>,
725}
726
727#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
728#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
729#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
730#[serde(deny_unknown_fields)]
731pub struct JsRuntimeLimits {
732 #[serde(
733 default,
734 rename = "v8HeapLimitMb",
735 skip_serializing_if = "Option::is_none"
736 )]
737 pub v8_heap_limit_mb: Option<u64>,
738 #[serde(
739 default,
740 rename = "syncRpcWaitTimeoutMs",
741 skip_serializing_if = "Option::is_none"
742 )]
743 pub sync_rpc_wait_timeout_ms: Option<u64>,
744 #[serde(
745 default,
746 rename = "cpuTimeLimitMs",
747 skip_serializing_if = "Option::is_none"
748 )]
749 pub cpu_time_limit_ms: Option<u64>,
750 #[serde(
751 default,
752 rename = "wallClockLimitMs",
753 skip_serializing_if = "Option::is_none"
754 )]
755 pub wall_clock_limit_ms: Option<u64>,
756 #[serde(
757 default,
758 rename = "importCacheMaterializeTimeoutMs",
759 skip_serializing_if = "Option::is_none"
760 )]
761 pub import_cache_materialize_timeout_ms: Option<u64>,
762 #[serde(
763 default,
764 rename = "capturedOutputLimitBytes",
765 skip_serializing_if = "Option::is_none"
766 )]
767 pub captured_output_limit_bytes: Option<u64>,
768 #[serde(
769 default,
770 rename = "stdinBufferLimitBytes",
771 skip_serializing_if = "Option::is_none"
772 )]
773 pub stdin_buffer_limit_bytes: Option<u64>,
774 #[serde(
775 default,
776 rename = "eventPayloadLimitBytes",
777 skip_serializing_if = "Option::is_none"
778 )]
779 pub event_payload_limit_bytes: Option<u64>,
780 #[serde(
781 default,
782 rename = "v8IpcMaxFrameBytes",
783 skip_serializing_if = "Option::is_none"
784 )]
785 pub v8_ipc_max_frame_bytes: Option<u64>,
786}
787
788#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
789#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
790#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
791#[serde(deny_unknown_fields)]
792pub struct PythonLimits {
793 #[serde(
794 default,
795 rename = "outputBufferMaxBytes",
796 skip_serializing_if = "Option::is_none"
797 )]
798 pub output_buffer_max_bytes: Option<u64>,
799 #[serde(
800 default,
801 rename = "executionTimeoutMs",
802 skip_serializing_if = "Option::is_none"
803 )]
804 pub execution_timeout_ms: Option<u64>,
805 #[serde(
806 default,
807 rename = "maxOldSpaceMb",
808 skip_serializing_if = "Option::is_none"
809 )]
810 pub max_old_space_mb: Option<u64>,
811 #[serde(
812 default,
813 rename = "vfsRpcTimeoutMs",
814 skip_serializing_if = "Option::is_none"
815 )]
816 pub vfs_rpc_timeout_ms: Option<u64>,
817}
818
819#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
820#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
821#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
822#[serde(deny_unknown_fields)]
823pub struct WasmLimits {
824 #[serde(
825 default,
826 rename = "maxModuleFileBytes",
827 skip_serializing_if = "Option::is_none"
828 )]
829 pub max_module_file_bytes: Option<u64>,
830 #[serde(
831 default,
832 rename = "capturedOutputLimitBytes",
833 skip_serializing_if = "Option::is_none"
834 )]
835 pub captured_output_limit_bytes: Option<u64>,
836 #[serde(
837 default,
838 rename = "syncReadLimitBytes",
839 skip_serializing_if = "Option::is_none"
840 )]
841 pub sync_read_limit_bytes: Option<u64>,
842 #[serde(
843 default,
844 rename = "prewarmTimeoutMs",
845 skip_serializing_if = "Option::is_none"
846 )]
847 pub prewarm_timeout_ms: Option<u64>,
848 #[serde(
849 default,
850 rename = "runnerHeapLimitMb",
851 skip_serializing_if = "Option::is_none"
852 )]
853 pub runner_heap_limit_mb: Option<u64>,
854 #[serde(
855 default,
856 rename = "runnerCpuTimeLimitMs",
857 skip_serializing_if = "Option::is_none"
858 )]
859 pub runner_cpu_time_limit_ms: Option<u64>,
860}
861
862#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
863#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
864#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
865#[serde(deny_unknown_fields)]
866pub struct ProcessLimits {
867 #[serde(
868 default,
869 rename = "maxSpawnFileActions",
870 skip_serializing_if = "Option::is_none"
871 )]
872 pub max_spawn_file_actions: Option<u64>,
873 #[serde(
874 default,
875 rename = "maxSpawnFileActionBytes",
876 skip_serializing_if = "Option::is_none"
877 )]
878 pub max_spawn_file_action_bytes: Option<u64>,
879 #[serde(
880 default,
881 rename = "pendingStdinBytes",
882 skip_serializing_if = "Option::is_none"
883 )]
884 pub pending_stdin_bytes: Option<u64>,
885 #[serde(
886 default,
887 rename = "pendingEventCount",
888 skip_serializing_if = "Option::is_none"
889 )]
890 pub pending_event_count: Option<u64>,
891 #[serde(
892 default,
893 rename = "pendingEventBytes",
894 skip_serializing_if = "Option::is_none"
895 )]
896 pub pending_event_bytes: Option<u64>,
897}
898
899#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
905#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
906#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
907#[serde(deny_unknown_fields)]
908pub struct Permissions {
909 #[serde(default, skip_serializing_if = "Option::is_none")]
910 pub fs: Option<FsPermissions>,
911 #[serde(default, skip_serializing_if = "Option::is_none")]
912 pub network: Option<PatternPermissions>,
913 #[serde(
914 default,
915 rename = "childProcess",
916 skip_serializing_if = "Option::is_none"
917 )]
918 pub child_process: Option<PatternPermissions>,
919 #[serde(default, skip_serializing_if = "Option::is_none")]
920 pub process: Option<PatternPermissions>,
921 #[serde(default, skip_serializing_if = "Option::is_none")]
922 pub env: Option<PatternPermissions>,
923 #[serde(
924 default,
925 rename = "hostFunction",
926 skip_serializing_if = "Option::is_none"
927 )]
928 pub host_function: Option<PatternPermissions>,
929}
930
931#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
933#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
934#[serde(rename_all = "lowercase")]
935pub enum PermissionMode {
936 Allow,
937 Deny,
938}
939
940#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
942#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
943#[serde(untagged)]
944pub enum FsPermissions {
945 Mode(PermissionMode),
946 Rules(RulePermissions<FsPermissionRule>),
947}
948
949#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
951#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
952#[serde(untagged)]
953pub enum PatternPermissions {
954 Mode(PermissionMode),
955 Rules(RulePermissions<PatternPermissionRule>),
956}
957
958#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
960#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
961#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
962#[serde(deny_unknown_fields)]
963pub struct RulePermissions<T> {
964 #[serde(default, skip_serializing_if = "Option::is_none")]
965 pub default: Option<PermissionMode>,
966 pub rules: Vec<T>,
967}
968
969#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
971#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
972#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
973#[serde(deny_unknown_fields)]
974pub struct FsPermissionRule {
975 pub mode: PermissionMode,
976 #[serde(default, skip_serializing_if = "Option::is_none")]
977 pub operations: Option<Vec<String>>,
978 #[serde(default, skip_serializing_if = "Option::is_none")]
979 pub paths: Option<Vec<String>>,
980}
981
982#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
984#[cfg_attr(feature = "contract", ts(rename_all = "camelCase"))]
985#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
986#[serde(deny_unknown_fields)]
987pub struct PatternPermissionRule {
988 pub mode: PermissionMode,
989 #[serde(default, skip_serializing_if = "Option::is_none")]
990 pub operations: Option<Vec<String>>,
991 #[serde(default, skip_serializing_if = "Option::is_none")]
992 pub patterns: Option<Vec<String>>,
993}
994
995#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1001#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1002pub struct RootFilesystemConfig {
1003 #[serde(default, rename = "type")]
1004 pub kind: RootFilesystemKind,
1005 #[serde(default, skip_serializing_if = "Option::is_none")]
1006 pub mode: Option<RootFilesystemMode>,
1007 #[serde(
1008 default,
1009 rename = "nativePlugin",
1010 skip_serializing_if = "Option::is_none"
1011 )]
1012 pub native_plugin: Option<MountPlugin>,
1013 #[serde(default, rename = "disableDefaultBaseLayer")]
1014 pub disable_default_base_layer: bool,
1015 #[serde(default)]
1016 pub lowers: Vec<RootLowerInput>,
1017}
1018
1019impl Default for RootFilesystemConfig {
1020 fn default() -> Self {
1021 Self {
1022 kind: RootFilesystemKind::Overlay,
1023 mode: None,
1024 native_plugin: None,
1025 disable_default_base_layer: false,
1026 lowers: Vec::new(),
1027 }
1028 }
1029}
1030
1031#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1033#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1034#[serde(rename_all = "lowercase")]
1035pub enum RootFilesystemKind {
1036 #[default]
1037 Overlay,
1038 Native,
1039}
1040
1041#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1043#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1044#[serde(rename_all = "kebab-case")]
1045pub enum RootFilesystemMode {
1046 Ephemeral,
1047 ReadOnly,
1048}
1049
1050#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1052#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1053#[serde(tag = "kind", rename_all = "kebab-case")]
1054pub enum RootLowerInput {
1055 BundledBaseFilesystem,
1057 #[serde(untagged)]
1059 SnapshotExport(crate::fs::RootSnapshotExport),
1060}
1061
1062pub enum MountConfig {
1068 Plain {
1070 path: String,
1071 driver: Arc<dyn VirtualFileSystem>,
1072 guest_source: Option<String>,
1073 guest_fstype: Option<String>,
1074 read_only: bool,
1075 },
1076 Native {
1078 path: String,
1079 plugin: MountPlugin,
1080 guest_source: Option<String>,
1081 guest_fstype: Option<String>,
1082 read_only: bool,
1083 },
1084 Overlay {
1086 path: String,
1087 filesystem: OverlayMountConfig,
1088 },
1089}
1090
1091#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1093#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1094pub struct MountPlugin {
1095 pub id: String,
1096 #[serde(default, skip_serializing_if = "Option::is_none")]
1097 pub config: Option<serde_json::Value>,
1098}
1099
1100pub fn node_modules_mount(host_node_modules_dir: impl Into<String>) -> MountConfig {
1108 MountConfig::Native {
1109 path: "/root/node_modules".to_string(),
1110 plugin: MountPlugin {
1111 id: "host_dir".to_string(),
1112 config: Some(serde_json::json!({
1113 "hostPath": host_node_modules_dir.into(),
1114 "readOnly": true,
1115 })),
1116 },
1117 guest_source: Some(String::from("host_dir")),
1118 guest_fstype: Some(String::from("host_dir")),
1119 read_only: true,
1120 }
1121}
1122
1123#[cfg_attr(feature = "contract", derive(ts_rs::TS))]
1125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1126pub struct OverlayMountConfig {
1127 #[serde(rename = "type")]
1128 pub kind: String,
1129 pub store: serde_json::Value,
1130 #[serde(default, skip_serializing_if = "Option::is_none")]
1131 pub mode: Option<RootFilesystemMode>,
1132 pub lowers: Vec<RootLowerInput>,
1133}
1134
1135pub enum AgentOsSidecarConfig {
1141 Shared { pool: Option<String> },
1143 Explicit {
1145 handle: Arc<crate::sidecar::AgentOsSidecar>,
1146 },
1147}
1148
1149pub type ScheduleCallback = Arc<dyn Fn() -> futures::future::BoxFuture<'static, ()> + Send + Sync>;
1159
1160#[derive(Clone)]
1163pub struct ScheduleEntry {
1164 pub id: String,
1166 pub schedule: String,
1168 pub callback: ScheduleCallback,
1170}
1171
1172pub trait ScheduleDriver: Send + Sync {
1178 fn schedule(&self, entry: ScheduleEntry) -> ScheduleHandle;
1181
1182 fn cancel(&self, handle: &ScheduleHandle);
1184
1185 fn dispose(&self);
1187}
1188
1189#[derive(Clone)]
1192pub struct ScheduleHandle {
1193 pub id: String,
1194}
1195
1196#[derive(Default)]
1203pub struct TimerScheduleDriver {
1204 timers: Arc<scc::HashMap<String, tokio_util::sync::CancellationToken>>,
1205}
1206
1207impl TimerScheduleDriver {
1208 pub fn new() -> Self {
1209 Self {
1210 timers: Arc::new(scc::HashMap::new()),
1211 }
1212 }
1213
1214 fn schedule_next(
1218 timers: Arc<scc::HashMap<String, tokio_util::sync::CancellationToken>>,
1219 entry: ScheduleEntry,
1220 cancel: tokio_util::sync::CancellationToken,
1221 ) {
1222 let now = chrono::Utc::now();
1223 let parsed = match crate::cron::parse_schedule(&entry.schedule) {
1224 Ok(parsed) => parsed,
1225 Err(_) => {
1226 let _ = timers.remove(&entry.id);
1227 return;
1228 }
1229 };
1230 let is_cron = parsed.is_cron();
1231 let next = match crate::cron::resolve_next_run(&parsed, now) {
1232 Some(next) => next,
1233 None => {
1234 let _ = timers.remove(&entry.id);
1236 return;
1237 }
1238 };
1239
1240 let delay = (next - now).to_std().unwrap_or(std::time::Duration::ZERO);
1241
1242 tokio::spawn(async move {
1243 tokio::select! {
1244 _ = cancel.cancelled() => {
1245 return;
1246 }
1247 _ = tokio::time::sleep(delay) => {}
1248 }
1249 if cancel.is_cancelled() {
1250 return;
1251 }
1252 (entry.callback)().await;
1254
1255 if is_cron && timers.contains(&entry.id) {
1256 Self::schedule_next(Arc::clone(&timers), entry, cancel);
1257 } else {
1258 let _ = timers.remove(&entry.id);
1259 }
1260 });
1261 }
1262}
1263
1264impl ScheduleDriver for TimerScheduleDriver {
1265 fn schedule(&self, entry: ScheduleEntry) -> ScheduleHandle {
1266 let id = entry.id.clone();
1267 let cancel = tokio_util::sync::CancellationToken::new();
1268 if let Some((_, old)) = self.timers.remove(&id) {
1270 old.cancel();
1271 }
1272 let _ = self.timers.insert(id.clone(), cancel.clone());
1273
1274 Self::schedule_next(Arc::clone(&self.timers), entry, cancel);
1275
1276 ScheduleHandle { id }
1277 }
1278
1279 fn cancel(&self, handle: &ScheduleHandle) {
1280 if let Some((_, cancel)) = self.timers.remove(&handle.id) {
1281 cancel.cancel();
1282 }
1283 }
1284
1285 fn dispose(&self) {
1286 self.timers.scan(|_, cancel| cancel.cancel());
1287 self.timers.clear();
1288 }
1289}