feat: remove volumes from environment settings

Drop the `volumes` field and `EnvironmentVolumeSettings` type from the
environment/sandbox configuration model across the whole stack:

- OpenAPI: remove `volumes` from Environment, Create/Replace requests,
  RunEnvironmentSettings, and EnvironmentSettings; delete the
  EnvironmentVolumeSettings schema.
- Generated clients: regenerate Rust types and TypeScript models;
  drop the EnvironmentVolumeSettings model and its exports.
- Config/env: remove the volumes layer, settings->layer conversion,
  and TOML serialization in fabro-config and fabro-environment.
- Sandbox/server/workflow: drop volume plumbing in fabro-sandbox
  (incl. Daytona), run manifest, and start operation.
- Migration: legacy daytona volumes are now reported unsupported
  rather than migrated.
- Docs and tests updated to match.

cargo check --workspace passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-05-31 22:28:45 -04:00
parent f8fdb5c2f4
commit 6e9d7e4079
No known key found for this signature in database
32 changed files with 70 additions and 412 deletions

View file

@ -258,7 +258,7 @@ On a same-machine setup, `settings.toml` is the shared machine-default layer und
On a remote setup, the client bundles workflow, project, and user config into the run manifest. The server then layers those bundled client configs over its own local defaults for run-shaped fields. Server-owned values like `[server.storage]`, `[server.api]`, `[server.web]`, and `[server.scheduler]` always come from the server machine's own `settings.toml` or `fabro server start` flags.
Merge rules follow the normative matrix: TOML `[run.inputs]` tables replace wholesale, CLI `-I` / `--input` values replayed from run manifests merge per key at highest precedence, environment `env` and `labels` merge by key, environment `volumes` replace as a whole list, `[run.prepare.steps]` replaces whole-list, and `[[run.hooks]]` merge by optional `id`. Most other fields use "higher-precedence wins" field-wise merging.
Merge rules follow the normative matrix: TOML `[run.inputs]` tables replace wholesale, CLI `-I` / `--input` values replayed from run manifests merge per key at highest precedence, environment `env` and `labels` merge by key, `[run.prepare.steps]` replaces whole-list, and `[[run.hooks]]` merge by optional `id`. Most other fields use "higher-precedence wins" field-wise merging.
### `[server.logging]` section

View file

@ -6316,7 +6316,6 @@ components:
- network
- lifecycle
- labels
- volumes
- env
properties:
id:
@ -6340,10 +6339,6 @@ components:
$ref: "#/components/schemas/EnvironmentLifecycleSettings"
labels:
$ref: "#/components/schemas/StringMap"
volumes:
type: array
items:
$ref: "#/components/schemas/EnvironmentVolumeSettings"
env:
type: object
additionalProperties:
@ -6361,7 +6356,6 @@ components:
- network
- lifecycle
- labels
- volumes
- env
properties:
id:
@ -6380,10 +6374,6 @@ components:
$ref: "#/components/schemas/EnvironmentLifecycleSettings"
labels:
$ref: "#/components/schemas/StringMap"
volumes:
type: array
items:
$ref: "#/components/schemas/EnvironmentVolumeSettings"
env:
type: object
additionalProperties:
@ -6400,7 +6390,6 @@ components:
- network
- lifecycle
- labels
- volumes
- env
properties:
provider:
@ -6415,10 +6404,6 @@ components:
$ref: "#/components/schemas/EnvironmentLifecycleSettings"
labels:
$ref: "#/components/schemas/StringMap"
volumes:
type: array
items:
$ref: "#/components/schemas/EnvironmentVolumeSettings"
env:
type: object
additionalProperties:
@ -13269,7 +13254,7 @@ components:
RunEnvironmentSettings:
type: object
required: [id, provider, image, resources, network, lifecycle, labels, volumes, env]
required: [id, provider, image, resources, network, lifecycle, labels, env]
properties:
id:
type: string
@ -13285,10 +13270,6 @@ components:
$ref: "#/components/schemas/EnvironmentLifecycleSettings"
labels:
$ref: "#/components/schemas/StringMap"
volumes:
type: array
items:
$ref: "#/components/schemas/EnvironmentVolumeSettings"
env:
type: object
additionalProperties:
@ -13296,7 +13277,7 @@ components:
EnvironmentSettings:
type: object
required: [provider, image, resources, network, lifecycle, labels, volumes, env]
required: [provider, image, resources, network, lifecycle, labels, env]
properties:
provider:
$ref: "#/components/schemas/EnvironmentProvider"
@ -13310,10 +13291,6 @@ components:
$ref: "#/components/schemas/EnvironmentLifecycleSettings"
labels:
$ref: "#/components/schemas/StringMap"
volumes:
type: array
items:
$ref: "#/components/schemas/EnvironmentVolumeSettings"
env:
type: object
additionalProperties:
@ -13373,17 +13350,6 @@ components:
auto_stop:
type: ["string", "null"]
EnvironmentVolumeSettings:
type: object
required: [id, mount_path, subpath]
properties:
id:
type: string
mount_path:
type: string
subpath:
type: ["string", "null"]
DockerfileSource:
oneOf:
- $ref: "#/components/schemas/DockerfileSourceInline"

