Open sandbox provider identity to plugin kinds

SandboxProviderKind is now a validated string newtype instead of a
closed enum. The bundled kinds (local, docker, daytona) keep their
constants and a BundledProvider enum for the code paths that still
dispatch on them; any other well-formed sandbox-driver kind name is
accepted and names a plugin executable. EnvironmentProvider is gone:
environment settings carry SandboxProviderKind directly, and
is_clone_based is replaced by a workspace policy where local runs in a
designated directory and every other provider clones.

Server sandbox policy is keyed by kind. [server.sandbox.providers.<kind>]
accepts the bundled kinds with `enabled` and any plugin kind with its
launch settings (path, sha256, dev, args, env, inherit_env); bundled
kinds reject the plugin keys and a kind with no entry is disabled. The
OpenAPI schema, generated Rust and TypeScript clients, web settings
pages, and docs follow. The environments table drops its provider CHECK
enumeration in favour of the kind name rules so a plugin environment
can be stored.

Bundled-only code paths (run start, preflight, reconnect, terminal,
details) now fail with an explicit message for a plugin kind until the
driver construction function lands in the next step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-09-09 15:17:32 -06:00
parent 124065ac52
commit 80bc51c40e
No known key found for this signature in database
95 changed files with 1390 additions and 813 deletions

View file

