Skip to main content

agentos_client/
config.rs

1//! Configuration types: `AgentOsConfig` (= TS `AgentOsOptions`), the permissions tree, root
2//! filesystem config, mount config, and the schedule-driver abstraction.
3//!
4//! Ported from `packages/core/src/agent-os.ts` (`AgentOsOptions`), `runtime.ts` (`Permissions`),
5//! `layers.ts` / `overlay-filesystem.ts` (root/overlay), and `cron/` (schedule driver).
6//!
7//! Non-serializable parameters (`MountConfig::Plain.driver`, `CronAction::Callback`) are in-process
8//! only and become `Arc<dyn ...>` trait objects; they cannot cross the wire and are gated exactly as
9//! the actor layer gates them.
10
11use std::sync::Arc;
12
13use serde::{Deserialize, Serialize};
14
15use crate::fs::VirtualFileSystem;
16pub use agentos_vm_config::{VmGroupConfig, VmUserAccountConfig, VmUserConfig};
17
18/// Resolved client options (= TS `AgentOsOptions`). All fields optional with documented defaults.
19///
20/// Keep this Rust mirror in sync with `packages/core/src/agent-os.ts::AgentOsOptions`
21/// and `packages/core/src/options-schema.ts::agentOsOptionsSchema`.
22#[derive(Default)]
23pub struct AgentOsConfig {
24    /// VM-scoped SQLite backend shared by VFS metadata/storage and AgentOS
25    /// durable state. Actor deployments inject `ActorUds`; standalone clients
26    /// normally provide a `SqliteFile` descriptor.
27    pub database: Option<agentos_vm_config::VmSqliteDescriptor>,
28    /// Initial virtual Linux credentials and account record. Defaults to `1000:1000` (`agentos`).
29    pub user: Option<VmUserConfig>,
30    /// Software packages to install (flattened). Default `[]`.
31    pub software: Vec<SoftwareInput>,
32    /// Package directories to project into the VM's `/opt/agentos` tree (the
33    /// secure-exec package projection). Each entry is a host dir containing an
34    /// `agentos-package.json` manifest + the package payload. Default `[]`.
35    pub packages: Vec<PackageRef>,
36    /// Guest mount point for the package projection. Default `/opt/agentos`
37    /// (secure-exec's `OPT_AGENTOS_ROOT`) when `None`.
38    pub packages_mount_at: Option<String>,
39    /// Loopback ports exempt from the default outbound-to-host block.
40    pub loopback_exempt_ports: Vec<u16>,
41    /// Allowed Node.js builtins. Default: the hardened native-bridge set.
42    pub allowed_node_builtins: Option<Vec<String>>,
43    /// Root filesystem configuration. Default: overlay + bundled base snapshot.
44    pub root_filesystem: RootFilesystemConfig,
45    /// Additional mounts.
46    pub mounts: Vec<MountConfig>,
47    /// Extra OS instructions appended to agent sessions.
48    pub additional_instructions: Option<String>,
49    /// Schedule driver used by the cron manager. Default: [`TimerScheduleDriver`].
50    pub schedule_driver: Option<Arc<dyn ScheduleDriver>>,
51    /// Binding collections to register.
52    pub bindings: Vec<Bindings>,
53    /// Rust-only sidecar callback handler for `js_bridge`-style plugin requests.
54    pub sidecar_js_bridge_callback: Option<SidecarJsBridgeCallback>,
55    /// Permission policy. Default: allow-all.
56    pub permissions: Option<Permissions>,
57    /// Operator-tunable VM limits. Default: sidecar/kernel built-ins.
58    pub limits: Option<AgentOsLimits>,
59    /// Sidecar placement/config. Default: shared `default` pool.
60    pub sidecar: Option<AgentOsSidecarConfig>,
61    /// Absolute path to the `agentos-sidecar` binary, resolved from the npm
62    /// package on the TypeScript side. Threaded to `SidecarProcess::spawn`
63    /// (mirroring rivetkit's `engine_binary_path`) instead of relying on the
64    /// `AGENTOS_SIDECAR_BIN` env var. `None` falls back to env, then `PATH`.
65    pub sidecar_binary_path: Option<String>,
66}
67
68/// Builder for [`AgentOsConfig`].
69#[derive(Default)]
70pub struct AgentOsConfigBuilder {
71    config: AgentOsConfig,
72}
73
74impl AgentOsConfigBuilder {
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    pub fn database(mut self, database: agentos_vm_config::VmSqliteDescriptor) -> Self {
80        self.config.database = Some(database);
81        self
82    }
83
84    pub fn packages(mut self, packages: Vec<PackageRef>) -> Self {
85        self.config.packages = packages;
86        self
87    }
88
89    pub fn packages_mount_at(mut self, mount_at: impl Into<String>) -> Self {
90        self.config.packages_mount_at = Some(mount_at.into());
91        self
92    }
93
94    pub fn loopback_exempt_ports(mut self, ports: Vec<u16>) -> Self {
95        self.config.loopback_exempt_ports = ports;
96        self
97    }
98
99    pub fn allowed_node_builtins(mut self, builtins: Vec<String>) -> Self {
100        self.config.allowed_node_builtins = Some(builtins);
101        self
102    }
103
104    pub fn user(mut self, user: VmUserConfig) -> Self {
105        self.config.user = Some(user);
106        self
107    }
108
109    pub fn root_filesystem(mut self, root: RootFilesystemConfig) -> Self {
110        self.config.root_filesystem = root;
111        self
112    }
113
114    pub fn mounts(mut self, mounts: Vec<MountConfig>) -> Self {
115        self.config.mounts = mounts;
116        self
117    }
118
119    pub fn additional_instructions(mut self, instructions: impl Into<String>) -> Self {
120        self.config.additional_instructions = Some(instructions.into());
121        self
122    }
123
124    pub fn schedule_driver(mut self, driver: Arc<dyn ScheduleDriver>) -> Self {
125        self.config.schedule_driver = Some(driver);
126        self
127    }
128
129    pub fn bindings(mut self, bindings: Vec<Bindings>) -> Self {
130        self.config.bindings = bindings;
131        self
132    }
133
134    pub fn sidecar_js_bridge_callback(mut self, callback: SidecarJsBridgeCallback) -> Self {
135        self.config.sidecar_js_bridge_callback = Some(callback);
136        self
137    }
138
139    pub fn permissions(mut self, permissions: Permissions) -> Self {
140        self.config.permissions = Some(permissions);
141        self
142    }
143
144    pub fn limits(mut self, limits: AgentOsLimits) -> Self {
145        self.config.limits = Some(limits);
146        self
147    }
148
149    pub fn sidecar(mut self, sidecar: AgentOsSidecarConfig) -> Self {
150        self.config.sidecar = Some(sidecar);
151        self
152    }
153
154    pub fn sidecar_binary_path(mut self, path: impl Into<String>) -> Self {
155        self.config.sidecar_binary_path = Some(path.into());
156        self
157    }
158
159    pub fn build(self) -> AgentOsConfig {
160        self.config
161    }
162}
163
164/// The kind of a software package, which decides how it is mounted into the VM. Mirrors the TS
165/// descriptor `type` discriminator (`packages/core/src/packages.ts`).
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
167#[serde(rename_all = "kebab-case")]
168pub enum SoftwareKind {
169    /// A directory of wasm command binaries. Mounted at `/__secure_exec/commands/{index}/` so the
170    /// sidecar's command discovery can resolve guest commands (`echo`, `sh`, `grep`, ...).
171    #[default]
172    WasmCommands,
173    /// An agent SDK/adapter package. Not mounted as a command directory.
174    Agent,
175    /// A host-binding package. Not mounted as a command directory.
176    Binding,
177}
178
179/// A flattened software package input.
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub struct SoftwareInput {
182    pub package: String,
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub version: Option<String>,
185    /// How the package is mounted into the VM. Defaults to [`SoftwareKind::WasmCommands`].
186    #[serde(default)]
187    pub kind: SoftwareKind,
188}
189
190/// A reference to a packed `.aospkg` package for the `/opt/agentos`
191/// projection. A directory path remains accepted for local transition
192/// fixtures.
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
194pub struct PackageRef {
195    /// Serialized as `packagePath` — the single package-ref spelling on every
196    /// JSON boundary (registry exports, actor config, client config).
197    #[serde(rename = "packagePath")]
198    pub path: String,
199}
200
201/// A host-side binding execute callback. Receives the validated JSON input, returns a JSON result or an
202/// error string. Stays host-side (never crosses to the guest); the guest invokes it by name via the
203/// sidecar host-callback channel.
204pub type BindingCallback = Arc<
205    dyn Fn(
206            serde_json::Value,
207        ) -> futures::future::BoxFuture<'static, Result<serde_json::Value, String>>
208        + Send
209        + Sync,
210>;
211
212/// A sidecar-initiated `js_bridge`-style filesystem callback.
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct SidecarJsBridgeCall {
215    pub call_id: String,
216    pub mount_id: String,
217    pub operation: String,
218    pub args: serde_json::Value,
219}
220
221/// Host-side handler for sidecar `JsBridgeCallRequest` payloads.
222///
223/// This is Rust-only and intentionally not JSON-serializable. RivetKit uses it to bind a native
224/// sidecar root filesystem to actor-owned SQLite (`ctx.db_*`) without teaching secure-exec about
225/// Rivet actors.
226pub type SidecarJsBridgeCallback = Arc<
227    dyn Fn(
228            SidecarJsBridgeCall,
229        )
230            -> futures::future::BoxFuture<'static, Result<Option<serde_json::Value>, String>>
231        + Send
232        + Sync,
233>;
234
235/// A single host binding within a [`Bindings`].
236#[derive(Clone)]
237pub struct Binding {
238    pub name: String,
239    pub description: String,
240    /// JSON Schema for the binding input (forwarded to the sidecar `register_host_callbacks` definition).
241    pub input_schema: serde_json::Value,
242    pub timeout_ms: Option<u64>,
243    /// Host-side implementation, invoked when the guest calls `<collection>:<binding>`.
244    pub execute: BindingCallback,
245}
246
247/// A registered binding collection (in-process; implementations stay host-side). Bindings are exposed to the
248/// guest as `<collection>:<binding>` and dispatched back to [`Binding::execute`] via the sidecar
249/// host-callback channel.
250#[derive(Clone)]
251pub struct Bindings {
252    pub name: String,
253    pub description: String,
254    pub bindings: Vec<Binding>,
255}
256
257// ---------------------------------------------------------------------------
258// VM limits (agent-os.ts AgentOsLimits / sidecar/limits.ts)
259// ---------------------------------------------------------------------------
260
261/// Operator-tunable runtime limits for a VM. Every field is optional; unset fields fall back to the
262/// sidecar defaults.
263#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
264pub struct AgentOsLimits {
265    #[serde(default, skip_serializing_if = "Option::is_none")]
266    pub resources: Option<ResourceLimits>,
267    #[serde(default, skip_serializing_if = "Option::is_none")]
268    pub http: Option<HttpLimits>,
269    #[serde(default, skip_serializing_if = "Option::is_none")]
270    pub bindings: Option<BindingLimits>,
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    pub plugins: Option<PluginLimits>,
273    #[serde(default, skip_serializing_if = "Option::is_none")]
274    pub acp: Option<AcpLimits>,
275    #[serde(default, skip_serializing_if = "Option::is_none")]
276    pub sqlite: Option<SqliteLimits>,
277    #[serde(default, rename = "jsRuntime", skip_serializing_if = "Option::is_none")]
278    pub js_runtime: Option<JsRuntimeLimits>,
279    #[serde(default, skip_serializing_if = "Option::is_none")]
280    pub python: Option<PythonLimits>,
281    #[serde(default, skip_serializing_if = "Option::is_none")]
282    pub wasm: Option<WasmLimits>,
283    #[serde(default, skip_serializing_if = "Option::is_none")]
284    pub process: Option<ProcessLimits>,
285}
286
287#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
288pub struct ResourceLimits {
289    #[serde(default, rename = "cpuCount", skip_serializing_if = "Option::is_none")]
290    pub cpu_count: Option<u64>,
291    #[serde(
292        default,
293        rename = "maxProcesses",
294        skip_serializing_if = "Option::is_none"
295    )]
296    pub max_processes: Option<u64>,
297    #[serde(
298        default,
299        rename = "maxOpenFds",
300        skip_serializing_if = "Option::is_none"
301    )]
302    pub max_open_fds: Option<u64>,
303    #[serde(default, rename = "maxPipes", skip_serializing_if = "Option::is_none")]
304    pub max_pipes: Option<u64>,
305    #[serde(default, rename = "maxPtys", skip_serializing_if = "Option::is_none")]
306    pub max_ptys: Option<u64>,
307    #[serde(
308        default,
309        rename = "maxSockets",
310        skip_serializing_if = "Option::is_none"
311    )]
312    pub max_sockets: Option<u64>,
313    #[serde(
314        default,
315        rename = "maxConnections",
316        skip_serializing_if = "Option::is_none"
317    )]
318    pub max_connections: Option<u64>,
319    #[serde(
320        default,
321        rename = "maxSocketBufferedBytes",
322        skip_serializing_if = "Option::is_none"
323    )]
324    pub max_socket_buffered_bytes: Option<u64>,
325    #[serde(
326        default,
327        rename = "maxSocketDatagramQueueLen",
328        skip_serializing_if = "Option::is_none"
329    )]
330    pub max_socket_datagram_queue_len: Option<u64>,
331    #[serde(
332        default,
333        rename = "maxFilesystemBytes",
334        skip_serializing_if = "Option::is_none"
335    )]
336    pub max_filesystem_bytes: Option<u64>,
337    #[serde(
338        default,
339        rename = "maxInodeCount",
340        skip_serializing_if = "Option::is_none"
341    )]
342    pub max_inode_count: Option<u64>,
343    #[serde(
344        default,
345        rename = "maxBlockingReadMs",
346        skip_serializing_if = "Option::is_none"
347    )]
348    pub max_blocking_read_ms: Option<u64>,
349    #[serde(
350        default,
351        rename = "maxPreadBytes",
352        skip_serializing_if = "Option::is_none"
353    )]
354    pub max_pread_bytes: Option<u64>,
355    #[serde(
356        default,
357        rename = "maxFdWriteBytes",
358        skip_serializing_if = "Option::is_none"
359    )]
360    pub max_fd_write_bytes: Option<u64>,
361    #[serde(
362        default,
363        rename = "maxProcessArgvBytes",
364        skip_serializing_if = "Option::is_none"
365    )]
366    pub max_process_argv_bytes: Option<u64>,
367    #[serde(
368        default,
369        rename = "maxProcessEnvBytes",
370        skip_serializing_if = "Option::is_none"
371    )]
372    pub max_process_env_bytes: Option<u64>,
373    #[serde(
374        default,
375        rename = "maxReaddirEntries",
376        skip_serializing_if = "Option::is_none"
377    )]
378    pub max_readdir_entries: Option<u64>,
379    #[serde(
380        default,
381        rename = "maxWasmFuel",
382        skip_serializing_if = "Option::is_none"
383    )]
384    pub max_wasm_fuel: Option<u64>,
385    #[serde(
386        default,
387        rename = "maxWasmMemoryBytes",
388        skip_serializing_if = "Option::is_none"
389    )]
390    pub max_wasm_memory_bytes: Option<u64>,
391    #[serde(
392        default,
393        rename = "maxWasmStackBytes",
394        skip_serializing_if = "Option::is_none"
395    )]
396    pub max_wasm_stack_bytes: Option<u64>,
397}
398
399#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
400pub struct HttpLimits {
401    #[serde(
402        default,
403        rename = "maxFetchResponseBytes",
404        skip_serializing_if = "Option::is_none"
405    )]
406    pub max_fetch_response_bytes: Option<u64>,
407}
408
409#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
410pub struct BindingLimits {
411    #[serde(
412        default,
413        rename = "defaultBindingTimeoutMs",
414        skip_serializing_if = "Option::is_none"
415    )]
416    pub default_binding_timeout_ms: Option<u64>,
417    #[serde(
418        default,
419        rename = "maxBindingTimeoutMs",
420        skip_serializing_if = "Option::is_none"
421    )]
422    pub max_binding_timeout_ms: Option<u64>,
423    #[serde(
424        default,
425        rename = "maxRegisteredCollections",
426        skip_serializing_if = "Option::is_none"
427    )]
428    pub max_registered_collections: Option<u64>,
429    #[serde(
430        default,
431        rename = "maxRegisteredBindingsPerVm",
432        skip_serializing_if = "Option::is_none"
433    )]
434    pub max_registered_bindings_per_vm: Option<u64>,
435    #[serde(
436        default,
437        rename = "maxBindingsPerCollection",
438        skip_serializing_if = "Option::is_none"
439    )]
440    pub max_bindings_per_collection: Option<u64>,
441    #[serde(
442        default,
443        rename = "maxBindingSchemaBytes",
444        skip_serializing_if = "Option::is_none"
445    )]
446    pub max_binding_schema_bytes: Option<u64>,
447    #[serde(
448        default,
449        rename = "maxExamplesPerBinding",
450        skip_serializing_if = "Option::is_none"
451    )]
452    pub max_examples_per_binding: Option<u64>,
453    #[serde(
454        default,
455        rename = "maxBindingExampleInputBytes",
456        skip_serializing_if = "Option::is_none"
457    )]
458    pub max_binding_example_input_bytes: Option<u64>,
459}
460
461#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
462pub struct PluginLimits {
463    #[serde(
464        default,
465        rename = "maxPersistedManifestBytes",
466        skip_serializing_if = "Option::is_none"
467    )]
468    pub max_persisted_manifest_bytes: Option<u64>,
469    #[serde(
470        default,
471        rename = "maxPersistedManifestFileBytes",
472        skip_serializing_if = "Option::is_none"
473    )]
474    pub max_persisted_manifest_file_bytes: Option<u64>,
475}
476
477#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
478pub struct AcpLimits {
479    #[serde(
480        default,
481        rename = "maxReadLineBytes",
482        skip_serializing_if = "Option::is_none"
483    )]
484    pub max_read_line_bytes: Option<u64>,
485    #[serde(
486        default,
487        rename = "stdoutBufferByteLimit",
488        skip_serializing_if = "Option::is_none"
489    )]
490    pub stdout_buffer_byte_limit: Option<u64>,
491    #[serde(
492        default,
493        rename = "maxCompletedMessageBytes",
494        skip_serializing_if = "Option::is_none"
495    )]
496    pub max_completed_message_bytes: Option<u64>,
497    #[serde(
498        default,
499        rename = "maxTurnOutputBytes",
500        skip_serializing_if = "Option::is_none"
501    )]
502    pub max_turn_output_bytes: Option<u64>,
503    #[serde(
504        default,
505        rename = "maxPromptBytes",
506        skip_serializing_if = "Option::is_none"
507    )]
508    pub max_prompt_bytes: Option<u64>,
509    #[serde(
510        default,
511        rename = "maxPromptBlocks",
512        skip_serializing_if = "Option::is_none"
513    )]
514    pub max_prompt_blocks: Option<u64>,
515    #[serde(
516        default,
517        rename = "maxFallbackContinuationBytes",
518        skip_serializing_if = "Option::is_none"
519    )]
520    pub max_fallback_continuation_bytes: Option<u64>,
521    #[serde(
522        default,
523        rename = "maxSessionHistoryBytes",
524        skip_serializing_if = "Option::is_none"
525    )]
526    pub max_session_history_bytes: Option<u64>,
527    #[serde(
528        default,
529        rename = "maxSessionHistoryEvents",
530        skip_serializing_if = "Option::is_none"
531    )]
532    pub max_session_history_events: Option<u64>,
533    #[serde(
534        default,
535        rename = "maxHistoryPageEntries",
536        skip_serializing_if = "Option::is_none"
537    )]
538    pub max_history_page_entries: Option<u64>,
539    #[serde(
540        default,
541        rename = "maxSessionListEntries",
542        skip_serializing_if = "Option::is_none"
543    )]
544    pub max_session_list_entries: Option<u64>,
545    #[serde(
546        default,
547        rename = "maxSessionsPerVm",
548        skip_serializing_if = "Option::is_none"
549    )]
550    pub max_sessions_per_vm: Option<u64>,
551    #[serde(
552        default,
553        rename = "maxPromptsPerSession",
554        skip_serializing_if = "Option::is_none"
555    )]
556    pub max_prompts_per_session: Option<u64>,
557    #[serde(
558        default,
559        rename = "maxPromptsPerVm",
560        skip_serializing_if = "Option::is_none"
561    )]
562    pub max_prompts_per_vm: Option<u64>,
563    #[serde(
564        default,
565        rename = "maxPendingPermissionsPerSession",
566        skip_serializing_if = "Option::is_none"
567    )]
568    pub max_pending_permissions_per_session: Option<u64>,
569    #[serde(
570        default,
571        rename = "maxPendingPermissionsPerVm",
572        skip_serializing_if = "Option::is_none"
573    )]
574    pub max_pending_permissions_per_vm: Option<u64>,
575    #[serde(
576        default,
577        rename = "maxPermissionOutcomesPerSession",
578        skip_serializing_if = "Option::is_none"
579    )]
580    pub max_permission_outcomes_per_session: Option<u64>,
581    #[serde(
582        default,
583        rename = "maxPermissionOutcomesPerVm",
584        skip_serializing_if = "Option::is_none"
585    )]
586    pub max_permission_outcomes_per_vm: Option<u64>,
587}
588
589#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
590pub struct SqliteLimits {
591    #[serde(
592        default,
593        rename = "maxResultBytes",
594        skip_serializing_if = "Option::is_none"
595    )]
596    pub max_result_bytes: Option<u64>,
597}
598
599#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
600pub struct JsRuntimeLimits {
601    #[serde(
602        default,
603        rename = "v8HeapLimitMb",
604        skip_serializing_if = "Option::is_none"
605    )]
606    pub v8_heap_limit_mb: Option<u64>,
607    #[serde(
608        default,
609        rename = "syncRpcWaitTimeoutMs",
610        skip_serializing_if = "Option::is_none"
611    )]
612    pub sync_rpc_wait_timeout_ms: Option<u64>,
613    #[serde(
614        default,
615        rename = "cpuTimeLimitMs",
616        skip_serializing_if = "Option::is_none"
617    )]
618    pub cpu_time_limit_ms: Option<u64>,
619    #[serde(
620        default,
621        rename = "wallClockLimitMs",
622        skip_serializing_if = "Option::is_none"
623    )]
624    pub wall_clock_limit_ms: Option<u64>,
625    #[serde(
626        default,
627        rename = "importCacheMaterializeTimeoutMs",
628        skip_serializing_if = "Option::is_none"
629    )]
630    pub import_cache_materialize_timeout_ms: Option<u64>,
631    #[serde(
632        default,
633        rename = "capturedOutputLimitBytes",
634        skip_serializing_if = "Option::is_none"
635    )]
636    pub captured_output_limit_bytes: Option<u64>,
637    #[serde(
638        default,
639        rename = "stdinBufferLimitBytes",
640        skip_serializing_if = "Option::is_none"
641    )]
642    pub stdin_buffer_limit_bytes: Option<u64>,
643    #[serde(
644        default,
645        rename = "eventPayloadLimitBytes",
646        skip_serializing_if = "Option::is_none"
647    )]
648    pub event_payload_limit_bytes: Option<u64>,
649    #[serde(
650        default,
651        rename = "v8IpcMaxFrameBytes",
652        skip_serializing_if = "Option::is_none"
653    )]
654    pub v8_ipc_max_frame_bytes: Option<u64>,
655}
656
657#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
658pub struct PythonLimits {
659    #[serde(
660        default,
661        rename = "outputBufferMaxBytes",
662        skip_serializing_if = "Option::is_none"
663    )]
664    pub output_buffer_max_bytes: Option<u64>,
665    #[serde(
666        default,
667        rename = "executionTimeoutMs",
668        skip_serializing_if = "Option::is_none"
669    )]
670    pub execution_timeout_ms: Option<u64>,
671    #[serde(
672        default,
673        rename = "maxOldSpaceMb",
674        skip_serializing_if = "Option::is_none"
675    )]
676    pub max_old_space_mb: Option<u64>,
677    #[serde(
678        default,
679        rename = "vfsRpcTimeoutMs",
680        skip_serializing_if = "Option::is_none"
681    )]
682    pub vfs_rpc_timeout_ms: Option<u64>,
683}
684
685#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
686pub struct WasmLimits {
687    #[serde(
688        default,
689        rename = "maxModuleFileBytes",
690        skip_serializing_if = "Option::is_none"
691    )]
692    pub max_module_file_bytes: Option<u64>,
693    #[serde(
694        default,
695        rename = "capturedOutputLimitBytes",
696        skip_serializing_if = "Option::is_none"
697    )]
698    pub captured_output_limit_bytes: Option<u64>,
699    #[serde(
700        default,
701        rename = "syncReadLimitBytes",
702        skip_serializing_if = "Option::is_none"
703    )]
704    pub sync_read_limit_bytes: Option<u64>,
705    #[serde(
706        default,
707        rename = "prewarmTimeoutMs",
708        skip_serializing_if = "Option::is_none"
709    )]
710    pub prewarm_timeout_ms: Option<u64>,
711    #[serde(
712        default,
713        rename = "runnerHeapLimitMb",
714        skip_serializing_if = "Option::is_none"
715    )]
716    pub runner_heap_limit_mb: Option<u64>,
717    #[serde(
718        default,
719        rename = "runnerCpuTimeLimitMs",
720        skip_serializing_if = "Option::is_none"
721    )]
722    pub runner_cpu_time_limit_ms: Option<u64>,
723}
724
725#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
726pub struct ProcessLimits {
727    #[serde(
728        default,
729        rename = "maxSpawnFileActions",
730        skip_serializing_if = "Option::is_none"
731    )]
732    pub max_spawn_file_actions: Option<u64>,
733    #[serde(
734        default,
735        rename = "maxSpawnFileActionBytes",
736        skip_serializing_if = "Option::is_none"
737    )]
738    pub max_spawn_file_action_bytes: Option<u64>,
739    #[serde(
740        default,
741        rename = "pendingStdinBytes",
742        skip_serializing_if = "Option::is_none"
743    )]
744    pub pending_stdin_bytes: Option<u64>,
745    #[serde(
746        default,
747        rename = "pendingEventCount",
748        skip_serializing_if = "Option::is_none"
749    )]
750    pub pending_event_count: Option<u64>,
751    #[serde(
752        default,
753        rename = "pendingEventBytes",
754        skip_serializing_if = "Option::is_none"
755    )]
756    pub pending_event_bytes: Option<u64>,
757}
758
759// ---------------------------------------------------------------------------
760// Permissions tree (runtime.ts)
761// ---------------------------------------------------------------------------
762
763/// Top-level permission policy. All domains optional (`allowAll` when omitted).
764#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
765pub struct Permissions {
766    #[serde(default, skip_serializing_if = "Option::is_none")]
767    pub fs: Option<FsPermissions>,
768    #[serde(default, skip_serializing_if = "Option::is_none")]
769    pub network: Option<PatternPermissions>,
770    #[serde(
771        default,
772        rename = "childProcess",
773        skip_serializing_if = "Option::is_none"
774    )]
775    pub child_process: Option<PatternPermissions>,
776    #[serde(default, skip_serializing_if = "Option::is_none")]
777    pub process: Option<PatternPermissions>,
778    #[serde(default, skip_serializing_if = "Option::is_none")]
779    pub env: Option<PatternPermissions>,
780    #[serde(default, skip_serializing_if = "Option::is_none")]
781    pub binding: Option<PatternPermissions>,
782}
783
784/// `"allow"` or `"deny"`.
785#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
786#[serde(rename_all = "lowercase")]
787pub enum PermissionMode {
788    Allow,
789    Deny,
790}
791
792/// `PermissionMode | RulePermissions<FsPermissionRule>`.
793#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
794#[serde(untagged)]
795pub enum FsPermissions {
796    Mode(PermissionMode),
797    Rules(RulePermissions<FsPermissionRule>),
798}
799
800/// `PermissionMode | RulePermissions<PatternPermissionRule>` (network/childProcess/process/env/binding).
801#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
802#[serde(untagged)]
803pub enum PatternPermissions {
804    Mode(PermissionMode),
805    Rules(RulePermissions<PatternPermissionRule>),
806}
807
808/// `{ default?: PermissionMode; rules: T[] }`.
809#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
810pub struct RulePermissions<T> {
811    #[serde(default, skip_serializing_if = "Option::is_none")]
812    pub default: Option<PermissionMode>,
813    pub rules: Vec<T>,
814}
815
816/// `{ mode; operations?; paths? }`.
817#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
818pub struct FsPermissionRule {
819    pub mode: PermissionMode,
820    #[serde(default, skip_serializing_if = "Option::is_none")]
821    pub operations: Option<Vec<String>>,
822    #[serde(default, skip_serializing_if = "Option::is_none")]
823    pub paths: Option<Vec<String>>,
824}
825
826/// `{ mode; operations?; patterns? }`.
827#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
828pub struct PatternPermissionRule {
829    pub mode: PermissionMode,
830    #[serde(default, skip_serializing_if = "Option::is_none")]
831    pub operations: Option<Vec<String>>,
832    #[serde(default, skip_serializing_if = "Option::is_none")]
833    pub patterns: Option<Vec<String>>,
834}
835
836// ---------------------------------------------------------------------------
837// Root filesystem (layers.ts / overlay-filesystem.ts)
838// ---------------------------------------------------------------------------
839
840/// Root filesystem configuration. Default: overlay + bundled base snapshot.
841#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
842pub struct RootFilesystemConfig {
843    #[serde(default, rename = "type")]
844    pub kind: RootFilesystemKind,
845    #[serde(default, skip_serializing_if = "Option::is_none")]
846    pub mode: Option<RootFilesystemMode>,
847    #[serde(
848        default,
849        rename = "nativePlugin",
850        skip_serializing_if = "Option::is_none"
851    )]
852    pub native_plugin: Option<MountPlugin>,
853    #[serde(default, rename = "disableDefaultBaseLayer")]
854    pub disable_default_base_layer: bool,
855    #[serde(default)]
856    pub lowers: Vec<RootLowerInput>,
857}
858
859impl Default for RootFilesystemConfig {
860    fn default() -> Self {
861        Self {
862            kind: RootFilesystemKind::Overlay,
863            mode: None,
864            native_plugin: None,
865            disable_default_base_layer: false,
866            lowers: Vec::new(),
867        }
868    }
869}
870
871/// The root filesystem kind.
872#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
873#[serde(rename_all = "lowercase")]
874pub enum RootFilesystemKind {
875    #[default]
876    Overlay,
877    Native,
878}
879
880/// Root filesystem mode.
881#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
882#[serde(rename_all = "kebab-case")]
883pub enum RootFilesystemMode {
884    Ephemeral,
885    ReadOnly,
886}
887
888/// A lower (immutable) snapshot layer input.
889#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
890#[serde(tag = "kind", rename_all = "kebab-case")]
891pub enum RootLowerInput {
892    /// The bundled base filesystem snapshot.
893    BundledBaseFilesystem,
894    /// A snapshot export (`{ kind: "snapshot-export", source }`).
895    #[serde(untagged)]
896    SnapshotExport(crate::fs::RootSnapshotExport),
897}
898
899// ---------------------------------------------------------------------------
900// Mounts
901// ---------------------------------------------------------------------------
902
903/// A filesystem mount. `Plain.driver` is an in-process trait object and cannot cross the wire.
904pub enum MountConfig {
905    /// Plain mount over an in-process [`VirtualFileSystem`] driver.
906    Plain {
907        path: String,
908        driver: Arc<dyn VirtualFileSystem>,
909        guest_source: Option<String>,
910        guest_fstype: Option<String>,
911        read_only: bool,
912    },
913    /// Native plugin mount (`{ id; config? }`).
914    Native {
915        path: String,
916        plugin: MountPlugin,
917        guest_source: Option<String>,
918        guest_fstype: Option<String>,
919        read_only: bool,
920    },
921    /// Overlay mount (`{ type: "overlay"; store; mode?; lowers }`).
922    Overlay {
923        path: String,
924        filesystem: OverlayMountConfig,
925    },
926}
927
928/// A native mount plugin descriptor.
929#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
930pub struct MountPlugin {
931    pub id: String,
932    #[serde(default, skip_serializing_if = "Option::is_none")]
933    pub config: Option<serde_json::Value>,
934}
935
936/// Mount a host `node_modules` directory into the VM at `/root/node_modules`.
937///
938/// Rust mirror of TS `nodeModulesMount(...)` (`packages/core/src/host-dir-mount.ts`).
939/// This is the explicit, mount-based replacement for the removed `moduleAccessCwd`
940/// option: the guest module resolver reads the mounted tree through the kernel VFS,
941/// so the caller supplies exactly the `node_modules` directory whose packages should
942/// be resolvable in the guest. The mount is read-only.
943pub fn node_modules_mount(host_node_modules_dir: impl Into<String>) -> MountConfig {
944    MountConfig::Native {
945        path: "/root/node_modules".to_string(),
946        plugin: MountPlugin {
947            id: "host_dir".to_string(),
948            config: Some(serde_json::json!({
949                "hostPath": host_node_modules_dir.into(),
950                "readOnly": true,
951            })),
952        },
953        guest_source: Some(String::from("host_dir")),
954        guest_fstype: Some(String::from("host_dir")),
955        read_only: true,
956    }
957}
958
959/// Overlay mount filesystem config.
960#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
961pub struct OverlayMountConfig {
962    #[serde(rename = "type")]
963    pub kind: String,
964    pub store: serde_json::Value,
965    #[serde(default, skip_serializing_if = "Option::is_none")]
966    pub mode: Option<RootFilesystemMode>,
967    pub lowers: Vec<RootLowerInput>,
968}
969
970// ---------------------------------------------------------------------------
971// Sidecar config
972// ---------------------------------------------------------------------------
973
974/// How the client obtains its sidecar handle.
975pub enum AgentOsSidecarConfig {
976    /// Use (or create) a shared pooled sidecar (`pool` default `"default"`).
977    Shared { pool: Option<String> },
978    /// Use an explicit sidecar handle.
979    Explicit {
980        handle: Arc<crate::sidecar::AgentOsSidecar>,
981    },
982}
983
984// ---------------------------------------------------------------------------
985// Schedule driver
986// ---------------------------------------------------------------------------
987
988/// The callback fired by a [`ScheduleDriver`] when a schedule entry triggers.
989///
990/// Mirrors the TS `ScheduleEntry.callback: () => void | Promise<void>`. The cron manager passes a
991/// closure that runs one job execution; the driver awaits it (and, for the default driver, reschedules
992/// the next cron fire afterwards).
993pub type ScheduleCallback = Arc<dyn Fn() -> futures::future::BoxFuture<'static, ()> + Send + Sync>;
994
995/// A schedule entry handed to a [`ScheduleDriver`]. Mirrors TS `ScheduleEntry`
996/// (`cron/schedule-driver.ts`).
997#[derive(Clone)]
998pub struct ScheduleEntry {
999    /// Unique ID for this job.
1000    pub id: String,
1001    /// 5/6/7-field cron expression OR an ISO-8601 one-shot timestamp.
1002    pub schedule: String,
1003    /// Called when the schedule fires.
1004    pub callback: ScheduleCallback,
1005}
1006
1007/// Driver-owned scheduling abstraction. Mirrors the TS `ScheduleDriver` interface
1008/// (`cron/schedule-driver.ts`) exactly: the driver parses the schedule, arms the timer, reschedules
1009/// cron entries after each fire, and tears everything down on [`ScheduleDriver::dispose`]. This is the
1010/// documented extension point: a custom driver (deterministic virtual-time test driver, fire-immediately
1011/// driver, etc.) fully controls timing.
1012pub trait ScheduleDriver: Send + Sync {
1013    /// Schedule a callback to fire on a cron expression or at a specific time. Returns a cancellation
1014    /// handle.
1015    fn schedule(&self, entry: ScheduleEntry) -> ScheduleHandle;
1016
1017    /// Cancel a previously scheduled entry.
1018    fn cancel(&self, handle: &ScheduleHandle);
1019
1020    /// Tear down all scheduled work.
1021    fn dispose(&self);
1022}
1023
1024/// Handle to a scheduled entry. Mirrors TS `ScheduleHandle { id }`. Identifies the entry to cancel via
1025/// [`ScheduleDriver::cancel`].
1026#[derive(Clone)]
1027pub struct ScheduleHandle {
1028    pub id: String,
1029}
1030
1031/// Default schedule driver backed by `tokio` timers and the system clock.
1032///
1033/// Mirrors the TS `TimerScheduleDriver`: for cron expressions it computes the next fire time and arms
1034/// a single timer, rescheduling after each fire; for one-shot timestamps it fires once and removes the
1035/// entry. Driver-held timer tasks are tracked so [`ScheduleDriver::cancel`] / [`ScheduleDriver::dispose`]
1036/// can abort them.
1037#[derive(Default)]
1038pub struct TimerScheduleDriver {
1039    timers: Arc<scc::HashMap<String, tokio_util::sync::CancellationToken>>,
1040}
1041
1042impl TimerScheduleDriver {
1043    pub fn new() -> Self {
1044        Self {
1045            timers: Arc::new(scc::HashMap::new()),
1046        }
1047    }
1048
1049    /// Arm the next fire for `entry`. For a one-shot or an exhausted cron the entry is dropped. For a
1050    /// recurring cron the timer reschedules itself after firing the callback. `cancel` is the per-entry
1051    /// cancellation token shared with the registry slot.
1052    fn schedule_next(
1053        timers: Arc<scc::HashMap<String, tokio_util::sync::CancellationToken>>,
1054        entry: ScheduleEntry,
1055        cancel: tokio_util::sync::CancellationToken,
1056    ) {
1057        let now = chrono::Utc::now();
1058        let parsed = match crate::cron::parse_schedule(&entry.schedule) {
1059            Ok(parsed) => parsed,
1060            Err(_) => {
1061                let _ = timers.remove(&entry.id);
1062                return;
1063            }
1064        };
1065        let is_cron = parsed.is_cron();
1066        let next = match crate::cron::resolve_next_run(&parsed, now) {
1067            Some(next) => next,
1068            None => {
1069                // No upcoming run (one-shot in the past, or exhausted cron).
1070                let _ = timers.remove(&entry.id);
1071                return;
1072            }
1073        };
1074
1075        let delay = (next - now).to_std().unwrap_or(std::time::Duration::ZERO);
1076
1077        tokio::spawn(async move {
1078            tokio::select! {
1079                _ = cancel.cancelled() => {
1080                    return;
1081                }
1082                _ = tokio::time::sleep(delay) => {}
1083            }
1084            if cancel.is_cancelled() {
1085                return;
1086            }
1087            // The driver is fire-and-forget; errors are the caller's responsibility.
1088            (entry.callback)().await;
1089
1090            if is_cron && timers.contains(&entry.id) {
1091                Self::schedule_next(Arc::clone(&timers), entry, cancel);
1092            } else {
1093                let _ = timers.remove(&entry.id);
1094            }
1095        });
1096    }
1097}
1098
1099impl ScheduleDriver for TimerScheduleDriver {
1100    fn schedule(&self, entry: ScheduleEntry) -> ScheduleHandle {
1101        let id = entry.id.clone();
1102        let cancel = tokio_util::sync::CancellationToken::new();
1103        // Replace any existing timer for this id, cancelling it first.
1104        if let Some((_, old)) = self.timers.remove(&id) {
1105            old.cancel();
1106        }
1107        let _ = self.timers.insert(id.clone(), cancel.clone());
1108
1109        Self::schedule_next(Arc::clone(&self.timers), entry, cancel);
1110
1111        ScheduleHandle { id }
1112    }
1113
1114    fn cancel(&self, handle: &ScheduleHandle) {
1115        if let Some((_, cancel)) = self.timers.remove(&handle.id) {
1116            cancel.cancel();
1117        }
1118    }
1119
1120    fn dispose(&self) {
1121        self.timers.scan(|_, cancel| cancel.cancel());
1122        self.timers.clear();
1123    }
1124}