From e7e4fb5ca14ac6acc62fc6f45cf2fdb7c8c3fa14 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp <19+brynary@users.noreply.github.com> Date: Thu, 14 May 2026 07:54:43 -0700 Subject: [PATCH] Add Daytona volume mount passthrough (#263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This adds run-configuration support for mounting existing Daytona volumes into Fabro-managed Daytona sandboxes. Concretely, this PR: - adds `[[run.sandbox.daytona.volumes]]` with `volume_id`, `mount_path`, and optional `subpath` - resolves that config through the layer/settings/runtime pipeline - forwards configured mounts to `daytona_sdk::SandboxBaseParams.volumes` when creating the sandbox - documents the configuration surface in the Daytona environment and run configuration docs ## Motivation Daytona already supports attaching volumes when a sandbox is created, but Fabro currently owns that sandbox creation call. That means users cannot attach a pre-created Daytona volume to a Fabro-managed sandbox from run config. The intended use is persistent, provider-owned state such as agent credentials, caches, datasets, or other files that should survive ephemeral sandbox lifecycles. ## Scope This is intentionally a narrow passthrough. Fabro does not create, delete, list, wait on, or otherwise manage Daytona volume lifecycle. Users create the volume in Daytona first, then reference its `volume_id` from Fabro run config. `volumes` defaults to an empty list in resolved settings for backwards compatibility with existing serialized settings. ## Testing - `cargo test -p fabro-server runtime_daytona_config_preserves_volume_mounts` - `cargo test -p fabro-config resolves_daytona_volume_mounts` - `cargo test -p fabro-sandbox --features daytona volume_mounts` - `cargo test -p fabro-workflow runtime_daytona_config_preserves_volume_mounts` - `cargo check -p fabro-server` --- _Re-opened from #262 (originally by @kimprobably) to land a rustfmt fix — the original PR came from an org-owned fork, which blocks maintainer pushes. Branch is now on the base repo. Original commit preserved; one additional commit fixes rustfmt formatting._ Co-authored-by: Tim Keen --- docs/public/execution/environments.mdx | 16 ++++++ docs/public/execution/run-configuration.mdx | 6 ++ lib/crates/fabro-config/src/layers/combine.rs | 12 +++- lib/crates/fabro-config/src/layers/mod.rs | 16 +++--- lib/crates/fabro-config/src/layers/run.rs | 12 ++++ lib/crates/fabro-config/src/lib.rs | 8 +-- lib/crates/fabro-config/src/resolve/run.rs | 27 ++++++--- .../fabro-config/src/tests/resolve_run.rs | 30 ++++++++++ lib/crates/fabro-sandbox/src/config.rs | 9 +++ lib/crates/fabro-sandbox/src/daytona/mod.rs | 56 ++++++++++++++++++- lib/crates/fabro-server/src/demo/mod.rs | 1 + lib/crates/fabro-server/src/run_manifest.rs | 31 +++++++++- lib/crates/fabro-types/src/settings/run.rs | 9 +++ .../fabro-workflow/src/operations/start.rs | 42 +++++++++++++- 14 files changed, 248 insertions(+), 27 deletions(-) diff --git a/docs/public/execution/environments.mdx b/docs/public/execution/environments.mdx index b29cbbe8a..c9fb967e2 100644 --- a/docs/public/execution/environments.mdx +++ b/docs/public/execution/environments.mdx @@ -160,6 +160,22 @@ dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update && apt-get instal If the snapshot already exists and is in `Active` state, Fabro uses it directly. If it's in `Building` or `Pending` state, Fabro polls with exponential backoff until it's ready. +### Volumes + +Mount existing Daytona volumes into each sandbox at creation time: + +```toml title="run.toml" +[run.sandbox] +provider = "daytona" + +[[run.sandbox.daytona.volumes]] +volume_id = "vol-agent-state" +mount_path = "/home/daytona/agent-state" +subpath = "agent-auth" +``` + +Fabro does not create or manage Daytona volumes. Create the volume in Daytona first, then reference its `volume_id` from the run config. Use `subpath` when a shared volume should expose only one prefix to the sandbox. + ### Labels Attach key-value labels to sandboxes for filtering and identification in the Daytona dashboard: diff --git a/docs/public/execution/run-configuration.mdx b/docs/public/execution/run-configuration.mdx index a5beb2ddc..b2f6240db 100644 --- a/docs/public/execution/run-configuration.mdx +++ b/docs/public/execution/run-configuration.mdx @@ -273,6 +273,11 @@ auto_stop_interval = 60 project = "fabro" env = "staging" +[[run.sandbox.daytona.volumes]] +volume_id = "vol-agent-state" +mount_path = "/home/daytona/agent-state" +subpath = "agent-auth" + [run.sandbox.daytona.snapshot] name = "my-snapshot" cpu = 4 @@ -287,6 +292,7 @@ dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update" |---|---| | `auto_stop_interval` | Minutes of inactivity before the sandbox auto-stops. | | `labels` | Key-value labels attached to the sandbox for filtering and identification. Labels merge across layers (sticky merge-by-key). | +| `volumes` | Existing Daytona volumes to mount at sandbox creation. Each entry requires `volume_id` and `mount_path`; `subpath` is optional. Fabro does not create or manage volume lifecycle. | | `snapshot.name` | Snapshot name to create or use for the sandbox. | | `snapshot.cpu` | CPU cores for the snapshot (integer). | | `snapshot.memory` | Memory size using human-readable units: `"8GB"`, `"16GiB"`, or bare integers that default to GB. | diff --git a/lib/crates/fabro-config/src/layers/combine.rs b/lib/crates/fabro-config/src/layers/combine.rs index c7d50c38b..c49e2a2d6 100644 --- a/lib/crates/fabro-config/src/layers/combine.rs +++ b/lib/crates/fabro-config/src/layers/combine.rs @@ -15,9 +15,9 @@ use super::cli::{CliAuthLayer, CliLoggingLayer, CliTargetLayer}; use super::features::FeaturesLayer; use super::llm::{CostRates, CredentialRef, HeaderValueRef, ReasoningEffortFeature}; use super::run::{ - DaytonaSnapshotLayer, HookAgentMarker, HookEntry, HookTlsMode, InterviewProviderLayer, - ModelRefOrSplice, NotificationProviderLayer, RunArtifactsLayer, RunCheckpointLayer, - RunGoalLayer, RunPrepareLayer, ScmGitHubLayer, StringOrSplice, + DaytonaSnapshotLayer, DaytonaVolumeLayer, HookAgentMarker, HookEntry, HookTlsMode, + InterviewProviderLayer, ModelRefOrSplice, NotificationProviderLayer, RunArtifactsLayer, + RunCheckpointLayer, RunGoalLayer, RunPrepareLayer, ScmGitHubLayer, StringOrSplice, }; use super::server::{ ObjectStoreLocalLayer, ObjectStoreS3Layer, ServerApiLayer, ServerAuthGithubLayer, @@ -43,6 +43,12 @@ impl Combine for Option { } } +impl Combine for Option> { + fn combine(self, other: Self) -> Self { + self.or(other) + } +} + macro_rules! impl_combine_or_option { ($($ty:ty),+ $(,)?) => { $( diff --git a/lib/crates/fabro-config/src/layers/mod.rs b/lib/crates/fabro-config/src/layers/mod.rs index fac687db8..46d933604 100644 --- a/lib/crates/fabro-config/src/layers/mod.rs +++ b/lib/crates/fabro-config/src/layers/mod.rs @@ -26,14 +26,14 @@ pub use log_filter::LogFilter; pub use maps::{MergeMap, ReplaceMap, StickyMap}; pub use project::ProjectLayer; pub use run::{ - DaytonaDockerfileLayer, DaytonaSandboxLayer, DaytonaSnapshotLayer, DockerSandboxLayer, - GitAuthorLayer, HookAgentMarker, HookEntry, HookTlsMode, InterviewProviderLayer, - InterviewsLayer, McpEntryLayer, ModelRefOrSplice, NotificationProviderLayer, - NotificationRouteLayer, PrepareStep, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer, - RunCloneLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer, - RunIntegrationsLayer, RunLayer, RunMetaBranchLayer, RunModelControlsLayer, RunModelLayer, - RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer, RunSandboxLayer, RunScmLayer, - ScmGitHubLayer, StringOrSplice, + DaytonaDockerfileLayer, DaytonaSandboxLayer, DaytonaSnapshotLayer, DaytonaVolumeLayer, + DockerSandboxLayer, GitAuthorLayer, HookAgentMarker, HookEntry, HookTlsMode, + InterviewProviderLayer, InterviewsLayer, McpEntryLayer, ModelRefOrSplice, + NotificationProviderLayer, NotificationRouteLayer, PrepareStep, RunAgentLayer, + RunArtifactsLayer, RunCheckpointLayer, RunCloneLayer, RunExecutionLayer, RunGitLayer, + RunGoalLayer, RunIntegrationsGithubLayer, RunIntegrationsLayer, RunLayer, RunMetaBranchLayer, + RunModelControlsLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer, + RunSandboxLayer, RunScmLayer, ScmGitHubLayer, StringOrSplice, }; pub use server::{ GithubIntegrationLayer, IntegrationWebhooksLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer, diff --git a/lib/crates/fabro-config/src/layers/run.rs b/lib/crates/fabro-config/src/layers/run.rs index f2b5a2fe2..fd7e994e9 100644 --- a/lib/crates/fabro-config/src/layers/run.rs +++ b/lib/crates/fabro-config/src/layers/run.rs @@ -356,12 +356,24 @@ pub struct DaytonaSandboxLayer { /// Sticky merge-by-key (provider-native labels). #[serde(default, skip_serializing_if = "StickyMap::is_empty")] pub labels: StickyMap, + /// Existing Daytona volumes to mount when creating the sandbox. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub volumes: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub snapshot: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub network: Option, } +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DaytonaVolumeLayer { + pub volume_id: String, + pub mount_path: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub subpath: Option, +} + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct DaytonaSnapshotLayer { diff --git a/lib/crates/fabro-config/src/lib.rs b/lib/crates/fabro-config/src/lib.rs index a5ada2c6a..49f22e54d 100644 --- a/lib/crates/fabro-config/src/lib.rs +++ b/lib/crates/fabro-config/src/lib.rs @@ -42,10 +42,10 @@ pub use layers::{ CliAuthLayer, CliExecAgentLayer, CliExecLayer, CliExecModelLayer, CliLayer, CliLoggingLayer, CliOutputLayer, CliTargetLayer, CliUpdatesLayer, CostRates, CredentialRef, CredentialRefParseError, DaytonaDockerfileLayer, DaytonaSandboxLayer, DaytonaSnapshotLayer, - DockerSandboxLayer, FeaturesLayer, GitAuthorLayer, GithubIntegrationLayer, HeaderValueRef, - HookAgentMarker, HookEntry, HookTlsMode, IntegrationWebhooksLayer, InterviewProviderLayer, - InterviewsLayer, LlmLayer, LlmModelFeatures, LlmModelLimits, LogFilter, McpEntryLayer, - MergeMap, ModelControls, ModelCostTable, ModelRefOrSplice, ModelSettings, + DaytonaVolumeLayer, DockerSandboxLayer, FeaturesLayer, GitAuthorLayer, GithubIntegrationLayer, + HeaderValueRef, HookAgentMarker, HookEntry, HookTlsMode, IntegrationWebhooksLayer, + InterviewProviderLayer, InterviewsLayer, LlmLayer, LlmModelFeatures, LlmModelLimits, LogFilter, + McpEntryLayer, MergeMap, ModelControls, ModelCostTable, ModelRefOrSplice, ModelSettings, NotificationProviderLayer, NotificationRouteLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer, PrepareStep, ProjectLayer, ProviderSettings, ReasoningEffortFeature, ReplaceMap, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer, RunCloneLayer, RunExecutionLayer, RunGitLayer, diff --git a/lib/crates/fabro-config/src/resolve/run.rs b/lib/crates/fabro-config/src/resolve/run.rs index 454ad5611..15d818b6c 100644 --- a/lib/crates/fabro-config/src/resolve/run.rs +++ b/lib/crates/fabro-config/src/resolve/run.rs @@ -1,13 +1,13 @@ use fabro_types::settings::InterpString; use fabro_types::settings::run::{ - ArtifactsSettings, DaytonaSettings, DaytonaSnapshotSettings, DockerSettings, DockerfileSource, - GitAuthorSettings, HookDefinition, HookType, InterviewProviderSettings, McpServerSettings, - McpTransport, MergeStrategy, NotificationProviderSettings, NotificationRouteSettings, - PullRequestSettings, RunAgentSettings, RunBranchSettings, RunCheckpointSettings, - RunCloneSettings, RunExecutionSettings, RunGitSettings, RunGoal, RunIntegrationsGithubSettings, - RunIntegrationsSettings, RunInterviewsSettings, RunMetaBranchSettings, RunModelControls, - RunModelSettings, RunNamespace, RunPrepareSettings, RunSandboxSettings, RunScmSettings, - ScmGitHubSettings, TlsMode, + ArtifactsSettings, DaytonaSettings, DaytonaSnapshotSettings, DaytonaVolumeSettings, + DockerSettings, DockerfileSource, GitAuthorSettings, HookDefinition, HookType, + InterviewProviderSettings, McpServerSettings, McpTransport, MergeStrategy, + NotificationProviderSettings, NotificationRouteSettings, PullRequestSettings, RunAgentSettings, + RunBranchSettings, RunCheckpointSettings, RunCloneSettings, RunExecutionSettings, + RunGitSettings, RunGoal, RunIntegrationsGithubSettings, RunIntegrationsSettings, + RunInterviewsSettings, RunMetaBranchSettings, RunModelControls, RunModelSettings, RunNamespace, + RunPrepareSettings, RunSandboxSettings, RunScmSettings, ScmGitHubSettings, TlsMode, }; use super::ResolveError; @@ -269,6 +269,17 @@ fn resolve_daytona(daytona: &DaytonaSandboxLayer) -> DaytonaSettings { DaytonaSettings { auto_stop_interval: daytona.auto_stop_interval, labels: daytona.labels.clone().into_inner(), + volumes: daytona + .volumes + .as_deref() + .unwrap_or(&[]) + .iter() + .map(|volume| DaytonaVolumeSettings { + volume_id: volume.volume_id.clone(), + mount_path: volume.mount_path.clone(), + subpath: volume.subpath.clone(), + }) + .collect(), snapshot: daytona.snapshot.as_ref().and_then(|snapshot| { snapshot.name.as_ref().map(|name| DaytonaSnapshotSettings { name: name.clone(), diff --git a/lib/crates/fabro-config/src/tests/resolve_run.rs b/lib/crates/fabro-config/src/tests/resolve_run.rs index 9024c0d5f..d81656dbe 100644 --- a/lib/crates/fabro-config/src/tests/resolve_run.rs +++ b/lib/crates/fabro-config/src/tests/resolve_run.rs @@ -61,6 +61,36 @@ fn resolves_run_defaults_from_empty_settings() { assert!(settings.pull_request.is_none()); } +#[test] +fn resolves_daytona_volume_mounts() { + let settings = WorkflowSettingsBuilder::from_toml( + r#" +_version = 1 + +[run.sandbox] +provider = "daytona" + +[[run.sandbox.daytona.volumes]] +volume_id = "vol_auth" +mount_path = "/home/daytona/.config" +subpath = "agents" +"#, + ) + .expect("daytona volume mount should resolve") + .run; + + let daytona = settings + .sandbox + .daytona + .as_ref() + .expect("daytona settings should resolve"); + + assert_eq!(daytona.volumes.len(), 1); + assert_eq!(daytona.volumes[0].volume_id, "vol_auth"); + assert_eq!(daytona.volumes[0].mount_path, "/home/daytona/.config"); + assert_eq!(daytona.volumes[0].subpath.as_deref(), Some("agents")); +} + #[test] fn resolves_run_level_clone_branch_controls() { let settings = WorkflowSettingsBuilder::from_toml( diff --git a/lib/crates/fabro-sandbox/src/config.rs b/lib/crates/fabro-sandbox/src/config.rs index 351865808..cab6759a5 100644 --- a/lib/crates/fabro-sandbox/src/config.rs +++ b/lib/crates/fabro-sandbox/src/config.rs @@ -16,12 +16,21 @@ use serde::{Deserialize, Serialize}; pub struct DaytonaSettings { pub auto_stop_interval: Option, pub labels: Option>, + #[serde(default)] + pub volumes: Vec, pub snapshot: Option, pub network: Option, #[serde(default)] pub skip_clone: bool, } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct DaytonaVolumeMount { + pub volume_id: String, + pub mount_path: String, + pub subpath: Option, +} + #[derive(Clone, Debug, PartialEq)] pub enum DaytonaNetwork { Block, diff --git a/lib/crates/fabro-sandbox/src/daytona/mod.rs b/lib/crates/fabro-sandbox/src/daytona/mod.rs index 6c5b5b128..c3f1e550b 100644 --- a/lib/crates/fabro-sandbox/src/daytona/mod.rs +++ b/lib/crates/fabro-sandbox/src/daytona/mod.rs @@ -52,7 +52,7 @@ pub const REQUIRED_DAYTONA_PERMISSIONS: &[Permissions] = &[ pub use crate::config::{ DaytonaNetwork, DaytonaSettings as DaytonaConfig, - DaytonaSnapshotSettings as DaytonaSnapshotConfig, DockerfileSource, + DaytonaSnapshotSettings as DaytonaSnapshotConfig, DaytonaVolumeMount, DockerfileSource, }; #[derive(Debug)] @@ -103,6 +103,20 @@ fn perm_wire_str(permission: Permissions) -> &'static str { } } +fn volume_mounts_for_create(config: &DaytonaConfig) -> Option> { + (!config.volumes.is_empty()).then(|| { + config + .volumes + .iter() + .map(|volume| daytona_sdk::VolumeMount { + volume_id: volume.volume_id.clone(), + mount_path: volume.mount_path.clone(), + subpath: volume.subpath.clone(), + }) + .collect() + }) +} + /// Build a [`daytona_sdk::Client`], forwarding an optional API key from the /// vault so the SDK doesn't have to rely on `DAYTONA_API_KEY` being in the /// process environment. @@ -448,6 +462,7 @@ impl DaytonaSandbox { ephemeral: Some(false), network_block_all, network_allow_list, + volumes: volume_mounts_for_create(&self.config), ..Default::default() } } @@ -2168,6 +2183,45 @@ mod tests { assert!(config.snapshot.is_none()); assert!(config.auto_stop_interval.is_none()); assert!(config.labels.is_none()); + assert!(config.volumes.is_empty()); + } + + #[test] + fn parses_volume_mounts_from_config() { + let config: DaytonaConfig = toml::from_str( + r#" +[[volumes]] +volume_id = "vol_auth" +mount_path = "/home/daytona/.config" +subpath = "agents" +"#, + ) + .expect("volume config should parse"); + + assert_eq!(config.volumes, vec![DaytonaVolumeMount { + volume_id: "vol_auth".to_string(), + mount_path: "/home/daytona/.config".to_string(), + subpath: Some("agents".to_string()), + }]); + } + + #[test] + fn maps_volume_mounts_to_daytona_create_params() { + let config = DaytonaConfig { + volumes: vec![DaytonaVolumeMount { + volume_id: "vol_auth".to_string(), + mount_path: "/home/daytona/.config".to_string(), + subpath: Some("agents".to_string()), + }], + ..DaytonaConfig::default() + }; + + let volumes = volume_mounts_for_create(&config).expect("volumes should map"); + + assert_eq!(volumes.len(), 1); + assert_eq!(volumes[0].volume_id, "vol_auth"); + assert_eq!(volumes[0].mount_path, "/home/daytona/.config"); + assert_eq!(volumes[0].subpath.as_deref(), Some("agents")); } #[tokio::test] diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs index 9cab3544e..eb8cb4fb7 100644 --- a/lib/crates/fabro-server/src/demo/mod.rs +++ b/lib/crates/fabro-server/src/demo/mod.rs @@ -1673,6 +1673,7 @@ mod runs { "project".to_string(), "api-server".to_string(), )]), + volumes: Vec::new(), snapshot: Some(DaytonaSnapshotSettings { name: "api-server-dev".into(), cpu: Some(4), diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs index 20b109880..8b1fe214e 100644 --- a/lib/crates/fabro-server/src/run_manifest.rs +++ b/lib/crates/fabro-server/src/run_manifest.rs @@ -17,7 +17,8 @@ use fabro_graphviz::render::apply_direction; use fabro_llm::model_test::{ModelTestStatus, run_basic_model_probe}; use fabro_model::{Catalog, ProviderId}; use fabro_sandbox::config::{ - DaytonaNetwork, DaytonaSnapshotSettings, DockerfileSource as SandboxDockerfileSource, + DaytonaNetwork, DaytonaSnapshotSettings, DaytonaVolumeMount, + DockerfileSource as SandboxDockerfileSource, }; use fabro_sandbox::daytona::DaytonaConfig; use fabro_sandbox::redact::redact_auth_url; @@ -1097,6 +1098,15 @@ fn runtime_daytona_config(settings: &DaytonaSettings, skip_clone: bool) -> Dayto DaytonaConfig { auto_stop_interval: settings.auto_stop_interval, labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()), + volumes: settings + .volumes + .iter() + .map(|volume| DaytonaVolumeMount { + volume_id: volume.volume_id.clone(), + mount_path: volume.mount_path.clone(), + subpath: volume.subpath.clone(), + }) + .collect(), snapshot: settings .snapshot .as_ref() @@ -1433,6 +1443,25 @@ enabled = {clone_enabled} (prepared, resolved) } + #[test] + fn runtime_daytona_config_preserves_volume_mounts() { + let settings = DaytonaSettings { + volumes: vec![fabro_types::settings::run::DaytonaVolumeSettings { + volume_id: "vol_auth".to_string(), + mount_path: "/home/daytona/.config".to_string(), + subpath: Some("agents".to_string()), + }], + ..DaytonaSettings::default() + }; + + let config = runtime_daytona_config(&settings, false); + + assert_eq!(config.volumes.len(), 1); + assert_eq!(config.volumes[0].volume_id, "vol_auth"); + assert_eq!(config.volumes[0].mount_path, "/home/daytona/.config"); + assert_eq!(config.volumes[0].subpath.as_deref(), Some("agents")); + } + #[test] fn prepare_manifest_inlines_project_config_daytona_dockerfile_from_bundle() { let mut manifest = minimal_manifest(); diff --git a/lib/crates/fabro-types/src/settings/run.rs b/lib/crates/fabro-types/src/settings/run.rs index d95a75226..8a10fe9dc 100644 --- a/lib/crates/fabro-types/src/settings/run.rs +++ b/lib/crates/fabro-types/src/settings/run.rs @@ -324,10 +324,19 @@ pub struct DockerSettings { pub struct DaytonaSettings { pub auto_stop_interval: Option, pub labels: HashMap, + #[serde(default)] + pub volumes: Vec, pub snapshot: Option, pub network: Option, } +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct DaytonaVolumeSettings { + pub volume_id: String, + pub mount_path: String, + pub subpath: Option, +} + #[derive(Debug, Clone, PartialEq)] pub enum DockerfileSource { Inline(String), diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs index 8b749fcfb..9ab9a61be 100644 --- a/lib/crates/fabro-workflow/src/operations/start.rs +++ b/lib/crates/fabro-workflow/src/operations/start.rs @@ -8,7 +8,8 @@ use fabro_interview::{AutoApproveInterviewer, Interviewer}; use fabro_mcp::config::{McpServerSettings, McpTransport}; use fabro_model::{AgentProfileKind, Catalog, FallbackTarget, Provider, ProviderId, adapter}; use fabro_sandbox::config::{ - DaytonaNetwork, DaytonaSnapshotSettings, DockerfileSource as SandboxDockerfileSource, + DaytonaNetwork, DaytonaSnapshotSettings, DaytonaVolumeMount, + DockerfileSource as SandboxDockerfileSource, }; use fabro_sandbox::daytona::DaytonaConfig; use fabro_sandbox::{DockerSandboxOptions, SandboxProvider, SandboxSpec}; @@ -680,6 +681,15 @@ fn runtime_daytona_config(settings: &DaytonaSettings, skip_clone: bool) -> Dayto DaytonaConfig { auto_stop_interval: settings.auto_stop_interval, labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()), + volumes: settings + .volumes + .iter() + .map(|volume| DaytonaVolumeMount { + volume_id: volume.volume_id.clone(), + mount_path: volume.mount_path.clone(), + subpath: volume.subpath.clone(), + }) + .collect(), snapshot: settings .snapshot .as_ref() @@ -1117,7 +1127,10 @@ mod tests { use std::time::Duration; use chrono::Utc; - use fabro_config::{RunCloneLayer, RunExecutionLayer, RunLayer, WorkflowSettingsBuilder}; + use fabro_config::{ + DaytonaSandboxLayer, DaytonaVolumeLayer, RunCloneLayer, RunExecutionLayer, RunLayer, + RunSandboxLayer, WorkflowSettingsBuilder, + }; use fabro_model::catalog::LlmCatalogSettings; use fabro_store::Database; use fabro_types::settings::ModelRef; @@ -1232,6 +1245,31 @@ mod tests { assert!(resolve_daytona_config(&settings.run).skip_clone); } + #[test] + fn runtime_daytona_config_preserves_volume_mounts() { + let settings = settings_from_run_layer(RunLayer { + sandbox: Some(RunSandboxLayer { + daytona: Some(DaytonaSandboxLayer { + volumes: Some(vec![DaytonaVolumeLayer { + volume_id: "vol_auth".to_string(), + mount_path: "/home/daytona/.config".to_string(), + subpath: Some("agents".to_string()), + }]), + ..DaytonaSandboxLayer::default() + }), + ..RunSandboxLayer::default() + }), + ..RunLayer::default() + }); + + let config = resolve_daytona_config(&settings.run); + + assert_eq!(config.volumes.len(), 1); + assert_eq!(config.volumes[0].volume_id, "vol_auth"); + assert_eq!(config.volumes[0].mount_path, "/home/daytona/.config"); + assert_eq!(config.volumes[0].subpath.as_deref(), Some("agents")); + } + #[test] fn start_record_git_options_honor_disabled_run_branch() { let mut settings = WorkflowSettings::default();