@ -3,7 +3,6 @@ import { ChevronRightIcon } from "@heroicons/react/20/solid";
import {
EnvironmentApiDockerfileSourceInlineTypeEnum,
EnvironmentNetworkMode,
EnvironmentProvider,
} from "@qltysh/fabro-api-client";
import type {
CreateEnvironmentRequest,
@ -15,6 +14,7 @@ import type {
ReplaceEnvironmentRequest,
} from "@qltysh/fabro-api-client";
import { DOCKER_PROVIDER, isCloneBasedProvider } from "../lib/environment-providers";
import { Label, Panel, Row } from "./settings-panel";
import { INPUT_CLASS } from "./ui";
import {
@ -25,11 +25,15 @@ import {
} from "./key-value-editor";
// Parse the `provider` query param used by the create flow into a creatable
// provider, defaulting to Docker for anything unexpected.
export function parseCreatableProvider(value: string | null): EnvironmentProvider {
return value === EnvironmentProvider.DAYTONA
? EnvironmentProvider.DAYTONA
: EnvironmentProvider.DOCKER;
// provider, defaulting to Docker for anything that cannot back a managed
// environment. Kind names are validated server-side on create.
const PROVIDER_KIND_PATTERN = /^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/;
export function parseCreatableProvider(value: string | null): string {
if (value && PROVIDER_KIND_PATTERN.test(value) && isCloneBasedProvider(value)) {
return value;
}
return DOCKER_PROVIDER;
}
// Environment ids are server-managed file names: lowercase, digits, hyphens.
@ -49,7 +53,7 @@ type ImageSource = "image" | "dockerfile";
export interface EnvironmentFormValues {
id: string;
provider: EnvironmentProvider;
provider: string;
imageSource: ImageSource;
dockerRef: string;
dockerfile: string;
@ -69,7 +73,7 @@ export interface EnvironmentFormValues {
export const EMPTY_ENVIRONMENT_FORM: EnvironmentFormValues = {
id: "",
provider: EnvironmentProvider.DOCKER,
provider: DOCKER_PROVIDER,
imageSource: "image",
dockerRef: "",
dockerfile: "",

View file

@ -1,17 +1,44 @@
import { EnvironmentProvider, type Environment } from "@qltysh/fabro-api-client";
import type { Environment, ServerSandboxProviderSettings } from "@qltysh/fabro-api-client";
// Providers a managed environment can be created with. `local` is a reserved,
// in-memory environment, never a managed-environment provider, so it is never
// offered. The provider is fixed at creation time and cannot be changed.
export const CREATABLE_PROVIDERS = [
EnvironmentProvider.DOCKER,
EnvironmentProvider.DAYTONA,
] as const;
// The providers linked into the server. Any other provider kind names a
// sandbox-driver plugin the operator configured under
// `server.sandbox.providers.<kind>`.
export const LOCAL_PROVIDER = "local";
export const DOCKER_PROVIDER = "docker";
export const DAYTONA_PROVIDER = "daytona";
export const BUNDLED_PROVIDERS = [LOCAL_PROVIDER, DOCKER_PROVIDER, DAYTONA_PROVIDER] as const;
export type ProviderSettingsMap = { [kind: string]: ServerSandboxProviderSettings };
// `local` runs in the caller's directory and never clones. Every other
// provider owns an isolated workspace that Fabro clones into.
export function isCloneBasedProvider(provider: string): boolean {
return provider !== LOCAL_PROVIDER;
}
// Whether a server-managed environment can back Git-targeted work such as
// automations: only the clone-based (creatable) providers qualify.
// automations: only clone-based providers qualify.
export function isCloneBasedEnvironment(environment: Environment): boolean {
return (CREATABLE_PROVIDERS as readonly string[]).includes(environment.provider);
return isCloneBasedProvider(environment.provider);
}
// Providers a managed environment can be created with: every enabled
// clone-based provider. `local` is a reserved, in-memory environment, never a
// managed-environment provider, so it is never offered.
export function creatableProviders(providers: ProviderSettingsMap): string[] {
return Object.keys(providers)
.filter((kind) => isCloneBasedProvider(kind) && providers[kind]?.enabled)
.sort(compareProviderKinds);
}
// Bundled kinds first, in their canonical order, then plugins alphabetically.
export function compareProviderKinds(left: string, right: string): number {
const rank = (kind: string) => {
const index = (BUNDLED_PROVIDERS as readonly string[]).indexOf(kind);
return index === -1 ? BUNDLED_PROVIDERS.length : index;
};
return rank(left) - rank(right) || left.localeCompare(right);
}
export function providerLabel(provider: string): string {

View file

@ -9,7 +9,7 @@ import type { Environment } from "@qltysh/fabro-api-client";
import { ApiError, apiData, environmentsApi } from "../lib/api-client";
import { useEnvironments, useServerSettings } from "../lib/queries";
import { queryKeys } from "../lib/query-keys";
import { CREATABLE_PROVIDERS, providerLabel } from "../lib/environment-providers";
import { creatableProviders, providerLabel } from "../lib/environment-providers";
import {
Badge,
Muted,
@ -67,9 +67,7 @@ const NEW_BUTTON_CLASS =
// environment's lifetime. `local` is never offered (it's reserved/in-memory).
function NewEnvironmentMenu() {
const { data } = useServerSettings();
const providers = data
? CREATABLE_PROVIDERS.filter((provider) => data.server.sandbox.providers[provider].enabled)
: [];
const providers = data ? creatableProviders(data.server.sandbox.providers) : [];
if (providers.length === 0) {
return (

View file

@ -2,7 +2,7 @@ import { useMemo, useState } from "react";
import { Link } from "react-router";
import { ChevronDownIcon } from "@heroicons/react/16/solid";
import { ComputerDesktopIcon } from "@heroicons/react/24/outline";
import type { ServerSandboxProvidersSettings } from "@qltysh/fabro-api-client";
import type { ServerSandboxProviderSettings } from "@qltysh/fabro-api-client";
import { useServerSettings } from "../lib/queries";
import {
Dot,
@ -12,24 +12,56 @@ import {
SettingsPageIntro,
} from "../components/settings-panel";
import { plural } from "../lib/plural";
import {
DAYTONA_PROVIDER,
DOCKER_PROVIDER,
LOCAL_PROVIDER,
compareProviderKinds,
providerLabel,
type ProviderSettingsMap,
} from "../lib/environment-providers";
export function meta() {
return [{ title: "Sandboxes — Fabro" }];
}
type SandboxProviderId = "local" | "docker" | "daytona";
type SandboxProvider = {
id: SandboxProviderId;
id: string;
name: string;
description: string;
enabled: boolean;
bundled: boolean;
secretName?: string;
};
const DESCRIPTION =
"Runtime environments where workflow stages execute. Configured via settings.toml.";
// Display copy for the providers linked into the server. Any other kind is a
// sandbox-driver plugin configured under `server.sandbox.providers.<kind>`.
const BUNDLED_PROVIDER_COPY: Record<string, Omit<SandboxProvider, "id" | "enabled" | "bundled">> = {
[LOCAL_PROVIDER]: {
name: "Local",
description: "Run stages directly on the Fabro host.",
},
[DOCKER_PROVIDER]: {
name: "Docker",
description: "Run stages in isolated Docker containers on the host daemon.",
},
[DAYTONA_PROVIDER]: {
name: "Daytona",
description: "Run stages in cloud sandboxes managed by Daytona.",
secretName: "DAYTONA_API_KEY",
},
};
function pluginDescription(settings: ServerSandboxProviderSettings): string {
const path = settings.plugin?.path;
return path
? `Sandbox plugin executable at ${path}.`
: "Sandbox plugin executable resolved from PATH.";
}
export default function SettingsSandboxes() {
const query = useServerSettings();
const settings = query.data;
@ -42,29 +74,24 @@ export default function SettingsSandboxes() {
);
}
function ProvidersPanel({ settings }: { settings: ServerSandboxProvidersSettings }) {
function ProvidersPanel({ settings }: { settings: ProviderSettingsMap }) {
const providers: SandboxProvider[] = useMemo(
() => [
{
id: "local",
name: "Local",
description: "Run stages directly on the Fabro host.",
enabled: settings.local.enabled,
},
{
id: "docker",
name: "Docker",
description: "Run stages in isolated Docker containers on the host daemon.",
enabled: settings.docker.enabled,
},
{
id: "daytona",
name: "Daytona",
description: "Run stages in cloud sandboxes managed by Daytona.",
enabled: settings.daytona.enabled,
secretName: "DAYTONA_API_KEY",
},
],
() =>
Object.keys(settings)
.sort(compareProviderKinds)
.map((id) => {
const entry = settings[id];
const copy = BUNDLED_PROVIDER_COPY[id];
return copy
? { id, enabled: entry.enabled, bundled: true, ...copy }
: {
id,
enabled: entry.enabled,
bundled: false,
name: providerLabel(id),
description: pluginDescription(entry),
};
}),
[settings],
);
@ -138,7 +165,7 @@ function ProviderLogo({ provider }: { provider: SandboxProvider }) {
"grid size-10 shrink-0 place-items-center rounded-md bg-ice-50 ring-1 ring-line-strong";
const dim = provider.enabled ? "" : "opacity-60";
if (provider.id === "local") {
if (provider.id === LOCAL_PROVIDER) {
return (
<span className={`${chip} text-page ${dim}`}>
<ComputerDesktopIcon className="size-6" aria-hidden="true" />
@ -146,7 +173,7 @@ function ProviderLogo({ provider }: { provider: SandboxProvider }) {
);
}
if (failed) {
if (failed || !provider.bundled) {
return (
<span className={`${chip} text-base font-medium text-page ${dim}`}>
{provider.name.charAt(0)}

View file

@ -5,12 +5,13 @@ description: "Sandboxing workflow execution"
Sandboxes isolate agent execution from the host machine. When an agent runs a shell command, edits a file, or searches code, it does so inside a sandbox — preventing unintended side effects on the host and providing a reproducible environment for each run.
Fabro supports three sandbox providers: `local` (no isolation), `docker` (container-level), and `daytona` (cloud VM). See [Environments](/execution/environments) for full provider-specific configuration.
Fabro bundles three sandbox providers: `local` (no isolation), `docker` (container-level), and `daytona` (cloud VM). Additional providers run as [sandbox-driver](https://github.com/lithoscomputer/sandbox-driver) plugins configured under `[server.sandbox.providers.<kind>]`; an environment selects one by its kind name. See [Environments](/execution/environments) for full provider-specific configuration and [Server configuration](/administration/server-configuration#serversandboxproviders-section) for plugin settings.
Operators can enable or disable which providers the server may launch with
`[server.sandbox.providers.<provider>]` in `settings.toml`. Missing entries default to
`enabled = true`; setting `enabled = false` rejects new runs whose effective provider is disabled.
Dry-run Docker/Daytona runs execute locally, so they are governed by the `local` provider policy.
`[server.sandbox.providers.<kind>]` in `settings.toml`. Missing bundled entries default to
`enabled = true`; setting `enabled = false` rejects new runs whose effective provider is disabled,
and a plugin kind with no entry is disabled. Dry-run runs on any non-local provider execute
locally, so they are governed by the `local` provider policy.
The API can also list Fabro-managed sandboxes directly from configured providers:

View file

@ -181,9 +181,10 @@ The GitHub OAuth client ID still lives under `[server.integrations.github].clien
### `[server.sandbox.providers]` section
Controls which sandbox providers the server may launch. Missing provider entries default to
`enabled = true` for backward compatibility. Disabling a provider rejects new runs whose effective
provider is disabled; dry-run Docker/Daytona runs use the local provider and are governed by
Controls which sandbox providers the server may launch, keyed by provider kind. The bundled
providers `local`, `docker`, and `daytona` run inside the server and default to `enabled = true`
when their entry is missing. Disabling a provider rejects new runs whose effective provider is
disabled; dry-run Docker/Daytona runs use the local provider and are governed by
`server.sandbox.providers.local.enabled`.
```toml title="settings.toml"
@ -197,6 +198,34 @@ enabled = true
enabled = true
```
Any other key names a [sandbox-driver](https://github.com/lithoscomputer/sandbox-driver) plugin:
an executable that speaks the sandbox-driver JSON-RPC protocol on stdin and stdout. The kind must
be lowercase ASCII letters, digits, and interior hyphens. The plugin starts with a scrubbed
environment: only `env` and the ambient variables listed in `inherit_env` reach it. Bundled
providers reject these plugin keys.
```toml title="settings.toml"
[server.sandbox.providers.e2b]
enabled = true
path = "/opt/fabro/plugins/fabro-sandbox-e2b" # default: `fabro-sandbox-<kind>` on PATH
sha256 = "0123…cdef" # pin the executable; `dev = true` skips it
args = []
inherit_env = ["PATH"]
[server.sandbox.providers.e2b.env]
E2B_API_URL = "https://api.e2b.example"
```
| Key | Description | Default |
|---|---|---|
| `enabled` | Whether runs may select this provider | `true` |
| `path` | Plugin executable path | `fabro-sandbox-<kind>` on `PATH` |
| `sha256` | Pinned SHA-256 of the executable, hex | none |
| `dev` | Allow launching without a checksum | `false` |
| `args` | Arguments passed to the executable | `[]` |
| `env` | Complete environment for the plugin, apart from `inherit_env` | `{}` |
| `inherit_env` | Ambient variables forwarded from the server process | `[]` |
### `[server.slatedb]` section
Configure the embedded SlateDB key-value store used for the remaining

View file

@ -7243,7 +7243,7 @@ components:
description: Stable revision used with `If-Match` for optimistic concurrency.
example: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
provider:
$ref: "#/components/schemas/EnvironmentProvider"
$ref: "#/components/schemas/SandboxProviderKind"
cwd:
type: ["string", "null"]
description: Local-provider command working directory for this environment. Docker and Daytona ignore this value.
@ -7282,7 +7282,7 @@ components:
pattern: "^[a-z0-9][a-z0-9-]{0,62}$"
example: docker
provider:
$ref: "#/components/schemas/EnvironmentProvider"
$ref: "#/components/schemas/SandboxProviderKind"
cwd:
type: ["string", "null"]
description: Local-provider command working directory for this environment. Docker and Daytona ignore this value.
@ -7316,7 +7316,7 @@ components:
- env
properties:
provider:
$ref: "#/components/schemas/EnvironmentProvider"
$ref: "#/components/schemas/SandboxProviderKind"
cwd:
type: ["string", "null"]
description: Local-provider command working directory for this environment. Docker and Daytona ignore this value.
@ -12875,12 +12875,13 @@ components:
example: 180000
SandboxProviderKind:
description: Sandbox provider discriminator.
description: |
Sandbox provider kind. `local`, `docker`, and `daytona` are bundled
with the server; any other value names a sandbox-driver plugin
configured under `server.sandbox.providers.<kind>`.
type: string
enum:
- local
- docker
- daytona
pattern: "^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$"
example: docker
RunSandboxKind:
description: Lifecycle state for a run sandbox request.
@ -14381,15 +14382,13 @@ components:
$ref: "#/components/schemas/ServerSandboxProvidersSettings"
ServerSandboxProvidersSettings:
description: |
Sandbox provider policy keyed by provider kind. The bundled kinds
(`local`, `docker`, `daytona`) are always present; any other key names
a sandbox-driver plugin and carries its launch settings.
type: object
required: [local, docker, daytona]
properties:
local:
$ref: "#/components/schemas/ServerSandboxProviderSettings"
docker:
$ref: "#/components/schemas/ServerSandboxProviderSettings"
daytona:
$ref: "#/components/schemas/ServerSandboxProviderSettings"
additionalProperties:
$ref: "#/components/schemas/ServerSandboxProviderSettings"
ServerSandboxProviderSettings:
type: object
@ -14397,6 +14396,32 @@ components:
properties:
enabled:
type: boolean
plugin:
$ref: "#/components/schemas/SandboxPluginSettings"
SandboxPluginSettings:
description: How the server launches a sandbox-driver plugin executable.
type: object
properties:
path:
type: string
description: Executable path. Absent means `fabro-sandbox-<kind>` on `PATH`.
sha256:
type: string
description: Pinned SHA-256 of the executable, hex.
dev:
type: boolean
description: Allow launching without a checksum.
args:
type: array
items:
type: string
env:
$ref: "#/components/schemas/StringMap"
inherit_env:
type: array
items:
type: string
ServerStorageSettings:
type: object
@ -14920,7 +14945,7 @@ components:
id:
type: string
provider:
$ref: "#/components/schemas/EnvironmentProvider"
$ref: "#/components/schemas/SandboxProviderKind"
cwd:
type: ["string", "null"]
description: Local-provider command working directory for this environment. Docker and Daytona ignore this value.
@ -14945,7 +14970,7 @@ components:
required: [provider, image, resources, network, lifecycle, labels, env]
properties:
provider:
$ref: "#/components/schemas/EnvironmentProvider"
$ref: "#/components/schemas/SandboxProviderKind"
cwd:
type: ["string", "null"]
description: Local-provider command working directory for this environment. Docker and Daytona ignore this value.
@ -14965,11 +14990,6 @@ components:
additionalProperties:
$ref: "#/components/schemas/InterpString"
EnvironmentProvider:
description: Desired environment provider.
type: string
enum: [local, docker, daytona]
EnvironmentImageSettings:
type: object
required: [docker, dockerfile]

View file

@ -270,7 +270,7 @@ memory = "4GB"
mode = "block"
```
Docker and Daytona are clone-based providers. When a run has a GitHub origin, Fabro clones it into the provider workspace with a history depth of 100. Set `[run.clone] enabled = false` to start a manifest-backed run with an empty workspace. Set `[run.clone] depth = 0` to clone full history. A version-backed run intent can instead submit the explicit `{ "kind": "none" }` target, which forces an empty provider workspace regardless of the workflow's clone setting. Its Git target may select a branch, an optional bare tag, an optional exact commit SHA, or both tag and SHA. Both providers attach the selected revision to the target's working branch; an exact SHA wins over a tag, and unavailable tags or commits fail without branch fallback. The `none` target is not supported by Local environments, while the Local-only `folder` target is rejected by Docker and Daytona. Docker and Daytona ignore `cwd`; use the provider-owned workspace layout and `run.working_dir` for repository-relative commands.
Every provider except `local` is clone-based, including Docker, Daytona, and sandbox-driver plugins. When a run has a GitHub origin, Fabro clones it into the provider workspace with a history depth of 100. Set `[run.clone] enabled = false` to start a manifest-backed run with an empty workspace. Set `[run.clone] depth = 0` to clone full history. A version-backed run intent can instead submit the explicit `{ "kind": "none" }` target, which forces an empty provider workspace regardless of the workflow's clone setting. Its Git target may select a branch, an optional bare tag, an optional exact commit SHA, or both tag and SHA. Both providers attach the selected revision to the target's working branch; an exact SHA wins over a tag, and unavailable tags or commits fail without branch fallback. The `none` target is not supported by Local environments, while the Local-only `folder` target is rejected by Docker and Daytona. Docker and Daytona ignore `cwd`; use the provider-owned workspace layout and `run.working_dir` for repository-relative commands.
The image must provide `/bin/bash`; Fabro evaluates every sandbox command with it and has no `sh` fallback. Commands run in a **non-login** shell, so login profiles (`/etc/profile.d/*.sh`, `~/.bash_profile`, and `nvm`/`rbenv`/`sdkman` initializers) are not sourced — put anything they set into the Dockerfile's `ENV` instead. Fabro verifies Bash during initialization and again on resume, and fails with remediation rather than reporting the sandbox ready.

View file

@ -3,8 +3,7 @@ use std::path::Path;
use anyhow::{Context as _, anyhow, bail};
use fabro_config::project;
use fabro_environment::{DEFAULT_ENVIRONMENT_ID, Environment};
use fabro_types::settings::run::EnvironmentProvider;
use fabro_types::{DirtyStatus, RunId, RunIntent, RunTarget};
use fabro_types::{DirtyStatus, RunId, RunIntent, RunTarget, SandboxProviderKind};
use fabro_util::terminal::Styles;
use super::overrides::prepare_intent_overrides;
@ -73,7 +72,7 @@ pub(crate) async fn create_run(
resolve_run_environment(client.as_ref(), args.environment.as_deref()),
)?;
let (target, dirty_worktree) =
run_target_for_environment(environment.settings.provider, &canonical_cwd)?;
run_target_for_environment(&environment.settings.provider, &canonical_cwd)?;
if dirty_worktree {
fabro_util::printerr!(
ctx.printer(),
@ -169,10 +168,10 @@ fn warn_untransmitted_settings(
/// provider. Returns the target plus whether a clone-based observation found a
/// dirty Git worktree, so the caller can warn about it.
fn run_target_for_environment(
provider: EnvironmentProvider,
provider: &SandboxProviderKind,
canonical_cwd: &Path,
) -> anyhow::Result<(RunTarget, bool)> {
if !provider.is_clone_based() {
if !provider.clones_workspace() {
let path = canonical_cwd.to_str().ok_or_else(|| {
anyhow!(
"caller working directory is not valid UTF-8: {}",

View file

@ -920,7 +920,7 @@ mod tests {
stage_started("code", "Code"),
Event::SandboxInitialized {
working_directory: "/home/daytona/workspace".into(),
provider: SandboxProviderKind::Daytona,
provider: SandboxProviderKind::DAYTONA,
id: "daytona:sandbox-id".into(),
repo_cloned: None,
clone_origin_url: None,
@ -1276,7 +1276,7 @@ mod tests {
emit(&mut ui, stage_started("code", "Code"));
emit(&mut ui, Event::SandboxInitialized {
working_directory: "/home/daytona/workspace".into(),
provider: SandboxProviderKind::Daytona,
provider: SandboxProviderKind::DAYTONA,
id: "daytona:sandbox-id".into(),
repo_cloned: None,
clone_origin_url: None,

View file

@ -1140,7 +1140,7 @@ fn requires_github_credentials(run: &RunNamespace) -> bool {
if run.integrations.github.is_token_requested() {
return true;
}
run.execution.mode != RunMode::DryRun && run.environment.provider.is_clone_based()
run.execution.mode != RunMode::DryRun && run.environment.provider.clones_workspace()
}
fn install_signal_handlers(
@ -1230,10 +1230,10 @@ mod tests {
#[test]
fn clone_sandbox_credentials_are_required_for_clone_based_providers() {
use fabro_types::settings::run::EnvironmentProvider;
assert!(EnvironmentProvider::Docker.is_clone_based());
assert!(EnvironmentProvider::Daytona.is_clone_based());
assert!(!EnvironmentProvider::Local.is_clone_based());
use fabro_types::SandboxProviderKind;
assert!(SandboxProviderKind::DOCKER.clones_workspace());
assert!(SandboxProviderKind::DAYTONA.clones_workspace());
assert!(!SandboxProviderKind::LOCAL.clones_workspace());
}
#[test]
@ -1743,10 +1743,10 @@ mod tests {
use std::collections::HashMap;
use fabro_types::SandboxProviderKind;
use fabro_types::settings::InterpString;
use fabro_types::settings::run::{
EnvironmentProvider, RunIntegrationsGithubSettings, RunIntegrationsSettings, RunMode,
RunNamespace,
RunIntegrationsGithubSettings, RunIntegrationsSettings, RunMode, RunNamespace,
};
use super::super::requires_github_credentials;
@ -1759,7 +1759,7 @@ mod tests {
let mut run = RunNamespace::default();
run.execution.mode = mode;
run.environment.provider = provider
.parse::<EnvironmentProvider>()
.parse::<SandboxProviderKind>()
.expect("test provider should parse");
run.integrations = RunIntegrationsSettings {
github: RunIntegrationsGithubSettings {

View file

@ -21,7 +21,7 @@ pub(crate) async fn dispatch(cmd: RunsCommands, base_ctx: &CommandContext) -> Re
list::list_command(&args, &styles, base_ctx).await
}
RunsCommands::Rm(args) => rm::remove_command(&args, base_ctx).await,
RunsCommands::Inspect(args) => inspect::run(&args, base_ctx).await,
RunsCommands::Inspect(args) => Box::pin(inspect::run(&args, base_ctx)).await,
RunsCommands::Approve(args) => approval::approve_command(&args, base_ctx).await,
RunsCommands::Deny(args) => approval::deny_command(&args, base_ctx).await,
RunsCommands::Archive(args) => archive::archive_command(&args, base_ctx).await,

View file

@ -1087,9 +1087,9 @@ mod runs {
use fabro_api::types::*;
use fabro_types::settings::run::{
EnvironmentImageSettings, EnvironmentLifecycleSettings, EnvironmentProvider,
EnvironmentResourcesSettings, EnvironmentSettings, PreparedStep, PreparedStepRun,
RunEnvironmentSettings, RunGoal, RunModelSettings, RunNamespace, RunPrepareSettings,
EnvironmentImageSettings, EnvironmentLifecycleSettings, EnvironmentResourcesSettings,
EnvironmentSettings, PreparedStep, PreparedStepRun, RunEnvironmentSettings, RunGoal,
RunModelSettings, RunNamespace, RunPrepareSettings,
};
use fabro_types::settings::{InterpString, ProjectNamespace, WorkflowNamespace};
use fabro_types::{
@ -1793,7 +1793,7 @@ mod runs {
pub(super) fn settings() -> serde_json::Value {
let environment = EnvironmentSettings {
provider: EnvironmentProvider::Daytona,
provider: SandboxProviderKind::DAYTONA,
image: EnvironmentImageSettings {
docker: Some("api-server-dev".into()),
dockerfile: None,

View file

@ -12,6 +12,7 @@ use fabro_model::{Catalog, ProviderId};
use fabro_redact::redact_string;
use fabro_sandbox::{DockerSandboxProvider, daytona};
use fabro_static::EnvVars;
use fabro_types::SandboxProviderKind;
use fabro_types::settings::ServerAuthMethod;
use fabro_types::settings::server::GithubIntegrationStrategy;
use fabro_util::check_report::{CheckDetail, CheckResult, CheckSection, CheckStatus};
@ -581,8 +582,7 @@ async fn check_docker_sandbox(state: &AppState) -> CheckResult {
.server
.sandbox
.providers
.docker
.enabled,
.is_enabled(&SandboxProviderKind::DOCKER),
|| async {
DockerSandboxProvider::check_daemon()
.await

View file

@ -31,10 +31,9 @@ use fabro_model::{Catalog, ProviderId};
use fabro_sandbox::daytona;
use fabro_static::EnvVars;
use fabro_store::ArtifactStore;
use fabro_types::ServerSettings;
use fabro_types::settings::run::EnvironmentProvider;
use fabro_types::settings::server::ObjectStoreSettings;
use fabro_types::settings::{is_wildcard_host, validate_public_url_with_label};
use fabro_types::{SandboxProviderKind, ServerSettings};
use fabro_util::version::FABRO_VERSION;
use fabro_util::{Home, session_secret};
use fabro_vault::SecretType as VaultSecretType;
@ -466,10 +465,10 @@ impl InstallSandboxState {
}
}
fn to_environment_provider(&self) -> EnvironmentProvider {
fn to_environment_provider(&self) -> SandboxProviderKind {
match &self.provider {
InstallSandboxProviderState::Docker => EnvironmentProvider::Docker,
InstallSandboxProviderState::Daytona { .. } => EnvironmentProvider::Daytona,
InstallSandboxProviderState::Docker => SandboxProviderKind::DOCKER,
InstallSandboxProviderState::Daytona { .. } => SandboxProviderKind::DAYTONA,
}
}
}

View file

@ -28,9 +28,10 @@ use fabro_static::EnvVars;
use fabro_types::settings::ModelRef;
use fabro_types::settings::cli::OutputVerbosity;
use fabro_types::settings::interp::InterpString;
use fabro_types::settings::run::{EnvironmentProvider, McpServerSettings, RunGoal, RunNamespace};
use fabro_types::settings::run::{McpServerSettings, RunGoal, RunNamespace};
use fabro_types::{
ManifestPath, RunId, RunNoticeLevel, SandboxProviderKind, ServerSettings, WorkflowSettings,
BundledProvider, ManifestPath, RunId, RunNoticeLevel, SandboxProviderKind, ServerSettings,
WorkflowSettings,
};
use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus};
use fabro_validate::Severity;
@ -439,7 +440,7 @@ async fn build_preflight_report(
let server_settings = state.server_settings();
let github_integration = &server_settings.server.integrations.github;
let sandbox_provider = effective_sandbox_provider(&resolved_run);
if let Some(error) = sandbox_provider_policy_error(&server_settings, sandbox_provider) {
if let Some(error) = sandbox_provider_policy_error(&server_settings, &sandbox_provider) {
checks.push(CheckResult {
name: "Sandbox Provider Policy".into(),
status: CheckStatus::Error,
@ -465,8 +466,8 @@ async fn build_preflight_report(
&ready_providers,
&resolved_run.model.fallbacks,
);
let needs_github_credentials =
sandbox_provider.is_clone_based() || resolved_run.integrations.github.is_token_requested();
let needs_github_credentials = sandbox_provider.clones_workspace()
|| resolved_run.integrations.github.is_token_requested();
let github_app = if needs_github_credentials {
match state.github_credentials(github_integration).await {
Ok(credentials) => credentials,
@ -486,7 +487,7 @@ async fn build_preflight_report(
let daytona_api_key = state.vault_secret(EnvVars::DAYTONA_API_KEY).await?;
let sandbox_ok = run_sandbox_check(
&mut checks,
sandbox_provider,
&sandbox_provider,
prepared,
&resolved_run,
github_app.clone(),
@ -495,7 +496,7 @@ async fn build_preflight_report(
.await;
let repository_access_ok = run_repository_access_check(
&mut checks,
sandbox_provider,
&sandbox_provider,
prepared,
&resolved_run,
github_app.clone(),
@ -650,23 +651,22 @@ fn base_preflight_checks(prepared: &PreparedManifest, graph: &Graph) -> Vec<Chec
pub(crate) fn sandbox_provider_policy_error(
server_settings: &ServerSettings,
provider: SandboxProviderKind,
provider: &SandboxProviderKind,
) -> Option<String> {
let enabled = server_settings
.server
.sandbox
.providers
.for_provider(provider)
.enabled;
(!enabled).then(|| {
format!(
let providers = &server_settings.server.sandbox.providers;
match providers.get(provider) {
Some(entry) if entry.enabled => None,
Some(_) => Some(format!(
"sandbox provider \"{provider}\" is disabled by server.sandbox.providers.{provider}.enabled"
)
})
)),
None => Some(format!(
"sandbox provider \"{provider}\" is not configured; add [server.sandbox.providers.{provider}] to settings.toml"
)),
}
}
pub(crate) fn configured_sandbox_provider(settings: &RunNamespace) -> SandboxProviderKind {
SandboxProviderKind::from(settings.environment.provider)
settings.environment.provider.clone()
}
pub(crate) fn effective_sandbox_provider(settings: &RunNamespace) -> SandboxProviderKind {
@ -687,11 +687,11 @@ struct GitRemoteRefCheck {
branch: Option<String>,
}
fn clone_disabled_for_provider(provider: SandboxProviderKind, resolved_run: &RunNamespace) -> bool {
match provider {
SandboxProviderKind::Docker | SandboxProviderKind::Daytona => !resolved_run.clone.enabled,
SandboxProviderKind::Local => false,
}
fn clone_disabled_for_provider(
provider: &SandboxProviderKind,
resolved_run: &RunNamespace,
) -> bool {
provider.clones_workspace() && !resolved_run.clone.enabled
}
fn run_environment_capability_check(checks: &mut Vec<CheckResult>, resolved_run: &RunNamespace) {
@ -714,8 +714,8 @@ fn run_environment_capability_check(checks: &mut Vec<CheckResult>, resolved_run:
fn environment_capability_warnings(resolved_run: &RunNamespace) -> Vec<String> {
let environment = &resolved_run.environment;
let mut warnings = Vec::new();
match environment.provider {
EnvironmentProvider::Local => {
match environment.provider.bundled() {
Some(BundledProvider::Local) => {
if environment.resources.cpu.is_some()
|| environment.resources.memory.is_some()
|| environment.resources.disk.is_some()
@ -729,7 +729,7 @@ fn environment_capability_warnings(resolved_run: &RunNamespace) -> Vec<String> {
warnings.push("local provider ignores lifecycle.auto_stop".to_string());
}
}
EnvironmentProvider::Docker => {
Some(BundledProvider::Docker) => {
if environment.cwd.is_some() {
warnings.push("docker provider ignores cwd".to_string());
}
@ -746,11 +746,16 @@ fn environment_capability_warnings(resolved_run: &RunNamespace) -> Vec<String> {
warnings.push("docker provider ignores image.dockerfile".to_string());
}
}
EnvironmentProvider::Daytona => {
Some(BundledProvider::Daytona) => {
if environment.cwd.is_some() {
warnings.push("daytona provider ignores cwd".to_string());
}
}
None => {
if environment.cwd.is_some() {
warnings.push(format!("{} provider ignores cwd", environment.provider));
}
}
}
warnings
}
@ -765,7 +770,7 @@ fn repository_access_details(request: &GitRemoteRefCheck) -> Vec<CheckDetail> {
async fn run_repository_access_check(
checks: &mut Vec<CheckResult>,
sandbox_provider: SandboxProviderKind,
sandbox_provider: &SandboxProviderKind,
prepared: &PreparedManifest,
resolved_run: &RunNamespace,
github_app: Option<fabro_github::GitHubCredentials>,
@ -783,7 +788,7 @@ async fn run_repository_access_check(
async fn run_repository_access_check_with<F, Fut>(
checks: &mut Vec<CheckResult>,
sandbox_provider: SandboxProviderKind,
sandbox_provider: &SandboxProviderKind,
prepared: &PreparedManifest,
resolved_run: &RunNamespace,
github_app: Option<fabro_github::GitHubCredentials>,
@ -793,7 +798,7 @@ where
F: FnOnce(GitRemoteRefCheck, Option<fabro_github::GitHubCredentials>) -> Fut,
Fut: Future<Output = Result<(), String>>,
{
if !sandbox_provider.is_clone_based()
if !sandbox_provider.clones_workspace()
|| clone_disabled_for_provider(sandbox_provider, resolved_run)
{
return true;
@ -910,7 +915,7 @@ async fn run_ls_remote(mut command: Command) -> std::result::Result<(), String>
}
fn preflight_sandbox_spec(
sandbox_provider: SandboxProviderKind,
sandbox_provider: &SandboxProviderKind,
prepared: &PreparedManifest,
resolved_run: &RunNamespace,
github_app: Option<fabro_github::GitHubCredentials>,
@ -922,15 +927,15 @@ fn preflight_sandbox_spec(
.map(|git| fabro_github::normalize_repo_origin_url(&git.origin_url));
let clone_branch = prepared.git.as_ref().map(|git| git.branch.clone());
Ok(match sandbox_provider {
SandboxProviderKind::Local => {
Ok(match sandbox_provider.bundled() {
Some(BundledProvider::Local) => {
let working_directory = local_working_directory_from_environment(
&resolved_run.environment,
Some(&prepared.source_directory),
)?;
SandboxSpec::Local { working_directory }
}
SandboxProviderKind::Docker => {
Some(BundledProvider::Docker) => {
let mut config = resolve_docker_config(resolved_run);
config.skip_clone = true;
SandboxSpec::Docker {
@ -943,7 +948,7 @@ fn preflight_sandbox_spec(
clone_commit_sha: None,
}
}
SandboxProviderKind::Daytona => {
Some(BundledProvider::Daytona) => {
let mut config = resolve_daytona_config(resolved_run);
config.skip_clone = true;
SandboxSpec::Daytona {
@ -957,12 +962,17 @@ fn preflight_sandbox_spec(
api_key: daytona_api_key,
}
}
None => {
return Err(fabro_sandbox::Error::message(format!(
"sandbox provider `{sandbox_provider}` is not bundled; plugin providers are constructed by the server"
)));
}
})
}
async fn run_sandbox_check(
checks: &mut Vec<CheckResult>,
sandbox_provider: SandboxProviderKind,
sandbox_provider: &SandboxProviderKind,
prepared: &PreparedManifest,
resolved_run: &RunNamespace,
github_app: Option<fabro_github::GitHubCredentials>,
@ -988,7 +998,7 @@ async fn run_sandbox_check(
}
};
let sandbox_result: Result<Arc<dyn Sandbox>, String> = spec.build(None).await.map_err(|err| {
if matches!(sandbox_provider, SandboxProviderKind::Daytona) {
if *sandbox_provider == SandboxProviderKind::DAYTONA {
format!("Daytona sandbox creation failed: {err}")
} else {
err.to_string()
@ -999,7 +1009,7 @@ async fn run_sandbox_check(
Ok(sandbox) => match sandbox.initialize().await {
Ok(()) => {
let mut details = vec![CheckDetail::new(format!("Provider: {sandbox_provider}"))];
if sandbox_provider.is_clone_based()
if sandbox_provider.clones_workspace()
&& prepared.git.is_none()
&& !clone_disabled_for_provider(sandbox_provider, resolved_run)
{
@ -1518,7 +1528,7 @@ where
{
let credential_context = fabro_sandbox::CredentialContext::from_snapshot(Some(&snapshot));
fabro_sandbox::retry_git_operation(
SandboxProviderKind::Local,
SandboxProviderKind::LOCAL,
"repository probe",
&fabro_sandbox::RetryPlan::repository_probe(),
|_attempt| run(),
@ -1983,7 +1993,7 @@ digraph Demo {{
}
fn prepared_and_resolved_for_sandbox(
provider: SandboxProviderKind,
provider: &SandboxProviderKind,
clone_enabled: bool,
git: Option<types::GitContext>,
) -> (PreparedManifest, RunNamespace) {
@ -2033,7 +2043,7 @@ enabled = {clone_enabled}
#[test]
fn docker_environment_cwd_is_reported_as_ignored() {
let mut resolved = RunNamespace::default();
resolved.environment.provider = EnvironmentProvider::Docker;
resolved.environment.provider = SandboxProviderKind::DOCKER;
resolved.environment.cwd = Some("/workspace/custom".to_string());
assert_eq!(environment_capability_warnings(&resolved), vec![
@ -2044,7 +2054,7 @@ enabled = {clone_enabled}
#[test]
fn daytona_environment_cwd_is_reported_as_ignored() {
let mut resolved = RunNamespace::default();
resolved.environment.provider = EnvironmentProvider::Daytona;
resolved.environment.provider = SandboxProviderKind::DAYTONA;
resolved.environment.cwd = Some("/home/daytona/workspace/custom".to_string());
assert_eq!(environment_capability_warnings(&resolved), vec![
@ -2079,14 +2089,14 @@ provider = "local"
assert_eq!(
prepared.settings.run.environment.provider,
EnvironmentProvider::Local
SandboxProviderKind::LOCAL
);
}
#[tokio::test]
async fn repository_access_check_skips_when_clone_is_disabled() {
let (prepared, resolved) = prepared_and_resolved_for_sandbox(
SandboxProviderKind::Docker,
&SandboxProviderKind::DOCKER,
false,
Some(git_context("https://github.com/acme/widgets", "main")),
);
@ -2096,7 +2106,7 @@ provider = "local"
let ok = run_repository_access_check_with(
&mut checks,
SandboxProviderKind::Docker,
&SandboxProviderKind::DOCKER,
&prepared,
&resolved,
None,
@ -2115,7 +2125,7 @@ provider = "local"
#[tokio::test]
async fn repository_access_check_rejects_non_github_origins_before_remote_probe() {
let (prepared, resolved) = prepared_and_resolved_for_sandbox(
SandboxProviderKind::Docker,
&SandboxProviderKind::DOCKER,
true,
Some(git_context("https://gitlab.com/acme/widgets", "main")),
);
@ -2125,7 +2135,7 @@ provider = "local"
let ok = run_repository_access_check_with(
&mut checks,
SandboxProviderKind::Docker,
&SandboxProviderKind::DOCKER,
&prepared,
&resolved,
None,
@ -2153,7 +2163,7 @@ provider = "local"
#[tokio::test]
async fn repository_access_check_probes_normalized_github_branch() {
let (prepared, resolved) = prepared_and_resolved_for_sandbox(
SandboxProviderKind::Docker,
&SandboxProviderKind::DOCKER,
true,
Some(git_context(
"git@github.com:acme/widgets.git",
@ -2166,7 +2176,7 @@ provider = "local"
let ok = run_repository_access_check_with(
&mut checks,
SandboxProviderKind::Docker,
&SandboxProviderKind::DOCKER,
&prepared,
&resolved,
None,
@ -2190,7 +2200,7 @@ provider = "local"
#[tokio::test]
async fn repository_access_check_surfaces_remote_probe_failure() {
let (prepared, resolved) = prepared_and_resolved_for_sandbox(
SandboxProviderKind::Docker,
&SandboxProviderKind::DOCKER,
true,
Some(git_context("https://github.com/acme/widgets", "missing")),
);
@ -2198,7 +2208,7 @@ provider = "local"
let ok = run_repository_access_check_with(
&mut checks,
SandboxProviderKind::Docker,
&SandboxProviderKind::DOCKER,
&prepared,
&resolved,
None,
@ -2222,13 +2232,13 @@ provider = "local"
#[test]
fn preflight_sandbox_spec_disables_docker_clone_but_preserves_clone_metadata() {
let (prepared, resolved) = prepared_and_resolved_for_sandbox(
SandboxProviderKind::Docker,
&SandboxProviderKind::DOCKER,
true,
Some(git_context("https://github.com/acme/widgets", "main")),
);
let spec = preflight_sandbox_spec(
SandboxProviderKind::Docker,
&SandboxProviderKind::DOCKER,
&prepared,
&resolved,
None,
@ -3293,7 +3303,7 @@ dockerfile = { path = "Dockerfile" }
fn declared(origin: &str, additional: &[&str]) -> (PreparedManifest, RunNamespace) {
let (prepared, mut resolved) = prepared_and_resolved_for_sandbox(
SandboxProviderKind::Local,
&SandboxProviderKind::LOCAL,
true,
Some(git_context(origin, "main")),
);

View file

@ -2336,15 +2336,15 @@ fn build_sandbox_provider_registry(
let provider_settings = &server_settings.server.sandbox.providers;
let mut providers: Vec<Arc<dyn SandboxProvider>> = Vec::new();
if provider_settings.local.enabled {
if provider_settings.is_enabled(&SandboxProviderKind::LOCAL) {
providers.push(Arc::new(LocalSandboxProvider));
}
if provider_settings.docker.enabled {
if provider_settings.is_enabled(&SandboxProviderKind::DOCKER) {
providers.push(Arc::new(DockerSandboxProvider::new()));
}
if provider_settings.daytona.enabled && daytona_api_key.is_some() {
if provider_settings.is_enabled(&SandboxProviderKind::DAYTONA) && daytona_api_key.is_some() {
let api_url = env_lookup(EnvVars::DAYTONA_API_URL)
.or_else(|| env_lookup(EnvVars::DAYTONA_SERVER_URL));
let organization_id = env_lookup(EnvVars::DAYTONA_ORGANIZATION_ID);
@ -2432,8 +2432,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
.server
.sandbox
.providers
.local
.enabled;
.is_enabled(&SandboxProviderKind::LOCAL);
let environment_pool = db_pool.clone();
let environment_store = Arc::new(
load_store_blocking("environment store", move || async move {
@ -3291,7 +3290,8 @@ async fn reject_run_if_sandbox_provider_disabled(
settings: &RunNamespace,
) -> bool {
let provider = run_manifest::effective_sandbox_provider(settings);
let Some(error) = run_manifest::sandbox_provider_policy_error(server_settings, provider) else {
let Some(error) = run_manifest::sandbox_provider_policy_error(server_settings, &provider)
else {
return false;
};
tracing::warn!(run_id = %run_id, error = %error, "Sandbox provider disabled by server policy");
@ -4081,7 +4081,7 @@ async fn execute_run_in_process(state: Arc<AppState>, run_id: RunId) {
let run_spec = persisted.run_spec();
let settings = &run_spec.settings.run;
let clone_can_use_github_credentials = settings.execution.mode != RunMode::DryRun
&& settings.environment.provider.is_clone_based()
&& settings.environment.provider.clones_workspace()
&& run_spec
.repo_origin_url()
.is_some_and(|origin| !origin.trim().is_empty());

View file

@ -7,7 +7,7 @@ use fabro_automation::{
};
use fabro_environment::EnvironmentId;
use fabro_store::{RunSummaryListQuery, RunSummaryVisibility};
use fabro_types::{AutomationRef, RunId, SandboxProviderKind};
use fabro_types::{AutomationRef, RunId};
use fabro_util::error as error_util;
use serde::Serialize;
@ -282,7 +282,7 @@ pub(in crate::server) fn resolve_automation_environment(
"automation_environment_not_found",
));
};
if !environment.settings.provider.is_clone_based() {
if !environment.settings.provider.clones_workspace() {
return Err(ApiError::with_code(
status,
format!(
@ -291,9 +291,9 @@ pub(in crate::server) fn resolve_automation_environment(
"automation_environment_incompatible",
));
}
let provider = SandboxProviderKind::from(environment.settings.provider);
let provider = environment.settings.provider.clone();
if let Some(message) =
run_manifest::sandbox_provider_policy_error(&state.server_settings(), provider)
run_manifest::sandbox_provider_policy_error(&state.server_settings(), &provider)
{
return Err(ApiError::with_code(
status,

View file

@ -3,11 +3,11 @@ use std::sync::Arc;
use axum::http::HeaderMap;
use fabro_environment::{Environment, EnvironmentDraft, EnvironmentId, EnvironmentStoreError};
use fabro_types::SandboxProviderKind;
use fabro_types::settings::InterpString;
use fabro_types::settings::run::{
DockerfileSource, EnvironmentImageSettings, EnvironmentLifecycleSettings,
EnvironmentNetworkSettings, EnvironmentProvider, EnvironmentResourcesSettings,
EnvironmentSettings,
EnvironmentNetworkSettings, EnvironmentResourcesSettings, EnvironmentSettings,
};
use serde::de::IgnoredAny;
use serde::{Deserialize, Serialize};
@ -33,7 +33,7 @@ struct EnvironmentListMeta {
#[serde(deny_unknown_fields)]
struct CreateEnvironmentRequest {
id: EnvironmentId,
provider: EnvironmentProvider,
provider: SandboxProviderKind,
cwd: Option<String>,
image: ApiEnvironmentImageSettings,
resources: EnvironmentResourcesSettings,
@ -46,7 +46,7 @@ struct CreateEnvironmentRequest {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ReplaceEnvironmentRequest {
provider: EnvironmentProvider,
provider: SandboxProviderKind,
cwd: Option<String>,
image: ApiEnvironmentImageSettings,
resources: EnvironmentResourcesSettings,

View file

@ -1102,21 +1102,20 @@ async fn validate_intent_environment(
let configured_provider = run_manifest::configured_sandbox_provider(&settings.run);
let effective_provider = run_manifest::effective_sandbox_provider(&settings.run);
let image = &settings.run.environment.image;
let image_incompatible = match effective_provider {
SandboxProviderKind::Docker => image.docker.is_none() && image.dockerfile.is_some(),
SandboxProviderKind::Local | SandboxProviderKind::Daytona => false,
};
let image_incompatible = effective_provider == SandboxProviderKind::DOCKER
&& image.docker.is_none()
&& image.dockerfile.is_some();
let (target_incompatible, detail) = match target {
RunTarget::Git(_) => (
configured_provider == SandboxProviderKind::Local || !settings.run.clone.enabled,
configured_provider == SandboxProviderKind::LOCAL || !settings.run.clone.enabled,
"Git targets require a compatible clone-enabled Docker or Daytona environment",
),
RunTarget::None {} => (
configured_provider == SandboxProviderKind::Local,
configured_provider == SandboxProviderKind::LOCAL,
"none targets require a compatible Docker or Daytona environment",
),
RunTarget::Folder { .. } => (
configured_provider != SandboxProviderKind::Local,
configured_provider != SandboxProviderKind::LOCAL,
"folder targets require a Local environment",
),
};
@ -1125,18 +1124,18 @@ async fn validate_intent_environment(
}
// Settings resolution drops `run.pull_request` unless it is enabled, so
// `Some` means automatic pull requests were requested.
if !configured_provider.is_clone_based() && settings.run.pull_request.is_some() {
if !configured_provider.clones_workspace() && settings.run.pull_request.is_some() {
return Err(EnvironmentSelectionError::AutomaticPullRequestUnsupported);
}
if let Some(detail) =
run_manifest::sandbox_provider_policy_error(&state.server_settings(), effective_provider)
run_manifest::sandbox_provider_policy_error(&state.server_settings(), &effective_provider)
{
return Err(EnvironmentSelectionError::ProviderDisabled {
provider: effective_provider,
detail,
});
}
if effective_provider == SandboxProviderKind::Daytona {
if effective_provider == SandboxProviderKind::DAYTONA {
match state.vault_secret(EnvVars::DAYTONA_API_KEY).await {
Ok(Some(key)) if !key.trim().is_empty() => {}
Ok(_) => {
@ -1380,7 +1379,7 @@ pub(crate) async fn create_run_from_manifest(
let prepared = prepared.with_web_url(state.run_web_url(&run_id));
let provider = run_manifest::effective_sandbox_provider(&prepared.settings().run);
if let Some(error) =
run_manifest::sandbox_provider_policy_error(&state.server_settings(), provider)
run_manifest::sandbox_provider_policy_error(&state.server_settings(), &provider)
{
return ApiError::bad_request(error).into_response();
}

View file

@ -6,7 +6,8 @@ use std::sync::Arc;
use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade};
use fabro_sandbox::{TerminalSize, open_terminal_for_run};
use fabro_types::{
RunSandboxInstance, SandboxProviderKind, SandboxServiceDiscoverySource, SandboxServiceListMeta,
BundledProvider, RunSandboxInstance, SandboxProviderKind, SandboxServiceDiscoverySource,
SandboxServiceListMeta,
};
use futures_util::FutureExt;
use futures_util::future::BoxFuture;
@ -417,8 +418,8 @@ async fn create_ssh_access(
Err(response) => return response,
};
match record.provider {
SandboxProviderKind::Daytona => {
match record.provider.bundled() {
Some(BundledProvider::Daytona) => {
let sandbox = match reconnect_daytona_sandbox_instance(&state, &record).await {
Ok(sandbox) => sandbox,
Err(response) => return response,
@ -432,7 +433,7 @@ async fn create_ssh_access(
}
}
}
SandboxProviderKind::Docker => {
Some(BundledProvider::Docker) | None => {
let sandbox = match reconnect_run_sandbox_instance(&state, &id, &record).await {
Ok(sandbox) => sandbox,
Err(response) => return response,
@ -451,7 +452,7 @@ async fn create_ssh_access(
}
}
}
SandboxProviderKind::Local => ApiError::new(
Some(BundledProvider::Local) => ApiError::new(
StatusCode::CONFLICT,
"Sandbox provider does not support access commands.",
)
@ -472,7 +473,7 @@ async fn create_sandbox_vnc_preview(
Ok(record) => record,
Err(response) => return response,
};
if record.provider != SandboxProviderKind::Daytona {
if record.provider != SandboxProviderKind::DAYTONA {
return ApiError::new(
StatusCode::NOT_IMPLEMENTED,
"Sandbox provider does not support VNC previews.",
@ -576,7 +577,7 @@ async fn list_sandbox_services(
Ok(record) => record,
Err(response) => return response,
};
let provider = record.provider;
let provider = record.provider.clone();
let sandbox = match reconnect_run_sandbox_instance(&state, &id, &record).await {
Ok(sandbox) => sandbox,
Err(response) => return response,
@ -604,7 +605,7 @@ async fn list_sandbox_services(
.into_response();
}
let discovery = parse_sandbox_services(&result.stdout, provider);
let discovery = parse_sandbox_services(&result.stdout, &provider);
Json(SandboxServiceListResponse {
data: discovery.services,
meta: SandboxServiceListMeta {
@ -631,7 +632,7 @@ struct SandboxServiceDiscovery {
source: SandboxServiceDiscoverySource,
}
fn parse_sandbox_services(output: &str, provider: SandboxProviderKind) -> SandboxServiceDiscovery {
fn parse_sandbox_services(output: &str, provider: &SandboxProviderKind) -> SandboxServiceDiscovery {
if output
.lines()
.any(|line| line.trim_start().starts_with("FABRO_PROC_NET_TCP "))
@ -648,7 +649,10 @@ fn parse_sandbox_services(output: &str, provider: SandboxProviderKind) -> Sandbo
}
}
fn parse_ss_listening_services(output: &str, provider: SandboxProviderKind) -> Vec<SandboxService> {
fn parse_ss_listening_services(
output: &str,
provider: &SandboxProviderKind,
) -> Vec<SandboxService> {
let mut services = BTreeMap::<u16, SandboxService>::new();
for line in output
.lines()
@ -681,7 +685,7 @@ enum ProcNetFamily {
fn parse_proc_net_listening_services(
output: &str,
provider: SandboxProviderKind,
provider: &SandboxProviderKind,
) -> Vec<SandboxService> {
let mut services = BTreeMap::<u16, SandboxService>::new();
let mut family = None;
@ -761,7 +765,7 @@ fn parse_proc_net_ipv6(value: &str) -> Option<Ipv6Addr> {
fn push_service(
services: &mut BTreeMap<u16, SandboxService>,
provider: SandboxProviderKind,
provider: &SandboxProviderKind,
port: u16,
address: String,
process: Option<String>,
@ -778,8 +782,8 @@ fn push_service(
}
}
fn preview_supported(provider: SandboxProviderKind, port: u16) -> bool {
provider == SandboxProviderKind::Daytona && (3000..=9999).contains(&port)
fn preview_supported(provider: &SandboxProviderKind, port: u16) -> bool {
*provider == SandboxProviderKind::DAYTONA && (3000..=9999).contains(&port)
}
fn push_unique(values: &mut Vec<String>, value: String) {
@ -899,7 +903,7 @@ async fn reconnect_daytona_sandbox_instance(
state: &Arc<AppState>,
record: &RunSandboxInstance,
) -> Result<DaytonaSandbox, Response> {
if record.provider != SandboxProviderKind::Daytona {
if record.provider != SandboxProviderKind::DAYTONA {
return Err(ApiError::new(
StatusCode::CONFLICT,
"Sandbox provider does not support this capability.",
@ -1061,7 +1065,7 @@ LISTEN 0 4096 0.0.0.0:5173 0.0.0.0:* users:(("vite",pid=84,fd=19))
LISTEN 0 4096 [::]:8080 [::]:* users:(("server",pid=126,fd=9))
LISTEN 0 4096 [::1]:2500 [::]:* users:(("debug",pid=168,fd=7))
"#,
SandboxProviderKind::Daytona,
&SandboxProviderKind::DAYTONA,
);
assert_eq!(services.len(), 4);
@ -1095,7 +1099,7 @@ not enough fields
LISTEN 0 4096 127.0.0.1:0 0.0.0.0:* users:(("zero",pid=1,fd=2))
LISTEN 0 4096 127.0.0.1:65536 0.0.0.0:* users:(("large",pid=1,fd=2))
"#,
SandboxProviderKind::Daytona,
&SandboxProviderKind::DAYTONA,
);
assert!(services.is_empty());
@ -1110,7 +1114,7 @@ LISTEN 0 4096 0.0.0.0:3000 0.0.0.0:* users:(("node",pid=42,fd=23))
LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:* users:(("node",pid=42,fd=23))
LISTEN 0 4096 [::]:3000 [::]:* users:(("vite",pid=84,fd=19))
"#,
SandboxProviderKind::Daytona,
&SandboxProviderKind::DAYTONA,
);
assert_eq!(services, vec![SandboxService {
@ -1142,7 +1146,7 @@ FABRO_PROC_NET_TCP /proc/net/tcp6
0: 00000000000000000000000000000000:1F90 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 501 0 44444
1: 00000000000000000000000001000000:09C4 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 501 0 55555
",
SandboxProviderKind::Daytona,
&SandboxProviderKind::DAYTONA,
);
assert_eq!(discovery.source, SandboxServiceDiscoverySource::Procfs);
@ -1176,11 +1180,11 @@ FABRO_PROC_NET_TCP /proc/net/tcp6
#[test]
fn preview_support_is_daytona_only_for_documented_range() {
assert!(!preview_supported(SandboxProviderKind::Daytona, 2500));
assert!(preview_supported(SandboxProviderKind::Daytona, 3000));
assert!(preview_supported(SandboxProviderKind::Daytona, 9999));
assert!(!preview_supported(SandboxProviderKind::Daytona, 10000));
assert!(!preview_supported(SandboxProviderKind::Docker, 3000));
assert!(!preview_supported(&SandboxProviderKind::DAYTONA, 2500));
assert!(preview_supported(&SandboxProviderKind::DAYTONA, 3000));
assert!(preview_supported(&SandboxProviderKind::DAYTONA, 9999));
assert!(!preview_supported(&SandboxProviderKind::DAYTONA, 10000));
assert!(!preview_supported(&SandboxProviderKind::DOCKER, 3000));
}
#[test]

View file

@ -113,9 +113,9 @@ mod tests {
#[tokio::test]
async fn list_returns_provider_backed_data_without_run_projection_state() {
let docker = fake_sandbox_info(SandboxProviderKind::Docker, "docker-native-id");
let docker = fake_sandbox_info(SandboxProviderKind::DOCKER, "docker-native-id");
let app = app_with_registry(fake_registry(vec![FakeSandboxProvider::new(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
FakeList::Ok(vec![docker]),
FakeGet::Missing,
)]));
@ -131,15 +131,15 @@ mod tests {
#[tokio::test]
async fn retrieve_searches_all_configured_providers() {
let daytona = fake_sandbox_info(SandboxProviderKind::Daytona, "native-id");
let daytona = fake_sandbox_info(SandboxProviderKind::DAYTONA, "native-id");
let app = app_with_registry(fake_registry(vec![
FakeSandboxProvider::new(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
FakeList::Ok(Vec::new()),
FakeGet::Missing,
),
FakeSandboxProvider::new(
SandboxProviderKind::Daytona,
SandboxProviderKind::DAYTONA,
FakeList::Ok(Vec::new()),
FakeGet::Found(Box::new(daytona)),
),
@ -160,12 +160,12 @@ mod tests {
async fn no_matching_sandbox_returns_404() {
let app = app_with_registry(fake_registry(vec![
FakeSandboxProvider::new(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
FakeList::Ok(Vec::new()),
FakeGet::Missing,
),
FakeSandboxProvider::new(
SandboxProviderKind::Daytona,
SandboxProviderKind::DAYTONA,
FakeList::Ok(Vec::new()),
FakeGet::Missing,
),
@ -183,18 +183,18 @@ mod tests {
async fn duplicate_native_ids_return_409() {
let app = app_with_registry(fake_registry(vec![
FakeSandboxProvider::new(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
FakeList::Ok(Vec::new()),
FakeGet::Found(Box::new(fake_sandbox_info(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
"same-id",
))),
),
FakeSandboxProvider::new(
SandboxProviderKind::Daytona,
SandboxProviderKind::DAYTONA,
FakeList::Ok(Vec::new()),
FakeGet::Found(Box::new(fake_sandbox_info(
SandboxProviderKind::Daytona,
SandboxProviderKind::DAYTONA,
"same-id",
))),
),
@ -219,12 +219,12 @@ mod tests {
async fn provider_lookup_uncertainty_returns_502() {
let app = app_with_registry(fake_registry(vec![
FakeSandboxProvider::new(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
FakeList::Ok(Vec::new()),
FakeGet::Missing,
),
FakeSandboxProvider::new(
SandboxProviderKind::Daytona,
SandboxProviderKind::DAYTONA,
FakeList::Ok(Vec::new()),
FakeGet::Err("daytona unavailable"),
),

View file

@ -24,7 +24,7 @@ use fabro_llm::types::{Message as LlmMessage, Request as LlmRequest, TokenCounts
use fabro_model::catalog::LlmCatalogSettings;
use fabro_model::{Catalog, ModelRef, ProviderId, ReasoningEffort, Speed};
use fabro_types::settings::ServerAuthMethod;
use fabro_types::settings::run::{ApprovalMode, EnvironmentProvider};
use fabro_types::settings::run::ApprovalMode;
use fabro_types::{
AgentBackend, AttrValue, AuthMethod, BlobHash, CommandTermination, FailureCategory,
FailureDetail, GitRunTarget, Graph, InterviewQuestionRecord, Node, Outcome, ParallelBranchId,
@ -86,7 +86,7 @@ fn manifest_run_defaults_from_toml(source: &str) -> fabro_config::RunLayer {
}
fn test_environment_store(
default_provider: Option<EnvironmentProvider>,
default_provider: Option<SandboxProviderKind>,
local_enabled: bool,
) -> (tempfile::TempDir, EnvironmentStore) {
let temp = tempfile::tempdir().expect("environment store tempdir should be created");
@ -1313,7 +1313,7 @@ id = "missing"
#[test]
fn system_sandbox_provider_uses_manifest_defaults() {
let (_environment_temp, environment_store) =
test_environment_store(Some(EnvironmentProvider::Daytona), true);
test_environment_store(Some(SandboxProviderKind::DAYTONA), true);
let (_mcp_temp, mcp_server_store) = test_mcp_server_store();
let source = r#"
_version = 1
@ -1367,8 +1367,11 @@ enabled = false
);
assert_eq!(
crate::run_manifest::sandbox_provider_policy_error(&settings, SandboxProviderKind::Daytona)
.as_deref(),
crate::run_manifest::sandbox_provider_policy_error(
&settings,
&SandboxProviderKind::DAYTONA
)
.as_deref(),
Some(
"sandbox provider \"daytona\" is disabled by server.sandbox.providers.daytona.enabled"
)
@ -1377,10 +1380,10 @@ enabled = false
#[test]
fn clone_sandbox_credentials_are_available_for_clone_based_providers() {
use fabro_types::settings::run::EnvironmentProvider;
assert!(EnvironmentProvider::Docker.is_clone_based());
assert!(EnvironmentProvider::Daytona.is_clone_based());
assert!(!EnvironmentProvider::Local.is_clone_based());
use fabro_types::SandboxProviderKind;
assert!(SandboxProviderKind::DOCKER.clones_workspace());
assert!(SandboxProviderKind::DAYTONA.clones_workspace());
assert!(!SandboxProviderKind::LOCAL.clones_workspace());
}
#[tokio::test]
@ -3530,7 +3533,7 @@ async fn post_run_intent_response(app: &Router, intent: serde_json::Value) -> Re
/// the only placement folder targets admit.
fn local_test_app_state() -> Arc<AppState> {
TestAppStateBuilder::new()
.default_environment_provider(Some(EnvironmentProvider::Local))
.default_environment_provider(Some(SandboxProviderKind::LOCAL))
.vault_entries([(fabro_static::EnvVars::OPENAI_API_KEY, "test-openai-api-key")])
.build()
}
@ -3729,7 +3732,7 @@ docker = "workflow-owned:latest"
);
assert_eq!(
projection.spec.settings.run.environment.provider,
EnvironmentProvider::Docker
SandboxProviderKind::DOCKER
);
assert_eq!(
projection
@ -3815,7 +3818,7 @@ async fn post_runs_run_intent_args_true_override_resolved_settings_without_start
let workspace = dir.path().join("workspace");
std::fs::create_dir(&workspace).unwrap();
let state = TestAppStateBuilder::new()
.default_environment_provider(Some(EnvironmentProvider::Local))
.default_environment_provider(Some(SandboxProviderKind::LOCAL))
.env_lookup(|_| None)
.vault_entries([(EnvVars::OPENAI_API_KEY, "test-openai-api-key")])
.build();
@ -3875,7 +3878,7 @@ async fn post_runs_run_intent_dry_run_uses_configured_target_provider() {
),
(
TestAppStateBuilder::new()
.default_environment_provider(Some(EnvironmentProvider::Daytona))
.default_environment_provider(Some(SandboxProviderKind::DAYTONA))
.vault_entries([(fabro_static::EnvVars::OPENAI_API_KEY, "test-openai-api-key")])
.build(),
Some("_version = 1\n[run.execution]\nmode = \"dry_run\"\n"),
@ -3884,7 +3887,7 @@ async fn post_runs_run_intent_dry_run_uses_configured_target_provider() {
),
(
TestAppStateBuilder::new()
.default_environment_provider(Some(EnvironmentProvider::Daytona))
.default_environment_provider(Some(SandboxProviderKind::DAYTONA))
.vault_entries([(fabro_static::EnvVars::OPENAI_API_KEY, "test-openai-api-key")])
.build(),
Some("_version = 1\n[run.execution]\nmode = \"dry_run\"\n"),
@ -3901,7 +3904,7 @@ async fn post_runs_run_intent_dry_run_uses_configured_target_provider() {
default_test_server_settings(),
manifest_run_defaults_from_toml("[run.execution]\nmode = \"dry_run\"\n"),
)
.default_environment_provider(Some(EnvironmentProvider::Local))
.default_environment_provider(Some(SandboxProviderKind::LOCAL))
.vault_entries([(fabro_static::EnvVars::OPENAI_API_KEY, "test-openai-api-key")])
.build(),
None,
@ -3959,7 +3962,7 @@ async fn post_runs_run_intent_dry_run_rejects_configured_target_mismatches() {
),
(
TestAppStateBuilder::new()
.default_environment_provider(Some(EnvironmentProvider::Daytona))
.default_environment_provider(Some(SandboxProviderKind::DAYTONA))
.vault_entries([(fabro_static::EnvVars::OPENAI_API_KEY, "test-openai-api-key")])
.build(),
json!({ "kind": "folder", "path": "/path-that-must-not-be-read" }),
@ -4094,7 +4097,7 @@ preserve = true
"#,
),
)
.default_environment_provider(Some(EnvironmentProvider::Local))
.default_environment_provider(Some(SandboxProviderKind::LOCAL))
.env_lookup(|_| None)
.vault_entries([(EnvVars::OPENAI_API_KEY, "test-openai-api-key")])
.build();
@ -4232,7 +4235,7 @@ async fn post_runs_run_intent_canonicalizes_and_persists_a_local_folder_target()
);
assert_eq!(
projection.spec.settings.run.environment.provider,
EnvironmentProvider::Local
SandboxProviderKind::LOCAL
);
assert_eq!(projection.spec.manifest_blob, None);
assert!(projection.spec.definition_blob.is_some());
@ -4327,7 +4330,7 @@ async fn post_runs_run_intent_accepts_automatic_pull_requests_for_configured_doc
assert_eq!(
projection.spec.settings.run.environment.provider,
EnvironmentProvider::Docker
SandboxProviderKind::DOCKER
);
assert_eq!(projection.spec.settings.run.execution.mode, RunMode::DryRun);
assert!(projection.spec.settings.run.pull_request.is_some());
@ -4424,7 +4427,7 @@ async fn post_runs_run_intent_applies_the_folder_target_environment_matrix() {
for state in [
test_app_state(),
TestAppStateBuilder::new()
.default_environment_provider(Some(EnvironmentProvider::Daytona))
.default_environment_provider(Some(SandboxProviderKind::DAYTONA))
.vault_entries([(fabro_static::EnvVars::OPENAI_API_KEY, "test-openai-api-key")])
.build(),
] {
@ -4460,7 +4463,7 @@ enabled = false
),
RunLayer::default(),
)
.default_environment_provider(Some(EnvironmentProvider::Local))
.default_environment_provider(Some(SandboxProviderKind::LOCAL))
.vault_entries([(fabro_static::EnvVars::OPENAI_API_KEY, "test-openai-api-key")])
.build();
let app = crate::test_support::build_test_router(Arc::clone(&disabled_state));
@ -4483,7 +4486,7 @@ enabled = false
#[tokio::test]
async fn post_runs_run_intent_accepts_none_target_with_ready_daytona_environment() {
let state = TestAppStateBuilder::new()
.default_environment_provider(Some(EnvironmentProvider::Daytona))
.default_environment_provider(Some(SandboxProviderKind::DAYTONA))
.vault_entries([
(fabro_static::EnvVars::OPENAI_API_KEY, "test-openai-api-key"),
(
@ -4520,7 +4523,7 @@ async fn post_runs_run_intent_accepts_none_target_with_ready_daytona_environment
);
assert_eq!(
projection.spec.settings.run.environment.provider,
EnvironmentProvider::Daytona
SandboxProviderKind::DAYTONA
);
assert_eq!(projection.spec.source_directory, None);
assert_eq!(projection.spec.git, None);
@ -4787,7 +4790,7 @@ enabled = false
assert_run_intent_targets_unavailable(&disabled_state).await;
let daytona_state = TestAppStateBuilder::new()
.default_environment_provider(Some(EnvironmentProvider::Daytona))
.default_environment_provider(Some(SandboxProviderKind::DAYTONA))
.vault_entries([(fabro_static::EnvVars::OPENAI_API_KEY, "test-openai-api-key")])
.build();
assert_run_intent_targets_unavailable(&daytona_state).await;
@ -16010,7 +16013,7 @@ async fn create_preserved_local_sandbox_run(state: &Arc<AppState>, run_id: RunId
definition_blob: None,
},
workflow_event::Event::SandboxInitialized {
provider: SandboxProviderKind::Local,
provider: SandboxProviderKind::LOCAL,
id: "sandbox-preserve-1".to_string(),
working_directory: "/tmp/fabro-preserved-sandbox".to_string(),
image: None,
@ -16764,7 +16767,7 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() {
workflow_event::Event::RunStarting,
workflow_event::Event::RunRunning,
workflow_event::Event::SandboxInitialized {
provider: SandboxProviderKind::Docker,
provider: SandboxProviderKind::DOCKER,
id: "missing-sandbox".to_string(),
working_directory: "/tmp/fabro-missing-sandbox".to_string(),
image: None,
@ -19523,7 +19526,7 @@ async fn list_runs_includes_live_metadata_from_run_state() {
workflow_event::Event::RunStarting,
workflow_event::Event::RunRunning,
workflow_event::Event::SandboxInitialized {
provider: SandboxProviderKind::Local,
provider: SandboxProviderKind::LOCAL,
id: "sb-test".to_string(),
working_directory: "/sandbox/workdir".to_string(),
image: None,
@ -19610,7 +19613,7 @@ async fn list_runs_page_limit_preserves_metadata_for_paged_items() {
workflow_event::Event::RunStarting,
workflow_event::Event::RunRunning,
workflow_event::Event::SandboxInitialized {
provider: SandboxProviderKind::Local,
provider: SandboxProviderKind::LOCAL,
id: sandbox_id.to_string(),
working_directory: "/sandbox/workdir".to_string(),
image: None,

View file

@ -24,8 +24,7 @@ use fabro_sandbox::SandboxProviderRegistry;
use fabro_static::EnvVars;
use fabro_store::{ArtifactStore, Database, test_support as store_test_support};
use fabro_types::settings::ServerAuthMethod;
use fabro_types::settings::run::EnvironmentProvider;
use fabro_types::{AuthMethod, IdpIdentity, ServerSettings};
use fabro_types::{AuthMethod, IdpIdentity, SandboxProviderKind, ServerSettings};
use fabro_vault::{SecretType, Vault};
use fabro_workflow::handler::HandlerRegistry;
use object_store::memory::InMemory as MemoryObjectStore;
@ -98,7 +97,7 @@ pub struct TestAppStateBuilder {
server_env_path: Option<PathBuf>,
active_config_path: Option<PathBuf>,
server_secret_env: HashMap<String, String>,
default_environment_provider: Option<EnvironmentProvider>,
default_environment_provider: Option<SandboxProviderKind>,
env_lookup: EnvLookup,
llm_catalog_settings: LlmCatalogSettings,
automation_materializer: Option<TestAutomationRunMaterializer>,
@ -120,7 +119,7 @@ impl Default for TestAppStateBuilder {
server_env_path: None,
active_config_path: None,
server_secret_env: HashMap::new(),
default_environment_provider: Some(EnvironmentProvider::Docker),
default_environment_provider: Some(SandboxProviderKind::DOCKER),
env_lookup: default_env_lookup(),
llm_catalog_settings: LlmCatalogSettings::default(),
automation_materializer: None,
@ -212,7 +211,7 @@ impl TestAppStateBuilder {
self
}
pub fn default_environment_provider(mut self, provider: Option<EnvironmentProvider>) -> Self {
pub fn default_environment_provider(mut self, provider: Option<SandboxProviderKind>) -> Self {
self.default_environment_provider = provider;
self
}
@ -557,13 +556,13 @@ pub fn test_store_bundle() -> (Arc<Database>, ArtifactStore) {
pub(crate) fn test_db_pool_for_vault_path(vault_path: &Path) -> anyhow::Result<DbPool> {
test_db_pool_for_vault_path_with_default_environment(
vault_path,
Some(EnvironmentProvider::Docker),
Some(SandboxProviderKind::DOCKER),
)
}
pub(crate) fn test_db_pool_for_vault_path_with_default_environment(
vault_path: &Path,
default_environment_provider: Option<EnvironmentProvider>,
default_environment_provider: Option<SandboxProviderKind>,
) -> anyhow::Result<DbPool> {
test_db_pool(
sqlite_path_for_vault_path(vault_path),
@ -597,7 +596,7 @@ pub async fn test_environment_from_storage_dir(
fn test_db_pool(
path: PathBuf,
vault_path: PathBuf,
default_environment_provider: Option<EnvironmentProvider>,
default_environment_provider: Option<SandboxProviderKind>,
) -> anyhow::Result<DbPool> {
std::thread::spawn(move || {
let runtime = TokioRuntimeBuilder::new_current_thread()

View file

@ -18,6 +18,7 @@ use fabro_server::install::{
InstallAppState, InstallFinishHook, InstallFinishInfo, build_install_router,
};
use fabro_server::test_support::test_environment_from_storage_dir;
use fabro_types::SandboxProviderKind;
use fabro_util::Home;
use fabro_vault::Vault;
use httpmock::Method::GET;
@ -58,9 +59,18 @@ fn assert_sandbox_provider_policy(
.server
.sandbox
.providers;
assert_eq!(resolved.local.enabled, local_enabled);
assert_eq!(resolved.docker.enabled, docker_enabled);
assert_eq!(resolved.daytona.enabled, daytona_enabled);
assert_eq!(
resolved.is_enabled(&SandboxProviderKind::LOCAL),
local_enabled
);
assert_eq!(
resolved.is_enabled(&SandboxProviderKind::DOCKER),
docker_enabled
);
assert_eq!(
resolved.is_enabled(&SandboxProviderKind::DAYTONA),
daytona_enabled
);
}
async fn seeded_default_environment(

View file

@ -136,7 +136,7 @@ async fn append_local_sandbox_initialized(store: &Database, run_id: &RunId) {
.expect("test should run inside a source checkout")
.display()
.to_string(),
provider: SandboxProviderKind::Local,
provider: SandboxProviderKind::LOCAL,
id: "local:test-sandbox".to_string(),
image: None,
snapshot: None,

View file

@ -1,7 +1,7 @@
use axum::body::Body;
use axum::http::{Request, StatusCode};
use fabro_model::{Catalog, ProviderId};
use fabro_types::settings::run::EnvironmentProvider;
use fabro_types::SandboxProviderKind;
use tower::ServiceExt;
use crate::helpers::{
@ -78,7 +78,7 @@ fn daytona_disabled_app() -> (axum::Router, tempfile::TempDir) {
let state = fabro_server::test_support::TestAppStateBuilder::new()
.runtime_settings(settings.server_settings, settings.manifest_run_defaults)
.active_config_path(active_config_path)
.default_environment_provider(Some(EnvironmentProvider::Daytona))
.default_environment_provider(Some(SandboxProviderKind::DAYTONA))
.build();
(
fabro_server::test_support::build_test_router(state),

View file

@ -35,7 +35,7 @@ pub async fn backfill_environment_selectors(
}
let compatible_ids = sqlx::query_scalar::<_, String>(
"SELECT id FROM environments WHERE provider IN ('docker', 'daytona') ORDER BY id",
"SELECT id FROM environments WHERE provider <> 'local' ORDER BY id",
)
.fetch_all(pool)
.await?;

View file

@ -564,12 +564,12 @@ mod tests {
});
projection.sandbox = Some(RunSandbox::ready(
RunSandboxPlan {
provider: SandboxProviderKind::Local,
provider: SandboxProviderKind::LOCAL,
image: None,
snapshot: None,
},
RunSandboxInstance {
provider: SandboxProviderKind::Local,
provider: SandboxProviderKind::LOCAL,
image: None,
snapshot: None,
runtime: fabro_types::RunSandboxRuntime {

View file

@ -9,8 +9,9 @@ use fabro_config::{
EnvironmentNetworkLayer, EnvironmentResourcesLayer, MergeMap, StickyMap,
};
use fabro_db::DbPool;
use fabro_types::settings::run::{DockerfileSource, EnvironmentProvider, EnvironmentSettings};
use fabro_types::settings::run::{DockerfileSource, EnvironmentSettings};
use fabro_types::settings::{Duration, InterpString, Size};
use fabro_types::{BundledProvider, SandboxProviderKind};
use serde::de::DeserializeOwned;
use sqlx::Row as _;
use sqlx::sqlite::SqliteRow;
@ -135,7 +136,7 @@ impl CatalogState {
fn synthetic_local_environment() -> Result<Environment, EnvironmentStoreError> {
let id = EnvironmentId::new(RESERVED_LOCAL_ID).expect("reserved local id is valid");
let settings = EnvironmentSettings {
provider: EnvironmentProvider::Local,
provider: SandboxProviderKind::LOCAL,
..EnvironmentSettings::default()
};
Environment::synthetic(id, &settings)
@ -598,17 +599,25 @@ impl EnvironmentSqlRow {
}
pub async fn seed_environments(pool: &DbPool) -> Result<(), EnvironmentStoreError> {
seed_default_environment(pool, EnvironmentProvider::Docker).await
seed_default_environment(pool, SandboxProviderKind::DOCKER).await
}
pub async fn seed_default_environment(
pool: &DbPool,
provider: EnvironmentProvider,
provider: SandboxProviderKind,
) -> Result<(), EnvironmentStoreError> {
let content = match provider {
EnvironmentProvider::Docker => DEFAULT_ENVIRONMENT_TOML,
EnvironmentProvider::Daytona => DAYTONA_DEFAULT_ENVIRONMENT_TOML,
EnvironmentProvider::Local => LOCAL_ENVIRONMENT_TOML,
let content = match provider.bundled() {
Some(BundledProvider::Docker) => DEFAULT_ENVIRONMENT_TOML,
Some(BundledProvider::Daytona) => DAYTONA_DEFAULT_ENVIRONMENT_TOML,
Some(BundledProvider::Local) => LOCAL_ENVIRONMENT_TOML,
None => {
return Err(EnvironmentValidationError::InvalidSettings {
errors: vec![format!(
"no built-in default environment exists for sandbox provider `{provider}`"
)],
}
.into());
}
};
let layer: EnvironmentLayer = toml::from_str(content).map_err(|source| {
EnvironmentStoreError::parse(PathBuf::from("built-in-default-environment.toml"), source)

View file

@ -5,11 +5,12 @@ use fabro_environment::{
EnvironmentDraft, EnvironmentId, EnvironmentStore, EnvironmentStoreError,
import_legacy_directory_once, seed_default_environment, seed_environments,
};
use fabro_types::SandboxProviderKind;
use fabro_types::settings::InterpString;
use fabro_types::settings::run::{
DockerfileSource, EnvironmentImageSettings, EnvironmentLifecycleSettings,
EnvironmentNetworkMode, EnvironmentNetworkSettings, EnvironmentProvider,
EnvironmentResourcesSettings, EnvironmentSettings,
EnvironmentNetworkMode, EnvironmentNetworkSettings, EnvironmentResourcesSettings,
EnvironmentSettings,
};
use tokio::fs;
@ -28,7 +29,7 @@ async fn test_store(local_enabled: bool) -> anyhow::Result<TestStore> {
Ok(TestStore { dir, pool, store })
}
fn settings(provider: EnvironmentProvider) -> EnvironmentSettings {
fn settings(provider: SandboxProviderKind) -> EnvironmentSettings {
EnvironmentSettings {
provider,
cwd: None,
@ -41,7 +42,7 @@ fn settings(provider: EnvironmentProvider) -> EnvironmentSettings {
}
}
fn draft(id: &str, provider: EnvironmentProvider) -> EnvironmentDraft {
fn draft(id: &str, provider: SandboxProviderKind) -> EnvironmentDraft {
EnvironmentDraft {
id: EnvironmentId::new(id).expect("test environment id should be valid"),
settings: settings(provider),
@ -83,7 +84,7 @@ async fn create_get_replace_delete_and_reload_round_trip_sql_rows() -> anyhow::R
let test = test_store(true).await?;
let created = test
.store
.create(draft("custom", EnvironmentProvider::Docker))
.create(draft("custom", SandboxProviderKind::DOCKER))
.await?;
assert_eq!(created.id.as_str(), "custom");
@ -104,7 +105,7 @@ async fn create_get_replace_delete_and_reload_round_trip_sql_rows() -> anyhow::R
created.revision
);
let mut replacement = settings(EnvironmentProvider::Local);
let mut replacement = settings(SandboxProviderKind::LOCAL);
replacement.cwd = Some("/workspace/custom".to_string());
replacement
.labels
@ -121,7 +122,7 @@ async fn create_get_replace_delete_and_reload_round_trip_sql_rows() -> anyhow::R
.replace(
&created.id,
&created.revision,
settings(EnvironmentProvider::Docker),
settings(SandboxProviderKind::DOCKER),
)
.await
.expect_err("stale revision should be rejected");
@ -142,7 +143,7 @@ async fn create_get_replace_delete_and_reload_round_trip_sql_rows() -> anyhow::R
#[tokio::test]
async fn default_is_deletable() -> anyhow::Result<()> {
let test = test_store(true).await?;
seed_default_environment(&test.pool, EnvironmentProvider::Docker).await?;
seed_default_environment(&test.pool, SandboxProviderKind::DOCKER).await?;
let store = EnvironmentStore::load(test.pool.clone(), true).await?;
let default = store
.get(&EnvironmentId::new("default").expect("valid id"))
@ -159,7 +160,7 @@ async fn default_is_deletable() -> anyhow::Result<()> {
#[tokio::test]
async fn maps_network_lifecycle_and_inline_dockerfile_round_trip() -> anyhow::Result<()> {
let test = test_store(true).await?;
let mut settings = settings(EnvironmentProvider::Daytona);
let mut settings = settings(SandboxProviderKind::DAYTONA);
settings.image.dockerfile = Some(DockerfileSource::Inline("FROM alpine\n".to_string()));
settings.resources.cpu = Some(4);
settings.resources.memory = Some("8GB".parse()?);
@ -197,7 +198,7 @@ async fn maps_network_lifecycle_and_inline_dockerfile_round_trip() -> anyhow::Re
#[tokio::test]
async fn direct_create_rejects_dockerfile_path_without_reading_it() -> anyhow::Result<()> {
let test = test_store(true).await?;
let mut settings = settings(EnvironmentProvider::Docker);
let mut settings = settings(SandboxProviderKind::DOCKER);
settings.image.dockerfile = Some(DockerfileSource::Path {
path: test.dir.path().join("Dockerfile").display().to_string(),
});
@ -287,7 +288,7 @@ cpu = 99
async fn legacy_import_keeps_existing_sql_row_and_inlines_dockerfile_path() -> anyhow::Result<()> {
let test = test_store(true).await?;
test.store
.create(draft("existing", EnvironmentProvider::Local))
.create(draft("existing", SandboxProviderKind::LOCAL))
.await?;
let environment_dir = test.dir.path().join("environments");
fs::create_dir(&environment_dir).await?;
@ -328,7 +329,7 @@ path = "Dockerfile"
.expect("existing row should win")
.settings
.provider,
EnvironmentProvider::Local
SandboxProviderKind::LOCAL
);
assert_eq!(
store
@ -362,7 +363,7 @@ async fn legacy_import_invalid_input_leaves_source_directory_in_place() -> anyho
assert_invalid_legacy_import_leaves_source_directory(
"invalid settings",
"invalid-settings.toml",
r#"provider = "bogus""#,
r#"provider = "Bogus Provider""#,
"validation",
)
.await?;

View file

@ -8,7 +8,7 @@ use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use fabro_config::{Storage, envfile};
use fabro_static::EnvVars;
use fabro_types::settings::run::EnvironmentProvider;
use fabro_types::{BundledProvider, SandboxProviderKind};
use fabro_util::dev_token;
use fabro_vault::SecretStore;
pub use fabro_vault::SecretStoreWrite;
@ -136,12 +136,12 @@ pub fn default_web_url() -> String {
}
pub async fn seed_environments_in_storage(storage_dir: &Path) -> Result<()> {
seed_default_environment_in_storage(storage_dir, EnvironmentProvider::Docker).await
seed_default_environment_in_storage(storage_dir, SandboxProviderKind::DOCKER).await
}
pub async fn seed_default_environment_in_storage(
storage_dir: &Path,
provider: EnvironmentProvider,
provider: SandboxProviderKind,
) -> Result<()> {
let database = open_migrated_database(storage_dir).await?;
fabro_environment::seed_default_environment(database.pool(), provider).await?;
@ -478,20 +478,15 @@ fn write_sandbox_provider_policy(
selection: InstallSandboxSelection,
allow_local: bool,
) -> Result<()> {
use fabro_types::SandboxProviderKind;
let sandbox = ensure_table(server, "sandbox")?;
let providers = ensure_table(sandbox, "providers")?;
for provider in [
SandboxProviderKind::Local,
SandboxProviderKind::Docker,
SandboxProviderKind::Daytona,
] {
for provider in SandboxProviderKind::bundled_kinds().filter_map(|kind| kind.bundled()) {
// Only enable the providers the operator configured or allowed in the
// install wizard: the chosen runtime, plus local when allowed.
let enabled = match provider {
SandboxProviderKind::Local => allow_local,
SandboxProviderKind::Docker => selection == InstallSandboxSelection::Docker,
SandboxProviderKind::Daytona => selection == InstallSandboxSelection::Daytona,
BundledProvider::Local => allow_local,
BundledProvider::Docker => selection == InstallSandboxSelection::Docker,
BundledProvider::Daytona => selection == InstallSandboxSelection::Daytona,
};
let entry = ensure_table(providers, &provider.to_string())?;
entry.insert("enabled".to_string(), toml::Value::Boolean(enabled));

View file

@ -1682,7 +1682,7 @@ impl Sandbox for DaytonaSandbox {
let clone_selector = git_clone_selector(branch.as_deref(), pin.as_ref());
let clone_plan = git_retry::RetryPlan::clone_default(None);
let clone_result = git_retry::retry_git_operation(
SandboxProviderKind::Daytona,
SandboxProviderKind::DAYTONA,
"clone",
&clone_plan,
|_attempt| {

View file

@ -4,8 +4,8 @@ use anyhow::Result;
#[cfg(any(feature = "docker", feature = "daytona"))]
use chrono::{DateTime, Utc};
use fabro_types::{
RunId, RunSandboxInstance, SandboxDetails, SandboxNetwork, SandboxProviderKind,
SandboxResources, SandboxState, SandboxTimestamps,
BundledProvider, RunId, RunSandboxInstance, SandboxDetails, SandboxNetwork, SandboxResources,
SandboxState, SandboxTimestamps,
};
/// Inspect the sandbox identified by `record` and return provider-neutral
@ -25,19 +25,13 @@ pub async fn sandbox_details(
daytona_organization_id: Option<String>,
run_id: Option<RunId>,
) -> Result<SandboxDetails> {
match record.provider {
SandboxProviderKind::Local => Ok(local_details(record)),
match record.provider.bundled() {
Some(BundledProvider::Local) => Ok(local_details(record)),
#[cfg(feature = "docker")]
SandboxProviderKind::Docker => docker::docker_details(record, run_id).await,
#[cfg(not(feature = "docker"))]
SandboxProviderKind::Docker => Err(anyhow::anyhow!(
"Sandbox provider '{}' has no details implementation",
record.provider
)),
Some(BundledProvider::Docker) => docker::docker_details(record, run_id).await,
#[cfg(feature = "daytona")]
SandboxProviderKind::Daytona => daytona::daytona_details(record, daytona_api_key).await,
#[cfg(not(feature = "daytona"))]
SandboxProviderKind::Daytona => Err(anyhow::anyhow!(
Some(BundledProvider::Daytona) => daytona::daytona_details(record, daytona_api_key).await,
_ => Err(anyhow::anyhow!(
"Sandbox provider '{}' has no details implementation",
record.provider
)),
@ -99,7 +93,7 @@ pub(crate) mod docker {
pub(crate) fn docker_info_from_inspect(inspect: &ContainerInspectResponse) -> SandboxInfo {
let fields = docker_fields_from_inspect(inspect);
SandboxInfo {
provider: SandboxProviderKind::Docker,
provider: SandboxProviderKind::DOCKER,
id: fields.id,
display_name: fields.display_name,
state: fields.state,
@ -280,7 +274,7 @@ pub(crate) mod docker {
fn record() -> RunSandboxInstance {
RunSandboxInstance {
provider: SandboxProviderKind::Docker,
provider: SandboxProviderKind::DOCKER,
image: None,
snapshot: None,
runtime: RunSandboxRuntime {
@ -531,7 +525,7 @@ pub(crate) mod daytona {
pub(crate) fn daytona_info_from_sdk_sandbox(sandbox: &daytona_sdk::Sandbox) -> SandboxInfo {
let fields = daytona_fields_from_sdk_sandbox(sandbox);
SandboxInfo {
provider: SandboxProviderKind::Daytona,
provider: SandboxProviderKind::DAYTONA,
id: sandbox.id.clone(),
display_name: Some(sandbox.name.clone()).filter(|name| !name.is_empty()),
state: fields.state,
@ -828,12 +822,14 @@ pub(crate) mod daytona {
#[cfg(test)]
mod tests {
use fabro_types::SandboxProviderKind;
use super::*;
#[test]
fn local_details_returns_running_with_no_metadata() {
let record = RunSandboxInstance {
provider: SandboxProviderKind::Local,
provider: SandboxProviderKind::LOCAL,
image: None,
snapshot: None,
runtime: fabro_types::RunSandboxRuntime {
@ -849,7 +845,7 @@ mod tests {
},
};
let details = local_details(&record);
assert_eq!(details.sandbox.provider, SandboxProviderKind::Local);
assert_eq!(details.sandbox.provider, SandboxProviderKind::LOCAL);
assert_eq!(details.state, SandboxState::Running);
let runtime = &details.sandbox.runtime;
assert_eq!(runtime.id, "local:01JNQVR7M0EJ5GKAT2SC4ERS1Z");

View file

@ -858,7 +858,7 @@ impl DockerSandbox {
) -> Result<(), DockerCloneFailure> {
let plan = git_retry::RetryPlan::clone_default(Some(clone_deadline));
git_retry::retry_git_operation(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
op,
&plan,
|_attempt| async move {

View file

@ -189,14 +189,15 @@ mod tests {
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use fabro_types::SandboxProviderKind;
use fabro_types::settings::run::{
EnvironmentImageSettings, EnvironmentLifecycleSettings, EnvironmentNetworkSettings,
EnvironmentProvider, EnvironmentResourcesSettings,
EnvironmentResourcesSettings,
};
use super::*;
fn run_environment(provider: EnvironmentProvider) -> RunEnvironmentSettings {
fn run_environment(provider: SandboxProviderKind) -> RunEnvironmentSettings {
RunEnvironmentSettings {
id: "host".to_string(),
provider,
@ -212,7 +213,7 @@ mod tests {
#[test]
fn local_working_directory_prefers_environment_cwd() {
let mut settings = run_environment(EnvironmentProvider::Local);
let mut settings = run_environment(SandboxProviderKind::LOCAL);
settings.cwd = Some("/srv/fabro/workspaces/team-a".to_string());
let missing_source = Path::new("/path/that/should/not/exist");
@ -225,7 +226,7 @@ mod tests {
#[test]
fn local_working_directory_uses_existing_source_directory_without_cwd() {
let settings = run_environment(EnvironmentProvider::Local);
let settings = run_environment(SandboxProviderKind::LOCAL);
let dir = tempfile::tempdir().unwrap();
let resolved = local_working_directory_from_environment(&settings, Some(dir.path()))
@ -236,7 +237,7 @@ mod tests {
#[test]
fn local_working_directory_rejects_missing_source_directory_without_cwd() {
let settings = run_environment(EnvironmentProvider::Local);
let settings = run_environment(SandboxProviderKind::LOCAL);
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("client-only");
@ -254,7 +255,7 @@ mod tests {
#[cfg(feature = "daytona")]
#[test]
fn daytona_config_maps_docker_image_to_snapshot() {
let mut settings = run_environment(EnvironmentProvider::Daytona);
let mut settings = run_environment(SandboxProviderKind::DAYTONA);
settings.image.docker = Some("ubuntu:24.04".to_string());
settings.resources.cpu = Some(2);

View file

@ -603,7 +603,7 @@ mod tests {
let attempts = Attempts::default();
let result = retry_git_operation(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
"clone",
&RetryPlan::clone_default(None),
|attempt| {
@ -623,7 +623,7 @@ mod tests {
let attempts = Attempts::default();
let result = retry_git_operation(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
"clone",
&RetryPlan::clone_default(None),
|attempt| {
@ -649,7 +649,7 @@ mod tests {
let attempts = Attempts::default();
let result = retry_git_operation(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
"clone",
&RetryPlan::clone_default(None),
|attempt| {
@ -673,7 +673,7 @@ mod tests {
let attempts = Attempts::default();
let result = retry_git_operation(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
"clone",
&RetryPlan::clone_default(None),
|attempt| {
@ -700,7 +700,7 @@ mod tests {
let deadline = time::Instant::now() + Duration::from_secs(2);
let result = retry_git_operation(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
"clone",
&RetryPlan::clone_default(Some(deadline)),
|attempt| {
@ -723,7 +723,7 @@ mod tests {
let attempts = Attempts::default();
let result = retry_git_operation(
SandboxProviderKind::Daytona,
SandboxProviderKind::DAYTONA,
"clone",
&RetryPlan::clone_default(None),
|attempt| {
@ -750,7 +750,7 @@ mod tests {
};
let result = retry_git_operation(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
"push",
&plan,
|attempt| {

View file

@ -157,7 +157,7 @@ pub struct LocalSandboxProvider;
#[async_trait]
impl SandboxProvider for LocalSandboxProvider {
fn kind(&self) -> SandboxProviderKind {
SandboxProviderKind::Local
SandboxProviderKind::LOCAL
}
async fn list(&self) -> crate::Result<Vec<SandboxInfo>> {
@ -198,16 +198,16 @@ mod tests {
#[tokio::test]
async fn list_returns_aggregate_data_from_successful_providers() {
let docker = fake_sandbox_info(SandboxProviderKind::Docker, "docker-1");
let daytona = fake_sandbox_info(SandboxProviderKind::Daytona, "daytona-1");
let docker = fake_sandbox_info(SandboxProviderKind::DOCKER, "docker-1");
let daytona = fake_sandbox_info(SandboxProviderKind::DAYTONA, "daytona-1");
let registry = fake_registry(vec![
FakeSandboxProvider::new(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
FakeList::Ok(vec![docker.clone()]),
FakeGet::Missing,
),
FakeSandboxProvider::new(
SandboxProviderKind::Daytona,
SandboxProviderKind::DAYTONA,
FakeList::Ok(vec![daytona.clone()]),
FakeGet::Missing,
),
@ -221,15 +221,15 @@ mod tests {
#[tokio::test]
async fn list_includes_provider_error_metadata_when_one_provider_fails() {
let docker = fake_sandbox_info(SandboxProviderKind::Docker, "docker-1");
let docker = fake_sandbox_info(SandboxProviderKind::DOCKER, "docker-1");
let registry = fake_registry(vec![
FakeSandboxProvider::new(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
FakeList::Ok(vec![docker.clone()]),
FakeGet::Missing,
),
FakeSandboxProvider::new(
SandboxProviderKind::Daytona,
SandboxProviderKind::DAYTONA,
FakeList::Err("daytona unavailable"),
FakeGet::Missing,
),
@ -240,7 +240,7 @@ mod tests {
assert_eq!(response.data, vec![docker]);
assert_eq!(response.meta.provider_errors, vec![
SandboxProviderLookupError {
provider: SandboxProviderKind::Daytona,
provider: SandboxProviderKind::DAYTONA,
message: "daytona unavailable".to_string(),
}
]);
@ -248,15 +248,15 @@ mod tests {
#[tokio::test]
async fn get_returns_one_matching_sandbox() {
let docker = fake_sandbox_info(SandboxProviderKind::Docker, "same-id");
let docker = fake_sandbox_info(SandboxProviderKind::DOCKER, "same-id");
let registry = fake_registry(vec![
FakeSandboxProvider::new(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
FakeList::Ok(Vec::new()),
FakeGet::Found(Box::new(docker.clone())),
),
FakeSandboxProvider::new(
SandboxProviderKind::Daytona,
SandboxProviderKind::DAYTONA,
FakeList::Ok(Vec::new()),
FakeGet::Missing,
),
@ -272,12 +272,12 @@ mod tests {
async fn get_returns_not_found_when_all_providers_miss() {
let registry = fake_registry(vec![
FakeSandboxProvider::new(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
FakeList::Ok(Vec::new()),
FakeGet::Missing,
),
FakeSandboxProvider::new(
SandboxProviderKind::Daytona,
SandboxProviderKind::DAYTONA,
FakeList::Ok(Vec::new()),
FakeGet::Missing,
),
@ -295,18 +295,18 @@ mod tests {
async fn get_returns_conflict_when_two_providers_match() {
let registry = fake_registry(vec![
FakeSandboxProvider::new(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
FakeList::Ok(Vec::new()),
FakeGet::Found(Box::new(fake_sandbox_info(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
"same-id",
))),
),
FakeSandboxProvider::new(
SandboxProviderKind::Daytona,
SandboxProviderKind::DAYTONA,
FakeList::Ok(Vec::new()),
FakeGet::Found(Box::new(fake_sandbox_info(
SandboxProviderKind::Daytona,
SandboxProviderKind::DAYTONA,
"same-id",
))),
),
@ -321,7 +321,7 @@ mod tests {
err,
SandboxLookupError::Conflict { id, providers }
if id == "same-id"
&& providers == vec![SandboxProviderKind::Docker, SandboxProviderKind::Daytona]
&& providers == vec![SandboxProviderKind::DOCKER, SandboxProviderKind::DAYTONA]
));
}
@ -329,12 +329,12 @@ mod tests {
async fn get_returns_provider_unavailable_when_no_match_and_one_provider_fails() {
let registry = fake_registry(vec![
FakeSandboxProvider::new(
SandboxProviderKind::Docker,
SandboxProviderKind::DOCKER,
FakeList::Ok(Vec::new()),
FakeGet::Missing,
),
FakeSandboxProvider::new(
SandboxProviderKind::Daytona,
SandboxProviderKind::DAYTONA,
FakeList::Ok(Vec::new()),
FakeGet::Err("daytona unavailable"),
),
@ -352,7 +352,7 @@ mod tests {
provider_errors
} if id == "maybe-missing"
&& provider_errors == vec![SandboxProviderLookupError {
provider: SandboxProviderKind::Daytona,
provider: SandboxProviderKind::DAYTONA,
message: "daytona unavailable".to_string(),
}]
));

View file

@ -52,7 +52,7 @@ impl DaytonaSandboxProvider {
#[async_trait]
impl SandboxProvider for DaytonaSandboxProvider {
fn kind(&self) -> SandboxProviderKind {
SandboxProviderKind::Daytona
SandboxProviderKind::DAYTONA
}
async fn list(&self) -> crate::Result<Vec<SandboxInfo>> {

View file

@ -38,7 +38,7 @@ impl DockerSandboxProvider {
#[async_trait]
impl SandboxProvider for DockerSandboxProvider {
fn kind(&self) -> SandboxProviderKind {
SandboxProviderKind::Docker
SandboxProviderKind::DOCKER
}
async fn list(&self) -> crate::Result<Vec<SandboxInfo>> {

View file

@ -5,7 +5,7 @@ use std::path::PathBuf;
reason = "Feature-gated branches consume these imports when optional backends are enabled."
)]
use anyhow::{Context, Result, bail};
use fabro_types::{RunId, RunSandboxInstance, SandboxProviderKind};
use fabro_types::{BundledProvider, RunId, RunSandboxInstance};
use crate::SandboxEventCallback;
#[cfg(feature = "daytona")]
@ -53,8 +53,8 @@ pub async fn reconnect_for_run_with_callback(
event_callback: Option<SandboxEventCallback>,
) -> Result<Box<dyn crate::Sandbox>> {
let runtime = &record.runtime;
match record.provider {
SandboxProviderKind::Local => {
match record.provider.bundled() {
Some(BundledProvider::Local) => {
let mut sandbox = LocalSandbox::new(PathBuf::from(&runtime.working_directory));
if let Some(callback) = event_callback {
sandbox.set_event_callback(callback);
@ -62,7 +62,7 @@ pub async fn reconnect_for_run_with_callback(
Ok(Box::new(sandbox))
}
#[cfg(feature = "docker")]
SandboxProviderKind::Docker => {
Some(BundledProvider::Docker) => {
let repo_cloned = runtime
.repo_cloned
.context("Docker run sandbox missing repo_cloned metadata")?;
@ -82,9 +82,9 @@ pub async fn reconnect_for_run_with_callback(
Ok(Box::new(sandbox))
}
#[cfg(not(feature = "docker"))]
SandboxProviderKind::Docker => bail!("Docker sandbox support is not enabled"),
Some(BundledProvider::Docker) => bail!("Docker sandbox support is not enabled"),
#[cfg(feature = "daytona")]
SandboxProviderKind::Daytona => {
Some(BundledProvider::Daytona) => {
let repo_cloned = runtime
.repo_cloned
.context("Daytona run sandbox missing repo_cloned metadata")?;
@ -105,6 +105,10 @@ pub async fn reconnect_for_run_with_callback(
Ok(Box::new(sandbox))
}
#[cfg(not(feature = "daytona"))]
SandboxProviderKind::Daytona => bail!("Daytona sandbox support is not enabled"),
Some(BundledProvider::Daytona) => bail!("Daytona sandbox support is not enabled"),
None => bail!(
"sandbox provider `{}` is not bundled; plugin reconnect is not wired yet",
record.provider
),
}
}

View file

@ -51,19 +51,21 @@ pub enum SandboxSpec {
impl SandboxSpec {
pub fn provider(&self) -> SandboxProviderKind {
match self {
Self::Local { .. } => SandboxProviderKind::Local,
Self::Local { .. } => SandboxProviderKind::LOCAL,
#[cfg(feature = "docker")]
Self::Docker { .. } => SandboxProviderKind::Docker,
Self::Docker { .. } => SandboxProviderKind::DOCKER,
#[cfg(feature = "daytona")]
Self::Daytona { .. } => SandboxProviderKind::Daytona,
Self::Daytona { .. } => SandboxProviderKind::DAYTONA,
}
}
pub fn provider_name(&self) -> &'static str {
match self.provider() {
SandboxProviderKind::Local => "local",
SandboxProviderKind::Docker => "docker",
SandboxProviderKind::Daytona => "daytona",
match self {
Self::Local { .. } => "local",
#[cfg(feature = "docker")]
Self::Docker { .. } => "docker",
#[cfg(feature = "daytona")]
Self::Daytona { .. } => "daytona",
}
}

View file

@ -1,7 +1,7 @@
use async_trait::async_trait;
#[cfg(feature = "daytona")]
use fabro_static::EnvVars;
use fabro_types::{RunId, RunSandboxInstance, SandboxProviderKind};
use fabro_types::{BundledProvider, RunId, RunSandboxInstance};
#[cfg(any(feature = "daytona", feature = "docker"))]
use crate::Sandbox;
@ -49,9 +49,9 @@ pub async fn open_terminal_for_run(
#[cfg(not(any(feature = "daytona", feature = "docker")))]
let _ = size;
match record.provider {
match record.provider.bundled() {
#[cfg(feature = "daytona")]
SandboxProviderKind::Daytona => {
Some(BundledProvider::Daytona) => {
let repo_cloned = runtime.repo_cloned.ok_or_else(|| {
crate::Error::message("Daytona run sandbox is missing clone metadata")
})?;
@ -78,11 +78,11 @@ pub async fn open_terminal_for_run(
Ok(Box::new(session))
}
#[cfg(not(feature = "daytona"))]
SandboxProviderKind::Daytona => Err(crate::Error::message(
Some(BundledProvider::Daytona) => Err(crate::Error::message(
"Daytona sandbox support is not enabled",
)),
#[cfg(feature = "docker")]
SandboxProviderKind::Docker => {
Some(BundledProvider::Docker) => {
let repo_cloned = runtime.repo_cloned.ok_or_else(|| {
crate::Error::message("Docker run sandbox is missing clone metadata")
})?;
@ -100,12 +100,16 @@ pub async fn open_terminal_for_run(
Ok(Box::new(session))
}
#[cfg(not(feature = "docker"))]
SandboxProviderKind::Docker => Err(crate::Error::message(
Some(BundledProvider::Docker) => Err(crate::Error::message(
"Docker sandbox support is not enabled",
)),
SandboxProviderKind::Local => Err(crate::Error::message(
Some(BundledProvider::Local) => Err(crate::Error::message(
"Local sandboxes do not support embedded terminals",
)),
None => Err(crate::Error::message(format!(
"Sandbox provider '{}' does not support embedded terminals yet",
record.provider
))),
}
}

View file

@ -771,7 +771,7 @@ mod fake_provider {
#[async_trait]
impl SandboxProvider for FakeSandboxProvider {
fn kind(&self) -> SandboxProviderKind {
self.kind
self.kind.clone()
}
async fn list(&self) -> crate::Result<Vec<SandboxInfo>> {

View file

@ -7,7 +7,7 @@ use fabro_types::run_event::{
AgentLlmStartedProps, CheckpointCompletedProps, RunCompletedProps, RunFailedProps,
StageCompletedProps, TodoCreatedProps, TodoDeletedProps, TodoUpdatedProps,
};
use fabro_types::settings::run::{EnvironmentProvider, RunEnvironmentSettings};
use fabro_types::settings::run::RunEnvironmentSettings;
use fabro_types::{
ActivatedSkill, AgentControlState, AskFabro, BilledModelUsage, BilledTokenCounts, Checkpoint,
CheckpointRecord, CommandTermination, Conclusion, EventBody, FailureCategory, FailureSignature,
@ -355,7 +355,7 @@ impl RunProjectionReducer for RunProjection {
EventBody::SandboxInitialized(props) => {
let plan = sandbox_plan_from_projection_or_settings(self);
self.sandbox = Some(RunSandbox::ready(plan, RunSandboxInstance {
provider: props.provider,
provider: props.provider.clone(),
image: props.image.clone(),
snapshot: props.snapshot.clone(),
runtime: RunSandboxRuntime {
@ -1135,10 +1135,9 @@ fn sandbox_plan_from_projection_or_settings(state: &RunProjection) -> RunSandbox
}
fn sandbox_plan(settings: &RunEnvironmentSettings) -> RunSandboxPlan {
let provider = SandboxProviderKind::from(settings.provider);
RunSandboxPlan {
provider,
image: (settings.provider == EnvironmentProvider::Docker)
provider: settings.provider.clone(),
image: (settings.provider == SandboxProviderKind::DOCKER)
.then(|| settings.image.docker.clone())
.flatten()
.filter(|image| !image.is_empty()),
@ -1754,7 +1753,7 @@ mod tests {
RunControlEffectProps, StageCompletedProps, StageFailedProps, StagePromptProps,
StageRetryingProps, StageStartedProps,
};
use fabro_types::settings::run::{DockerfileSource, EnvironmentProvider};
use fabro_types::settings::run::DockerfileSource;
use fabro_types::{
AgentBackend, AgentControlState, AttrValue, AutomationRef, BilledModelUsage,
BilledTokenCounts, BlobHash, BlockedReason, Checkpoint, CheckpointRecord,
@ -1762,11 +1761,11 @@ mod tests {
McpServerStatus, Node, Outcome, ParallelBranchId, PendingReason, PermissionLevel,
PullRequestCreationStatus, PullRequestLink, QuestionType, ReasoningEffort,
RunApprovalState, RunBillingSummary, RunControlAction, RunDiff, RunEvent, RunSize, RunSpec,
RunStatus, Speed, StageContextWindowBreakdownItem, StageContextWindowCategory,
StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness,
StageContextWindowWarning, StageHandler, StageModelUsage, StageOutcome, StageState,
StageTiming, SubAgentStatus, SuccessReason, WorkflowSettings, first_event_seq, fixtures,
test_support,
RunStatus, SandboxProviderKind, Speed, StageContextWindowBreakdownItem,
StageContextWindowCategory, StageContextWindowCountMethod, StageContextWindowProjection,
StageContextWindowStaleness, StageContextWindowWarning, StageHandler, StageModelUsage,
StageOutcome, StageState, StageTiming, SubAgentStatus, SuccessReason, WorkflowSettings,
first_event_seq, fixtures, test_support,
};
use serde_json::json;
@ -2387,7 +2386,7 @@ mod tests {
#[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.provider = SandboxProviderKind::DOCKER;
docker.image.docker = Some("ubuntu:24.04".to_string());
let planned_docker = super::sandbox_plan(&docker);
@ -2395,7 +2394,7 @@ mod tests {
assert_eq!(planned_docker.snapshot, None);
let mut daytona = WorkflowSettings::default().run.environment;
daytona.provider = EnvironmentProvider::Daytona;
daytona.provider = SandboxProviderKind::DAYTONA;
daytona.image.dockerfile = Some(DockerfileSource::Inline("FROM ubuntu:24.04".to_string()));
let planned_daytona = super::sandbox_plan(&daytona);

View file

@ -92,12 +92,12 @@ fn serializable_projection_round_trips_and_trims_bulky_node_fields() {
diff: RunDiff::default(),
});
let sandbox_plan = RunSandboxPlan {
provider: SandboxProviderKind::Local,
provider: SandboxProviderKind::LOCAL,
image: None,
snapshot: None,
};
projection.sandbox = Some(RunSandbox::ready(sandbox_plan, RunSandboxInstance {
provider: SandboxProviderKind::Local,
provider: SandboxProviderKind::LOCAL,
image: None,
snapshot: None,
runtime: RunSandboxRuntime {

View file

@ -1145,7 +1145,7 @@ fn event_body_from_event(event: &Event) -> EventBody {
primary_repo_link,
} => EventBody::SandboxInitialized(fabro_types::SandboxInitializedProps {
working_directory: working_directory.clone(),
provider: *provider,
provider: provider.clone(),
id: id.clone(),
image: image.clone(),
snapshot: snapshot.clone(),

View file

@ -333,7 +333,7 @@ mod tests {
.await
.unwrap();
event::append_event(&source_store, &source_run_id, &Event::SandboxInitialized {
provider: fabro_types::SandboxProviderKind::Local,
provider: fabro_types::SandboxProviderKind::LOCAL,
id: "sandbox-source".to_string(),
working_directory: "/tmp/source".to_string(),
image: None,

View file

@ -24,8 +24,8 @@ use fabro_types::settings::run::{
RunPrepareSettings as ResolvedRunPrepareSettings,
};
use fabro_types::{
ManifestPath, RunId, RunRunnableSource, RunSpec, RunTarget, SandboxProviderKind,
TargetValidationError,
BundledProvider, ManifestPath, RunId, RunRunnableSource, RunSpec, RunTarget,
SandboxProviderKind, TargetValidationError,
};
use fabro_util::error::collect_chain;
use fabro_vault::Vault;
@ -485,14 +485,14 @@ impl RunSession {
})
.collect::<Result<Vec<_>, _>>()?;
if configured_sandbox_provider != SandboxProviderKind::Local
if configured_sandbox_provider != SandboxProviderKind::LOCAL
&& matches!(record.target, Some(RunTarget::Folder { .. }))
{
return Err(Error::engine(
"persisted folder run targets require the Local sandbox provider",
));
}
if configured_sandbox_provider == SandboxProviderKind::Local {
if configured_sandbox_provider == SandboxProviderKind::LOCAL {
if let Some(target @ (RunTarget::Git(_) | RunTarget::None {})) = record.target.as_ref()
{
return Err(Error::engine(format!(
@ -501,11 +501,11 @@ impl RunSession {
)));
}
}
let sandbox = match sandbox_provider {
SandboxProviderKind::Local if dry_run_clone_target => SandboxSpec::Local {
let sandbox = match sandbox_provider.bundled() {
Some(BundledProvider::Local) if dry_run_clone_target => SandboxSpec::Local {
working_directory: dry_run_workspace_for_target(persisted).await?,
},
SandboxProviderKind::Local => match record.target.as_ref() {
Some(BundledProvider::Local) => match record.target.as_ref() {
Some(target @ (RunTarget::Git(_) | RunTarget::None {})) => {
return Err(Error::engine(format!(
"persisted {} run targets require a clone-based sandbox provider",
@ -529,7 +529,7 @@ impl RunSession {
SandboxSpec::Local { working_directory }
}
},
SandboxProviderKind::Docker => {
Some(BundledProvider::Docker) => {
let mut config = resolve_docker_config(resolved, secret_lookup)?;
config.skip_clone |= clone_source.skip_clone;
SandboxSpec::Docker {
@ -542,7 +542,7 @@ impl RunSession {
clone_commit_sha: clone_source.commit_sha,
}
}
SandboxProviderKind::Daytona => {
Some(BundledProvider::Daytona) => {
let api_key = vault_guard
.get(EnvVars::DAYTONA_API_KEY)
.map(str::to_string);
@ -559,6 +559,11 @@ impl RunSession {
api_key,
}
}
None => {
return Err(Error::engine(format!(
"sandbox provider `{sandbox_provider}` is not bundled; plugin providers are not wired into run start yet"
)));
}
};
let toml_env = resolved
@ -812,7 +817,7 @@ async fn load_accepted_run_definition(
}
fn resolve_sandbox_provider(settings: &ResolvedRunSettings) -> SandboxProviderKind {
SandboxProviderKind::from(settings.environment.provider)
settings.environment.provider.clone()
}
fn resolve_daytona_config(settings: &ResolvedRunSettings) -> DaytonaConfig {
@ -1316,8 +1321,8 @@ mod tests {
use fabro_store::Database;
use fabro_types::settings::InterpString;
use fabro_types::settings::run::{
EnvironmentProvider, McpTransport as ResolvedMcpTransport, PreparedStep, PreparedStepRun,
RunMode, RunPrepareSettings,
McpTransport as ResolvedMcpTransport, PreparedStep, PreparedStepRun, RunMode,
RunPrepareSettings,
};
use fabro_types::{
BilledModelUsage, GitContext, ManifestPath, RunTarget, StageTiming, WorkflowSettings,
@ -1914,7 +1919,7 @@ reasoning = false
}),
..RunLayer::default()
});
settings.run.environment.provider = EnvironmentProvider::Docker;
settings.run.environment.provider = SandboxProviderKind::DOCKER;
settings.run.environment.image.docker = Some("buildpack-deps:noble".to_string());
let (persisted, store) = persisted_workflow_with_settings_and_target(
MINIMAL_DOT,
@ -1976,7 +1981,7 @@ reasoning = false
}),
..RunLayer::default()
});
settings.run.environment.provider = EnvironmentProvider::Daytona;
settings.run.environment.provider = SandboxProviderKind::DAYTONA;
settings.run.environment.image.docker = None;
let (persisted, store) = persisted_workflow_with_settings_and_target(
MINIMAL_DOT,
@ -2037,7 +2042,7 @@ reasoning = false
let temp = tempfile::tempdir().unwrap();
let (storage_root, _run_dir) = storage_root_and_run_dir(&temp);
let mut settings = settings_from_run_layer(RunLayer::default());
settings.run.environment.provider = EnvironmentProvider::Local;
settings.run.environment.provider = SandboxProviderKind::LOCAL;
let (persisted, store) = persisted_workflow_with_settings_and_target(
MINIMAL_DOT,
&storage_root,
@ -2080,7 +2085,7 @@ reasoning = false
}),
..RunLayer::default()
});
settings.run.environment.provider = EnvironmentProvider::Docker;
settings.run.environment.provider = SandboxProviderKind::DOCKER;
settings.run.environment.image.docker = Some("buildpack-deps:noble".to_string());
let (persisted, store) = persisted_workflow_with_settings_and_target(
MINIMAL_DOT,
@ -2125,7 +2130,7 @@ reasoning = false
}),
..RunLayer::default()
});
local_settings.run.environment.provider = EnvironmentProvider::Local;
local_settings.run.environment.provider = SandboxProviderKind::LOCAL;
let (persisted, store) = persisted_workflow_with_settings_and_target(
MINIMAL_DOT,
&storage_root,
@ -2155,7 +2160,7 @@ reasoning = false
}),
..RunLayer::default()
});
docker_settings.run.environment.provider = EnvironmentProvider::Docker;
docker_settings.run.environment.provider = SandboxProviderKind::DOCKER;
let (persisted, store) =
persisted_workflow_with_settings(MINIMAL_DOT, &storage_root, docker_settings).await;
let persisted = persisted_with_target_projection(
@ -2186,7 +2191,7 @@ reasoning = false
let environment_cwd = temp.path().join("environment-cwd");
std::fs::create_dir_all(&environment_cwd).unwrap();
let mut settings = settings_from_run_layer(RunLayer::default());
settings.run.environment.provider = EnvironmentProvider::Local;
settings.run.environment.provider = SandboxProviderKind::LOCAL;
settings.run.environment.cwd = Some(environment_cwd.to_string_lossy().into_owned());
let (persisted, store) =
persisted_workflow_with_settings(MINIMAL_DOT, &storage_root, settings).await;
@ -2225,16 +2230,14 @@ reasoning = false
#[tokio::test]
async fn run_session_new_folder_target_rejects_clone_based_providers() {
for provider in [EnvironmentProvider::Docker, EnvironmentProvider::Daytona] {
for provider in [SandboxProviderKind::DOCKER, SandboxProviderKind::DAYTONA] {
let temp = tempfile::tempdir().unwrap();
let (storage_root, _run_dir) = storage_root_and_run_dir(&temp);
let (_, canonical_text) = canonical_folder(&temp);
let mut settings = settings_from_run_layer(RunLayer::default());
settings.run.environment.image.docker = (provider == SandboxProviderKind::DOCKER)
.then(|| "buildpack-deps:noble".to_string());
settings.run.environment.provider = provider;
settings.run.environment.image.docker = match provider {
EnvironmentProvider::Docker => Some("buildpack-deps:noble".to_string()),
EnvironmentProvider::Daytona | EnvironmentProvider::Local => None,
};
let (persisted, store) =
persisted_workflow_with_settings(MINIMAL_DOT, &storage_root, settings).await;
let persisted = persisted_with_target_projection(
@ -2271,7 +2274,7 @@ reasoning = false
let environment_cwd = temp.path().join("environment-cwd");
std::fs::create_dir_all(&environment_cwd).unwrap();
let mut settings = settings_from_run_layer(RunLayer::default());
settings.run.environment.provider = EnvironmentProvider::Local;
settings.run.environment.provider = SandboxProviderKind::LOCAL;
settings.run.environment.cwd = Some(environment_cwd.to_string_lossy().into_owned());
let (persisted, store) =
persisted_workflow_with_settings(MINIMAL_DOT, &storage_root, settings).await;

View file

@ -412,7 +412,7 @@ async fn resumed_in_flight_node_starts_a_new_stage_execution() {
// Resume reconnects to the previously recorded sandbox.
append_event(&run_store, &run_id, &Event::SandboxInitialized {
working_directory: std::env::current_dir().unwrap().display().to_string(),
provider: fabro_types::SandboxProviderKind::Local,
provider: fabro_types::SandboxProviderKind::LOCAL,
id: "local".to_string(),
image: None,
snapshot: None,

View file

@ -26,7 +26,7 @@ const DOCKER_CP_IMAGE: &str = "buildpack-deps:noble";
fn local_record(working_directory: &std::path::Path) -> RunSandboxInstance {
RunSandboxInstance {
provider: SandboxProviderKind::Local,
provider: SandboxProviderKind::LOCAL,
image: None,
snapshot: None,
runtime: RunSandboxRuntime {
@ -137,7 +137,7 @@ async fn local_cp_creates_parent_dirs() {
fn docker_record(container_id: &str) -> RunSandboxInstance {
RunSandboxInstance {
provider: SandboxProviderKind::Docker,
provider: SandboxProviderKind::DOCKER,
image: None,
snapshot: None,
runtime: RunSandboxRuntime {

View file

@ -1529,7 +1529,7 @@ async fn daytona_cp_upload_download_round_trip() {
// 2. Build initialized sandbox metadata (same as `fabro run` would persist)
let record = RunSandboxInstance {
provider: SandboxProviderKind::Daytona,
provider: SandboxProviderKind::DAYTONA,
image: None,
snapshot: None,
runtime: fabro_types::RunSandboxRuntime {

View file

@ -282,6 +282,11 @@ fn main() {
"fabro_types::settings::server::ServerSandboxProviderSettings",
&[],
),
(
"SandboxPluginSettings",
"fabro_types::settings::server::SandboxPluginSettings",
&[],
),
(
"ServerStorageSettings",
"fabro_types::settings::server::ServerStorageSettings",

View file

@ -21,12 +21,12 @@ fn run_sandbox_reuses_domain_types() {
fn run_sandbox_json_matches_openapi_shape() {
let sandbox = RunSandbox::ready(
RunSandboxPlan {
provider: SandboxProviderKind::Docker,
provider: SandboxProviderKind::DOCKER,
image: Some("ghcr.io/fabro/sandbox:latest".to_string()),
snapshot: None,
},
RunSandboxInstance {
provider: SandboxProviderKind::Docker,
provider: SandboxProviderKind::DOCKER,
image: None,
snapshot: None,
runtime: RunSandboxRuntime {

View file

@ -33,7 +33,7 @@ fn sandbox_details_json_matches_openapi_shape() {
let created_at = Utc.with_ymd_and_hms(2026, 5, 9, 12, 0, 0).unwrap();
let details = SandboxDetails {
sandbox: RunSandboxInstance {
provider: SandboxProviderKind::Docker,
provider: SandboxProviderKind::DOCKER,
image: Some("ghcr.io/fabro/sandbox:latest".to_string()),
snapshot: None,
runtime: RunSandboxRuntime {
@ -130,7 +130,7 @@ fn sandbox_details_deserializes_when_optional_fields_are_absent() {
}))
.unwrap();
assert_eq!(details.sandbox.provider, SandboxProviderKind::Local);
assert_eq!(details.sandbox.provider, SandboxProviderKind::LOCAL);
assert_eq!(
details.sandbox.runtime.id.as_str(),
"local:01JNQVR7M0EJ5GKAT2SC4ERS1Z"

View file

@ -28,7 +28,7 @@ fn sandbox_inventory_round_trip_json_matches_openapi_shape() {
let created_at = Utc.with_ymd_and_hms(2026, 5, 25, 12, 0, 0).unwrap();
let response = SandboxListResponse {
data: vec![SandboxInfo {
provider: SandboxProviderKind::Daytona,
provider: SandboxProviderKind::DAYTONA,
id: "sandbox-abc123".to_string(),
display_name: Some("fabro-01KSGHGMCFM8W2FHXNMJ7MVY65".to_string()),
state: SandboxState::Running,
@ -60,7 +60,7 @@ fn sandbox_inventory_round_trip_json_matches_openapi_shape() {
}],
meta: SandboxListMeta {
provider_errors: vec![SandboxProviderLookupError {
provider: SandboxProviderKind::Docker,
provider: SandboxProviderKind::DOCKER,
message: "Failed to connect to Docker daemon".to_string(),
}],
},

View file

@ -2344,8 +2344,7 @@ mod tests {
use std::time::Duration;
use chrono::Duration as ChronoDuration;
use fabro_types::WorkflowPath;
use fabro_types::settings::run::EnvironmentProvider;
use fabro_types::{SandboxProviderKind, WorkflowPath};
use fabro_util::exit;
use httpmock::Method::{GET, POST};
use httpmock::{HttpMockResponse, MockServer};
@ -2465,7 +2464,7 @@ mod tests {
mock.assert_async().await;
assert_eq!(environment.id.as_str(), "local");
assert_eq!(environment.settings.provider, EnvironmentProvider::Local);
assert_eq!(environment.settings.provider, SandboxProviderKind::LOCAL);
}
#[tokio::test]
@ -2491,7 +2490,7 @@ mod tests {
assert_eq!(environments[0].id.as_str(), "production");
assert_eq!(
environments[0].settings.provider,
EnvironmentProvider::Daytona
SandboxProviderKind::DAYTONA
);
}

View file

@ -2,7 +2,7 @@ use std::fmt;
use std::path::Path;
use std::str::FromStr;
use fabro_types::settings::run::EnvironmentProvider;
use fabro_types::{BundledProvider, SandboxProviderKind};
use toml_edit::{DocumentMut, Item, Table, Value};
use crate::{Error, Result};
@ -91,14 +91,16 @@ fn migrate_document(doc: &mut DocumentMut) -> std::result::Result<(), MigrationF
}
let provider_str = sandbox.get("provider").and_then(Item::as_str);
let active_provider = provider_str.and_then(|provider| {
if let Ok(provider) = EnvironmentProvider::from_str(provider) {
Some(provider)
} else {
unsupported.push("run.sandbox.provider".to_string());
None
}
});
// Legacy `[run.sandbox]` only ever named a bundled provider; any other
// value is unsupported even if it is a well-formed plugin kind today.
let active_provider =
provider_str.and_then(|provider| match SandboxProviderKind::from_str(provider) {
Ok(provider) if provider.bundled().is_some() => Some(provider),
_ => {
unsupported.push("run.sandbox.provider".to_string());
None
}
});
if provider_str.is_none() {
unsupported.push("run.sandbox.provider".to_string());
}
@ -106,12 +108,12 @@ fn migrate_document(doc: &mut DocumentMut) -> std::result::Result<(), MigrationF
// Inspect each provider's table once: if it's the active provider, capture
// skip_clone; otherwise, report it as unsupported.
let mut disable_clone = false;
for provider in [EnvironmentProvider::Daytona, EnvironmentProvider::Docker] {
let key: &'static str = provider.into();
for provider in [SandboxProviderKind::DAYTONA, SandboxProviderKind::DOCKER] {
let key = provider.as_str();
let Some(item) = sandbox.get(key) else {
continue;
};
if Some(provider) == active_provider {
if active_provider.as_ref() == Some(&provider) {
if item
.as_table()
.and_then(|table| table.get("skip_clone"))
@ -129,9 +131,10 @@ 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",
let environment_id = if active_provider.as_ref() == Some(&SandboxProviderKind::DAYTONA) {
"daytona"
} else {
"default"
};
ensure_table(doc.as_table_mut(), &["run", "environment"])["id"] =
Item::Value(Value::from(environment_id));
@ -155,10 +158,13 @@ fn migrate_document(doc: &mut DocumentMut) -> std::result::Result<(), MigrationF
}
}
match active_provider {
Some(EnvironmentProvider::Daytona) => migrate_daytona(&sandbox, env, &mut unsupported),
Some(EnvironmentProvider::Docker) => migrate_docker(&sandbox, env, &mut unsupported),
_ => {}
match active_provider
.as_ref()
.and_then(SandboxProviderKind::bundled)
{
Some(BundledProvider::Daytona) => migrate_daytona(&sandbox, env, &mut unsupported),
Some(BundledProvider::Docker) => migrate_docker(&sandbox, env, &mut unsupported),
Some(BundledProvider::Local) | None => {}
}
if !unsupported.is_empty() {
@ -358,7 +364,7 @@ provider = "daytona"
.run;
assert_eq!(resolved.environment.id, "daytona");
assert_eq!(resolved.environment.provider, EnvironmentProvider::Daytona);
assert_eq!(resolved.environment.provider, SandboxProviderKind::DAYTONA);
assert!(migrated.contains("[run.environment]"));
assert!(migrated.contains("[environments.daytona]"));
assert!(!migrated.contains("[run.sandbox]"));
@ -490,7 +496,7 @@ cpu_quota = 200000
.run
.environment;
assert_eq!(resolved.provider, EnvironmentProvider::Docker);
assert_eq!(resolved.provider, SandboxProviderKind::DOCKER);
assert_eq!(
resolved.image.docker.as_deref(),
Some("buildpack-deps:noble")

View file

@ -695,8 +695,9 @@ fn finish_dense_result<T>(
mod tests {
use std::collections::HashMap;
use fabro_types::SandboxProviderKind;
use fabro_types::settings::cli::OutputVerbosity;
use fabro_types::settings::run::{ApprovalMode, EnvironmentProvider, RunMode};
use fabro_types::settings::run::{ApprovalMode, RunMode};
use super::{RunSettingsBuilder, WorkflowSettingsBuilder, server_runtime_settings_from_toml};
use crate::{
@ -799,7 +800,7 @@ provider = "local"
assert_eq!(
settings.run.environment.provider,
EnvironmentProvider::Local
SandboxProviderKind::LOCAL
);
}
@ -824,7 +825,7 @@ provider = "docker"
assert_eq!(
settings.run.environment.provider,
EnvironmentProvider::Docker
SandboxProviderKind::DOCKER
);
}

View file

@ -3,9 +3,7 @@ use std::collections::{BTreeMap, HashMap};
use fabro_model::{AgentProfileKind, BillingPolicy, CodecKind, ProviderAuthConfig};
use fabro_types::PermissionLevel;
use fabro_types::settings::cli::{CliAuthStrategy, OutputFormat, OutputVerbosity};
use fabro_types::settings::run::{
ApprovalMode, EnvironmentNetworkMode, EnvironmentProvider, MergeStrategy, RunMode,
};
use fabro_types::settings::run::{ApprovalMode, EnvironmentNetworkMode, MergeStrategy, RunMode};
use fabro_types::settings::server::{
GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, ServerAuthMethod,
WebhookStrategy,
@ -116,6 +114,12 @@ impl Combine for Option<BTreeMap<String, CostRates>> {
}
}
impl Combine for Option<BTreeMap<String, String>> {
fn combine(self, other: Self) -> Self {
self.or(other)
}
}
impl Combine for Option<HashMap<String, toml::Value>> {
fn combine(self, other: Self) -> Self {
self.or(other)
@ -145,7 +149,6 @@ impl_combine_self!(
CliLoggingLayer,
CliTargetLayer,
EnvironmentNetworkMode,
EnvironmentProvider,
EnvironmentDockerfileLayer,
InterviewProviderLayer,
NotificationProviderLayer,

View file

@ -1,5 +1,8 @@
//! Sparse `[server]` settings layer definitions.
use std::collections::BTreeMap;
use fabro_types::SandboxProviderKind;
use fabro_types::settings::server::{
GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, ServerAuthMethod,
WebhookStrategy,
@ -8,6 +11,7 @@ use fabro_types::settings::{Duration, InterpString};
use serde::{Deserialize, Serialize};
use super::LogFilter;
use super::combine::Combine;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
#[serde(deny_unknown_fields)]
@ -99,22 +103,46 @@ pub struct ServerSandboxLayer {
pub providers: Option<ServerSandboxProvidersLayer>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
#[serde(deny_unknown_fields)]
/// `[server.sandbox.providers.<kind>]`, keyed by provider kind. Bundled
/// kinds carry only `enabled`; any other kind names a plugin executable.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ServerSandboxProvidersLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub local: Option<ServerSandboxProviderLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub docker: Option<ServerSandboxProviderLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub daytona: Option<ServerSandboxProviderLayer>,
pub entries: BTreeMap<SandboxProviderKind, ServerSandboxProviderLayer>,
}
impl Combine for ServerSandboxProvidersLayer {
fn combine(self, other: Self) -> Self {
let mut combined = other.entries;
for (kind, layer) in self.entries {
let layer = match combined.remove(&kind) {
Some(fallback) => layer.combine(fallback),
None => layer,
};
combined.insert(kind, layer);
}
Self { entries: combined }
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
#[serde(deny_unknown_fields)]
pub struct ServerSandboxProviderLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
pub enabled: Option<bool>,
/// Plugin executable path. Rejected for bundled kinds at resolve time.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sha256: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dev: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub args: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub env: Option<BTreeMap<String, String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inherit_env: Option<Vec<String>>,
}
/// `[server.storage]` — single managed local disk root.

View file

@ -2,9 +2,10 @@ use std::path::Path;
use fabro_types::settings::run::{
DockerfileSource, EnvironmentImageSettings, EnvironmentLifecycleSettings,
EnvironmentNetworkMode, EnvironmentNetworkSettings, EnvironmentProvider,
EnvironmentResourcesSettings, EnvironmentSettings, RunEnvironmentSettings,
EnvironmentNetworkMode, EnvironmentNetworkSettings, EnvironmentResourcesSettings,
EnvironmentSettings, RunEnvironmentSettings,
};
use fabro_types::{BundledProvider, SandboxProviderKind};
use super::ResolveError;
use crate::{
@ -68,7 +69,7 @@ fn resolve_environment_fields(
errors.push(ResolveError::Missing {
path: format!("{path}.provider"),
});
EnvironmentProvider::Local
SandboxProviderKind::LOCAL
};
let environment = EnvironmentSettings {
@ -103,15 +104,16 @@ fn resolve_cwd(raw: Option<&str>, path: &str, errors: &mut Vec<ResolveError>) ->
Some(raw.to_string())
}
fn parse_provider(raw: &str, path: &str, errors: &mut Vec<ResolveError>) -> EnvironmentProvider {
if let Ok(provider) = raw.parse::<EnvironmentProvider>() {
provider
} else {
errors.push(ResolveError::Invalid {
path: path.to_string(),
reason: format!("unknown environment provider: {raw}"),
});
EnvironmentProvider::Local
fn parse_provider(raw: &str, path: &str, errors: &mut Vec<ResolveError>) -> SandboxProviderKind {
match raw.parse::<SandboxProviderKind>() {
Ok(provider) => provider,
Err(error) => {
errors.push(ResolveError::Invalid {
path: path.to_string(),
reason: format!("invalid environment provider: {error}"),
});
SandboxProviderKind::LOCAL
}
}
}
@ -205,38 +207,38 @@ fn validate_provider_capabilities(
path: &str,
errors: &mut Vec<ResolveError>,
) {
match environment.provider {
EnvironmentProvider::Local => {
match environment.provider.bundled() {
Some(BundledProvider::Local)
if matches!(
environment.network.mode,
EnvironmentNetworkMode::Block | EnvironmentNetworkMode::CidrAllowList
) {
errors.push(ResolveError::Invalid {
path: format!("{path}.network.mode"),
reason:
"local environments cannot enforce blocked or CIDR allow-list networking"
.to_string(),
});
}
) =>
{
errors.push(ResolveError::Invalid {
path: format!("{path}.network.mode"),
reason: "local environments cannot enforce blocked or CIDR allow-list networking"
.to_string(),
});
}
EnvironmentProvider::Docker => {
if environment.network.mode == EnvironmentNetworkMode::CidrAllowList {
errors.push(ResolveError::Invalid {
path: format!("{path}.network.mode"),
reason: "docker environments cannot enforce CIDR allow-list networking"
.to_string(),
});
}
Some(BundledProvider::Docker)
if environment.network.mode == EnvironmentNetworkMode::CidrAllowList =>
{
errors.push(ResolveError::Invalid {
path: format!("{path}.network.mode"),
reason: "docker environments cannot enforce CIDR allow-list networking".to_string(),
});
}
EnvironmentProvider::Daytona => {
if environment.image.docker.is_some() && environment.image.dockerfile.is_some() {
errors.push(ResolveError::Invalid {
path: format!("{path}.image"),
reason: "daytona environments accept either image.docker or image.dockerfile, \
not both"
.to_string(),
});
}
Some(BundledProvider::Daytona)
if environment.image.docker.is_some() && environment.image.dockerfile.is_some() =>
{
errors.push(ResolveError::Invalid {
path: format!("{path}.image"),
reason: "daytona environments accept either image.docker or image.dockerfile, not \
both"
.to_string(),
});
}
// Plugin providers validate their own spec at create time.
Some(_) | None => {}
}
}

View file

@ -1,13 +1,15 @@
use std::collections::BTreeMap;
use std::path::Path;
use fabro_types::SandboxProviderKind;
use fabro_types::settings::server::{
GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings,
ObjectStoreProvider, ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings,
ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings,
ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSandboxProviderSettings,
ServerSandboxProvidersSettings, ServerSandboxSettings, ServerSchedulerSettings,
ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings,
WebhookStrategy,
ObjectStoreProvider, ObjectStoreSettings, SandboxPluginSettings, ServerApiSettings,
ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings,
ServerIntegrationsSettings, ServerListenSettings, ServerLoggingSettings, ServerNamespace,
ServerSandboxProviderSettings, ServerSandboxProvidersSettings, ServerSandboxSettings,
ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings,
SlackIntegrationSettings, WebhookStrategy,
};
use fabro_util::Home;
@ -39,7 +41,7 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec<ResolveError>) -> Se
api: ServerApiSettings { url: api_url },
web,
auth,
sandbox: resolve_sandbox(layer.sandbox.as_ref()),
sandbox: resolve_sandbox(layer.sandbox.as_ref(), errors),
storage: storage.clone(),
artifacts: resolve_artifacts(layer.artifacts.as_ref(), &storage.root, errors),
slatedb: resolve_slatedb(layer.slatedb.as_ref(), &storage.root, errors),
@ -66,28 +68,77 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec<ResolveError>) -> Se
}
}
fn resolve_sandbox(layer: Option<&ServerSandboxLayer>) -> ServerSandboxSettings {
let providers = layer.and_then(|sandbox| sandbox.providers.as_ref());
fn resolve_sandbox(
layer: Option<&ServerSandboxLayer>,
errors: &mut Vec<ResolveError>,
) -> ServerSandboxSettings {
let configured = layer
.and_then(|sandbox| sandbox.providers.as_ref())
.map(|providers| &providers.entries);
let mut entries = BTreeMap::new();
// Bundled providers always have a policy entry; missing means enabled.
for kind in SandboxProviderKind::bundled_kinds() {
let layer = configured.and_then(|entries| entries.get(&kind));
let path = format!("server.sandbox.providers.{kind}");
if let Some(layer) = layer {
reject_plugin_fields_for_bundled(layer, &path, errors);
}
entries.insert(kind, ServerSandboxProviderSettings {
enabled: layer.and_then(|provider| provider.enabled).unwrap_or(true),
plugin: None,
});
}
for (kind, layer) in configured.into_iter().flatten() {
if kind.bundled().is_some() {
continue;
}
entries.insert(kind.clone(), ServerSandboxProviderSettings {
enabled: layer.enabled.unwrap_or(true),
plugin: Some(SandboxPluginSettings {
path: layer.path.clone(),
sha256: layer.sha256.clone(),
dev: layer.dev.unwrap_or(false),
args: layer.args.clone().unwrap_or_default(),
env: layer.env.clone().unwrap_or_default(),
inherit_env: layer.inherit_env.clone().unwrap_or_default(),
}),
});
}
ServerSandboxSettings {
providers: ServerSandboxProvidersSettings {
local: resolve_sandbox_provider(
providers.and_then(|providers| providers.local.as_ref()),
),
docker: resolve_sandbox_provider(
providers.and_then(|providers| providers.docker.as_ref()),
),
daytona: resolve_sandbox_provider(
providers.and_then(|providers| providers.daytona.as_ref()),
),
},
providers: ServerSandboxProvidersSettings { entries },
}
}
fn resolve_sandbox_provider(
layer: Option<&ServerSandboxProviderLayer>,
) -> ServerSandboxProviderSettings {
ServerSandboxProviderSettings {
enabled: layer.and_then(|provider| provider.enabled).unwrap_or(true),
fn reject_plugin_fields_for_bundled(
layer: &ServerSandboxProviderLayer,
path: &str,
errors: &mut Vec<ResolveError>,
) {
let ServerSandboxProviderLayer {
enabled: _,
path: plugin_path,
sha256,
dev,
args,
env,
inherit_env,
} = layer;
let set = [
("path", plugin_path.is_some()),
("sha256", sha256.is_some()),
("dev", dev.is_some()),
("args", args.is_some()),
("env", env.is_some()),
("inherit_env", inherit_env.is_some()),
];
for (field, is_set) in set {
if is_set {
errors.push(ResolveError::Invalid {
path: format!("{path}.{field}"),
reason: "bundled sandbox providers run in-process and take no plugin settings"
.to_string(),
});
}
}
}

View file

@ -58,7 +58,7 @@ id = "bad"
workflow_source,
r#"
[environments.bad]
provider = "not-a-provider"
provider = "Not A Provider"
"#
.parse::<SettingsLayer>()
.expect("bad environment catalog should parse")
@ -173,7 +173,7 @@ id = "bad"
"#,
r#"
[environments.bad]
provider = "not-a-provider"
provider = "Not A Provider"
"#
.parse::<SettingsLayer>()
.expect("bad environment catalog should parse")
@ -208,7 +208,7 @@ command = ["echo", "hi"]
"#,
r#"
[environments.bad]
provider = "not-a-provider"
provider = "Not A Provider"
"#
.parse::<SettingsLayer>()
.expect("bad environment catalog should parse")

View file

@ -1,7 +1,6 @@
use fabro_types::SandboxProviderKind;
use fabro_types::settings::InterpString;
use fabro_types::settings::run::{
ApprovalMode, EnvironmentNetworkMode, EnvironmentProvider, RunGoal, RunMode,
};
use fabro_types::settings::run::{ApprovalMode, EnvironmentNetworkMode, RunGoal, RunMode};
use crate::{MergeMap, SettingsLayer};
@ -109,7 +108,7 @@ fn resolves_run_defaults_from_empty_settings() {
assert_eq!(settings.execution.approval, ApprovalMode::Prompt);
assert_eq!(settings.prepare.timeout_ms, 300_000);
assert_eq!(settings.environment.id, "default");
assert_eq!(settings.environment.provider, EnvironmentProvider::Docker);
assert_eq!(settings.environment.provider, SandboxProviderKind::DOCKER);
assert_eq!(
settings.environment.image.docker.as_deref(),
Some("buildpack-deps:noble")
@ -180,7 +179,7 @@ NODE_ENV = "development"
let environment = settings.run.environment;
assert_eq!(environment.id, "fabro-dev");
assert_eq!(environment.provider, EnvironmentProvider::Daytona);
assert_eq!(environment.provider, SandboxProviderKind::DAYTONA);
assert_eq!(environment.image.docker.as_deref(), None);
assert!(environment.image.dockerfile.is_some());
assert_eq!(environment.resources.cpu, Some(8));
@ -555,7 +554,7 @@ provider = "local"
.run;
assert_eq!(settings.environment.id, "host");
assert_eq!(settings.environment.provider, EnvironmentProvider::Local);
assert_eq!(settings.environment.provider, SandboxProviderKind::LOCAL);
assert!(settings.environment.image.docker.is_none());
}
@ -652,7 +651,7 @@ dockerfile = { path = "Dockerfile" }
.expect("daytona dockerfile should not need a user-supplied snapshot name")
.run;
assert_eq!(settings.environment.provider, EnvironmentProvider::Daytona);
assert_eq!(settings.environment.provider, SandboxProviderKind::DAYTONA);
assert!(settings.environment.image.docker.is_none());
assert!(settings.environment.image.dockerfile.is_some());
}
@ -677,7 +676,7 @@ docker = "ubuntu:24.04"
.expect("daytona should accept docker image selection")
.run;
assert_eq!(settings.environment.provider, EnvironmentProvider::Daytona);
assert_eq!(settings.environment.provider, SandboxProviderKind::DAYTONA);
assert_eq!(
settings.environment.image.docker.as_deref(),
Some("ubuntu:24.04")

View file

@ -3,6 +3,7 @@
reason = "sync test fixture setup and raw template source assertions; not on a Tokio path"
)]
use fabro_types::SandboxProviderKind;
use fabro_types::settings::server::{
GithubIntegrationStrategy, LogDestination, ObjectStoreSettings, ServerAuthMethod,
ServerListenSettings, ServerNamespace,
@ -207,9 +208,15 @@ methods = ["dev-token"]
.expect("server settings should resolve");
let sandbox = settings.server.sandbox;
assert!(sandbox.providers.local.enabled);
assert!(sandbox.providers.docker.enabled);
assert!(sandbox.providers.daytona.enabled);
assert!(sandbox.providers.is_enabled(&SandboxProviderKind::LOCAL));
assert!(sandbox.providers.is_enabled(&SandboxProviderKind::DOCKER));
assert!(sandbox.providers.is_enabled(&SandboxProviderKind::DAYTONA));
assert!(
!sandbox
.providers
.is_enabled(&SandboxProviderKind::try_new("e2b").unwrap()),
"an unconfigured plugin kind is disabled"
);
}
#[test]
@ -228,13 +235,57 @@ enabled = false
.expect("server settings should resolve");
let sandbox = settings.server.sandbox;
assert!(sandbox.providers.local.enabled);
assert!(sandbox.providers.docker.enabled);
assert!(!sandbox.providers.daytona.enabled);
assert!(sandbox.providers.is_enabled(&SandboxProviderKind::LOCAL));
assert!(sandbox.providers.is_enabled(&SandboxProviderKind::DOCKER));
assert!(!sandbox.providers.is_enabled(&SandboxProviderKind::DAYTONA));
}
#[test]
fn parsing_rejects_unknown_server_sandbox_provider() {
fn server_sandbox_accepts_plugin_providers_by_kind() {
let settings = ServerSettingsBuilder::from_toml(
r#"
_version = 1
[server.auth]
methods = ["dev-token"]
[server.sandbox.providers.e2b]
path = "/opt/fabro/plugins/fabro-sandbox-e2b"
sha256 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
args = ["--region", "us"]
inherit_env = ["PATH"]
[server.sandbox.providers.e2b.env]
E2B_API_URL = "https://api.e2b.example"
"#,
)
.expect("server settings should resolve");
let providers = settings.server.sandbox.providers;
let kind = SandboxProviderKind::try_new("e2b").unwrap();
assert!(providers.is_enabled(&kind));
let plugin = providers
.get(&kind)
.and_then(|entry| entry.plugin.as_ref())
.expect("plugin kinds carry launch settings");
assert_eq!(
plugin.path.as_deref(),
Some("/opt/fabro/plugins/fabro-sandbox-e2b")
);
assert_eq!(plugin.args, vec!["--region", "us"]);
assert_eq!(plugin.inherit_env, vec!["PATH"]);
assert_eq!(
plugin.env.get("E2B_API_URL").map(String::as_str),
Some("https://api.e2b.example")
);
assert!(!plugin.dev);
let enabled: Vec<_> = providers.enabled_kinds().map(ToString::to_string).collect();
assert_eq!(enabled, vec!["daytona", "docker", "e2b", "local"]);
assert_eq!(providers.enabled_plugins().count(), 1);
}
#[test]
fn server_sandbox_rejects_plugin_settings_on_bundled_providers() {
let err = ServerSettingsBuilder::from_toml(
r#"
_version = 1
@ -242,14 +293,36 @@ _version = 1
[server.auth]
methods = ["dev-token"]
[server.sandbox.providers.exe]
[server.sandbox.providers.docker]
path = "/usr/local/bin/fabro-sandbox-docker"
"#,
)
.expect_err("bundled providers take no plugin settings");
assert!(
err.to_string()
.contains("server.sandbox.providers.docker.path"),
"unexpected error: {err}"
);
}
#[test]
fn parsing_rejects_invalid_server_sandbox_provider_kind() {
let err = ServerSettingsBuilder::from_toml(
r#"
_version = 1
[server.auth]
methods = ["dev-token"]
[server.sandbox.providers."Bad Kind"]
enabled = true
"#,
)
.expect_err("unknown sandbox provider should be rejected");
.expect_err("an invalid provider kind name should be rejected");
assert!(
err.to_string().contains("unknown field `exe`"),
err.to_string().contains("invalid sandbox provider kind"),
"unexpected error: {err}"
);
}

View file

@ -0,0 +1,56 @@
-- no-transaction
-- Open the environment provider column to any sandbox-driver kind name.
-- SQLite cannot drop a CHECK constraint, so the table is rebuilt following
-- the documented procedure: foreign keys off, copy, swap, verify, on again.
-- `automations.environment_id` references this table by name; the drop and
-- rename leave that reference pointing at the rebuilt table.
PRAGMA foreign_keys = OFF;
BEGIN;
CREATE TABLE environments_new (
id TEXT PRIMARY KEY NOT NULL,
revision TEXT NOT NULL,
provider TEXT NOT NULL,
cwd TEXT,
image_docker TEXT,
image_dockerfile_inline TEXT,
resources_cpu INTEGER,
resources_memory TEXT,
resources_disk TEXT,
network_mode TEXT NOT NULL,
network_allow_json TEXT NOT NULL DEFAULT '[]',
lifecycle_preserve INTEGER NOT NULL,
lifecycle_stop_on_terminal INTEGER NOT NULL,
lifecycle_auto_stop TEXT,
labels_json TEXT NOT NULL DEFAULT '{}',
env_json TEXT NOT NULL DEFAULT '{}',
CHECK (length(id) BETWEEN 1 AND 63),
CHECK (substr(id, 1, 1) GLOB '[a-z0-9]'),
CHECK (id NOT GLOB '*[^a-z0-9-]*'),
CHECK (id <> 'local'),
CHECK (length(revision) = 64),
CHECK (revision NOT GLOB '*[^0-9a-f]*'),
CHECK (length(provider) BETWEEN 1 AND 64),
CHECK (substr(provider, 1, 1) GLOB '[a-z0-9]'),
CHECK (substr(provider, -1, 1) GLOB '[a-z0-9]'),
CHECK (provider NOT GLOB '*[^a-z0-9-]*'),
CHECK (network_mode IN ('allow_all', 'block', 'cidr_allow_list')),
CHECK (lifecycle_preserve IN (0, 1)),
CHECK (lifecycle_stop_on_terminal IN (0, 1)),
CHECK (json_valid(network_allow_json)),
CHECK (json_valid(labels_json)),
CHECK (json_valid(env_json))
);
INSERT INTO environments_new SELECT * FROM environments;
DROP TABLE environments;
ALTER TABLE environments_new RENAME TO environments;
PRAGMA foreign_key_check;
COMMIT;
PRAGMA foreign_keys = ON;

View file

@ -1376,11 +1376,15 @@ async fn environments_schema_rejects_invalid_rows() -> anyhow::Result<()> {
database.migrate().await?;
insert_minimal_environment(database.pool(), "valid", "docker", "allow_all").await?;
// Any well-formed sandbox-driver kind name is a valid provider: plugins
// are configured by kind, not enumerated in the schema.
insert_minimal_environment(database.pool(), "plugin", "e2b-cloud", "allow_all").await?;
for (id, provider, network_mode) in [
("Bad", "docker", "allow_all"),
("local", "docker", "allow_all"),
("bad-provider", "bogus", "allow_all"),
("bad-provider", "Bogus Provider", "allow_all"),
("bad-provider-hyphen", "-e2b", "allow_all"),
("bad-network", "docker", "bogus"),
] {
let result = insert_minimal_environment(database.pool(), id, provider, network_mode).await;

View file

@ -166,7 +166,9 @@ pub use sandbox_details::{
pub use sandbox_inventory::{
SandboxInfo, SandboxListMeta, SandboxListResponse, SandboxProviderLookupError,
};
pub use sandbox_provider::SandboxProviderKind;
pub use sandbox_provider::{
BundledProvider, InvalidSandboxProviderKind, SandboxProviderKind, WorkspacePolicy,
};
pub use sandbox_services::{
SandboxService, SandboxServiceDiscoverySource, SandboxServiceListMeta,
SandboxServiceListResponse,

View file

@ -188,7 +188,7 @@ mod tests {
fn serializes_with_snake_case_state() {
let details = SandboxDetails {
sandbox: RunSandboxInstance {
provider: crate::SandboxProviderKind::Docker,
provider: crate::SandboxProviderKind::DOCKER,
image: Some("ghcr.io/fabro/sandbox:latest".to_string()),
snapshot: None,
runtime: crate::RunSandboxRuntime {
@ -282,7 +282,7 @@ mod tests {
}))
.unwrap();
assert_eq!(details.sandbox.provider, crate::SandboxProviderKind::Local);
assert_eq!(details.sandbox.provider, crate::SandboxProviderKind::LOCAL);
assert_eq!(
details.sandbox.runtime.id.as_str(),
"local:01JNQVR7M0EJ5GKAT2SC4ERS1Z"

View file

@ -1,85 +1,309 @@
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use std::borrow::Cow;
use std::fmt;
use std::str::FromStr;
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize};
use strum::VariantArray as _;
use crate::settings::run::RunMode;
/// Sandbox provider discriminator for agent tool operations.
/// Identity of a sandbox provider.
///
/// Open by design: the bundled providers (`local`, `docker`, `daytona`) run
/// in-process, and any other kind names a sandbox-driver plugin executable
/// configured under `server.sandbox.providers.<kind>`. Run records,
/// inventory, and the API carry this type so a plugin sandbox persists and
/// reconnects through the same code path as a bundled one.
///
/// Kind names follow the sandbox-driver rules: lowercase ASCII letters,
/// digits, and interior hyphens, at most 64 bytes. Parsing accepts any ASCII
/// case and lowercases it.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
#[serde(transparent)]
pub struct SandboxProviderKind(Cow<'static, str>);
/// The providers linked into fabro itself.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Display, EnumString,
Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, strum::EnumString, strum::VariantArray,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
pub enum SandboxProviderKind {
/// Run tools on the local host.
#[default]
#[strum(serialize_all = "lowercase")]
pub enum BundledProvider {
/// Run tools on the fabro host in a caller-designated directory.
Local,
/// Run tools inside a Docker container.
/// Run tools inside a Docker container on the operator's daemon.
Docker,
/// Run tools inside a Daytona cloud sandbox.
Daytona,
}
impl BundledProvider {
#[must_use]
pub const fn kind(self) -> SandboxProviderKind {
match self {
Self::Local => SandboxProviderKind::LOCAL,
Self::Docker => SandboxProviderKind::DOCKER,
Self::Daytona => SandboxProviderKind::DAYTONA,
}
}
}
/// How a provider obtains the run's workspace.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkspacePolicy {
/// The caller designates an existing host directory; nothing is cloned.
DesignatedDirectory,
/// The provider owns an isolated workspace and fabro clones into it.
Clone,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("invalid sandbox provider kind {value:?}: {reason}")]
pub struct InvalidSandboxProviderKind {
pub value: String,
pub reason: &'static str,
}
const MAX_KIND_LEN: usize = 64;
impl SandboxProviderKind {
/// True only for Local. Used by dry-run to force local execution.
/// NOT the same as "runs on the host" (Docker is host-adjacent but not
/// dry-run compatible).
pub const DAYTONA: Self = Self(Cow::Borrowed("daytona"));
pub const DOCKER: Self = Self(Cow::Borrowed("docker"));
pub const LOCAL: Self = Self(Cow::Borrowed("local"));
/// Validates and wraps a provider kind name. Input is lowercased.
pub fn try_new(value: impl AsRef<str>) -> Result<Self, InvalidSandboxProviderKind> {
let raw = value.as_ref();
let value = raw.trim().to_ascii_lowercase();
let invalid = |reason| InvalidSandboxProviderKind {
value: raw.to_string(),
reason,
};
if value.is_empty() {
return Err(invalid("must not be empty"));
}
if value.len() > MAX_KIND_LEN {
return Err(invalid("exceeds 64 bytes"));
}
let charset_ok = value
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-');
if !charset_ok || value.starts_with('-') || value.ends_with('-') {
return Err(invalid(
"must be lowercase ASCII letters, digits, and interior hyphens",
));
}
Ok(BundledProvider::VARIANTS
.iter()
.map(|bundled| bundled.kind())
.find(|bundled| bundled.0 == value)
.unwrap_or(Self(Cow::Owned(value))))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
/// The bundled provider this kind names, or `None` for a plugin kind.
#[must_use]
pub fn bundled(&self) -> Option<BundledProvider> {
BundledProvider::VARIANTS
.iter()
.copied()
.find(|bundled| bundled.kind() == *self)
}
/// All bundled provider kinds, in display order.
pub fn bundled_kinds() -> impl Iterator<Item = Self> {
BundledProvider::VARIANTS
.iter()
.map(|bundled| bundled.kind())
}
/// True only for `local`. Used by dry-run to force local execution.
#[must_use]
pub fn is_local(&self) -> bool {
matches!(self, Self::Local)
*self == Self::LOCAL
}
/// How a run's workspace is obtained on this provider: `local` runs in a
/// caller-designated directory, every other provider clones.
#[must_use]
pub fn workspace_policy(&self) -> WorkspacePolicy {
if self.is_local() {
WorkspacePolicy::DesignatedDirectory
} else {
WorkspacePolicy::Clone
}
}
/// True for providers that clone repository sources into their workspace.
#[must_use]
pub fn is_clone_based(&self) -> bool {
matches!(self, Self::Docker | Self::Daytona)
pub fn clones_workspace(&self) -> bool {
self.workspace_policy() == WorkspacePolicy::Clone
}
/// Coerce non-local providers to `Local` under dry-run; otherwise
/// Coerce non-local providers to `local` under dry-run; otherwise
/// unchanged.
#[must_use]
pub fn effective_for(self, mode: RunMode) -> Self {
pub fn effective_for(&self, mode: RunMode) -> Self {
if mode == RunMode::DryRun && !self.is_local() {
Self::Local
Self::LOCAL
} else {
self
self.clone()
}
}
}
impl Default for SandboxProviderKind {
fn default() -> Self {
Self::LOCAL
}
}
impl fmt::Display for SandboxProviderKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for SandboxProviderKind {
type Err = InvalidSandboxProviderKind;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::try_new(value)
}
}
impl AsRef<str> for SandboxProviderKind {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<BundledProvider> for SandboxProviderKind {
fn from(value: BundledProvider) -> Self {
value.kind()
}
}
impl PartialEq<BundledProvider> for SandboxProviderKind {
fn eq(&self, other: &BundledProvider) -> bool {
*self == other.kind()
}
}
impl<'de> Deserialize<'de> for SandboxProviderKind {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = Cow::<'de, str>::deserialize(deserializer)?;
Self::try_new(raw.as_ref()).map_err(D::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::SandboxProviderKind;
use super::{BundledProvider, SandboxProviderKind, WorkspacePolicy};
use crate::settings::run::RunMode;
#[test]
fn sandbox_provider_default_is_local() {
assert_eq!(SandboxProviderKind::default(), SandboxProviderKind::Local);
assert_eq!(SandboxProviderKind::default(), SandboxProviderKind::LOCAL);
}
#[test]
fn sandbox_provider_from_str() {
assert_eq!(
"local".parse::<SandboxProviderKind>().unwrap(),
SandboxProviderKind::Local
SandboxProviderKind::LOCAL
);
assert_eq!(
"docker".parse::<SandboxProviderKind>().unwrap(),
SandboxProviderKind::Docker
SandboxProviderKind::DOCKER
);
assert_eq!(
"daytona".parse::<SandboxProviderKind>().unwrap(),
SandboxProviderKind::Daytona
SandboxProviderKind::DAYTONA
);
assert_eq!(
"LOCAL".parse::<SandboxProviderKind>().unwrap(),
SandboxProviderKind::Local
SandboxProviderKind::LOCAL
);
assert!("invalid".parse::<SandboxProviderKind>().is_err());
let plugin = "e2b-cloud".parse::<SandboxProviderKind>().unwrap();
assert_eq!(plugin.as_str(), "e2b-cloud");
assert_eq!(plugin.bundled(), None);
for invalid in ["", "-e2b", "e2b-", "e 2b", "E2B_cloud", &"x".repeat(65)] {
assert!(
invalid.parse::<SandboxProviderKind>().is_err(),
"{invalid:?} should be rejected"
);
}
}
#[test]
fn sandbox_provider_display() {
assert_eq!(SandboxProviderKind::Local.to_string(), "local");
assert_eq!(SandboxProviderKind::Docker.to_string(), "docker");
assert_eq!(SandboxProviderKind::Daytona.to_string(), "daytona");
assert_eq!(SandboxProviderKind::LOCAL.to_string(), "local");
assert_eq!(SandboxProviderKind::DOCKER.to_string(), "docker");
assert_eq!(SandboxProviderKind::DAYTONA.to_string(), "daytona");
}
#[test]
fn bundled_kinds_round_trip_through_bundled() {
for bundled in [
BundledProvider::Local,
BundledProvider::Docker,
BundledProvider::Daytona,
] {
let kind = SandboxProviderKind::from(bundled);
assert_eq!(kind.bundled(), Some(bundled));
assert_eq!(kind.to_string(), bundled.to_string());
assert_eq!(kind, bundled);
}
}
#[test]
fn workspace_policy_designates_local_and_clones_everything_else() {
assert_eq!(
SandboxProviderKind::LOCAL.workspace_policy(),
WorkspacePolicy::DesignatedDirectory
);
assert_eq!(
SandboxProviderKind::DOCKER.workspace_policy(),
WorkspacePolicy::Clone
);
assert_eq!(
SandboxProviderKind::try_new("host")
.unwrap()
.workspace_policy(),
WorkspacePolicy::Clone
);
assert!(!SandboxProviderKind::LOCAL.clones_workspace());
assert!(SandboxProviderKind::DAYTONA.clones_workspace());
}
#[test]
fn dry_run_coerces_every_non_local_kind_to_local() {
assert_eq!(
SandboxProviderKind::try_new("host")
.unwrap()
.effective_for(RunMode::DryRun),
SandboxProviderKind::LOCAL
);
assert_eq!(
SandboxProviderKind::DOCKER.effective_for(RunMode::Normal),
SandboxProviderKind::DOCKER
);
}
#[test]
fn serde_is_a_validated_plain_string() {
let json = serde_json::to_string(&SandboxProviderKind::DAYTONA).unwrap();
assert_eq!(json, "\"daytona\"");
let parsed: SandboxProviderKind = serde_json::from_str("\"host\"").unwrap();
assert_eq!(parsed.as_str(), "host");
assert!(serde_json::from_str::<SandboxProviderKind>("\"Bad Kind\"").is_err());
let keyed: std::collections::BTreeMap<SandboxProviderKind, bool> =
serde_json::from_str(r#"{"docker": true, "host": false}"#).unwrap();
assert_eq!(keyed.get(&SandboxProviderKind::DOCKER), Some(&true));
}
}

View file

@ -35,14 +35,14 @@ pub use public_url::{
};
pub use run::{
ArtifactsSettings, DockerfileSource, EnvironmentImageSettings, EnvironmentLifecycleSettings,
EnvironmentNetworkMode, EnvironmentNetworkSettings, EnvironmentProvider,
EnvironmentResourcesSettings, EnvironmentSettings, GitAuthorSettings, HookDefinition, HookType,
InterviewProviderSettings, McpServerRef, McpServerSettings, McpTransport,
NotificationProviderSettings, NotificationRouteSettings, PreparedStep, PullRequestSettings,
ResolvedMcpEntry, RunAgentSettings, RunCheckpointSettings, RunEnvironmentSettings,
RunExecutionSettings, RunGitSettings, RunGoal, RunIntegrationsGithubSettings,
RunIntegrationsSettings, RunInterviewsSettings, RunModelControls, RunModelSettings,
RunNamespace, RunPrepareSettings, RunScmSettings, ScmGitHubSettings, TlsMode,
EnvironmentNetworkMode, EnvironmentNetworkSettings, EnvironmentResourcesSettings,
EnvironmentSettings, GitAuthorSettings, HookDefinition, HookType, InterviewProviderSettings,
McpServerRef, McpServerSettings, McpTransport, NotificationProviderSettings,
NotificationRouteSettings, PreparedStep, PullRequestSettings, ResolvedMcpEntry,
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

@ -19,6 +19,7 @@ use super::duration::Duration;
use super::interp::{InterpString, Namespace, ResolveCtx, ResolveError};
use super::model_ref::ModelRef;
use super::size::Size;
use crate::SandboxProviderKind;
/// A structurally resolved `[run]` view for consumers.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -1127,50 +1128,6 @@ impl Default for RunMetaBranchSettings {
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Default,
Serialize,
Deserialize,
strum::Display,
strum::EnumString,
strum::IntoStaticStr,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
pub enum EnvironmentProvider {
#[default]
Local,
Docker,
Daytona,
}
impl EnvironmentProvider {
#[must_use]
pub fn is_local(self) -> bool {
matches!(self, Self::Local)
}
#[must_use]
pub fn is_clone_based(self) -> bool {
matches!(self, Self::Docker | Self::Daytona)
}
}
impl From<EnvironmentProvider> for crate::SandboxProviderKind {
fn from(value: EnvironmentProvider) -> Self {
match value {
EnvironmentProvider::Local => Self::Local,
EnvironmentProvider::Docker => Self::Docker,
EnvironmentProvider::Daytona => Self::Daytona,
}
}
}
#[derive(
Debug,
Clone,
@ -1245,7 +1202,7 @@ impl Default for EnvironmentLifecycleSettings {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EnvironmentSettings {
pub provider: EnvironmentProvider,
pub provider: SandboxProviderKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
pub image: EnvironmentImageSettings,
@ -1259,7 +1216,7 @@ pub struct EnvironmentSettings {
impl Default for EnvironmentSettings {
fn default() -> Self {
Self {
provider: EnvironmentProvider::Local,
provider: SandboxProviderKind::LOCAL,
cwd: None,
image: EnvironmentImageSettings::default(),
resources: EnvironmentResourcesSettings::default(),
@ -1274,7 +1231,7 @@ impl Default for EnvironmentSettings {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunEnvironmentSettings {
pub id: String,
pub provider: EnvironmentProvider,
pub provider: SandboxProviderKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
pub image: EnvironmentImageSettings,

View file

@ -5,6 +5,7 @@
//! scheduler, logging, integrations). Same-host and split-host deployments
//! use the same schema.
use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::time::Duration as StdDuration;
@ -12,6 +13,7 @@ use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::duration::Duration;
use crate::SandboxProviderKind;
/// A structurally resolved `[server]` view for consumers.
///
@ -110,44 +112,101 @@ pub struct ServerAuthGithubSettings {
pub allowed_usernames: Vec<String>,
}
/// `[server.sandbox]` — which sandbox providers this server may launch.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerSandboxSettings {
pub providers: ServerSandboxProvidersSettings,
}
/// Per-provider policy keyed by provider kind.
///
/// The resolver always materializes the bundled kinds (`local`, `docker`,
/// `daytona`). Any other key names a sandbox-driver plugin executable and
/// carries its launch settings.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ServerSandboxProvidersSettings {
pub local: ServerSandboxProviderSettings,
pub docker: ServerSandboxProviderSettings,
pub daytona: ServerSandboxProviderSettings,
pub entries: BTreeMap<SandboxProviderKind, ServerSandboxProviderSettings>,
}
impl ServerSandboxProvidersSettings {
/// Per-provider policy entry.
/// Policy for one provider kind, when the server knows the kind.
#[must_use]
pub fn for_provider(
pub fn get(&self, provider: &SandboxProviderKind) -> Option<&ServerSandboxProviderSettings> {
self.entries.get(provider)
}
/// Whether the server may launch this provider. A kind without an entry
/// is disabled: nothing configured it.
#[must_use]
pub fn is_enabled(&self, provider: &SandboxProviderKind) -> bool {
self.get(provider).is_some_and(|entry| entry.enabled)
}
/// Kinds the server may launch, in key order.
pub fn enabled_kinds(&self) -> impl Iterator<Item = &SandboxProviderKind> {
self.entries
.iter()
.filter(|(_, entry)| entry.enabled)
.map(|(kind, _)| kind)
}
/// Enabled kinds that are served by a plugin executable.
pub fn enabled_plugins(
&self,
provider: crate::SandboxProviderKind,
) -> &ServerSandboxProviderSettings {
match provider {
crate::SandboxProviderKind::Local => &self.local,
crate::SandboxProviderKind::Docker => &self.docker,
crate::SandboxProviderKind::Daytona => &self.daytona,
) -> impl Iterator<Item = (&SandboxProviderKind, &SandboxPluginSettings)> {
self.entries.iter().filter_map(|(kind, entry)| {
(entry.enabled)
.then_some(entry.plugin.as_ref())
.flatten()
.map(|plugin| (kind, plugin))
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerSandboxProviderSettings {
pub enabled: bool,
/// Launch settings for a plugin provider. Absent for bundled kinds.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub plugin: Option<SandboxPluginSettings>,
}
impl Default for ServerSandboxProviderSettings {
// The resolver defaults each bundled provider to enabled; keep the struct
// default aligned with that so callers that bypass the resolver behave
// identically.
fn default() -> Self {
Self {
enabled: true,
plugin: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerSandboxProviderSettings {
pub enabled: bool,
}
impl Default for ServerSandboxProviderSettings {
// The resolver defaults each provider to enabled; keep the struct default
// aligned with that so callers that bypass the resolver behave identically.
fn default() -> Self {
Self { enabled: true }
}
/// How the server launches a sandbox-driver plugin executable.
///
/// The executable speaks the sandbox-driver JSON-RPC protocol on stdio. It
/// starts with a scrubbed environment: only `env` and the ambient variables
/// named in `inherit_env` reach it.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SandboxPluginSettings {
/// Executable path. When absent the server searches `PATH` for
/// `fabro-sandbox-<kind>`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
/// Pinned SHA-256 of the executable, hex.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sha256: Option<String>,
/// Allow launching without a checksum.
#[serde(default)]
pub dev: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub env: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inherit_env: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]

View file

@ -13,7 +13,7 @@ fn sandbox_inventory_serializes_provider_backed_shape() {
let created_at = Utc.with_ymd_and_hms(2026, 5, 25, 12, 0, 0).unwrap();
let response = SandboxListResponse {
data: vec![SandboxInfo {
provider: SandboxProviderKind::Docker,
provider: SandboxProviderKind::DOCKER,
id: "container-abc123".to_string(),
display_name: Some("fabro-run-abc".to_string()),
state: SandboxState::Running,
@ -43,7 +43,7 @@ fn sandbox_inventory_serializes_provider_backed_shape() {
}],
meta: SandboxListMeta {
provider_errors: vec![SandboxProviderLookupError {
provider: SandboxProviderKind::Daytona,
provider: SandboxProviderKind::DAYTONA,
message: "Daytona API key is not configured".to_string(),
}],
},
@ -102,7 +102,7 @@ fn sandbox_inventory_deserializes_when_optional_fields_are_absent() {
}))
.unwrap();
assert_eq!(info.provider, SandboxProviderKind::Local);
assert_eq!(info.provider, SandboxProviderKind::LOCAL);
assert_eq!(info.id, "local:01KSGHGMCFM8W2FHXNMJ7MVY65");
assert_eq!(info.state, SandboxState::Unknown);
assert!(info.display_name.is_none());

View file

@ -11,12 +11,12 @@ use serde_json::json;
fn run_sandbox_serializes_canonical_identity_without_identifier() {
let sandbox = RunSandbox::ready(
RunSandboxPlan {
provider: SandboxProviderKind::Docker,
provider: SandboxProviderKind::DOCKER,
image: None,
snapshot: None,
},
RunSandboxInstance {
provider: SandboxProviderKind::Docker,
provider: SandboxProviderKind::DOCKER,
image: None,
snapshot: None,
runtime: RunSandboxRuntime {
@ -75,7 +75,7 @@ fn run_sandbox_ready_requires_instance() {
fn sandbox_details_requires_canonical_id_and_working_directory() {
let details = SandboxDetails {
sandbox: RunSandboxInstance {
provider: SandboxProviderKind::Daytona,
provider: SandboxProviderKind::DAYTONA,
image: Some("ubuntu:24.04".to_string()),
snapshot: None,
runtime: RunSandboxRuntime {
@ -137,18 +137,22 @@ fn sandbox_details_requires_canonical_id_and_working_directory() {
}
#[test]
fn sandbox_provider_rejects_unknown_values() {
fn sandbox_provider_accepts_plugin_kinds_and_rejects_malformed_names() {
assert_eq!(
serde_json::from_value::<SandboxProviderKind>(json!("local")).unwrap(),
SandboxProviderKind::Local
SandboxProviderKind::LOCAL
);
assert_eq!(
serde_json::from_value::<SandboxProviderKind>(json!("docker")).unwrap(),
SandboxProviderKind::Docker
SandboxProviderKind::DOCKER
);
assert_eq!(
serde_json::from_value::<SandboxProviderKind>(json!("daytona")).unwrap(),
SandboxProviderKind::Daytona
SandboxProviderKind::DAYTONA
);
assert!(serde_json::from_value::<SandboxProviderKind>(json!("other")).is_err());
let plugin = serde_json::from_value::<SandboxProviderKind>(json!("other")).unwrap();
assert_eq!(plugin.as_str(), "other");
assert_eq!(plugin.bundled(), None);
assert!(serde_json::from_value::<SandboxProviderKind>(json!("Not Valid")).is_err());
assert!(serde_json::from_value::<SandboxProviderKind>(json!("")).is_err());
}

View file

@ -134,7 +134,6 @@ models/environment-list-meta.ts
models/environment-list-response.ts
models/environment-network-mode.ts
models/environment-network-settings.ts
models/environment-provider.ts
models/environment-resources-settings.ts
models/environment-settings.ts
models/environment.ts
@ -434,7 +433,7 @@ models/sandbox-list-response.ts
models/sandbox-network-policy-mode.ts
models/sandbox-network-policy.ts
models/sandbox-network.ts
models/sandbox-provider-kind.ts
models/sandbox-plugin-settings.ts
models/sandbox-provider-lookup-error.ts
models/sandbox-resources.ts
models/sandbox-service-discovery-source.ts
@ -460,7 +459,6 @@ models/server-listen-unix-settings.ts
models/server-logging-settings.ts
models/server-namespace.ts
models/server-sandbox-provider-settings.ts
models/server-sandbox-providers-settings.ts
models/server-sandbox-settings.ts
models/server-scheduler-settings.ts
models/server-settings.ts

View file

@ -372,7 +372,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration)
};
},
/**
* Creates a new workflow run in `submitted` status from either a self-contained legacy manifest or an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The intent lane responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`).
* Creates a new workflow run in `submitted` status from either a self-contained legacy manifest or an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The intent lane responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `pull_request_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`).
* @summary Create Run
* @param {CreateRunRequest} createRunRequest
* @param {*} [options] Override http request option.
@ -1677,7 +1677,7 @@ export const RunsApiFp = function(configuration?: Configuration) {
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Creates a new workflow run in `submitted` status from either a self-contained legacy manifest or an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The intent lane responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`).
* Creates a new workflow run in `submitted` status from either a self-contained legacy manifest or an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The intent lane responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `pull_request_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`).
* @summary Create Run
* @param {CreateRunRequest} createRunRequest
* @param {*} [options] Override http request option.
@ -2137,7 +2137,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath?
return localVarFp.closeRunPullRequest(id, options).then((request) => request(axios, basePath));
},
/**
* Creates a new workflow run in `submitted` status from either a self-contained legacy manifest or an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The intent lane responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`).
* Creates a new workflow run in `submitted` status from either a self-contained legacy manifest or an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The intent lane responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `pull_request_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`).
* @summary Create Run
* @param {CreateRunRequest} createRunRequest
* @param {*} [options] Override http request option.
@ -2518,7 +2518,7 @@ export class RunsApi extends BaseAPI {
}
/**
* Creates a new workflow run in `submitted` status from either a self-contained legacy manifest or an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The intent lane responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`).
* Creates a new workflow run in `submitted` status from either a self-contained legacy manifest or an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The intent lane responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `pull_request_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`).
* @summary Create Run
* @param {CreateRunRequest} createRunRequest
* @param {*} [options] Override http request option.

View file

@ -24,9 +24,6 @@ import type { EnvironmentLifecycleSettings } from './environment-lifecycle-setti
import type { EnvironmentNetworkSettings } from './environment-network-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentProvider } from './environment-provider';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentResourcesSettings } from './environment-resources-settings';
/**
@ -34,7 +31,10 @@ import type { EnvironmentResourcesSettings } from './environment-resources-setti
*/
export interface CreateEnvironmentRequest {
'id': string;
'provider': EnvironmentProvider;
/**
* Sandbox provider kind. `local`, `docker`, and `daytona` are bundled with the server; any other value names a sandbox-driver plugin configured under `server.sandbox.providers.<kind>`.
*/
'provider': string;
/**
* Local-provider command working directory for this environment. Docker and Daytona ignore this value.
*/

View file

@ -13,11 +13,11 @@
*/
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxProviderKind } from './sandbox-provider-kind';
export interface DeleteRunSandbox {
'provider': SandboxProviderKind;
/**
* Sandbox provider kind. `local`, `docker`, and `daytona` are bundled with the server; any other value names a sandbox-driver plugin configured under `server.sandbox.providers.<kind>`.
*/
'provider': string;
'id': string;
}

View file

@ -1,27 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Desired environment provider.
*/
export const EnvironmentProvider = {
LOCAL: 'local',
DOCKER: 'docker',
DAYTONA: 'daytona'
} as const;
export type EnvironmentProvider = typeof EnvironmentProvider[keyof typeof EnvironmentProvider];

View file

@ -24,13 +24,13 @@ import type { EnvironmentLifecycleSettings } from './environment-lifecycle-setti
import type { EnvironmentNetworkSettings } from './environment-network-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentProvider } from './environment-provider';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentResourcesSettings } from './environment-resources-settings';
export interface EnvironmentSettings {
'provider': EnvironmentProvider;
/**
* Sandbox provider kind. `local`, `docker`, and `daytona` are bundled with the server; any other value names a sandbox-driver plugin configured under `server.sandbox.providers.<kind>`.
*/
'provider': string;
/**
* Local-provider command working directory for this environment. Docker and Daytona ignore this value.
*/

View file

@ -24,9 +24,6 @@ import type { EnvironmentLifecycleSettings } from './environment-lifecycle-setti
import type { EnvironmentNetworkSettings } from './environment-network-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentProvider } from './environment-provider';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentResourcesSettings } from './environment-resources-settings';
/**
@ -38,7 +35,10 @@ export interface Environment {
* Stable revision used with `If-Match` for optimistic concurrency.
*/
'revision': string;
'provider': EnvironmentProvider;
/**
* Sandbox provider kind. `local`, `docker`, and `daytona` are bundled with the server; any other value names a sandbox-driver plugin configured under `server.sandbox.providers.<kind>`.
*/
'provider': string;
/**
* Local-provider command working directory for this environment. Docker and Daytona ignore this value.
*/

View file

@ -106,7 +106,6 @@ export * from './environment-list-meta';
export * from './environment-list-response';
export * from './environment-network-mode';
export * from './environment-network-settings';
export * from './environment-provider';
export * from './environment-resources-settings';
export * from './environment-settings';
export * from './error-response';
@ -404,7 +403,7 @@ export * from './sandbox-list-response';
export * from './sandbox-network';
export * from './sandbox-network-policy';
export * from './sandbox-network-policy-mode';
export * from './sandbox-provider-kind';
export * from './sandbox-plugin-settings';
export * from './sandbox-provider-lookup-error';
export * from './sandbox-resources';
export * from './sandbox-service';
@ -430,7 +429,6 @@ export * from './server-listen-unix-settings';
export * from './server-logging-settings';
export * from './server-namespace';
export * from './server-sandbox-provider-settings';
export * from './server-sandbox-providers-settings';
export * from './server-sandbox-settings';
export * from './server-scheduler-settings';
export * from './server-settings';

View file

@ -24,16 +24,16 @@ import type { EnvironmentLifecycleSettings } from './environment-lifecycle-setti
import type { EnvironmentNetworkSettings } from './environment-network-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentProvider } from './environment-provider';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentResourcesSettings } from './environment-resources-settings';
/**
* Request body for replacing a server-managed environment. The path id is authoritative.
*/
export interface ReplaceEnvironmentRequest {
'provider': EnvironmentProvider;
/**
* Sandbox provider kind. `local`, `docker`, and `daytona` are bundled with the server; any other value names a sandbox-driver plugin configured under `server.sandbox.providers.<kind>`.
*/
'provider': string;
/**
* Local-provider command working directory for this environment. Docker and Daytona ignore this value.
*/

View file

@ -24,14 +24,14 @@ import type { EnvironmentLifecycleSettings } from './environment-lifecycle-setti
import type { EnvironmentNetworkSettings } from './environment-network-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentProvider } from './environment-provider';
// May contain unused imports in some cases
// @ts-ignore
import type { EnvironmentResourcesSettings } from './environment-resources-settings';
export interface RunEnvironmentSettings {
'id': string;
'provider': EnvironmentProvider;
/**
* Sandbox provider kind. `local`, `docker`, and `daytona` are bundled with the server; any other value names a sandbox-driver plugin configured under `server.sandbox.providers.<kind>`.
*/
'provider': string;
/**
* Local-provider command working directory for this environment. Docker and Daytona ignore this value.
*/

View file

@ -16,15 +16,15 @@
// May contain unused imports in some cases
// @ts-ignore
import type { RunSandboxRuntime } from './run-sandbox-runtime';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxProviderKind } from './sandbox-provider-kind';
/**
* Initialized sandbox provider and runtime metadata.
*/
export interface RunSandboxInstance {
'provider': SandboxProviderKind;
/**
* Sandbox provider kind. `local`, `docker`, and `daytona` are bundled with the server; any other value names a sandbox-driver plugin configured under `server.sandbox.providers.<kind>`.
*/
'provider': string;
'image'?: string | null;
'snapshot'?: string | null;
'runtime': RunSandboxRuntime;

View file

@ -13,15 +13,15 @@
*/
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxProviderKind } from './sandbox-provider-kind';
/**
* Requested sandbox provider and base image/snapshot from run settings.
*/
export interface RunSandboxPlan {
'provider': SandboxProviderKind;
/**
* Sandbox provider kind. `local`, `docker`, and `daytona` are bundled with the server; any other value names a sandbox-driver plugin configured under `server.sandbox.providers.<kind>`.
*/
'provider': string;
'image'?: string | null;
'snapshot'?: string | null;
}

View file

@ -18,9 +18,6 @@
import type { SandboxNetwork } from './sandbox-network';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxProviderKind } from './sandbox-provider-kind';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxResources } from './sandbox-resources';
// May contain unused imports in some cases
// @ts-ignore
@ -33,7 +30,10 @@ import type { SandboxTimestamps } from './sandbox-timestamps';
* Provider-backed inventory record for a Fabro-managed sandbox.
*/
export interface SandboxInfo {
'provider': SandboxProviderKind;
/**
* Sandbox provider kind. `local`, `docker`, and `daytona` are bundled with the server; any other value names a sandbox-driver plugin configured under `server.sandbox.providers.<kind>`.
*/
'provider': string;
/**
* Provider-native sandbox id.
*/

View file

@ -0,0 +1,36 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* How the server launches a sandbox-driver plugin executable.
*/
export interface SandboxPluginSettings {
/**
* Executable path. Absent means `fabro-sandbox-<kind>` on `PATH`.
*/
'path'?: string;
/**
* Pinned SHA-256 of the executable, hex.
*/
'sha256'?: string;
/**
* Allow launching without a checksum.
*/
'dev'?: boolean;
'args'?: Array<string>;
'env'?: { [key: string]: string; };
'inherit_env'?: Array<string>;
}

View file

@ -1,27 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Sandbox provider discriminator.
*/
export const SandboxProviderKind = {
LOCAL: 'local',
DOCKER: 'docker',
DAYTONA: 'daytona'
} as const;
export type SandboxProviderKind = typeof SandboxProviderKind[keyof typeof SandboxProviderKind];

View file

@ -13,14 +13,14 @@
*/
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxProviderKind } from './sandbox-provider-kind';
/**
* Provider error captured during fail-soft sandbox inventory lookup.
*/
export interface SandboxProviderLookupError {
'provider': SandboxProviderKind;
/**
* Sandbox provider kind. `local`, `docker`, and `daytona` are bundled with the server; any other value names a sandbox-driver plugin configured under `server.sandbox.providers.<kind>`.
*/
'provider': string;
'message': string;
}

View file

@ -13,7 +13,11 @@
*/
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxPluginSettings } from './sandbox-plugin-settings';
export interface ServerSandboxProviderSettings {
'enabled': boolean;
'plugin'?: SandboxPluginSettings;
}

View file

@ -1,24 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.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 { ServerSandboxProviderSettings } from './server-sandbox-provider-settings';
export interface ServerSandboxProvidersSettings {
'local': ServerSandboxProviderSettings;
'docker': ServerSandboxProviderSettings;
'daytona': ServerSandboxProviderSettings;
}

View file

@ -15,8 +15,11 @@
// May contain unused imports in some cases
// @ts-ignore
import type { ServerSandboxProvidersSettings } from './server-sandbox-providers-settings';
import type { ServerSandboxProviderSettings } from './server-sandbox-provider-settings';
export interface ServerSandboxSettings {
'providers': ServerSandboxProvidersSettings;
/**
* Sandbox provider policy keyed by provider kind. The bundled kinds (`local`, `docker`, `daytona`) are always present; any other key names a sandbox-driver plugin and carries its launch settings.
*/
'providers': { [key: string]: ServerSandboxProviderSettings; };
}