View file

@ -5,7 +5,7 @@ description: "Reusable named execution environments for workflow runs"
Fabro separates **environments** from **sandboxes**:
- An **environment** is reusable desired configuration: provider, image, resources, network, lifecycle, labels, volumes, and environment variables.
- An **environment** is reusable desired configuration: provider, image, resources, network, lifecycle, labels, and environment variables.
- A **sandbox** is the concrete runtime instance Fabro creates for a run from the selected environment.
<Warning>
@ -52,11 +52,6 @@ auto_stop = "30m"
[environments.fabro-dev.labels]
repo = "fabro-sh/fabro"
[[environments.fabro-dev.volumes]]
id = "vol-agent-state"
mount_path = "/home/daytona/agent-state"
subpath = "auth"
[environments.fabro-dev.env]
NODE_ENV = "development"
```
@ -74,7 +69,7 @@ memory = "32GB"
preserve = true
```
`env` and `labels` merge by key. `volumes` replace as a whole list when set at a higher-precedence layer.
`env` and `labels` merge by key.
## Selecting an environment from the CLI
@ -122,7 +117,6 @@ stop_on_terminal = true
| `network.mode = "block"` | Error | Docker `none` network | Daytona block |
| `network.mode = "cidr_allow_list"` | Error | Error | Daytona CIDR allow-list |
| `labels` | Warning; ignored | Warning; ignored | Daytona labels |
| `volumes` | Warning; ignored | Warning; ignored | Daytona volume mounts |
| `lifecycle.auto_stop` | Warning; ignored | Warning; ignored | Daytona auto-stop |
| `env` | Process environment overlay | Container environment | Sandbox environment |
@ -191,11 +185,3 @@ mode = "cidr_allow_list"
allow = ["208.80.154.232/32", "10.0.0.0/8"]
```
Daytona volumes reference existing provider-managed volumes:
```toml
[[environments.cloud.volumes]]
id = "vol-agent-state"
mount_path = "/home/daytona/agent-state"
subpath = "agent-auth"
```

View file

@ -282,7 +282,6 @@ memory = "8GB"
| `lifecycle.stop_on_terminal` | Stop the sandbox when the run reaches a terminal state. |
| `lifecycle.auto_stop` | Daytona auto-stop duration, such as `"30m"`. |
| `labels` | Provider labels. Merge by key across layers. |
| `volumes` | Provider volume hints. Lists replace wholesale across layers. |
| `env` | Environment variables passed to command and agent execution. Merge by key across layers. |
When `provider = "local"`, Fabro runs directly in the resolved working
@ -560,7 +559,7 @@ provider = "daytona"
dockerfile = { path = "Dockerfile" }
```
Project defaults and workflow config values merge per the normative merge matrix: most fields merge by field (higher-precedence wins per key), TOML `run.inputs` tables replace wholesale, CLI input flags merge per key at highest precedence, environment `env` and `labels` merge by key, environment `volumes` replace as a whole list, and `run.prepare.steps` replaces whole-list.
Project defaults and workflow config values merge per the normative merge matrix: most fields merge by field (higher-precedence wins per key), TOML `run.inputs` tables replace wholesale, CLI input flags merge per key at highest precedence, environment `env` and `labels` merge by key, and `run.prepare.steps` replaces whole-list.
### Machine defaults

View file

@ -36,7 +36,6 @@ fn environment_settings_json() -> serde_json::Value {
"auto_stop": null
},
"labels": {},
"volumes": [],
"env": {}
})
}

View file

@ -955,8 +955,7 @@ fn attach_json_errors_without_prompting_for_human_input() {
"cpu": null,
"disk": null,
"memory": null
},
"volumes": []
}
},
"execution": {
"approval": "prompt",

View file

@ -183,7 +183,6 @@ fn inspect_resolves_selector_via_server_endpoint() {
"auto_stop": null
},
"labels": {},
"volumes": [],
"env": {}
},
"notifications": {},

View file

