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    /// agentos 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    /// (agentos'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 `/__agentos/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 agentos 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    #[serde(
598        default,
599        rename = "maxInFlightRequests",
600        skip_serializing_if = "Option::is_none"
601    )]
602    pub max_in_flight_requests: Option<u64>,
603    #[serde(
604        default,
605        rename = "maxQueuedRequestBytes",
606        skip_serializing_if = "Option::is_none"
607    )]
608    pub max_queued_request_bytes: Option<u64>,
609}
610
611#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
612pub struct JsRuntimeLimits {
613    #[serde(
614        default,
615        rename = "v8HeapLimitMb",
616        skip_serializing_if = "Option::is_none"
617    )]
618    pub v8_heap_limit_mb: Option<u64>,
619    #[serde(
620        default,
621        rename = "syncRpcWaitTimeoutMs",
622        skip_serializing_if = "Option::is_none"
623    )]
624    pub sync_rpc_wait_timeout_ms: Option<u64>,
625    #[serde(
626        default,
627        rename = "cpuTimeLimitMs",
628        skip_serializing_if = "Option::is_none"
629    )]
630    pub cpu_time_limit_ms: Option<u64>,
631    #[serde(
632        default,
633        rename = "wallClockLimitMs",
634        skip_serializing_if = "Option::is_none"
635    )]
636    pub wall_clock_limit_ms: Option<u64>,
637    #[serde(
638        default,
639        rename = "importCacheMaterializeTimeoutMs",
640        skip_serializing_if = "Option::is_none"
641    )]
642    pub import_cache_materialize_timeout_ms: Option<u64>,
643    #[serde(
644        default,
645        rename = "capturedOutputLimitBytes",
646        skip_serializing_if = "Option::is_none"
647    )]
648    pub captured_output_limit_bytes: Option<u64>,
649    #[serde(
650        default,
651        rename = "stdinBufferLimitBytes",
652        skip_serializing_if = "Option::is_none"
653    )]
654    pub stdin_buffer_limit_bytes: Option<u64>,
655    #[serde(
656        default,
657        rename = "eventPayloadLimitBytes",
658        skip_serializing_if = "Option::is_none"
659    )]
660    pub event_payload_limit_bytes: Option<u64>,
661    #[serde(
662        default,
663        rename = "v8IpcMaxFrameBytes",
664        skip_serializing_if = "Option::is_none"
665    )]
666    pub v8_ipc_max_frame_bytes: Option<u64>,
667}
668
669#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
670pub struct PythonLimits {
671    #[serde(
672        default,
673        rename = "outputBufferMaxBytes",
674        skip_serializing_if = "Option::is_none"
675    )]
676    pub output_buffer_max_bytes: Option<u64>,
677    #[serde(
678        default,
679        rename = "executionTimeoutMs",
680        skip_serializing_if = "Option::is_none"
681    )]
682    pub execution_timeout_ms: Option<u64>,
683    #[serde(
684        default,
685        rename = "maxOldSpaceMb",
686        skip_serializing_if = "Option::is_none"
687    )]
688    pub max_old_space_mb: Option<u64>,
689    #[serde(
690        default,
691        rename = "vfsRpcTimeoutMs",
692        skip_serializing_if = "Option::is_none"
693    )]
694    pub vfs_rpc_timeout_ms: Option<u64>,
695}
696
697#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
698pub struct WasmLimits {
699    #[serde(
700        default,
701        rename = "maxModuleFileBytes",
702        skip_serializing_if = "Option::is_none"
703    )]
704    pub max_module_file_bytes: Option<u64>,
705    #[serde(
706        default,
707        rename = "capturedOutputLimitBytes",
708        skip_serializing_if = "Option::is_none"
709    )]
710    pub captured_output_limit_bytes: Option<u64>,
711    #[serde(
712        default,
713        rename = "syncReadLimitBytes",
714        skip_serializing_if = "Option::is_none"
715    )]
716    pub sync_read_limit_bytes: Option<u64>,
717    #[serde(
718        default,
719        rename = "prewarmTimeoutMs",
720        skip_serializing_if = "Option::is_none"
721    )]
722    pub prewarm_timeout_ms: Option<u64>,
723    #[serde(
724        default,
725        rename = "runnerHeapLimitMb",
726        skip_serializing_if = "Option::is_none"
727    )]
728    pub runner_heap_limit_mb: Option<u64>,
729    #[serde(
730        default,
731        rename = "runnerCpuTimeLimitMs",
732        skip_serializing_if = "Option::is_none"
733    )]
734    pub runner_cpu_time_limit_ms: Option<u64>,
735}
736
737#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
738pub struct ProcessLimits {
739    #[serde(
740        default,
741        rename = "maxSpawnFileActions",
742        skip_serializing_if = "Option::is_none"
743    )]
744    pub max_spawn_file_actions: Option<u64>,
745    #[serde(
746        default,
747        rename = "maxSpawnFileActionBytes",
748        skip_serializing_if = "Option::is_none"
749    )]
750    pub max_spawn_file_action_bytes: Option<u64>,
751    #[serde(
752        default,
753        rename = "pendingStdinBytes",
754        skip_serializing_if = "Option::is_none"
755    )]
756    pub pending_stdin_bytes: Option<u64>,
757    #[serde(
758        default,
759        rename = "pendingEventCount",
760        skip_serializing_if = "Option::is_none"
761    )]
762    pub pending_event_count: Option<u64>,
763    #[serde(
764        default,
765        rename = "pendingEventBytes",
766        skip_serializing_if = "Option::is_none"
767    )]
768    pub pending_event_bytes: Option<u64>,
769}
770
771// ---------------------------------------------------------------------------
772// Permissions tree (runtime.ts)
773// ---------------------------------------------------------------------------
774
775/// Top-level permission policy. All domains optional (`allowAll` when omitted).
776#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
777pub struct Permissions {
778    #[serde(default, skip_serializing_if = "Option::is_none")]
779    pub fs: Option<FsPermissions>,
780    #[serde(default, skip_serializing_if = "Option::is_none")]
781    pub network: Option<PatternPermissions>,
782    #[serde(
783        default,
784        rename = "childProcess",
785        skip_serializing_if = "Option::is_none"
786    )]
787    pub child_process: Option<PatternPermissions>,
788    #[serde(default, skip_serializing_if = "Option::is_none")]
789    pub process: Option<PatternPermissions>,
790    #[serde(default, skip_serializing_if = "Option::is_none")]
791    pub env: Option<PatternPermissions>,
792    #[serde(default, skip_serializing_if = "Option::is_none")]
793    pub binding: Option<PatternPermissions>,
794}
795
796/// `"allow"` or `"deny"`.
797#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
798#[serde(rename_all = "lowercase")]
799pub enum PermissionMode {
800    Allow,
801    Deny,
802}
803
804/// `PermissionMode | RulePermissions<FsPermissionRule>`.
805#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
806#[serde(untagged)]
807pub enum FsPermissions {
808    Mode(PermissionMode),
809    Rules(RulePermissions<FsPermissionRule>),
810}
811
812/// `PermissionMode | RulePermissions<PatternPermissionRule>` (network/childProcess/process/env/binding).
813#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
814#[serde(untagged)]
815pub enum PatternPermissions {
816    Mode(PermissionMode),
817    Rules(RulePermissions<PatternPermissionRule>),
818}
819
820/// `{ default?: PermissionMode; rules: T[] }`.
821#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
822pub struct RulePermissions<T> {
823    #[serde(default, skip_serializing_if = "Option::is_none")]
824    pub default: Option<PermissionMode>,
825    pub rules: Vec<T>,
826}
827
828/// `{ mode; operations?; paths? }`.
829#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
830pub struct FsPermissionRule {
831    pub mode: PermissionMode,
832    #[serde(default, skip_serializing_if = "Option::is_none")]
833    pub operations: Option<Vec<String>>,
834    #[serde(default, skip_serializing_if = "Option::is_none")]
835    pub paths: Option<Vec<String>>,
836}
837
838/// `{ mode; operations?; patterns? }`.
839#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
840pub struct PatternPermissionRule {
841    pub mode: PermissionMode,
842    #[serde(default, skip_serializing_if = "Option::is_none")]
843    pub operations: Option<Vec<String>>,
844    #[serde(default, skip_serializing_if = "Option::is_none")]
845    pub patterns: Option<Vec<String>>,
846}
847
848// ---------------------------------------------------------------------------
849// Root filesystem (layers.ts / overlay-filesystem.ts)
850// ---------------------------------------------------------------------------
851
852/// Root filesystem configuration. Default: overlay + bundled base snapshot.
853#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
854pub struct RootFilesystemConfig {
855    #[serde(default, rename = "type")]
856    pub kind: RootFilesystemKind,
857    #[serde(default, skip_serializing_if = "Option::is_none")]
858    pub mode: Option<RootFilesystemMode>,
859    #[serde(
860        default,
861        rename = "nativePlugin",
862        skip_serializing_if = "Option::is_none"
863    )]
864    pub native_plugin: Option<MountPlugin>,
865    #[serde(default, rename = "disableDefaultBaseLayer")]
866    pub disable_default_base_layer: bool,
867    #[serde(default)]
868    pub lowers: Vec<RootLowerInput>,
869}
870
871impl Default for RootFilesystemConfig {
872    fn default() -> Self {
873        Self {
874            kind: RootFilesystemKind::Overlay,
875            mode: None,
876            native_plugin: None,
877            disable_default_base_layer: false,
878            lowers: Vec::new(),
879        }
880    }
881}
882
883/// The root filesystem kind.
884#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
885#[serde(rename_all = "lowercase")]
886pub enum RootFilesystemKind {
887    #[default]
888    Overlay,
889    Native,
890}
891
892/// Root filesystem mode.
893#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
894#[serde(rename_all = "kebab-case")]
895pub enum RootFilesystemMode {
896    Ephemeral,
897    ReadOnly,
898}
899
900/// A lower (immutable) snapshot layer input.
901#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
902#[serde(tag = "kind", rename_all = "kebab-case")]
903pub enum RootLowerInput {
904    /// The bundled base filesystem snapshot.
905    BundledBaseFilesystem,
906    /// A snapshot export (`{ kind: "snapshot-export", source }`).
907    #[serde(untagged)]
908    SnapshotExport(crate::fs::RootSnapshotExport),
909}
910
911// ---------------------------------------------------------------------------
912// Mounts
913// ---------------------------------------------------------------------------
914
915/// A filesystem mount. `Plain.driver` is an in-process trait object and cannot cross the wire.
916pub enum MountConfig {
917    /// Plain mount over an in-process [`VirtualFileSystem`] driver.
918    Plain {
919        path: String,
920        driver: Arc<dyn VirtualFileSystem>,
921        guest_source: Option<String>,
922        guest_fstype: Option<String>,
923        read_only: bool,
924    },
925    /// Native plugin mount (`{ id; config? }`).
926    Native {
927        path: String,
928        plugin: MountPlugin,
929        guest_source: Option<String>,
930        guest_fstype: Option<String>,
931        read_only: bool,
932    },
933    /// Overlay mount (`{ type: "overlay"; store; mode?; lowers }`).
934    Overlay {
935        path: String,
936        filesystem: OverlayMountConfig,
937    },
938}
939
940/// A native mount plugin descriptor.
941#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
942pub struct MountPlugin {
943    pub id: String,
944    #[serde(default, skip_serializing_if = "Option::is_none")]
945    pub config: Option<serde_json::Value>,
946}
947
948/// Mount a host `node_modules` directory into the VM at `/root/node_modules`.
949///
950/// Rust mirror of TS `nodeModulesMount(...)` (`packages/core/src/host-dir-mount.ts`).
951/// This is the explicit, mount-based replacement for the removed `moduleAccessCwd`
952/// option: the guest module resolver reads the mounted tree through the kernel VFS,
953/// so the caller supplies exactly the `node_modules` directory whose packages should
954/// be resolvable in the guest. The mount is read-only.
955pub fn node_modules_mount(host_node_modules_dir: impl Into<String>) -> MountConfig {
956    MountConfig::Native {
957        path: "/root/node_modules".to_string(),
958        plugin: MountPlugin {
959            id: "host_dir".to_string(),
960            config: Some(serde_json::json!({
961                "hostPath": host_node_modules_dir.into(),
962                "readOnly": true,
963            })),
964        },
965        guest_source: Some(String::from("host_dir")),
966        guest_fstype: Some(String::from("host_dir")),
967        read_only: true,
968    }
969}
970
971/// Overlay mount filesystem config.
972#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
973pub struct OverlayMountConfig {
974    #[serde(rename = "type")]
975    pub kind: String,
976    pub store: serde_json::Value,
977    #[serde(default, skip_serializing_if = "Option::is_none")]
978    pub mode: Option<RootFilesystemMode>,
979    pub lowers: Vec<RootLowerInput>,
980}
981
982// ---------------------------------------------------------------------------
983// Sidecar config
984// ---------------------------------------------------------------------------
985
986/// How the client obtains its sidecar handle.
987pub enum AgentOsSidecarConfig {
988    /// Use (or create) a shared pooled sidecar (`pool` default `"default"`).
989    Shared { pool: Option<String> },
990    /// Use an explicit sidecar handle.
991    Explicit {
992        handle: Arc<crate::sidecar::AgentOsSidecar>,
993    },
994}
995
996// ---------------------------------------------------------------------------
997// Schedule driver
998// ---------------------------------------------------------------------------
999
1000/// The callback fired by a [`ScheduleDriver`] when a schedule entry triggers.
1001///
1002/// Mirrors the TS `ScheduleEntry.callback: () => void | Promise<void>`. The cron manager passes a
1003/// closure that runs one job execution; the driver awaits it (and, for the default driver, reschedules
1004/// the next cron fire afterwards).
1005pub type ScheduleCallback = Arc<dyn Fn() -> futures::future::BoxFuture<'static, ()> + Send + Sync>;
1006
1007/// A schedule entry handed to a [`ScheduleDriver`]. Mirrors TS `ScheduleEntry`
1008/// (`cron/schedule-driver.ts`).
1009#[derive(Clone)]
1010pub struct ScheduleEntry {
1011    /// Unique ID for this job.
1012    pub id: String,
1013    /// 5/6/7-field cron expression OR an ISO-8601 one-shot timestamp.
1014    pub schedule: String,
1015    /// Called when the schedule fires.
1016    pub callback: ScheduleCallback,
1017}
1018
1019/// Driver-owned scheduling abstraction. Mirrors the TS `ScheduleDriver` interface
1020/// (`cron/schedule-driver.ts`) exactly: the driver parses the schedule, arms the timer, reschedules
1021/// cron entries after each fire, and tears everything down on [`ScheduleDriver::dispose`]. This is the
1022/// documented extension point: a custom driver (deterministic virtual-time test driver, fire-immediately
1023/// driver, etc.) fully controls timing.
1024pub trait ScheduleDriver: Send + Sync {
1025    /// Schedule a callback to fire on a cron expression or at a specific time. Returns a cancellation
1026    /// handle.
1027    fn schedule(&self, entry: ScheduleEntry) -> ScheduleHandle;
1028
1029    /// Cancel a previously scheduled entry.
1030    fn cancel(&self, handle: &ScheduleHandle);
1031
1032    /// Tear down all scheduled work.
1033    fn dispose(&self);
1034}
1035
1036/// Handle to a scheduled entry. Mirrors TS `ScheduleHandle { id }`. Identifies the entry to cancel via
1037/// [`ScheduleDriver::cancel`].
1038#[derive(Clone)]
1039pub struct ScheduleHandle {
1040    pub id: String,
1041}
1042
1043/// Default schedule driver backed by `tokio` timers and the system clock.
1044///
1045/// Mirrors the TS `TimerScheduleDriver`: for cron expressions it computes the next fire time and arms
1046/// a single timer, rescheduling after each fire; for one-shot timestamps it fires once and removes the
1047/// entry. Driver-held timer tasks are tracked so [`ScheduleDriver::cancel`] / [`ScheduleDriver::dispose`]
1048/// can abort them.
1049#[derive(Default)]
1050pub struct TimerScheduleDriver {
1051    timers: Arc<scc::HashMap<String, tokio_util::sync::CancellationToken>>,
1052}
1053
1054impl TimerScheduleDriver {
1055    pub fn new() -> Self {
1056        Self {
1057            timers: Arc::new(scc::HashMap::new()),
1058        }
1059    }
1060
1061    /// Arm the next fire for `entry`. For a one-shot or an exhausted cron the entry is dropped. For a
1062    /// recurring cron the timer reschedules itself after firing the callback. `cancel` is the per-entry
1063    /// cancellation token shared with the registry slot.
1064    fn schedule_next(
1065        timers: Arc<scc::HashMap<String, tokio_util::sync::CancellationToken>>,
1066        entry: ScheduleEntry,
1067        cancel: tokio_util::sync::CancellationToken,
1068    ) {
1069        let now = chrono::Utc::now();
1070        let parsed = match crate::cron::parse_schedule(&entry.schedule) {
1071            Ok(parsed) => parsed,
1072            Err(_) => {
1073                let _ = timers.remove(&entry.id);
1074                return;
1075            }
1076        };
1077        let is_cron = parsed.is_cron();
1078        let next = match crate::cron::resolve_next_run(&parsed, now) {
1079            Some(next) => next,
1080            None => {
1081                // No upcoming run (one-shot in the past, or exhausted cron).
1082                let _ = timers.remove(&entry.id);
1083                return;
1084            }
1085        };
1086
1087        let delay = (next - now).to_std().unwrap_or(std::time::Duration::ZERO);
1088
1089        tokio::spawn(async move {
1090            tokio::select! {
1091                _ = cancel.cancelled() => {
1092                    return;
1093                }
1094                _ = tokio::time::sleep(delay) => {}
1095            }
1096            if cancel.is_cancelled() {
1097                return;
1098            }
1099            // The driver is fire-and-forget; errors are the caller's responsibility.
1100            (entry.callback)().await;
1101
1102            if is_cron && timers.contains(&entry.id) {
1103                Self::schedule_next(Arc::clone(&timers), entry, cancel);
1104            } else {
1105                let _ = timers.remove(&entry.id);
1106            }
1107        });
1108    }
1109}
1110
1111impl ScheduleDriver for TimerScheduleDriver {
1112    fn schedule(&self, entry: ScheduleEntry) -> ScheduleHandle {
1113        let id = entry.id.clone();
1114        let cancel = tokio_util::sync::CancellationToken::new();
1115        // Replace any existing timer for this id, cancelling it first.
1116        if let Some((_, old)) = self.timers.remove(&id) {
1117            old.cancel();
1118        }
1119        let _ = self.timers.insert(id.clone(), cancel.clone());
1120
1121        Self::schedule_next(Arc::clone(&self.timers), entry, cancel);
1122
1123        ScheduleHandle { id }
1124    }
1125
1126    fn cancel(&self, handle: &ScheduleHandle) {
1127        if let Some((_, cancel)) = self.timers.remove(&handle.id) {
1128            cancel.cancel();
1129        }
1130    }
1131
1132    fn dispose(&self) {
1133        self.timers.scan(|_, cancel| cancel.cancel());
1134        self.timers.clear();
1135    }
1136}