From ec1b3f2084066046d3c3c3029e168223d1e6151f Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp <19+brynary@users.noreply.github.com> Date: Wed, 27 May 2026 11:52:35 -0400 Subject: [PATCH] feat(sandbox): secure daytona snapshot names (#429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Secures Daytona custom snapshot creation by removing user-controlled snapshot/image references and replacing them with deterministic names Fabro computes internally. Docker image selection now uses `image.docker`, while Daytona only accepts `image.dockerfile` for custom snapshots and continues to use `daytona-medium` when no Dockerfile is configured. ## Changes - Replaces public `image.ref` config/API shape with Docker-specific `image.docker` across Rust settings, OpenAPI, generated TypeScript client, docs, defaults, examples, and web samples. - Adds Daytona snapshot identity generation using HMAC-SHA256 over a canonical manifest keyed by the Daytona API key, producing `fabro-` snapshot names without exposing Dockerfile text or key material. - Routes Daytona custom Dockerfiles, including devcontainer-generated Dockerfiles, through the same computed identity path before calling Daytona snapshot APIs. - Updates sandbox initialization events and store projections so initialized run state can show the resolved image and computed Daytona snapshot after startup. - Updates legacy config migration behavior so Docker image refs map to `image.docker`, while Daytona legacy snapshot names are not preserved. ## Breaking Changes - `image.ref` is no longer accepted in new environment config. - Docker environments should use `image.docker` for image selection. - Daytona environments reject `image.docker`; use `image.dockerfile` to request a custom computed snapshot. ## Verification - `cargo build -p fabro-api` - `cd lib/packages/fabro-api-client && bun run generate` - `cd lib/packages/fabro-api-client && bun run typecheck` - `cd apps/fabro-web && bun run typecheck` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` - `ulimit -n 4096 && cargo nextest run --no-fail-fast -p fabro-cli -p fabro-config -p fabro-sandbox -p fabro-workflow -p fabro-store -p fabro-server -p fabro-api` - `cargo insta pending-snapshots` --- [![Compound Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) --- .fabro/project.toml | 1 - .fabro/workflows/daytona-medium/workflow.toml | 3 - Cargo.lock | 3 + .../app/routes/automation-detail.tsx | 2 +- docs/public/agents/mcp.mdx | 3 - docs/public/api-reference/fabro-api.yaml | 4 +- docs/public/examples/solitaire.mdx | 1 - docs/public/execution/devcontainers.mdx | 3 +- docs/public/execution/environments.mdx | 14 +- docs/public/execution/run-configuration.mdx | 9 +- docs/public/integrations/daytona.mdx | 10 +- .../fabro-api/tests/run_event_round_trip.rs | 29 ++ .../tests/workflow_settings_round_trip.rs | 5 + .../src/commands/run/run_progress/mod.rs | 4 + lib/crates/fabro-cli/tests/it/cmd/attach.rs | 4 +- lib/crates/fabro-cli/tests/it/cmd/inspect.rs | 2 +- ...26050101_legacy_sandbox_to_environments.rs | 45 ++- lib/crates/fabro-config/src/defaults.toml | 4 +- .../fabro-config/src/layers/environment.rs | 4 +- .../fabro-config/src/resolve/environment.rs | 14 +- .../fabro-config/src/tests/resolve_run.rs | 62 +++- lib/crates/fabro-manifest/src/lib.rs | 3 +- lib/crates/fabro-sandbox/Cargo.toml | 5 +- lib/crates/fabro-sandbox/src/config.rs | 1 - lib/crates/fabro-sandbox/src/daytona/mod.rs | 325 ++++++++++++++++-- .../fabro-sandbox/src/from_environment.rs | 27 +- lib/crates/fabro-sandbox/src/sandbox.rs | 10 + lib/crates/fabro-sandbox/src/sandbox_spec.rs | 5 +- lib/crates/fabro-server/src/demo/mod.rs | 2 +- lib/crates/fabro-server/src/run_manifest.rs | 2 - lib/crates/fabro-server/src/server/tests.rs | 8 + lib/crates/fabro-store/src/run_state.rs | 53 ++- lib/crates/fabro-types/src/run_event/infra.rs | 4 + lib/crates/fabro-types/src/settings/run.rs | 3 +- .../fabro-workflow/src/devcontainer_bridge.rs | 39 +-- .../fabro-workflow/src/event/convert.rs | 4 + lib/crates/fabro-workflow/src/event/events.rs | 4 + .../fabro-workflow/src/operations/retry.rs | 2 + .../fabro-workflow/src/operations/start.rs | 2 +- .../fabro-workflow/src/pipeline/initialize.rs | 2 + .../tests/it/daytona_integration.rs | 19 +- .../src/models/daytona-settings.ts | 28 -- .../src/models/daytona-snapshot-settings.ts | 26 -- .../src/models/environment-image-settings.ts | 2 +- .../src/models/run-sandbox-settings.ts | 34 -- 45 files changed, 564 insertions(+), 272 deletions(-) delete mode 100644 lib/packages/fabro-api-client/src/models/daytona-settings.ts delete mode 100644 lib/packages/fabro-api-client/src/models/daytona-snapshot-settings.ts delete mode 100644 lib/packages/fabro-api-client/src/models/run-sandbox-settings.ts diff --git a/.fabro/project.toml b/.fabro/project.toml index c6710f052..e4a408181 100644 --- a/.fabro/project.toml +++ b/.fabro/project.toml @@ -17,7 +17,6 @@ auto_stop = "30m" repo = "fabro-sh/fabro" [environments.fabro-dev.image] -ref = "fabro-v12" dockerfile = { path = "Dockerfile" } [environments.fabro-dev.resources] diff --git a/.fabro/workflows/daytona-medium/workflow.toml b/.fabro/workflows/daytona-medium/workflow.toml index 73032f3d0..337ec3e1e 100644 --- a/.fabro/workflows/daytona-medium/workflow.toml +++ b/.fabro/workflows/daytona-medium/workflow.toml @@ -8,6 +8,3 @@ id = "daytona-medium" [environments.daytona-medium] provider = "daytona" - -[environments.daytona-medium.image] -ref = "daytona-medium" diff --git a/Cargo.lock b/Cargo.lock index 42edbe8a5..b74f11aa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2238,12 +2238,15 @@ dependencies = [ "futures-util", "git2", "glob", + "hex", + "hmac", "httpmock", "rand 0.9.4", "reqwest-middleware", "rustls", "serde", "serde_json", + "sha2", "shlex", "strum", "tar", diff --git a/apps/fabro-web/app/routes/automation-detail.tsx b/apps/fabro-web/app/routes/automation-detail.tsx index fe6ebe3a0..ad6c2085a 100644 --- a/apps/fabro-web/app/routes/automation-detail.tsx +++ b/apps/fabro-web/app/routes/automation-detail.tsx @@ -44,7 +44,7 @@ function sampleSettings({ const environmentId = labels.project ?? snapshot; const environment = { provider: "daytona", - image: { ref: snapshot, dockerfile: null }, + image: { docker: null, dockerfile: null }, resources: { cpu, memory: `${memoryGb}GB`, diff --git a/docs/public/agents/mcp.mdx b/docs/public/agents/mcp.mdx index 18ddb6856..b3fda877c 100644 --- a/docs/public/agents/mcp.mdx +++ b/docs/public/agents/mcp.mdx @@ -255,9 +255,6 @@ id = "cloud" [environments.cloud] provider = "daytona" -[environments.cloud.image] -ref = "daytona-medium" - [run.artifacts] include = ["screenshots/**"] diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 74a50a00e..a319ff041 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -12347,9 +12347,9 @@ components: EnvironmentImageSettings: type: object - required: [ref, dockerfile] + required: [docker, dockerfile] properties: - ref: + docker: type: ["string", "null"] dockerfile: oneOf: diff --git a/docs/public/examples/solitaire.mdx b/docs/public/examples/solitaire.mdx index b13c699b2..9b6abeb86 100644 --- a/docs/public/examples/solitaire.mdx +++ b/docs/public/examples/solitaire.mdx @@ -278,7 +278,6 @@ id = "python-dev" provider = "daytona" [environments.python-dev.image] -ref = "python-dev" dockerfile = "FROM python:3.12-slim\nRUN apt-get update && apt-get install -y git libncurses-dev" [environments.python-dev.resources] diff --git a/docs/public/execution/devcontainers.mdx b/docs/public/execution/devcontainers.mdx index b807ad9fe..972d4e808 100644 --- a/docs/public/execution/devcontainers.mdx +++ b/docs/public/execution/devcontainers.mdx @@ -15,7 +15,7 @@ id = "dev" provider = "docker" [environments.dev.image] -ref = "ghcr.io/acme/project-devcontainer:latest" +docker = "ghcr.io/acme/project-devcontainer:latest" ``` For Daytona snapshot creation, provide a Dockerfile path on the selected environment: @@ -28,6 +28,5 @@ id = "cloud-dev" provider = "daytona" [environments.cloud-dev.image] -ref = "project-dev" dockerfile = { path = ".devcontainer/Dockerfile" } ``` diff --git a/docs/public/execution/environments.mdx b/docs/public/execution/environments.mdx index b45c4eb74..e9ff5858b 100644 --- a/docs/public/execution/environments.mdx +++ b/docs/public/execution/environments.mdx @@ -33,7 +33,6 @@ id = "fabro-dev" provider = "daytona" # local | docker | daytona [environments.fabro-dev.image] -ref = "fabro-v11" # Docker image or Daytona snapshot name dockerfile = { path = "Dockerfile" } [environments.fabro-dev.resources] @@ -99,7 +98,7 @@ id = "default" provider = "docker" [environments.default.image] -ref = "buildpack-deps:noble" +docker = "buildpack-deps:noble" [environments.default.resources] cpu = 2 @@ -114,8 +113,8 @@ stop_on_terminal = true | Environment field | Local | Docker | Daytona | |---|---|---|---| -| `image.ref` | Ignored | Docker image | Snapshot name | -| `image.dockerfile` | Ignored | Warning; ignored | Snapshot Dockerfile; requires `image.ref` | +| `image.docker` | Ignored | Docker image | Error | +| `image.dockerfile` | Ignored | Warning; ignored | Snapshot Dockerfile; Fabro computes the snapshot name | | `resources.cpu` | Warning; ignored | `cpu_quota = cpu * 100000` | Snapshot CPU | | `resources.memory` | Warning; ignored | Container memory limit | Snapshot memory | | `resources.disk` | Warning; ignored | Warning; ignored | Snapshot disk | @@ -143,7 +142,7 @@ Fabro hard-errors if a local environment asks for blocked or CIDR-restricted net ## Docker -Docker runs tools inside a container created from `image.ref`. Docker is the built-in default provider. +Docker runs tools inside a container created from `image.docker`. Docker is the built-in default provider. ```toml [run.environment] @@ -153,7 +152,7 @@ id = "ci" provider = "docker" [environments.ci.image] -ref = "buildpack-deps:noble" +docker = "buildpack-deps:noble" [environments.ci.resources] cpu = 2 @@ -167,7 +166,7 @@ Docker and Daytona are clone-based providers. When a run has a GitHub origin, Fa ## Daytona -Daytona runs tools in a cloud sandbox. `image.ref` is the snapshot name. If `image.dockerfile` is set and the snapshot does not exist, Fabro creates the snapshot; `image.ref` is required so the snapshot has a name. +Daytona runs tools in a cloud sandbox. Without `image.dockerfile`, Fabro uses Daytona's built-in `daytona-medium` snapshot. With `image.dockerfile`, Fabro computes a deterministic internal snapshot name from the Dockerfile, resource hints, a single-tenant scope, and the Daytona API key. ```toml [run.environment] @@ -177,7 +176,6 @@ id = "cloud" provider = "daytona" [environments.cloud.image] -ref = "rust-dev" dockerfile = { path = "Dockerfile" } [environments.cloud.resources] diff --git a/docs/public/execution/run-configuration.mdx b/docs/public/execution/run-configuration.mdx index b2b664565..8d9d55ad4 100644 --- a/docs/public/execution/run-configuration.mdx +++ b/docs/public/execution/run-configuration.mdx @@ -71,7 +71,6 @@ project = "fabro" env = "ci" [environments.cloud.image] -ref = "node-20" dockerfile = "FROM node:20-slim\nRUN apt-get update && apt-get install -y git" [environments.cloud.resources] @@ -248,7 +247,7 @@ id = "ci" provider = "docker" # local | docker | daytona [environments.ci.image] -ref = "buildpack-deps:noble" +docker = "buildpack-deps:noble" [environments.ci.resources] cpu = 2 @@ -274,8 +273,8 @@ memory = "8GB" |---|---| | `run.environment.id` | Environment slug to select. Defaults to `default`. | | `environments..provider` | Required provider: `local`, `docker`, or `daytona`. | -| `image.ref` | Docker image or Daytona snapshot name. | -| `image.dockerfile` | Inline Dockerfile or `{ path = "Dockerfile" }`; for Daytona, requires `image.ref`. | +| `image.docker` | Docker image. Daytona rejects this field. | +| `image.dockerfile` | Inline Dockerfile or `{ path = "Dockerfile" }`; Daytona uses it to create or reuse an internally named snapshot. | | `resources.cpu` / `memory` / `disk` | Best-effort resource hints. Unsupported provider fields warn and continue. | | `network.mode` | `allow_all`, `block`, or `cidr_allow_list`. Local cannot enforce blocked/CIDR networking; Docker cannot enforce CIDR allow-lists. | | `network.allow` | CIDRs for `cidr_allow_list`; entries are validated as CIDRs. | @@ -558,7 +557,7 @@ id = "cloud" provider = "daytona" [environments.cloud.image] -ref = "my-project-snapshot" +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. diff --git a/docs/public/integrations/daytona.mdx b/docs/public/integrations/daytona.mdx index 1cce4969b..6ad19b9b3 100644 --- a/docs/public/integrations/daytona.mdx +++ b/docs/public/integrations/daytona.mdx @@ -71,7 +71,6 @@ overwritten. [environments.cloud.image] -ref = "rust-dev" dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update && apt-get install -y git ripgrep" # Or keep the Dockerfile next to this TOML file: # dockerfile = { path = "./Dockerfile" } @@ -111,7 +110,6 @@ Snapshots let you pre-build an environment image so each run starts with depende ```toml title="run.toml" [environments.cloud.image] -ref = "my-snapshot" dockerfile = "FROM node:20-slim\nRUN apt-get update && apt-get install -y git" [environments.cloud.resources] @@ -120,10 +118,10 @@ memory = 8 disk = 20 ``` -When a run starts with a snapshot configured, Fabro looks up the snapshot by name. If it doesn't exist and a `dockerfile` is provided, Fabro creates it automatically and polls until it reaches `Active` state (up to 10 minutes). `dockerfile` can be inline content or `{ path = "..." }`; paths are resolved relative to the TOML file that declares them and are bundled into run manifests. If the snapshot already exists, it's reused immediately. +When a run starts with `image.dockerfile`, Fabro computes an internal snapshot name and looks up that snapshot in Daytona. If it doesn't exist, Fabro creates it automatically and polls until it reaches `Active` state (up to 10 minutes). `dockerfile` can be inline content or `{ path = "..." }`; paths are resolved relative to the TOML file that declares them and are bundled into run manifests. If the snapshot already exists, it's reused immediately. -If a snapshot is configured by name but doesn't exist and no `dockerfile` is provided, the run fails immediately. If no snapshot is configured at all, sandboxes are created from the `daytona-medium` snapshot which includes standard dev tools (git, etc.). +If no Dockerfile is configured, sandboxes are created from the `daytona-medium` snapshot which includes standard dev tools (git, etc.). To force a new custom snapshot, change the Dockerfile text, for example by adding a comment. ## Private repositories @@ -214,9 +212,9 @@ The `DAYTONA_API_KEY` vault secret is missing, invalid, or missing the required If doctor reports missing scopes, regenerate the Daytona key with `write:snapshots`, `delete:snapshots`, `write:sandboxes`, and `delete:sandboxes`, then save it again with `fabro secret set DAYTONA_API_KEY`. -### "Snapshot does not exist and no dockerfile provided" +### Custom snapshot did not roll -The run config references a snapshot name that doesn't exist on Daytona, and no `dockerfile` is provided to create it. Either create the snapshot manually in the Daytona dashboard or add `image.dockerfile` under the selected `[environments..image]`. +Custom Daytona snapshot names are computed from the Dockerfile, resource hints, tenant scope, and Daytona API key. To force a new custom snapshot, change `image.dockerfile` text under the selected `[environments..image]`. ### "Timed out waiting for snapshot to become active" diff --git a/lib/crates/fabro-api/tests/run_event_round_trip.rs b/lib/crates/fabro-api/tests/run_event_round_trip.rs index e39f3d8f4..dc2c9297f 100644 --- a/lib/crates/fabro-api/tests/run_event_round_trip.rs +++ b/lib/crates/fabro-api/tests/run_event_round_trip.rs @@ -46,6 +46,35 @@ fn run_event_round_trips_run_created_with_web_url() { assert_run_event_round_trip(value); } +#[test] +fn run_event_round_trips_sandbox_initialized_image_and_snapshot() { + assert_run_event_round_trip(json!({ + "id": "evt_sandbox_initialized", + "ts": "2026-04-29T12:00:00Z", + "run_id": fixtures::RUN_1, + "event": "sandbox.initialized", + "properties": { + "provider": "daytona", + "id": "fabro-run-sandbox", + "working_directory": "/home/daytona/workspace", + "snapshot": "fabro-11111111-2222-8333-8444-555555555555" + } + })); + + assert_run_event_round_trip(json!({ + "id": "evt_sandbox_initialized_docker", + "ts": "2026-04-29T12:01:00Z", + "run_id": fixtures::RUN_1, + "event": "sandbox.initialized", + "properties": { + "provider": "docker", + "id": "container-id", + "working_directory": "/workspace", + "image": "ubuntu:24.04" + } + })); +} + #[test] fn run_event_round_trips_run_interrupt() { let value = json!({ diff --git a/lib/crates/fabro-api/tests/workflow_settings_round_trip.rs b/lib/crates/fabro-api/tests/workflow_settings_round_trip.rs index 77842ff9e..1f528ecb5 100644 --- a/lib/crates/fabro-api/tests/workflow_settings_round_trip.rs +++ b/lib/crates/fabro-api/tests/workflow_settings_round_trip.rs @@ -40,6 +40,11 @@ approval = "auto" assert_eq!(json["run"]["goal"]["type"], "inline"); assert_eq!(json["run"]["goal"]["value"], "Ship it"); assert_eq!(json["run"]["execution"]["approval"], "auto"); + assert_eq!( + json["run"]["environment"]["image"]["docker"], + "buildpack-deps:noble" + ); + assert!(json["run"]["environment"]["image"].get("ref").is_none()); let round_trip: ApiWorkflowSettings = serde_json::from_value(json).expect("workflow settings should deserialize"); diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs b/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs index 782d2badf..c6a03674a 100644 --- a/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs +++ b/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs @@ -746,6 +746,8 @@ mod tests { repos_root: None, primary_repo_path: None, primary_repo_link: None, + image: None, + snapshot: None, }, agent_event("code", AgentEvent::ToolCallStarted { tool_name: "read_file".into(), @@ -1128,6 +1130,8 @@ mod tests { repos_root: None, primary_repo_path: None, primary_repo_link: None, + image: None, + snapshot: None, }); emit( &mut ui, diff --git a/lib/crates/fabro-cli/tests/it/cmd/attach.rs b/lib/crates/fabro-cli/tests/it/cmd/attach.rs index 04556ad4b..829891585 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/attach.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/attach.rs @@ -937,8 +937,8 @@ fn attach_json_errors_without_prompting_for_human_input() { "env": {}, "id": "local", "image": { - "dockerfile": null, - "ref": null + "docker": null, + "dockerfile": null }, "labels": {}, "lifecycle": { diff --git a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs index ebcf1a2a0..ec0d191e3 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs @@ -165,7 +165,7 @@ fn inspect_resolves_selector_via_server_endpoint() { "id": "default", "provider": "local", "image": { - "ref": null, + "docker": null, "dockerfile": null }, "resources": { diff --git a/lib/crates/fabro-config/migrations/2026050101_legacy_sandbox_to_environments.rs b/lib/crates/fabro-config/migrations/2026050101_legacy_sandbox_to_environments.rs index b6b4c2515..d276c9891 100644 --- a/lib/crates/fabro-config/migrations/2026050101_legacy_sandbox_to_environments.rs +++ b/lib/crates/fabro-config/migrations/2026050101_legacy_sandbox_to_environments.rs @@ -64,8 +64,14 @@ pub(crate) fn migrate_settings_path( )) })?; + let environment_id = layer + .run + .as_ref() + .and_then(|run| run.environment.as_ref()) + .and_then(|environment| environment.id.as_deref()) + .unwrap_or("default"); let warning = format!( - "Migrated legacy [run.sandbox] settings in {} to [run.environment] and [environments.default]. Backup written to {}. {REMOVAL_NOTE}", + "Migrated legacy [run.sandbox] settings in {} to [run.environment] and [environments.{environment_id}]. Backup written to {}. {REMOVAL_NOTE}", path.display(), backup_path.display() ); @@ -88,7 +94,7 @@ fn migrate_contents(original_contents: &str, path: &Path) -> Result bool { .and_then(Item::as_table) .and_then(|run| run.get("environment")) .is_some(); - let has_default_environment = doc - .get("environments") - .and_then(Item::as_table) - .and_then(|envs| envs.get("default")) - .is_some(); - has_run_environment || has_default_environment + let has_environment_catalog = doc.get("environments").and_then(Item::as_table).is_some(); + has_run_environment || has_environment_catalog } fn migrate_document(doc: &mut DocumentMut) -> std::result::Result<(), MigrationFailure> { @@ -185,10 +187,14 @@ fn migrate_document(doc: &mut DocumentMut) -> std::result::Result<(), MigrationF ensure_table(doc.as_table_mut(), &["run", "clone"])["enabled"] = Item::Value(Value::from(false)); } + let environment_id = match active_provider { + Some(EnvironmentProvider::Daytona) => "daytona", + _ => "default", + }; ensure_table(doc.as_table_mut(), &["run", "environment"])["id"] = - Item::Value(Value::from("default")); + Item::Value(Value::from(environment_id)); - let env = ensure_table(doc.as_table_mut(), &["environments", "default"]); + let env = ensure_table(doc.as_table_mut(), &["environments", environment_id]); if let Some(p) = provider_str { env["provider"] = Item::Value(Value::from(p)); } @@ -271,7 +277,7 @@ fn migrate_daytona_snapshot(snapshot_item: &Item, env: &mut Table, unsupported: for (key, item) in snapshot { match key { - "name" => ensure_table(env, &["image"])["ref"] = item.clone(), + "name" => {} "cpu" => ensure_table(env, &["resources"])["cpu"] = item.clone(), "memory" => ensure_table(env, &["resources"])["memory"] = item.clone(), "disk" => ensure_table(env, &["resources"])["disk"] = item.clone(), @@ -301,7 +307,7 @@ fn migrate_docker(sandbox: &Table, env: &mut Table, unsupported: &mut Vec ensure_table(env, &["image"])["ref"] = item.clone(), + "image" => ensure_table(env, &["image"])["docker"] = item.clone(), "memory_limit" => ensure_table(env, &["resources"])["memory"] = item.clone(), "cpu_quota" => { if let Some(cpu_quota) = item.as_integer() { @@ -488,7 +494,7 @@ mod tests { } #[test] - fn provider_only_daytona_config_migrates_to_default_environment() { + fn provider_only_daytona_config_migrates_to_daytona_environment() { let migrated = migrate( r#" _version = 1 @@ -505,10 +511,10 @@ provider = "daytona" .expect("migrated settings should resolve") .run; - assert_eq!(resolved.environment.id, "default"); + assert_eq!(resolved.environment.id, "daytona"); assert_eq!(resolved.environment.provider, EnvironmentProvider::Daytona); assert!(migrated.contains("[run.environment]")); - assert!(migrated.contains("[environments.default]")); + assert!(migrated.contains("[environments.daytona]")); assert!(!migrated.contains("[run.sandbox]")); } @@ -561,7 +567,9 @@ subpath = "agents" .run .environment; - assert_eq!(resolved.image.reference.as_deref(), Some("fabro-v11")); + assert_eq!(resolved.id, "daytona"); + assert_eq!(resolved.image.docker.as_deref(), None); + assert!(resolved.image.dockerfile.is_some()); assert_eq!(resolved.resources.cpu, Some(8)); assert_eq!( resolved.resources.memory.map(|size| size.as_bytes()), @@ -619,7 +627,7 @@ cpu_quota = 200000 assert_eq!(resolved.provider, EnvironmentProvider::Docker); assert_eq!( - resolved.image.reference.as_deref(), + resolved.image.docker.as_deref(), Some("buildpack-deps:noble") ); assert_eq!(resolved.resources.cpu, Some(2)); @@ -674,7 +682,8 @@ provider = "daytona" assert_eq!(backup, original); assert!(rewritten.contains("[run.environment]")); - assert!(rewritten.contains("[environments.default]")); + assert!(rewritten.contains("[environments.daytona]")); + assert!(report.warning.contains("[environments.daytona]")); assert!(report.warning.contains("temporary compatibility migration")); } diff --git a/lib/crates/fabro-config/src/defaults.toml b/lib/crates/fabro-config/src/defaults.toml index 4c78d1609..faecfdeb5 100644 --- a/lib/crates/fabro-config/src/defaults.toml +++ b/lib/crates/fabro-config/src/defaults.toml @@ -30,7 +30,7 @@ id = "default" provider = "docker" [environments.default.image] -ref = "buildpack-deps:noble" +docker = "buildpack-deps:noble" [environments.default.resources] cpu = 2 @@ -47,7 +47,7 @@ provider = "local" provider = "docker" [environments.docker.image] -ref = "buildpack-deps:noble" +docker = "buildpack-deps:noble" [environments.docker.resources] cpu = 2 diff --git a/lib/crates/fabro-config/src/layers/environment.rs b/lib/crates/fabro-config/src/layers/environment.rs index e72103fb0..f2c39dd30 100644 --- a/lib/crates/fabro-config/src/layers/environment.rs +++ b/lib/crates/fabro-config/src/layers/environment.rs @@ -67,8 +67,8 @@ impl RunEnvironmentLayer { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)] #[serde(deny_unknown_fields)] pub struct EnvironmentImageLayer { - #[serde(default, rename = "ref", skip_serializing_if = "Option::is_none")] - pub reference: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub docker: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub dockerfile: Option, } diff --git a/lib/crates/fabro-config/src/resolve/environment.rs b/lib/crates/fabro-config/src/resolve/environment.rs index 7706254d8..32f1907b6 100644 --- a/lib/crates/fabro-config/src/resolve/environment.rs +++ b/lib/crates/fabro-config/src/resolve/environment.rs @@ -66,7 +66,7 @@ fn resolve_environment_layer( volumes: resolve_volumes(layer.volumes.as_deref()), env: layer.env.clone().into_inner(), }; - validate_daytona_snapshot_name(&environment, path, errors); + validate_daytona_image_settings(&environment, path, errors); environment } @@ -87,7 +87,7 @@ fn resolve_image(layer: Option<&EnvironmentImageLayer>) -> EnvironmentImageSetti return EnvironmentImageSettings::default(); }; EnvironmentImageSettings { - reference: layer.reference.clone(), + docker: layer.docker.clone(), dockerfile: layer.dockerfile.as_ref().map(dockerfile_source), } } @@ -179,19 +179,15 @@ fn dockerfile_source(dockerfile: &EnvironmentDockerfileLayer) -> DockerfileSourc } } -fn validate_daytona_snapshot_name( +fn validate_daytona_image_settings( environment: &EnvironmentSettings, path: &str, errors: &mut Vec, ) { - if environment.provider == EnvironmentProvider::Daytona - && environment.image.dockerfile.is_some() - && environment.image.reference.is_none() - { + if environment.provider == EnvironmentProvider::Daytona && environment.image.docker.is_some() { errors.push(ResolveError::Invalid { path: format!("{path}.image"), - reason: "daytona environments with image.dockerfile must also set image.ref" - .to_string(), + reason: "daytona environments do not support image.docker; use image.dockerfile for custom snapshots".to_string(), }); } } diff --git a/lib/crates/fabro-config/src/tests/resolve_run.rs b/lib/crates/fabro-config/src/tests/resolve_run.rs index 408210a54..930117984 100644 --- a/lib/crates/fabro-config/src/tests/resolve_run.rs +++ b/lib/crates/fabro-config/src/tests/resolve_run.rs @@ -48,7 +48,7 @@ fn resolves_run_defaults_from_empty_settings() { assert_eq!(settings.environment.id, "default"); assert_eq!(settings.environment.provider, EnvironmentProvider::Docker); assert_eq!( - settings.environment.image.reference.as_deref(), + settings.environment.image.docker.as_deref(), Some("buildpack-deps:noble") ); assert_eq!(settings.environment.resources.cpu, Some(2)); @@ -89,7 +89,7 @@ preserve = true provider = "daytona" [environments.fabro-dev.image] -ref = "fabro-v11" +dockerfile = "FROM ubuntu:24.04" [environments.fabro-dev.resources] cpu = 8 @@ -123,7 +123,8 @@ NODE_ENV = "development" assert_eq!(environment.id, "fabro-dev"); assert_eq!(environment.provider, EnvironmentProvider::Daytona); - assert_eq!(environment.image.reference.as_deref(), Some("fabro-v11")); + assert_eq!(environment.image.docker.as_deref(), None); + assert!(environment.image.dockerfile.is_some()); assert_eq!(environment.resources.cpu, Some(8)); assert_eq!( environment.resources.memory.map(|size| size.as_bytes()), @@ -390,7 +391,7 @@ provider = "local" assert_eq!(settings.environment.id, "host"); assert_eq!(settings.environment.provider, EnvironmentProvider::Local); - assert!(settings.environment.image.reference.is_none()); + assert!(settings.environment.image.docker.is_none()); } #[test] @@ -465,8 +466,8 @@ mode = "block" } #[test] -fn daytona_dockerfile_without_image_ref_errors() { - let err = WorkflowSettingsBuilder::from_toml( +fn daytona_dockerfile_without_image_ref_resolves() { + let settings = WorkflowSettingsBuilder::from_toml( r#" _version = 1 @@ -480,12 +481,55 @@ provider = "daytona" dockerfile = { path = "Dockerfile" } "#, ) - .expect_err("daytona dockerfile needs a snapshot name"); + .expect("daytona dockerfile should not need a user-supplied snapshot name") + .run; + + assert_eq!(settings.environment.provider, EnvironmentProvider::Daytona); + assert!(settings.environment.image.docker.is_none()); + assert!(settings.environment.image.dockerfile.is_some()); +} + +#[test] +fn daytona_image_docker_errors() { + let err = WorkflowSettingsBuilder::from_toml( + r#" +_version = 1 + +[run.environment] +id = "cloud" + +[environments.cloud] +provider = "daytona" + +[environments.cloud.image] +docker = "ubuntu:24.04" +"#, + ) + .expect_err("daytona should reject docker image selection"); let message = err.to_string(); assert!( - message.contains("image.ref"), - "expected daytona dockerfile/image.ref diagnostic, got: {message}" + message.contains("image.docker") && message.contains("daytona"), + "expected daytona image.docker diagnostic, got: {message}" + ); +} + +#[test] +fn image_ref_is_rejected_as_unknown_field() { + let err = WorkflowSettingsBuilder::from_toml( + r#" +_version = 1 + +[run.environment.image] +ref = "ubuntu:24.04" +"#, + ) + .expect_err("image.ref should not be accepted"); + + let message = err.to_string(); + assert!( + message.contains("unknown field") && message.contains("ref"), + "expected unknown field diagnostic for image.ref, got: {message}" ); } diff --git a/lib/crates/fabro-manifest/src/lib.rs b/lib/crates/fabro-manifest/src/lib.rs index 088178b6b..f8c8eafe1 100644 --- a/lib/crates/fabro-manifest/src/lib.rs +++ b/lib/crates/fabro-manifest/src/lib.rs @@ -84,7 +84,7 @@ pub fn build_run_overrides(input: RunOverrideInput<'_>) -> RunLayer { .then(|| RunEnvironmentLayer { id: input.environment.map(ToOwned::to_owned), image: input.docker_image.map(|image| EnvironmentImageLayer { - reference: Some(image.to_string()), + docker: Some(image.to_string()), ..EnvironmentImageLayer::default() }), lifecycle: input @@ -1215,7 +1215,6 @@ id = "daytona" provider = "daytona" [environments.daytona.image] -ref = "fabro-test" dockerfile = { path = "Dockerfile" } "#, ) diff --git a/lib/crates/fabro-sandbox/Cargo.toml b/lib/crates/fabro-sandbox/Cargo.toml index cdb6aad9d..3ccf6e5e3 100644 --- a/lib/crates/fabro-sandbox/Cargo.toml +++ b/lib/crates/fabro-sandbox/Cargo.toml @@ -30,6 +30,10 @@ serde_json.workspace = true strum.workspace = true tracing.workspace = true base64.workspace = true +hmac.workspace = true +sha2.workspace = true +hex.workspace = true +uuid.workspace = true fabro-proc = { path = "../fabro-proc" } fabro-static.workspace = true fabro-util = { path = "../fabro-util" } @@ -66,7 +70,6 @@ rustls = { version = "0.23", default-features = false, features = ["std", "ring" [dev-dependencies] tokio = { workspace = true, features = ["test-util", "macros"] } tempfile = "3" -uuid.workspace = true serde_json.workspace = true toml.workspace = true httpmock = "0.8" diff --git a/lib/crates/fabro-sandbox/src/config.rs b/lib/crates/fabro-sandbox/src/config.rs index cab6759a5..68fe23127 100644 --- a/lib/crates/fabro-sandbox/src/config.rs +++ b/lib/crates/fabro-sandbox/src/config.rs @@ -125,7 +125,6 @@ pub enum DockerfileSource { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct DaytonaSnapshotSettings { - pub name: String, pub cpu: Option, pub memory: Option, pub disk: Option, diff --git a/lib/crates/fabro-sandbox/src/daytona/mod.rs b/lib/crates/fabro-sandbox/src/daytona/mod.rs index 3f7faceca..0b07a88b5 100644 --- a/lib/crates/fabro-sandbox/src/daytona/mod.rs +++ b/lib/crates/fabro-sandbox/src/daytona/mod.rs @@ -57,6 +57,74 @@ pub use crate::config::{ DaytonaSnapshotSettings as DaytonaSnapshotConfig, DaytonaVolumeMount, DockerfileSource, }; +pub mod snapshot_identity { + use hmac::{Hmac, Mac}; + use serde::Serialize; + use sha2::{Digest, Sha256}; + use uuid::Uuid; + + use super::{DaytonaSnapshotConfig, DockerfileSource}; + + const IDENTITY_VERSION: u8 = 1; + const PROVIDER: &str = "daytona"; + const TENANT: &str = "single-tenant"; + + type HmacSha256 = Hmac; + + #[derive(Serialize)] + struct SnapshotManifest<'a> { + identity_version: u8, + provider: &'static str, + tenant: &'static str, + dockerfile_sha256: &'a str, + cpu: Option, + memory_gb: Option, + disk_gb: Option, + entrypoint: Option<&'static str>, + } + + pub fn snapshot_name(api_key: &str, config: &DaytonaSnapshotConfig) -> crate::Result { + let manifest = canonical_manifest(config)?; + let mut mac = HmacSha256::new_from_slice(api_key.as_bytes()) + .expect("HMAC-SHA256 accepts keys of any length"); + mac.update(&manifest); + let digest = mac.finalize().into_bytes(); + let mut bytes = [0_u8; 16]; + bytes.copy_from_slice(&digest[..16]); + Ok(format!("fabro-{}", Uuid::new_v8(bytes))) + } + + fn canonical_manifest(config: &DaytonaSnapshotConfig) -> crate::Result> { + let dockerfile = match &config.dockerfile { + Some(DockerfileSource::Inline(text)) => text.as_str(), + Some(DockerfileSource::Path { .. }) => { + return Err(crate::Error::message( + "Daytona snapshot dockerfile path should have been resolved to inline content before sandbox creation", + )); + } + None => { + return Err(crate::Error::message( + "Daytona custom snapshots require image.dockerfile", + )); + } + }; + let dockerfile_sha256 = hex::encode(Sha256::digest(dockerfile.as_bytes())); + let manifest = SnapshotManifest { + identity_version: IDENTITY_VERSION, + provider: PROVIDER, + tenant: TENANT, + dockerfile_sha256: &dockerfile_sha256, + cpu: config.cpu, + memory_gb: config.memory, + disk_gb: config.disk, + entrypoint: None, + }; + serde_json::to_vec(&manifest).map_err(|err| { + crate::Error::context("Failed to serialize Daytona snapshot identity", err) + }) + } +} + #[derive(Debug)] pub struct DaytonaKeyCheck { pub key_name: String, @@ -128,6 +196,18 @@ async fn build_daytona_client( build_daytona_client_with(api_key, None, None, None).await } +#[expect( + clippy::disallowed_methods, + reason = "Standalone Daytona sandbox construction falls back to the documented process env var." +)] +fn resolve_daytona_api_key(api_key: Option) -> Option { + api_key.filter(|key| !key.is_empty()).or_else(|| { + std::env::var(EnvVars::DAYTONA_API_KEY) + .ok() + .filter(|key| !key.is_empty()) + }) +} + pub(crate) async fn build_daytona_client_with( api_key: Option, api_url: Option, @@ -242,8 +322,10 @@ fn command_kind(command: &str) -> &'static str { pub struct DaytonaSandbox { config: DaytonaConfig, client: daytona_sdk::Client, + api_key: Option, github_app: Option, sandbox: OnceCell, + snapshot_name: OnceCell, rg_available: OnceCell, event_callback: Option, /// HTTPS origin URL stored after clone so we can refresh push credentials @@ -271,14 +353,17 @@ impl DaytonaSandbox { clone_branch: Option, api_key: Option, ) -> crate::Result { - let client = build_daytona_client(api_key) + let api_key = resolve_daytona_api_key(api_key); + let client = build_daytona_client(api_key.clone()) .await .map_err(|e| crate::Error::context("Failed to create Daytona client", e))?; Ok(Self { config, client, + api_key, github_app, sandbox: OnceCell::new(), + snapshot_name: OnceCell::new(), rg_available: OnceCell::const_new(), event_callback: None, origin_url: OnceCell::new(), @@ -302,7 +387,8 @@ impl DaytonaSandbox { clone_origin_url: Option, clone_branch: Option, ) -> crate::Result { - let client = build_daytona_client(api_key) + let api_key = resolve_daytona_api_key(api_key); + let client = build_daytona_client(api_key.clone()) .await .map_err(|e| crate::Error::context("Failed to create Daytona client", e))?; let sdk_sandbox = client.get(sandbox_name).await.map_err(|e| { @@ -326,8 +412,10 @@ impl DaytonaSandbox { Ok(Self { config: DaytonaConfig::default(), client, + api_key, github_app: None, sandbox: sandbox_cell, + snapshot_name: OnceCell::new(), rg_available: OnceCell::const_new(), event_callback: None, origin_url, @@ -477,8 +565,12 @@ impl DaytonaSandbox { /// If the snapshot doesn't exist and a dockerfile is provided, creates it /// and polls until it reaches `Active` state. Returns an error if the /// snapshot is in a terminal failure state. - async fn ensure_snapshot(&self, snap_cfg: &DaytonaSnapshotConfig) -> crate::Result<()> { - match self.client.snapshot.get(&snap_cfg.name).await { + async fn ensure_snapshot( + &self, + name: &str, + snap_cfg: &DaytonaSnapshotConfig, + ) -> crate::Result<()> { + match self.client.snapshot.get(name).await { Ok(dto) => { use daytona_api_client::models::SnapshotState; match dto.state { @@ -486,7 +578,7 @@ impl DaytonaSandbox { SnapshotState::Error | SnapshotState::BuildFailed => { return Err(crate::Error::message(format!( "Snapshot '{}' is in state '{}': {}", - snap_cfg.name, + name, dto.state, dto.error_reason.unwrap_or_default() ))); @@ -494,7 +586,7 @@ impl DaytonaSandbox { _ => { // Building/Pending/Pulling — fall through to poll self.emit(SandboxEvent::SnapshotCreating { - name: snap_cfg.name.clone(), + name: name.to_string(), }); } } @@ -504,24 +596,22 @@ impl DaytonaSandbox { Some(DockerfileSource::Inline(s)) => s.as_str(), Some(DockerfileSource::Path { .. }) => { return Err(crate::Error::message(format!( - "Snapshot '{}': dockerfile path should have been resolved to inline content before sandbox creation", - snap_cfg.name + "Snapshot '{name}': dockerfile path should have been resolved to inline content before sandbox creation" ))); } None => { return Err(crate::Error::message(format!( - "Snapshot '{}' does not exist and no dockerfile provided to create it", - snap_cfg.name + "Snapshot '{name}' does not exist and no dockerfile provided to create it" ))); } }; self.emit(SandboxEvent::SnapshotCreating { - name: snap_cfg.name.clone(), + name: name.to_string(), }); let params = daytona_sdk::CreateSnapshotParams { - name: snap_cfg.name.clone(), + name: name.to_string(), image: daytona_sdk::ImageSource::Custom( daytona_sdk::DockerImage::from_dockerfile(dockerfile), ), @@ -534,22 +624,19 @@ impl DaytonaSandbox { entrypoint: None, }; self.client.snapshot.create(¶ms).await.map_err(|e| { - crate::Error::context( - format!("Failed to create snapshot '{}'", snap_cfg.name), - e, - ) + crate::Error::context(format!("Failed to create snapshot '{name}'"), e) })?; } Err(e) => { return Err(crate::Error::context( - format!("Failed to get snapshot '{}'", snap_cfg.name), + format!("Failed to get snapshot '{name}'"), e, )); } } // Poll until Active (or terminal failure). - self.poll_snapshot_active(&snap_cfg.name).await + self.poll_snapshot_active(name).await } /// Poll a snapshot until it reaches `Active` state, with exponential @@ -689,11 +776,29 @@ impl Sandbox for DaytonaSandbox { }); let init_start = Instant::now(); - let params = if let Some(ref snap_cfg) = self.config.snapshot { + let params = if let Some(snap_cfg) = self + .config + .snapshot + .as_ref() + .filter(|snapshot| snapshot.dockerfile.is_some()) + { + let api_key = self.api_key.as_deref().ok_or_else(|| { + self.fail_init( + init_start, + crate::Error::message(format!( + "{} is required to compute Daytona snapshot identity", + EnvVars::DAYTONA_API_KEY + )), + ) + })?; + let snapshot_name = match snapshot_identity::snapshot_name(api_key, snap_cfg) { + Ok(name) => name, + Err(err) => return Err(self.fail_init(init_start, err)), + }; let snap_start = Instant::now(); - if let Err(e) = self.ensure_snapshot(snap_cfg).await { + if let Err(e) = self.ensure_snapshot(&snapshot_name, snap_cfg).await { self.emit(SandboxEvent::SnapshotFailed { - name: snap_cfg.name.clone(), + name: snapshot_name.clone(), error: e.to_string(), causes: e.causes(), }); @@ -701,15 +806,17 @@ impl Sandbox for DaytonaSandbox { } let snap_duration = u64::try_from(snap_start.elapsed().as_millis()).unwrap_or(u64::MAX); self.emit(SandboxEvent::SnapshotReady { - name: snap_cfg.name.clone(), + name: snapshot_name.clone(), duration_ms: snap_duration, }); + let _ = self.snapshot_name.set(snapshot_name.clone()); daytona_sdk::CreateParams::Snapshot(daytona_sdk::SnapshotParams { base: self.base_params(), - snapshot: snap_cfg.name.clone(), + snapshot: snapshot_name, }) } else { + let _ = self.snapshot_name.set(DEFAULT_SNAPSHOT.to_string()); daytona_sdk::CreateParams::Snapshot(daytona_sdk::SnapshotParams { base: self.base_params(), snapshot: DEFAULT_SNAPSHOT.to_string(), @@ -1144,6 +1251,10 @@ impl Sandbox for DaytonaSandbox { .unwrap_or_default() } + fn snapshot_info(&self) -> Option { + self.snapshot_name.get().cloned() + } + async fn setup_git( &self, intent: &crate::GitSetupIntent, @@ -2200,6 +2311,57 @@ mod tests { .await } + async fn mock_daytona_sandbox( + server: &MockServer, + api_key: &str, + config: DaytonaConfig, + ) -> DaytonaSandbox { + let client = build_daytona_client_with( + Some(api_key.to_string()), + Some(server.base_url()), + None, + Some(fabro_test::test_http_client()), + ) + .await + .expect("mock Daytona client should build"); + + DaytonaSandbox { + config, + client, + api_key: Some(api_key.to_string()), + github_app: None, + sandbox: OnceCell::new(), + snapshot_name: OnceCell::new(), + rg_available: OnceCell::const_new(), + event_callback: None, + origin_url: OnceCell::new(), + repo_cloned: OnceCell::new(), + working_directory: OnceCell::new(), + run_id: None, + clone_origin_url: None, + clone_branch: None, + } + } + + fn snapshot_body(name: &str) -> serde_json::Value { + serde_json::json!({ + "id": name, + "name": name, + "state": "active", + "general": false, + "cpu": 2.0, + "gpu": 0.0, + "mem": 4.0, + "disk": 20.0, + "size": null, + "entrypoint": null, + "errorReason": null, + "lastUsedAt": null, + "createdAt": "2026-05-01T00:00:00Z", + "updatedAt": "2026-05-01T00:00:00Z" + }) + } + #[test] fn daytona_config_defaults() { let config = DaytonaConfig::default(); @@ -2247,6 +2409,123 @@ subpath = "agents" assert_eq!(volumes[0].subpath.as_deref(), Some("agents")); } + #[test] + fn computed_snapshot_identity_is_deterministic_and_keyed() { + let config = DaytonaSnapshotConfig { + cpu: Some(2), + memory: Some(4), + disk: Some(10), + dockerfile: Some(DockerfileSource::Inline( + "FROM ubuntu:24.04\nRUN apt-get update".to_string(), + )), + }; + + let first = snapshot_identity::snapshot_name("dtn_secret", &config).unwrap(); + let second = snapshot_identity::snapshot_name("dtn_secret", &config).unwrap(); + let rotated_key = snapshot_identity::snapshot_name("dtn_rotated", &config).unwrap(); + + assert_eq!(first, second); + assert_ne!(first, rotated_key); + let uuid = first + .strip_prefix("fabro-") + .and_then(|raw| uuid::Uuid::parse_str(raw).ok()) + .expect("snapshot name should be fabro-"); + assert_eq!(uuid.get_version_num(), 8); + assert_eq!(uuid.get_variant(), uuid::Variant::RFC4122); + } + + #[test] + fn computed_snapshot_identity_changes_for_generation_inputs() { + let base = DaytonaSnapshotConfig { + cpu: Some(2), + memory: Some(4), + disk: Some(10), + dockerfile: Some(DockerfileSource::Inline("FROM ubuntu:24.04".to_string())), + }; + let base_name = snapshot_identity::snapshot_name("dtn_secret", &base).unwrap(); + + let cases = [ + DaytonaSnapshotConfig { + dockerfile: Some(DockerfileSource::Inline( + "FROM ubuntu:24.04\n# roll cache".to_string(), + )), + ..base.clone() + }, + DaytonaSnapshotConfig { + cpu: Some(4), + ..base.clone() + }, + DaytonaSnapshotConfig { + memory: Some(8), + ..base.clone() + }, + DaytonaSnapshotConfig { + disk: Some(20), + ..base.clone() + }, + ]; + + for changed in cases { + let changed_name = snapshot_identity::snapshot_name("dtn_secret", &changed).unwrap(); + assert_ne!(base_name, changed_name); + } + } + + #[test] + fn computed_snapshot_identity_excludes_raw_dockerfile_and_key_material() { + let config = DaytonaSnapshotConfig { + cpu: None, + memory: None, + disk: None, + dockerfile: Some(DockerfileSource::Inline( + "FROM private.example.com/secret-image\nRUN echo raw-secret".to_string(), + )), + }; + + let name = snapshot_identity::snapshot_name("dtn_super_secret_key", &config).unwrap(); + + assert!(name.starts_with("fabro-")); + assert!(!name.contains("private.example.com")); + assert!(!name.contains("raw-secret")); + assert!(!name.contains("dtn_super_secret_key")); + } + + #[tokio::test] + async fn ensure_snapshot_uses_computed_snapshot_name_for_daytona_api_calls() { + let api_key = "dtn_secret"; + let snapshot = DaytonaSnapshotConfig { + cpu: Some(2), + memory: Some(4), + disk: Some(10), + dockerfile: Some(DockerfileSource::Inline("FROM ubuntu:24.04".to_string())), + }; + let computed_name = snapshot_identity::snapshot_name(api_key, &snapshot).unwrap(); + let server = MockServer::start_async().await; + let path = format!("/snapshots/{computed_name}"); + let get_snapshot = server + .mock_async(|when, then| { + when.method(GET) + .path(path.as_str()) + .header("authorization", "Bearer dtn_secret"); + then.status(200) + .header("content-type", "application/json") + .json_body(snapshot_body(&computed_name)); + }) + .await; + let config = DaytonaConfig { + snapshot: Some(snapshot.clone()), + ..DaytonaConfig::default() + }; + let sandbox = mock_daytona_sandbox(&server, api_key, config).await; + + sandbox + .ensure_snapshot(&computed_name, &snapshot) + .await + .expect("existing computed snapshot should be accepted"); + + get_snapshot.assert_async().await; + } + #[tokio::test] async fn base_params_create_run_owned_non_ephemeral_sandbox() { let sandbox = DaytonaSandbox::new( diff --git a/lib/crates/fabro-sandbox/src/from_environment.rs b/lib/crates/fabro-sandbox/src/from_environment.rs index 811ff620c..84a176ad9 100644 --- a/lib/crates/fabro-sandbox/src/from_environment.rs +++ b/lib/crates/fabro-sandbox/src/from_environment.rs @@ -40,10 +40,9 @@ pub fn daytona_config_from_environment( .collect(), snapshot: settings .image - .reference + .dockerfile .as_ref() - .map(|name| DaytonaSnapshotSettings { - name: name.clone(), + .map(|dockerfile| DaytonaSnapshotSettings { cpu: settings.resources.cpu, memory: settings .resources @@ -53,18 +52,14 @@ pub fn daytona_config_from_environment( .resources .disk .map(|size| size_to_gb_i32(size.as_bytes())), - dockerfile: settings - .image - .dockerfile - .as_ref() - .map(|dockerfile| match dockerfile { - ResolvedDockerfileSource::Inline(text) => { - SandboxDockerfileSource::Inline(text.clone()) - } - ResolvedDockerfileSource::Path { path } => { - SandboxDockerfileSource::Path { path: path.clone() } - } - }), + dockerfile: Some(match dockerfile { + ResolvedDockerfileSource::Inline(text) => { + SandboxDockerfileSource::Inline(text.clone()) + } + ResolvedDockerfileSource::Path { path } => { + SandboxDockerfileSource::Path { path: path.clone() } + } + }), }), network: Some(match settings.network.mode { EnvironmentNetworkMode::Block => DaytonaNetwork::Block, @@ -94,7 +89,7 @@ pub fn docker_config_from_environment( DockerSandboxOptions { image: settings .image - .reference + .docker .clone() .unwrap_or(default_options.image), network_mode: match settings.network.mode { diff --git a/lib/crates/fabro-sandbox/src/sandbox.rs b/lib/crates/fabro-sandbox/src/sandbox.rs index 7138a5d06..a1d2da18d 100644 --- a/lib/crates/fabro-sandbox/src/sandbox.rs +++ b/lib/crates/fabro-sandbox/src/sandbox.rs @@ -192,6 +192,10 @@ macro_rules! delegate_sandbox { self.$field.sandbox_info() } + fn snapshot_info(&self) -> Option { + self.$field.snapshot_info() + } + async fn refresh_push_credentials(&self) -> $crate::Result<()> { self.$field.refresh_push_credentials().await } @@ -948,6 +952,12 @@ pub trait Sandbox: Send + Sync { String::new() } + /// Return the provider snapshot used by an initialized sandbox, when the + /// provider has a snapshot concept. + fn snapshot_info(&self) -> Option { + None + } + /// Refresh git push credentials (e.g. rotate an expiring GitHub App token). /// Default is a no-op; Daytona overrides to update the remote URL with a /// fresh token. diff --git a/lib/crates/fabro-sandbox/src/sandbox_spec.rs b/lib/crates/fabro-sandbox/src/sandbox_spec.rs index 12c4bf000..32677c769 100644 --- a/lib/crates/fabro-sandbox/src/sandbox_spec.rs +++ b/lib/crates/fabro-sandbox/src/sandbox_spec.rs @@ -136,10 +136,7 @@ impl SandboxSpec { RunSandbox { provider: self.provider(), image: None, - snapshot: config - .snapshot - .as_ref() - .map(|snapshot| snapshot.name.clone()), + snapshot: sandbox.snapshot_info(), runtime: Some(RunSandboxRuntime { id, working_directory: working_directory.clone(), diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs index 6508dd971..d189b2a21 100644 --- a/lib/crates/fabro-server/src/demo/mod.rs +++ b/lib/crates/fabro-server/src/demo/mod.rs @@ -1757,7 +1757,7 @@ mod runs { let environment = EnvironmentSettings { provider: EnvironmentProvider::Daytona, image: EnvironmentImageSettings { - reference: Some("api-server-dev".into()), + docker: Some("api-server-dev".into()), dockerfile: None, }, resources: EnvironmentResourcesSettings { diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs index a004872cb..00a97e13a 100644 --- a/lib/crates/fabro-server/src/run_manifest.rs +++ b/lib/crates/fabro-server/src/run_manifest.rs @@ -1546,7 +1546,6 @@ id = "cloud" provider = "daytona" [environments.cloud.image] -ref = "fabro-test" dockerfile = { path = "Dockerfile" } "# .to_string(), @@ -1606,7 +1605,6 @@ id = "cloud" provider = "daytona" [environments.cloud.image] -ref = "fabro-test" dockerfile = { path = "Dockerfile" } "# .to_string(), diff --git a/lib/crates/fabro-server/src/server/tests.rs b/lib/crates/fabro-server/src/server/tests.rs index 6b08f4ed8..83e98195d 100644 --- a/lib/crates/fabro-server/src/server/tests.rs +++ b/lib/crates/fabro-server/src/server/tests.rs @@ -11624,6 +11624,8 @@ async fn create_preserved_local_sandbox_run(state: &Arc, run_id: RunId provider: SandboxProviderKind::Local, id: "sandbox-preserve-1".to_string(), working_directory: "/tmp/fabro-preserved-sandbox".to_string(), + image: None, + snapshot: None, repo_cloned: None, clone_origin_url: None, clone_branch: None, @@ -12375,6 +12377,8 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() { provider: SandboxProviderKind::Docker, id: "missing-sandbox".to_string(), working_directory: "/tmp/fabro-missing-sandbox".to_string(), + image: None, + snapshot: None, repo_cloned: Some(false), clone_origin_url: None, clone_branch: None, @@ -14269,6 +14273,8 @@ async fn list_runs_includes_live_metadata_from_run_state() { provider: SandboxProviderKind::Local, id: "sb-test".to_string(), working_directory: "/sandbox/workdir".to_string(), + image: None, + snapshot: None, repo_cloned: None, clone_origin_url: None, clone_branch: None, @@ -14348,6 +14354,8 @@ async fn list_runs_page_limit_preserves_metadata_for_paged_items() { provider: SandboxProviderKind::Local, id: sandbox_id.to_string(), working_directory: "/sandbox/workdir".to_string(), + image: None, + snapshot: None, repo_cloned: None, clone_origin_url: None, clone_branch: None, diff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs index 552891d2e..c1358fe0c 100644 --- a/lib/crates/fabro-store/src/run_state.rs +++ b/lib/crates/fabro-store/src/run_state.rs @@ -267,6 +267,8 @@ impl RunProjectionReducer for RunProjection { runtime: None, }); sandbox.provider = props.provider; + sandbox.image.clone_from(&props.image); + sandbox.snapshot.clone_from(&props.snapshot); sandbox.runtime = Some(RunSandboxRuntime { id: props.id.clone(), working_directory: props.working_directory.clone(), @@ -800,12 +802,10 @@ fn planned_sandbox(settings: &RunEnvironmentSettings) -> RunSandbox { RunSandbox { provider, image: (settings.provider == EnvironmentProvider::Docker) - .then(|| settings.image.reference.clone()) + .then(|| settings.image.docker.clone()) .flatten() .filter(|image| !image.is_empty()), - snapshot: (settings.provider == EnvironmentProvider::Daytona) - .then(|| settings.image.reference.clone()) - .flatten(), + snapshot: None, runtime: None, } } @@ -1246,6 +1246,7 @@ mod tests { RunCompletedProps, RunControlEffectProps, StageCompletedProps, StageFailedProps, StagePromptProps, StageRetryingProps, StageStartedProps, }; + use fabro_types::settings::run::{DockerfileSource, EnvironmentProvider}; use fabro_types::{ AgentBackend, BilledModelUsage, BilledTokenCounts, BlockedReason, Checkpoint, CheckpointRecord, CommandTermination, EventBody, FailureCategory, FailureDetail, @@ -1371,6 +1372,50 @@ mod tests { state } + #[test] + fn planned_sandbox_uses_docker_image_and_hides_daytona_snapshot_until_init() { + let mut docker = WorkflowSettings::default().run.environment; + docker.provider = EnvironmentProvider::Docker; + docker.image.docker = Some("ubuntu:24.04".to_string()); + + let planned_docker = super::planned_sandbox(&docker); + assert_eq!(planned_docker.image.as_deref(), Some("ubuntu:24.04")); + assert_eq!(planned_docker.snapshot, None); + + let mut daytona = WorkflowSettings::default().run.environment; + daytona.provider = EnvironmentProvider::Daytona; + daytona.image.dockerfile = Some(DockerfileSource::Inline("FROM ubuntu:24.04".to_string())); + + let planned_daytona = super::planned_sandbox(&daytona); + assert_eq!(planned_daytona.image, None); + assert_eq!(planned_daytona.snapshot, None); + } + + #[test] + fn sandbox_initialized_updates_image_and_snapshot_projection_fields() { + let mut state = initialized_projection(); + state + .apply_event(&test_raw_event( + 1, + "sandbox.initialized", + &json!({ + "provider": "daytona", + "id": "fabro-run-sandbox", + "working_directory": "/home/daytona/workspace", + "snapshot": "fabro-11111111-2222-8333-8444-555555555555" + }), + None, + )) + .unwrap(); + + let sandbox = state.sandbox.expect("sandbox should be projected"); + assert_eq!(sandbox.image, None); + assert_eq!( + sandbox.snapshot.as_deref(), + Some("fabro-11111111-2222-8333-8444-555555555555") + ); + } + #[test] fn legacy_run_created_projects_retried_from_none() { let event = test_raw_event( diff --git a/lib/crates/fabro-types/src/run_event/infra.rs b/lib/crates/fabro-types/src/run_event/infra.rs index e6673ff24..d13302148 100644 --- a/lib/crates/fabro-types/src/run_event/infra.rs +++ b/lib/crates/fabro-types/src/run_event/infra.rs @@ -332,6 +332,10 @@ pub struct SandboxInitializedProps { pub provider: SandboxProviderKind, pub id: String, #[serde(default, skip_serializing_if = "Option::is_none")] + pub image: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub snapshot: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub repo_cloned: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub clone_origin_url: Option, diff --git a/lib/crates/fabro-types/src/settings/run.rs b/lib/crates/fabro-types/src/settings/run.rs index 78a201aa5..992d684e9 100644 --- a/lib/crates/fabro-types/src/settings/run.rs +++ b/lib/crates/fabro-types/src/settings/run.rs @@ -755,8 +755,7 @@ pub enum EnvironmentNetworkMode { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct EnvironmentImageSettings { - #[serde(rename = "ref")] - pub reference: Option, + pub docker: Option, pub dockerfile: Option, } diff --git a/lib/crates/fabro-workflow/src/devcontainer_bridge.rs b/lib/crates/fabro-workflow/src/devcontainer_bridge.rs index b7ec92107..6fc081782 100644 --- a/lib/crates/fabro-workflow/src/devcontainer_bridge.rs +++ b/lib/crates/fabro-workflow/src/devcontainer_bridge.rs @@ -4,23 +4,14 @@ use fabro_agent::sandbox::Sandbox; use fabro_devcontainer::DevcontainerSpec; use fabro_sandbox::daytona::{DaytonaSnapshotConfig, DockerfileSource}; use futures::future::try_join_all; -use sha2::{Digest, Sha256}; use tokio_util::sync::CancellationToken; use crate::error::Error; use crate::event::{Emitter, Event}; -/// Compute a deterministic snapshot name from Dockerfile content. -pub fn snapshot_name_for_dockerfile(dockerfile: &str) -> String { - let hash = Sha256::digest(dockerfile.as_bytes()); - let hex = hex::encode(hash); - format!("devcontainer-{}", &hex[..12]) -} - /// Map a `DevcontainerSpec` to a `DaytonaSnapshotConfig`. pub fn devcontainer_to_snapshot_config(dc: &DevcontainerSpec) -> DaytonaSnapshotConfig { DaytonaSnapshotConfig { - name: snapshot_name_for_dockerfile(&dc.dockerfile), dockerfile: Some(DockerfileSource::Inline(dc.dockerfile.clone())), cpu: None, memory: None, @@ -379,29 +370,6 @@ mod tests { } } - #[test] - fn snapshot_name_is_deterministic() { - let dockerfile = "FROM ubuntu:22.04\nRUN apt-get update"; - let name1 = snapshot_name_for_dockerfile(dockerfile); - let name2 = snapshot_name_for_dockerfile(dockerfile); - assert_eq!(name1, name2); - } - - #[test] - fn snapshot_name_differs_for_different_dockerfiles() { - let name1 = snapshot_name_for_dockerfile("FROM ubuntu:22.04"); - let name2 = snapshot_name_for_dockerfile("FROM rust:1.85"); - assert_ne!(name1, name2); - } - - #[test] - fn snapshot_name_has_prefix() { - let name = snapshot_name_for_dockerfile("FROM ubuntu:22.04"); - assert!(name.starts_with("devcontainer-"), "name: {name}"); - // prefix + 12 hex chars - assert_eq!(name.len(), "devcontainer-".len() + 12); - } - #[test] fn maps_dockerfile_to_inline() { let dc = test_devcontainer_config("FROM rust:1.85\nRUN cargo install sccache"); @@ -413,11 +381,12 @@ mod tests { } #[test] - fn snapshot_name_from_dockerfile_hash() { + fn devcontainer_snapshot_uses_runtime_daytona_identity_path() { let dc = test_devcontainer_config("FROM ubuntu:22.04"); let snapshot = devcontainer_to_snapshot_config(&dc); - let expected = snapshot_name_for_dockerfile(&dc.dockerfile); - assert_eq!(snapshot.name, expected); + assert_eq!(snapshot.cpu, None); + assert_eq!(snapshot.memory, None); + assert_eq!(snapshot.disk, None); } #[tokio::test] diff --git a/lib/crates/fabro-workflow/src/event/convert.rs b/lib/crates/fabro-workflow/src/event/convert.rs index 1db6d3b56..032aae39b 100644 --- a/lib/crates/fabro-workflow/src/event/convert.rs +++ b/lib/crates/fabro-workflow/src/event/convert.rs @@ -1015,6 +1015,8 @@ fn event_body_from_event(event: &Event) -> EventBody { working_directory, provider, id, + image, + snapshot, repo_cloned, clone_origin_url, clone_branch, @@ -1026,6 +1028,8 @@ fn event_body_from_event(event: &Event) -> EventBody { working_directory: working_directory.clone(), provider: *provider, id: id.clone(), + image: image.clone(), + snapshot: snapshot.clone(), repo_cloned: *repo_cloned, clone_origin_url: clone_origin_url.clone(), clone_branch: clone_branch.clone(), diff --git a/lib/crates/fabro-workflow/src/event/events.rs b/lib/crates/fabro-workflow/src/event/events.rs index cf458c121..8615f53bd 100644 --- a/lib/crates/fabro-workflow/src/event/events.rs +++ b/lib/crates/fabro-workflow/src/event/events.rs @@ -509,6 +509,10 @@ pub enum Event { provider: SandboxProviderKind, id: String, #[serde(default, skip_serializing_if = "Option::is_none")] + image: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + snapshot: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] repo_cloned: Option, #[serde(default, skip_serializing_if = "Option::is_none")] clone_origin_url: Option, diff --git a/lib/crates/fabro-workflow/src/operations/retry.rs b/lib/crates/fabro-workflow/src/operations/retry.rs index 6551687c5..ce8ddbc7c 100644 --- a/lib/crates/fabro-workflow/src/operations/retry.rs +++ b/lib/crates/fabro-workflow/src/operations/retry.rs @@ -304,6 +304,8 @@ mod tests { provider: fabro_types::SandboxProviderKind::Local, id: "sandbox-source".to_string(), working_directory: "/tmp/source".to_string(), + image: None, + snapshot: None, repo_cloned: None, clone_origin_url: None, clone_branch: None, diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs index a962cd0e5..1caa26f47 100644 --- a/lib/crates/fabro-workflow/src/operations/start.rs +++ b/lib/crates/fabro-workflow/src/operations/start.rs @@ -1302,7 +1302,7 @@ reasoning = false let settings = settings_from_run_layer(RunLayer { environment: Some(RunEnvironmentLayer { image: Some(EnvironmentImageLayer { - reference: Some("ubuntu:24.04".to_string()), + docker: Some("ubuntu:24.04".to_string()), ..EnvironmentImageLayer::default() }), resources: Some(EnvironmentResourcesLayer { diff --git a/lib/crates/fabro-workflow/src/pipeline/initialize.rs b/lib/crates/fabro-workflow/src/pipeline/initialize.rs index 761a58e42..c8755e7c7 100644 --- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs @@ -519,6 +519,8 @@ pub async fn initialize( working_directory: runtime.working_directory.clone(), provider: run_sandbox.provider, id: runtime.id.clone(), + image: run_sandbox.image.clone(), + snapshot: run_sandbox.snapshot.clone(), repo_cloned: runtime.repo_cloned, clone_origin_url: runtime.clone_origin_url.clone(), clone_branch: runtime.clone_branch.clone(), diff --git a/lib/crates/fabro-workflow/tests/it/daytona_integration.rs b/lib/crates/fabro-workflow/tests/it/daytona_integration.rs index 0c8bd171d..2d223a550 100644 --- a/lib/crates/fabro-workflow/tests/it/daytona_integration.rs +++ b/lib/crates/fabro-workflow/tests/it/daytona_integration.rs @@ -24,7 +24,7 @@ use std::sync::Arc; use fabro_agent::Sandbox; use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node}; -use fabro_sandbox::daytona::{DaytonaConfig, DaytonaSandbox, DaytonaSnapshotConfig}; +use fabro_sandbox::daytona::{DaytonaConfig, DaytonaSandbox}; use fabro_static::EnvVars; use fabro_store::{ArtifactKey, ArtifactStore, Database}; use fabro_types::{RunId, StageId, WorkflowSettings}; @@ -368,7 +368,6 @@ async fn daytona_snapshot_sandbox() { let config = DaytonaConfig { auto_stop_interval: Some(60), snapshot: Some(DaytonaSnapshotConfig { - name: "fabro-test-snapshot".to_string(), cpu: Some(2), memory: Some(4), disk: Some(10), @@ -1804,13 +1803,7 @@ async fn daytona_cp_upload_download_round_trip() { async fn daytona_computer_use_browser_screenshot() { use base64::Engine; let config = DaytonaConfig { - snapshot: Some(DaytonaSnapshotConfig { - name: "daytona-medium".into(), - cpu: None, - memory: None, - disk: None, - dockerfile: None, - }), + snapshot: None, skip_clone: true, ..DaytonaConfig::default() }; @@ -1958,13 +1951,7 @@ async fn daytona_playwright_mcp_sandbox_transport() { // Create sandbox from daytona-medium (has Node.js + Chromium) let config = DaytonaConfig { - snapshot: Some(DaytonaSnapshotConfig { - name: "daytona-medium".into(), - cpu: None, - memory: None, - disk: None, - dockerfile: None, - }), + snapshot: None, skip_clone: true, ..DaytonaConfig::default() }; diff --git a/lib/packages/fabro-api-client/src/models/daytona-settings.ts b/lib/packages/fabro-api-client/src/models/daytona-settings.ts deleted file mode 100644 index 4e3127313..000000000 --- a/lib/packages/fabro-api-client/src/models/daytona-settings.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -// May contain unused imports in some cases -// @ts-ignore -import type { DaytonaNetworkLayer } from './daytona-network-layer'; -// May contain unused imports in some cases -// @ts-ignore -import type { DaytonaSnapshotSettings } from './daytona-snapshot-settings'; - -export interface DaytonaSettings { - 'auto_stop_interval': number | null; - 'labels': { [key: string]: string; }; - 'snapshot': DaytonaSnapshotSettings | null; - 'network': DaytonaNetworkLayer | null; -} diff --git a/lib/packages/fabro-api-client/src/models/daytona-snapshot-settings.ts b/lib/packages/fabro-api-client/src/models/daytona-snapshot-settings.ts deleted file mode 100644 index b067a5a93..000000000 --- a/lib/packages/fabro-api-client/src/models/daytona-snapshot-settings.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -// May contain unused imports in some cases -// @ts-ignore -import type { DockerfileSource } from './dockerfile-source'; - -export interface DaytonaSnapshotSettings { - 'name': string; - 'cpu': number | null; - 'memory_gb': number | null; - 'disk_gb': number | null; - 'dockerfile': DockerfileSource | null; -} diff --git a/lib/packages/fabro-api-client/src/models/environment-image-settings.ts b/lib/packages/fabro-api-client/src/models/environment-image-settings.ts index 57b3f1026..997f1d64c 100644 --- a/lib/packages/fabro-api-client/src/models/environment-image-settings.ts +++ b/lib/packages/fabro-api-client/src/models/environment-image-settings.ts @@ -18,6 +18,6 @@ import type { DockerfileSource } from './dockerfile-source'; export interface EnvironmentImageSettings { - 'ref': string | null; + 'docker': string | null; 'dockerfile': DockerfileSource | null; } diff --git a/lib/packages/fabro-api-client/src/models/run-sandbox-settings.ts b/lib/packages/fabro-api-client/src/models/run-sandbox-settings.ts deleted file mode 100644 index 4fe24f6be..000000000 --- a/lib/packages/fabro-api-client/src/models/run-sandbox-settings.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -// May contain unused imports in some cases -// @ts-ignore -import type { DaytonaSettings } from './daytona-settings'; -// May contain unused imports in some cases -// @ts-ignore -import type { DockerSettings } from './docker-settings'; -// May contain unused imports in some cases -// @ts-ignore -import type { SandboxProvider } from './sandbox-provider'; - -export interface RunSandboxSettings { - 'provider': SandboxProvider; - 'preserve': boolean; - 'stop_on_terminal': boolean; - 'devcontainer': boolean; - 'env': { [key: string]: string; }; - 'docker': DockerSettings | null; - 'daytona': DaytonaSettings | null; -}