@ -3,7 +3,7 @@ use std::path::Path;
use std::str::FromStr;
use fabro_types::settings::run::EnvironmentProvider;
use toml_edit::{ArrayOfTables, DocumentMut, Item, Table, Value};
use toml_edit::{DocumentMut, Item, Table, Value};
use crate::{Error, Result};
@ -205,7 +205,6 @@ fn migrate_daytona(sandbox: &Table, env: &mut Table, unsupported: &mut Vec<Strin
}
}
"snapshot" => migrate_daytona_snapshot(item, env, unsupported),
"volumes" => copy_array_of_tables_with_volume_id(item, env, unsupported),
_ => item_path_keys(&format!("run.sandbox.daytona.{key}"), item, unsupported),
}
}
@ -291,50 +290,6 @@ fn copy_table(source: &Item, target: &mut Table) {
}
}
fn copy_array_of_tables_with_volume_id(
source: &Item,
target: &mut Table,
unsupported: &mut Vec<String>,
) {
let Some(volumes) = source.as_array_of_tables() else {
unsupported.push("run.sandbox.daytona.volumes".to_string());
return;
};
let mut migrated = ArrayOfTables::new();
for volume in volumes {
let mut migrated_volume = Table::new();
let mut has_id = false;
let mut has_mount_path = false;
for (key, item) in volume {
match key {
"volume_id" => {
has_id = true;
migrated_volume["id"] = item.clone();
}
"mount_path" => {
has_mount_path = true;
migrated_volume["mount_path"] = item.clone();
}
"subpath" => migrated_volume["subpath"] = item.clone(),
_ => item_path_keys(
&format!("run.sandbox.daytona.volumes.{key}"),
item,
unsupported,
),
}
}
if !has_id {
unsupported.push("run.sandbox.daytona.volumes.volume_id".to_string());
}
if !has_mount_path {
unsupported.push("run.sandbox.daytona.volumes.mount_path".to_string());
}
migrated.push(migrated_volume);
}
target["volumes"] = Item::ArrayOfTables(migrated);
}
fn item_path_keys(prefix: &str, item: &Item, out: &mut Vec<String>) {
if let Some(table) = item.as_table() {
if table.is_empty() {
@ -418,7 +373,7 @@ provider = "daytona"
}
#[test]
fn daytona_snapshot_labels_lifecycle_and_volumes_migrate() {
fn daytona_snapshot_labels_and_lifecycle_migrate() {
let migrated = migrate(
r#"
_version = 1
@ -442,11 +397,6 @@ cpu = 8
memory = "16GB"
disk = "20GB"
dockerfile = { path = "Dockerfile" }
[[run.sandbox.daytona.volumes]]
volume_id = "vol_auth"
mount_path = "/home/daytona/.config"
subpath = "agents"
"#,
);
@ -486,10 +436,30 @@ subpath = "agents"
resolved.env.get("NODE_ENV").map(InterpString::as_source),
Some("development".to_string())
);
assert_eq!(resolved.volumes.len(), 1);
assert_eq!(resolved.volumes[0].id, "vol_auth");
assert_eq!(resolved.volumes[0].mount_path, "/home/daytona/.config");
assert_eq!(resolved.volumes[0].subpath.as_deref(), Some("agents"));
}
#[test]
fn daytona_volumes_are_reported_unsupported() {
let err = migrate_contents(
r#"
_version = 1
[run.sandbox]
provider = "daytona"
[[run.sandbox.daytona.volumes]]
volume_id = "vol_auth"
mount_path = "/home/daytona/.config"
"#,
Path::new("settings.toml"),
)
.expect_err("daytona volumes can no longer be migrated");
let message = err.to_string();
assert!(
message.contains("run.sandbox.daytona.volumes"),
"message was: {message}"
);
}
#[test]

View file

@ -14,7 +14,7 @@ use fabro_types::settings::{Duration, InterpString, Size};
use super::LogFilter;
use super::cli::{CliAuthLayer, CliLoggingLayer, CliTargetLayer};
use super::environment::{EnvironmentDockerfileLayer, EnvironmentVolumeLayer};
use super::environment::EnvironmentDockerfileLayer;
use super::llm::{CostRates, CredentialRef, HeaderValueRef, ReasoningEffortFeature};
use super::run::{
HookAgentMarker, HookEntry, HookTlsMode, InterviewProviderLayer, ModelRefOrSplice,
@ -45,12 +45,6 @@ impl<T: Combine> Combine for Option<T> {
}
}
impl Combine for Option<Vec<EnvironmentVolumeLayer>> {
fn combine(self, other: Self) -> Self {
self.or(other)
}
}
macro_rules! impl_combine_or_option {
($($ty:ty),+ $(,)?) => {
$(

View file

@ -21,8 +21,6 @@ pub struct EnvironmentLayer {
pub lifecycle: Option<EnvironmentLifecycleLayer>,
#[serde(default, skip_serializing_if = "StickyMap::is_empty")]
pub labels: StickyMap<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub volumes: Option<Vec<EnvironmentVolumeLayer>>,
#[serde(default, skip_serializing_if = "StickyMap::is_empty")]
pub env: StickyMap<InterpString>,
}
@ -42,8 +40,6 @@ pub struct RunEnvironmentLayer {
pub lifecycle: Option<EnvironmentLifecycleLayer>,
#[serde(default, skip_serializing_if = "StickyMap::is_empty")]
pub labels: StickyMap<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub volumes: Option<Vec<EnvironmentVolumeLayer>>,
#[serde(default, skip_serializing_if = "StickyMap::is_empty")]
pub env: StickyMap<InterpString>,
}
@ -58,7 +54,6 @@ impl RunEnvironmentLayer {
network: self.network,
lifecycle: self.lifecycle,
labels: self.labels,
volumes: self.volumes,
env: self.env,
}
}
@ -117,15 +112,6 @@ pub struct EnvironmentLifecycleLayer {
pub auto_stop: Option<Duration>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EnvironmentVolumeLayer {
pub id: String,
pub mount_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subpath: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
pub enum EnvironmentDockerfileLayer {

View file

@ -18,8 +18,7 @@ pub use cli::{
pub(crate) use combine::Combine;
pub use environment::{
EnvironmentDockerfileLayer, EnvironmentImageLayer, EnvironmentLayer, EnvironmentLifecycleLayer,
EnvironmentNetworkLayer, EnvironmentResourcesLayer, EnvironmentVolumeLayer,
RunEnvironmentLayer,
EnvironmentNetworkLayer, EnvironmentResourcesLayer, RunEnvironmentLayer,
};
pub use llm::{
CostRates, CredentialRef, CredentialRefParseError, HeaderValueRef, LlmLayer, ModelControls,

View file

@ -44,21 +44,21 @@ pub use layers::{
CliAuthLayer, CliExecAgentLayer, CliExecLayer, CliExecModelLayer, CliLayer, CliLoggingLayer,
CliOutputLayer, CliTargetLayer, CliUpdatesLayer, CostRates, CredentialRef,
CredentialRefParseError, EnvironmentDockerfileLayer, EnvironmentImageLayer, EnvironmentLayer,
EnvironmentLifecycleLayer, EnvironmentNetworkLayer, EnvironmentResourcesLayer,
EnvironmentVolumeLayer, 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, RunEnvironmentLayer, RunExecutionLayer,
RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer, RunIntegrationsLayer, RunLayer,
RunMetaBranchLayer, RunModelControlsLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer,
RunRunBranchLayer, RunScmLayer, ScmGitHubLayer, ServerApiLayer, ServerArtifactsLayer,
ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerLayer,
ServerListenLayer, ServerLoggingLayer, ServerSandboxLayer, ServerSandboxProviderLayer,
ServerSandboxProvidersLayer, ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer,
ServerWebLayer, SettingsLayer, SlackIntegrationLayer, StickyMap, StringOrSplice, WorkflowLayer,
EnvironmentLifecycleLayer, EnvironmentNetworkLayer, EnvironmentResourcesLayer, 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, RunEnvironmentLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer,
RunIntegrationsGithubLayer, RunIntegrationsLayer, RunLayer, RunMetaBranchLayer,
RunModelControlsLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer,
RunScmLayer, ScmGitHubLayer, ServerApiLayer, ServerArtifactsLayer, ServerAuthGithubLayer,
ServerAuthLayer, ServerIntegrationsLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer,
ServerSandboxLayer, ServerSandboxProviderLayer, ServerSandboxProvidersLayer,
ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer, SettingsLayer,
SlackIntegrationLayer, StickyMap, StringOrSplice, WorkflowLayer,
};
pub use logging::{resolve_log_destination, resolve_log_destination_with_env};
pub use parse::ParseError;

View file

@ -1,15 +1,14 @@
use fabro_types::settings::run::{
DockerfileSource, EnvironmentImageSettings, EnvironmentLifecycleSettings,
EnvironmentNetworkMode, EnvironmentNetworkSettings, EnvironmentProvider,
EnvironmentResourcesSettings, EnvironmentSettings, EnvironmentVolumeSettings,
RunEnvironmentSettings,
EnvironmentResourcesSettings, EnvironmentSettings, RunEnvironmentSettings,
};
use super::ResolveError;
use crate::{
Combine, EnvironmentDockerfileLayer, EnvironmentImageLayer, EnvironmentLayer,
EnvironmentLifecycleLayer, EnvironmentNetworkLayer, EnvironmentResourcesLayer,
EnvironmentVolumeLayer, MergeMap, RunEnvironmentLayer,
EnvironmentLifecycleLayer, EnvironmentNetworkLayer, EnvironmentResourcesLayer, MergeMap,
RunEnvironmentLayer,
};
pub(crate) fn resolve_run_environment(
@ -77,7 +76,6 @@ fn resolve_environment_fields(
network: resolve_network(layer.network.as_ref(), &format!("{path}.network"), errors),
lifecycle: resolve_lifecycle(layer.lifecycle.as_ref()),
labels: layer.labels.clone().into_inner(),
volumes: resolve_volumes(layer.volumes.as_deref()),
env: layer.env.clone().into_inner(),
};
validate_daytona_image_settings(&environment, path, errors);
@ -174,18 +172,6 @@ fn resolve_lifecycle(layer: Option<&EnvironmentLifecycleLayer>) -> EnvironmentLi
}
}
fn resolve_volumes(layers: Option<&[EnvironmentVolumeLayer]>) -> Vec<EnvironmentVolumeSettings> {
layers
.unwrap_or(&[])
.iter()
.map(|volume| EnvironmentVolumeSettings {
id: volume.id.clone(),
mount_path: volume.mount_path.clone(),
subpath: volume.subpath.clone(),
})
.collect()
}
fn dockerfile_source(dockerfile: &EnvironmentDockerfileLayer) -> DockerfileSource {
match dockerfile {
EnvironmentDockerfileLayer::Inline(text) => DockerfileSource::Inline(text.clone()),

View file

@ -127,11 +127,6 @@ auto_stop = "30m"
[environments.fabro-dev.labels]
repo = "fabro-sh/fabro"
[[environments.fabro-dev.volumes]]
id = "vol_auth"
mount_path = "/home/daytona/.config"
subpath = "agents"
[environments.fabro-dev.env]
NODE_ENV = "development"
"#,
@ -170,10 +165,6 @@ NODE_ENV = "development"
environment.labels.get("repo").map(String::as_str),
Some("fabro-sh/fabro")
);
assert_eq!(environment.volumes.len(), 1);
assert_eq!(environment.volumes[0].id, "vol_auth");
assert_eq!(environment.volumes[0].mount_path, "/home/daytona/.config");
assert_eq!(environment.volumes[0].subpath.as_deref(), Some("agents"));
assert_eq!(
environment
.env

View file

@ -3,17 +3,17 @@ use std::path::Path;
use fabro_config::{
EnvironmentDockerfileLayer, EnvironmentImageLayer, EnvironmentLayer, EnvironmentLifecycleLayer,
EnvironmentNetworkLayer, EnvironmentResourcesLayer, EnvironmentVolumeLayer, StickyMap,
EnvironmentNetworkLayer, EnvironmentResourcesLayer, StickyMap,
};
use fabro_types::settings::InterpString;
use fabro_types::settings::run::{
DockerfileSource, EnvironmentImageSettings, EnvironmentLifecycleSettings,
EnvironmentNetworkMode, EnvironmentNetworkSettings, EnvironmentResourcesSettings,
EnvironmentSettings, EnvironmentVolumeSettings,
EnvironmentSettings,
};
use serde::{Deserialize, Serialize};
use tokio::fs;
use toml_edit::{Array, ArrayOfTables, DocumentMut, Item, Table, Value, value};
use toml_edit::{Array, DocumentMut, Item, Table, Value, value};
use crate::{
EnvironmentId, EnvironmentRevision, EnvironmentStoreError, EnvironmentValidationError,
@ -114,9 +114,6 @@ pub(crate) fn canonical_bytes(layer: &EnvironmentLayer) -> String {
append_lifecycle(doc.as_table_mut(), lifecycle);
}
append_string_map(doc.as_table_mut(), "labels", &layer.labels);
if let Some(volumes) = layer.volumes.as_deref() {
append_volumes(doc.as_table_mut(), volumes);
}
append_interp_map(doc.as_table_mut(), "env", &layer.env);
doc.to_string()
}
@ -188,7 +185,6 @@ fn environment_settings_to_layer(settings: &EnvironmentSettings) -> EnvironmentL
network: network_settings_to_layer(&settings.network),
lifecycle: lifecycle_settings_to_layer(&settings.lifecycle),
labels: StickyMap::from(settings.labels.clone()),
volumes: volumes_settings_to_layer(&settings.volumes),
env: StickyMap::from(settings.env.clone()),
}
}
@ -248,23 +244,6 @@ fn lifecycle_settings_to_layer(
})
}
fn volumes_settings_to_layer(
settings: &[EnvironmentVolumeSettings],
) -> Option<Vec<EnvironmentVolumeLayer>> {
if settings.is_empty() {
return None;
}
Some(settings.iter().map(volume_settings_to_layer).collect())
}
fn volume_settings_to_layer(settings: &EnvironmentVolumeSettings) -> EnvironmentVolumeLayer {
EnvironmentVolumeLayer {
id: settings.id.clone(),
mount_path: settings.mount_path.clone(),
subpath: settings.subpath.clone(),
}
}
fn append_image(root: &mut Table, image: &EnvironmentImageLayer) {
let table = ensure_table(root, &["image"]);
if let Some(docker) = image.docker.as_deref() {
@ -339,23 +318,6 @@ fn append_interp_map(root: &mut Table, name: &str, map: &StickyMap<InterpString>
}
}
fn append_volumes(root: &mut Table, volumes: &[EnvironmentVolumeLayer]) {
if volumes.is_empty() {
return;
}
let mut array = ArrayOfTables::new();
for volume in volumes {
let mut table = Table::new();
table["id"] = value(volume.id.as_str());
table["mount_path"] = value(volume.mount_path.as_str());
if let Some(subpath) = volume.subpath.as_deref() {
table["subpath"] = value(subpath);
}
array.push(table);
}
root["volumes"] = Item::ArrayOfTables(array);
}
fn ensure_table<'a>(root: &'a mut Table, path: &[&str]) -> &'a mut Table {
let mut current = root;
for key in path {

View file

@ -466,7 +466,6 @@ mod tests {
network: EnvironmentNetworkSettings::default(),
lifecycle: EnvironmentLifecycleSettings::default(),
labels: HashMap::new(),
volumes: Vec::new(),
env: HashMap::new(),
}
}

View file

@ -16,21 +16,12 @@ use serde::{Deserialize, Serialize};
pub struct DaytonaSettings {
pub auto_stop_interval: Option<i32>,
pub labels: Option<HashMap<String, String>>,
#[serde(default)]
pub volumes: Vec<DaytonaVolumeMount>,
pub snapshot: Option<DaytonaSnapshotSettings>,
pub network: Option<DaytonaNetwork>,
#[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<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum DaytonaNetwork {
Block,

View file

@ -54,7 +54,7 @@ pub const REQUIRED_DAYTONA_PERMISSIONS: &[Permissions] = &[
pub use crate::config::{
DaytonaNetwork, DaytonaSettings as DaytonaConfig,
DaytonaSnapshotSettings as DaytonaSnapshotConfig, DaytonaVolumeMount, DockerfileSource,
DaytonaSnapshotSettings as DaytonaSnapshotConfig, DockerfileSource,
};
pub mod snapshot_identity {
@ -173,20 +173,6 @@ fn perm_wire_str(permission: Permissions) -> &'static str {
}
}
fn volume_mounts_for_create(config: &DaytonaConfig) -> Option<Vec<daytona_sdk::VolumeMount>> {
(!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.
@ -555,7 +541,6 @@ impl DaytonaSandbox {
ephemeral: Some(false),
network_block_all,
network_allow_list,
volumes: volume_mounts_for_create(&self.config),
..Default::default()
}
}
@ -2368,45 +2353,6 @@ 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"));
}
#[test]

View file

@ -9,8 +9,7 @@ use fabro_types::settings::run::{EnvironmentNetworkMode, RunEnvironmentSettings}
#[cfg(feature = "daytona")]
use crate::config::{
DaytonaNetwork, DaytonaSnapshotSettings, DaytonaVolumeMount,
DockerfileSource as SandboxDockerfileSource,
DaytonaNetwork, DaytonaSnapshotSettings, DockerfileSource as SandboxDockerfileSource,
};
#[cfg(feature = "daytona")]
use crate::daytona::DaytonaConfig;
@ -29,15 +28,6 @@ pub fn daytona_config_from_environment(
.auto_stop
.map(|duration| duration_to_minutes_i32(duration.as_std())),
labels: (!settings.labels.is_empty()).then(|| settings.labels.clone()),
volumes: settings
.volumes
.iter()
.map(|volume| DaytonaVolumeMount {
volume_id: volume.id.clone(),
mount_path: volume.mount_path.clone(),
subpath: volume.subpath.clone(),
})
.collect(),
snapshot: settings
.image
.dockerfile

View file

@ -696,9 +696,6 @@ fn environment_capability_warnings(resolved_run: &RunNamespace) -> Vec<String> {
{
warnings.push("local provider ignores resource limits".to_string());
}
if !environment.volumes.is_empty() {
warnings.push("local provider ignores volume mounts".to_string());
}
if !environment.labels.is_empty() {
warnings.push("local provider ignores labels".to_string());
}
@ -710,9 +707,6 @@ fn environment_capability_warnings(resolved_run: &RunNamespace) -> Vec<String> {
if environment.resources.disk.is_some() {
warnings.push("docker provider ignores disk resource limits".to_string());
}
if !environment.volumes.is_empty() {
warnings.push("docker provider ignores volume mounts".to_string());
}
if !environment.labels.is_empty() {
warnings.push("docker provider ignores labels".to_string());
}
@ -1545,27 +1539,6 @@ enabled = {clone_enabled}
(prepared, resolved)
}
#[test]
fn runtime_daytona_config_preserves_volume_mounts() {
let settings = fabro_types::settings::run::RunEnvironmentSettings::from_environment(
"cloud".to_string(),
fabro_types::settings::run::EnvironmentSettings {
volumes: vec![fabro_types::settings::run::EnvironmentVolumeSettings {
id: "vol_auth".to_string(),
mount_path: "/home/daytona/.config".to_string(),
subpath: Some("agents".to_string()),
}],
..fabro_types::settings::run::EnvironmentSettings::default()
},
);
let config = daytona_config_from_environment(&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_accepts_project_environment_catalog_definitions() {
let mut manifest = minimal_manifest();

View file

@ -7,7 +7,7 @@ use fabro_types::settings::InterpString;
use fabro_types::settings::run::{
DockerfileSource, EnvironmentImageSettings, EnvironmentLifecycleSettings,
EnvironmentNetworkSettings, EnvironmentProvider, EnvironmentResourcesSettings,
EnvironmentSettings, EnvironmentVolumeSettings,
EnvironmentSettings,
};
use serde::de::IgnoredAny;
use serde::{Deserialize, Serialize};
@ -39,7 +39,6 @@ struct CreateEnvironmentRequest {
network: EnvironmentNetworkSettings,
lifecycle: EnvironmentLifecycleSettings,
labels: HashMap<String, String>,
volumes: Vec<EnvironmentVolumeSettings>,
env: HashMap<String, InterpString>,
}
@ -52,7 +51,6 @@ struct ReplaceEnvironmentRequest {
network: EnvironmentNetworkSettings,
lifecycle: EnvironmentLifecycleSettings,
labels: HashMap<String, String>,
volumes: Vec<EnvironmentVolumeSettings>,
env: HashMap<String, InterpString>,
}
@ -88,7 +86,6 @@ impl CreateEnvironmentRequest {
network: self.network,
lifecycle: self.lifecycle,
labels: self.labels,
volumes: self.volumes,
env: self.env,
},
})
@ -104,7 +101,6 @@ impl ReplaceEnvironmentRequest {
network: self.network,
lifecycle: self.lifecycle,
labels: self.labels,
volumes: self.volumes,
env: self.env,
})
}

View file

@ -34,7 +34,6 @@ fn environment_settings(provider: &str) -> Value {
"auto_stop": null
},
"labels": {},
"volumes": [],
"env": {}
})
}

View file

@ -36,13 +36,13 @@ pub use public_url::{
pub use run::{
ArtifactsSettings, DockerfileSource, EnvironmentImageSettings, EnvironmentLifecycleSettings,
EnvironmentNetworkMode, EnvironmentNetworkSettings, EnvironmentProvider,
EnvironmentResourcesSettings, EnvironmentSettings, EnvironmentVolumeSettings,
GitAuthorSettings, HookDefinition, HookType, InterviewProviderSettings, McpServerSettings,
McpTransport, NotificationProviderSettings, NotificationRouteSettings, PullRequestSettings,
RunAgentSettings, RunCheckpointSettings, RunEnvironmentSettings, RunExecutionSettings,
RunGitSettings, RunGoal, RunIntegrationsGithubSettings, RunIntegrationsSettings,
RunInterviewsSettings, RunModelControls, RunModelSettings, RunNamespace, RunPrepareSettings,
RunScmSettings, ScmGitHubSettings, TlsMode,
EnvironmentResourcesSettings, EnvironmentSettings, GitAuthorSettings, HookDefinition, HookType,
InterviewProviderSettings, McpServerSettings, McpTransport, NotificationProviderSettings,
NotificationRouteSettings, PullRequestSettings, RunAgentSettings, RunCheckpointSettings,
RunEnvironmentSettings, RunExecutionSettings, RunGitSettings, RunGoal,
RunIntegrationsGithubSettings, RunIntegrationsSettings, RunInterviewsSettings,
RunModelControls, RunModelSettings, RunNamespace, RunPrepareSettings, RunScmSettings,
ScmGitHubSettings, TlsMode,
};
pub use server::{
GithubIntegrationSettings, IntegrationWebhooksSettings, LogDestination, ObjectStoreSettings,

View file

@ -260,11 +260,6 @@ where
substitute_dockerfile_source(&mut environment.image.dockerfile, lookup)?;
substitute_string_vec(&mut environment.network.allow, lookup)?;
substitute_string_map(&mut environment.labels, lookup)?;
for volume in &mut environment.volumes {
substitute_string(&mut volume.id, lookup)?;
substitute_string(&mut volume.mount_path, lookup)?;
substitute_option_string(&mut volume.subpath, lookup)?;
}
Ok(())
}
@ -309,8 +304,8 @@ mod run_namespace_variable_substitution_tests {
use super::{
ArtifactsSettings, DockerfileSource, EnvironmentImageSettings, EnvironmentNetworkMode,
EnvironmentNetworkSettings, EnvironmentVolumeSettings, HookDefinition, HookEvent, HookType,
InterpString, McpHttpProtocol, McpServerSettings, McpTransport, RunCheckpointSettings,
EnvironmentNetworkSettings, HookDefinition, HookEvent, HookType, InterpString,
McpHttpProtocol, McpServerSettings, McpTransport, RunCheckpointSettings,
RunEnvironmentSettings, RunGoal, RunNamespace, RunPrepareSettings,
};
@ -428,11 +423,6 @@ mod run_namespace_variable_substitution_tests {
allow: vec!["{{ vars.CIDR }}".to_string()],
},
labels: HashMap::from([("deploy-env".to_string(), "{{ vars.ENV }}".to_string())]),
volumes: vec![EnvironmentVolumeSettings {
id: "vol_{{ vars.ENV }}".to_string(),
mount_path: "/mnt/{{ vars.ENV }}".to_string(),
subpath: Some("cache/{{ vars.ENV }}".to_string()),
}],
..RunEnvironmentSettings::default()
},
artifacts: ArtifactsSettings {
@ -464,12 +454,6 @@ mod run_namespace_variable_substitution_tests {
run.environment.labels.get("deploy-env").map(String::as_str),
Some("prod")
);
assert_eq!(run.environment.volumes[0].id, "vol_prod");
assert_eq!(run.environment.volumes[0].mount_path, "/mnt/prod");
assert_eq!(
run.environment.volumes[0].subpath.as_deref(),
Some("cache/prod")
);
assert_eq!(run.artifacts.include, vec!["reports/prod/**"]);
}
}
@ -803,13 +787,6 @@ impl Default for EnvironmentLifecycleSettings {
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct EnvironmentVolumeSettings {
pub id: String,
pub mount_path: String,
pub subpath: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EnvironmentSettings {
pub provider: EnvironmentProvider,
@ -818,7 +795,6 @@ pub struct EnvironmentSettings {
pub network: EnvironmentNetworkSettings,
pub lifecycle: EnvironmentLifecycleSettings,
pub labels: HashMap<String, String>,
pub volumes: Vec<EnvironmentVolumeSettings>,
pub env: HashMap<String, InterpString>,
}
@ -831,7 +807,6 @@ impl Default for EnvironmentSettings {
network: EnvironmentNetworkSettings::default(),
lifecycle: EnvironmentLifecycleSettings::default(),
labels: HashMap::new(),
volumes: Vec::new(),
env: HashMap::new(),
}
}
@ -846,7 +821,6 @@ pub struct RunEnvironmentSettings {
pub network: EnvironmentNetworkSettings,
pub lifecycle: EnvironmentLifecycleSettings,
pub labels: HashMap<String, String>,
pub volumes: Vec<EnvironmentVolumeSettings>,
pub env: HashMap<String, InterpString>,
}
@ -861,7 +835,6 @@ impl RunEnvironmentSettings {
network: environment.network,
lifecycle: environment.lifecycle,
labels: environment.labels,
volumes: environment.volumes,
env: environment.env,
}
}

View file

@ -1122,9 +1122,8 @@ mod tests {
use chrono::Utc;
use fabro_config::{
EnvironmentImageLayer, EnvironmentNetworkLayer, EnvironmentResourcesLayer,
EnvironmentVolumeLayer, RunCloneLayer, RunEnvironmentLayer, RunExecutionLayer, RunLayer,
StickyMap, WorkflowSettingsBuilder,
EnvironmentImageLayer, EnvironmentNetworkLayer, EnvironmentResourcesLayer, RunCloneLayer,
RunEnvironmentLayer, RunExecutionLayer, RunLayer, StickyMap, WorkflowSettingsBuilder,
};
use fabro_store::Database;
use fabro_types::settings::run::RunMode;
@ -1353,28 +1352,6 @@ reasoning = false
assert_eq!(config.env_vars, vec!["NODE_ENV=test"]);
}
#[test]
fn runtime_daytona_config_preserves_volume_mounts() {
let settings = settings_from_run_layer(RunLayer {
environment: Some(RunEnvironmentLayer {
volumes: Some(vec![EnvironmentVolumeLayer {
id: "vol_auth".to_string(),
mount_path: "/home/daytona/.config".to_string(),
subpath: Some("agents".to_string()),
}]),
..RunEnvironmentLayer::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();

View file

@ -131,7 +131,6 @@ models/environment-network-settings.ts
models/environment-provider.ts
models/environment-resources-settings.ts
models/environment-settings.ts
models/environment-volume-settings.ts
models/environment.ts
models/error-response-entry.ts
models/error-response.ts

View file

@ -28,9 +28,6 @@ import type { EnvironmentProvider } from './environment-provider';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentResourcesSettings } from './environment-resources-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentVolumeSettings } from './environment-volume-settings';
/**
* Request body for creating a server-managed environment.
@ -43,6 +40,5 @@ export interface CreateEnvironmentRequest {
'network': EnvironmentNetworkSettings;
'lifecycle': EnvironmentLifecycleSettings;
'labels': { [key: string]: string; };
'volumes': Array<EnvironmentVolumeSettings>;
'env': { [key: string]: string; };
}

View file

@ -28,9 +28,6 @@ import type { EnvironmentProvider } from './environment-provider';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentResourcesSettings } from './environment-resources-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentVolumeSettings } from './environment-volume-settings';
export interface EnvironmentSettings {
'provider': EnvironmentProvider;
@ -39,6 +36,5 @@ export interface EnvironmentSettings {
'network': EnvironmentNetworkSettings;
'lifecycle': EnvironmentLifecycleSettings;
'labels': { [key: string]: string; };
'volumes': Array<EnvironmentVolumeSettings>;
'env': { [key: string]: string; };
}

View file

@ -28,9 +28,6 @@ import type { EnvironmentProvider } from './environment-provider';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentResourcesSettings } from './environment-resources-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentVolumeSettings } from './environment-volume-settings';
/**
* Public server-managed environment definition.
@ -47,6 +44,5 @@ export interface Environment {
'network': EnvironmentNetworkSettings;
'lifecycle': EnvironmentLifecycleSettings;
'labels': { [key: string]: string; };
'volumes': Array<EnvironmentVolumeSettings>;
'env': { [key: string]: string; };
}

View file

@ -105,7 +105,6 @@ export * from './environment-network-settings';
export * from './environment-provider';
export * from './environment-resources-settings';
export * from './environment-settings';
export * from './environment-volume-settings';
export * from './error-response';
export * from './error-response-entry';
export * from './event-envelope';

View file

@ -28,9 +28,6 @@ import type { EnvironmentProvider } from './environment-provider';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentResourcesSettings } from './environment-resources-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentVolumeSettings } from './environment-volume-settings';
/**
* Request body for replacing a server-managed environment. The path id is authoritative.
@ -42,6 +39,5 @@ export interface ReplaceEnvironmentRequest {
'network': EnvironmentNetworkSettings;
'lifecycle': EnvironmentLifecycleSettings;
'labels': { [key: string]: string; };
'volumes': Array<EnvironmentVolumeSettings>;
'env': { [key: string]: string; };
}

View file

@ -28,9 +28,6 @@ import type { EnvironmentProvider } from './environment-provider';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentResourcesSettings } from './environment-resources-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentVolumeSettings } from './environment-volume-settings';
export interface RunEnvironmentSettings {
'id': string;
@ -40,6 +37,5 @@ export interface RunEnvironmentSettings {
'network': EnvironmentNetworkSettings;
'lifecycle': EnvironmentLifecycleSettings;
'labels': { [key: string]: string; };
'volumes': Array<EnvironmentVolumeSettings>;
'env': { [key: string]: string; };
}