mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-04 02:33:56 +00:00
fabro(01KS9BXFGAZ32SGNRE4YJV1354): implement (succeeded)
Fabro-Run: 01KS9BXFGAZ32SGNRE4YJV1354
Fabro-Completed: 7
Fabro-Checkpoint: e2b2831ff6
⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
parent
be6f0ae4b7
commit
0b562dadb6
75 changed files with 2478 additions and 1360 deletions
|
|
@ -4,18 +4,26 @@ _version = 1
|
|||
enabled = true
|
||||
draft = false
|
||||
|
||||
[run.sandbox.daytona]
|
||||
auto_stop_interval = 30
|
||||
[run.environment]
|
||||
id = "fabro-dev"
|
||||
|
||||
[run.sandbox.daytona.labels]
|
||||
[environments.fabro-dev]
|
||||
provider = "daytona"
|
||||
|
||||
[environments.fabro-dev.lifecycle]
|
||||
auto_stop = "30m"
|
||||
|
||||
[environments.fabro-dev.labels]
|
||||
repo = "fabro-sh/fabro"
|
||||
|
||||
[run.sandbox.daytona.snapshot]
|
||||
name = "fabro-v11"
|
||||
[environments.fabro-dev.image]
|
||||
ref = "fabro-v11"
|
||||
dockerfile = { path = "Dockerfile" }
|
||||
|
||||
[environments.fabro-dev.resources]
|
||||
cpu = 8
|
||||
memory = "16GB"
|
||||
disk = "20GB"
|
||||
dockerfile = { path = "Dockerfile" }
|
||||
|
||||
# [[run.hooks]]
|
||||
# id = "cargo-fmt"
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ _version = 1
|
|||
[workflow]
|
||||
graph = "workflow.fabro"
|
||||
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "daytona-medium"
|
||||
|
||||
[environments.daytona-medium]
|
||||
provider = "daytona"
|
||||
|
||||
[run.sandbox.daytona.snapshot]
|
||||
name = "daytona-medium"
|
||||
[environments.daytona-medium.image]
|
||||
ref = "daytona-medium"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Fabro supports three sandbox providers: `local` (no isolation), `docker` (contai
|
|||
|
||||
## Network access control
|
||||
|
||||
For cloud sandboxes (Daytona), you can control outbound network access with the `network` field in `[run.sandbox.daytona]`. Three modes are available: `"allow_all"` (default), `"block"`, and `{ allow_list = ["..."] }` for CIDR-based egress filtering.
|
||||
For cloud sandboxes (Daytona), you can control outbound network access with `[environments.<slug>.network]`. Three modes are available: `"allow_all"` (default), `"block"`, and `"cidr_allow_list"` with an `allow = ["..."]` CIDR list.
|
||||
|
||||
Server defaults in `settings.toml` apply when a run config doesn't specify `network`. Individual run configs can override the server default.
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ Fabro only reads `settings.toml`. Older `server.toml`, `user.toml`, and `cli.tom
|
|||
| Scope | Examples |
|
||||
|---|---|
|
||||
| Server-owned (runtime-only from local `settings.toml`) | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.storage]`, `[server.artifacts]`, `[server.slatedb]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` |
|
||||
| Shared run defaults (layered through `.fabro/project.toml`/`workflow.toml`) | `[run.model]`, `[run.prepare]`, `[run.sandbox]`, `[run.checkpoint]`, `[run.inputs]`, `[run.pull_request]`, `[run.git]`, `[run.hooks]`, `[run.agent]` |
|
||||
| Shared run defaults (layered through `.fabro/project.toml`/`workflow.toml`) | `[run.model]`, `[run.prepare]`, `[run.environment]`, `[environments.<slug>]`, `[run.checkpoint]`, `[run.inputs]`, `[run.pull_request]`, `[run.git]`, `[run.hooks]`, `[run.agent]` |
|
||||
|
||||
The CLI-only `[cli.*]` sections (including `[cli.target]`) belong in the client machine's `settings.toml`. They tell CLI commands how to reach a server. The server process does not read `[cli.*]` for its own binding or routing.
|
||||
|
||||
|
|
@ -88,13 +88,16 @@ fallbacks = ["gemini", "openai"]
|
|||
[[run.prepare.steps]]
|
||||
script = "npm install"
|
||||
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "cloud"
|
||||
|
||||
[environments.cloud]
|
||||
provider = "daytona"
|
||||
|
||||
[run.sandbox.daytona]
|
||||
auto_stop_interval = 60
|
||||
[environments.cloud.lifecycle]
|
||||
auto_stop = "60m"
|
||||
|
||||
[run.sandbox.daytona.labels]
|
||||
[environments.cloud.labels]
|
||||
team = "platform"
|
||||
|
||||
[run.checkpoint]
|
||||
|
|
@ -121,7 +124,7 @@ Several `settings.toml` settings can be overridden via `fabro server start` flag
|
|||
| `--foreground` | — | Run in the foreground instead of daemonizing |
|
||||
| `--model` | — | Override default LLM model |
|
||||
| `--provider` | — | Override default LLM provider |
|
||||
| `--sandbox` | — | Override default sandbox provider |
|
||||
| `--environment` | — | Override default environment slug |
|
||||
| `--max-concurrent-runs` | `5` | Maximum concurrent run executions |
|
||||
| `--config` | `~/.fabro/settings.toml` | Path to server config file |
|
||||
|
||||
|
|
@ -228,7 +231,7 @@ On a same-machine setup, `settings.toml` is the shared machine-default layer und
|
|||
|
||||
On a remote setup, the client bundles workflow, project, and user config into the run manifest. The server then layers those bundled client configs over its own local defaults for run-shaped fields. Server-owned values like `[server.storage]`, `[server.api]`, `[server.web]`, and `[server.scheduler]` always come from the server machine's own `settings.toml` or `fabro server start` flags.
|
||||
|
||||
Merge rules follow the normative matrix: TOML `[run.inputs]` tables replace wholesale, CLI `-I` / `--input` values replayed from run manifests merge per key at highest precedence, `[run.sandbox.env]` and `[run.sandbox.daytona.labels]` merge by key, `[run.prepare.steps]` replaces whole-list, and `[[run.hooks]]` merge by optional `id`. Most other fields use "higher-precedence wins" field-wise merging.
|
||||
Merge rules follow the normative matrix: TOML `[run.inputs]` tables replace wholesale, CLI `-I` / `--input` values replayed from run manifests merge per key at highest precedence, environment `env` and `labels` merge by key, environment `volumes` replace as a whole list, `[run.prepare.steps]` replaces whole-list, and `[[run.hooks]]` merge by optional `id`. Most other fields use "higher-precedence wins" field-wise merging.
|
||||
|
||||
### `[server.logging]` section
|
||||
|
||||
|
|
|
|||
|
|
@ -230,11 +230,14 @@ graph = "workflow.fabro"
|
|||
[run]
|
||||
goal = "Test the login page"
|
||||
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "cloud"
|
||||
|
||||
[environments.cloud]
|
||||
provider = "daytona"
|
||||
|
||||
[run.sandbox.daytona.snapshot]
|
||||
name = "daytona-medium"
|
||||
[environments.cloud.image]
|
||||
ref = "daytona-medium"
|
||||
|
||||
[run.artifacts]
|
||||
include = ["screenshots/**"]
|
||||
|
|
|
|||
|
|
@ -6572,11 +6572,12 @@ components:
|
|||
type: string
|
||||
provider:
|
||||
type: string
|
||||
sandbox:
|
||||
environment:
|
||||
type: string
|
||||
description: Named environment slug to select for the run.
|
||||
docker_image:
|
||||
type: string
|
||||
description: Per-run Docker sandbox image override.
|
||||
description: Per-run environment image override.
|
||||
verbose:
|
||||
type: boolean
|
||||
dry_run:
|
||||
|
|
@ -10400,12 +10401,17 @@ components:
|
|||
required:
|
||||
- project
|
||||
- workflow
|
||||
- environments
|
||||
- run
|
||||
properties:
|
||||
project:
|
||||
$ref: "#/components/schemas/ProjectNamespace"
|
||||
workflow:
|
||||
$ref: "#/components/schemas/WorkflowNamespace"
|
||||
environments:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/EnvironmentSettings"
|
||||
run:
|
||||
$ref: "#/components/schemas/RunNamespace"
|
||||
|
||||
|
|
@ -10460,7 +10466,7 @@ components:
|
|||
- clone
|
||||
- run_branch
|
||||
- meta_branch
|
||||
- sandbox
|
||||
- environment
|
||||
- notifications
|
||||
- interviews
|
||||
- agent
|
||||
|
|
@ -10500,8 +10506,8 @@ components:
|
|||
$ref: "#/components/schemas/RunBranchSettings"
|
||||
meta_branch:
|
||||
$ref: "#/components/schemas/RunMetaBranchSettings"
|
||||
sandbox:
|
||||
$ref: "#/components/schemas/RunSandboxSettings"
|
||||
environment:
|
||||
$ref: "#/components/schemas/RunEnvironmentSettings"
|
||||
notifications:
|
||||
type: object
|
||||
additionalProperties:
|
||||
|
|
@ -10670,88 +10676,123 @@ components:
|
|||
push:
|
||||
type: boolean
|
||||
|
||||
RunSandboxSettings:
|
||||
RunEnvironmentSettings:
|
||||
type: object
|
||||
required: [provider, preserve, stop_on_terminal, devcontainer, env, docker, daytona]
|
||||
required: [id, provider, image, resources, network, lifecycle, labels, volumes, env]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
provider:
|
||||
$ref: "#/components/schemas/SandboxProvider"
|
||||
preserve:
|
||||
type: boolean
|
||||
stop_on_terminal:
|
||||
type: boolean
|
||||
devcontainer:
|
||||
type: boolean
|
||||
$ref: "#/components/schemas/EnvironmentProvider"
|
||||
image:
|
||||
$ref: "#/components/schemas/EnvironmentImageSettings"
|
||||
resources:
|
||||
$ref: "#/components/schemas/EnvironmentResourcesSettings"
|
||||
network:
|
||||
$ref: "#/components/schemas/EnvironmentNetworkSettings"
|
||||
lifecycle:
|
||||
$ref: "#/components/schemas/EnvironmentLifecycleSettings"
|
||||
labels:
|
||||
$ref: "#/components/schemas/StringMap"
|
||||
volumes:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/EnvironmentVolumeSettings"
|
||||
env:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/InterpString"
|
||||
docker:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/DockerSettings"
|
||||
- type: "null"
|
||||
daytona:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/DaytonaSettings"
|
||||
- type: "null"
|
||||
|
||||
DockerSettings:
|
||||
EnvironmentSettings:
|
||||
type: object
|
||||
required: [image, network_mode, memory_limit, cpu_quota, env_vars]
|
||||
required: [provider, image, resources, network, lifecycle, labels, volumes, env]
|
||||
properties:
|
||||
provider:
|
||||
$ref: "#/components/schemas/EnvironmentProvider"
|
||||
image:
|
||||
type: string
|
||||
network_mode:
|
||||
type: ["string", "null"]
|
||||
memory_limit:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
cpu_quota:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
env_vars:
|
||||
$ref: "#/components/schemas/EnvironmentImageSettings"
|
||||
resources:
|
||||
$ref: "#/components/schemas/EnvironmentResourcesSettings"
|
||||
network:
|
||||
$ref: "#/components/schemas/EnvironmentNetworkSettings"
|
||||
lifecycle:
|
||||
$ref: "#/components/schemas/EnvironmentLifecycleSettings"
|
||||
labels:
|
||||
$ref: "#/components/schemas/StringMap"
|
||||
volumes:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/EnvironmentVolumeSettings"
|
||||
env:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/InterpString"
|
||||
|
||||
DaytonaSettings:
|
||||
type: object
|
||||
required: [auto_stop_interval, labels, snapshot, network]
|
||||
properties:
|
||||
auto_stop_interval:
|
||||
type: ["integer", "null"]
|
||||
format: int32
|
||||
labels:
|
||||
$ref: "#/components/schemas/StringMap"
|
||||
snapshot:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/DaytonaSnapshotSettings"
|
||||
- type: "null"
|
||||
network:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/DaytonaNetworkLayer"
|
||||
- type: "null"
|
||||
EnvironmentProvider:
|
||||
description: Desired environment provider.
|
||||
type: string
|
||||
enum: [local, docker, daytona]
|
||||
|
||||
DaytonaSnapshotSettings:
|
||||
EnvironmentImageSettings:
|
||||
type: object
|
||||
required: [name, cpu, memory_gb, disk_gb, dockerfile]
|
||||
required: [ref, dockerfile]
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
cpu:
|
||||
type: ["integer", "null"]
|
||||
format: int32
|
||||
memory_gb:
|
||||
type: ["integer", "null"]
|
||||
format: int32
|
||||
disk_gb:
|
||||
type: ["integer", "null"]
|
||||
format: int32
|
||||
ref:
|
||||
type: ["string", "null"]
|
||||
dockerfile:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/DockerfileSource"
|
||||
- type: "null"
|
||||
|
||||
EnvironmentResourcesSettings:
|
||||
type: object
|
||||
required: [cpu, memory, disk]
|
||||
properties:
|
||||
cpu:
|
||||
type: ["integer", "null"]
|
||||
format: int32
|
||||
memory:
|
||||
type: ["string", "null"]
|
||||
disk:
|
||||
type: ["string", "null"]
|
||||
|
||||
EnvironmentNetworkSettings:
|
||||
type: object
|
||||
required: [mode, allow]
|
||||
properties:
|
||||
mode:
|
||||
$ref: "#/components/schemas/EnvironmentNetworkMode"
|
||||
allow:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
EnvironmentNetworkMode:
|
||||
type: string
|
||||
enum: [allow_all, block, cidr_allow_list]
|
||||
|
||||
EnvironmentLifecycleSettings:
|
||||
type: object
|
||||
required: [preserve, stop_on_terminal, auto_stop]
|
||||
properties:
|
||||
preserve:
|
||||
type: boolean
|
||||
stop_on_terminal:
|
||||
type: boolean
|
||||
auto_stop:
|
||||
type: ["string", "null"]
|
||||
|
||||
EnvironmentVolumeSettings:
|
||||
type: object
|
||||
required: [id, mount_path, subpath]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
mount_path:
|
||||
type: string
|
||||
subpath:
|
||||
type: ["string", "null"]
|
||||
|
||||
DockerfileSource:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/DockerfileSourceInline"
|
||||
|
|
@ -10777,23 +10818,6 @@ components:
|
|||
path:
|
||||
type: string
|
||||
|
||||
DaytonaNetworkLayer:
|
||||
description: Daytona network access policy.
|
||||
oneOf:
|
||||
- type: string
|
||||
enum: [block, allow_all]
|
||||
- type: object
|
||||
required: [allow_list]
|
||||
properties:
|
||||
allow_list:
|
||||
type: object
|
||||
required: [allow_list]
|
||||
properties:
|
||||
allow_list:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
NotificationRouteSettings:
|
||||
type: object
|
||||
required: [enabled, provider, events, slack]
|
||||
|
|
@ -11651,4 +11675,4 @@ components:
|
|||
login:
|
||||
type: string
|
||||
description: User's login identifier (e.g. GitHub username).
|
||||
example: octocat
|
||||
example: octocat
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ Node handlers execute tools (bash commands, file edits) inside a **sandbox**. Fa
|
|||
| `local` | Tools run directly on the host machine |
|
||||
| `daytona` | Tools run in a cloud VM with SSH access |
|
||||
|
||||
The sandbox is configured per-run via CLI flags (`--sandbox docker`) or the run config TOML. See [Environments](/execution/environments) for details on each provider.
|
||||
Runs select a named environment via CLI flags (`--environment ci`) or run config TOML (`[run.environment] id = "ci"`). Fabro then creates a concrete sandbox from that environment. See [Environments](/execution/environments) for details.
|
||||
|
||||
## Events and observability
|
||||
|
||||
|
|
|
|||
|
|
@ -271,15 +271,20 @@ fallbacks = ["openai", "gemini"]
|
|||
[[run.prepare.steps]]
|
||||
script = "python3 -m venv .venv && . .venv/bin/activate && pip install pytest curses"
|
||||
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "python-dev"
|
||||
|
||||
[environments.python-dev]
|
||||
provider = "daytona"
|
||||
|
||||
[run.sandbox.daytona.snapshot]
|
||||
name = "python-dev"
|
||||
[environments.python-dev.image]
|
||||
ref = "python-dev"
|
||||
dockerfile = "FROM python:3.12-slim\nRUN apt-get update && apt-get install -y git libncurses-dev"
|
||||
|
||||
[environments.python-dev.resources]
|
||||
cpu = 4
|
||||
memory = "8GB"
|
||||
disk = "20GB"
|
||||
dockerfile = "FROM python:3.12-slim\nRUN apt-get update && apt-get install -y git libncurses-dev"
|
||||
```
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -1,45 +1,33 @@
|
|||
---
|
||||
title: "Devcontainers"
|
||||
description: "Run Fabro workflows inside development containers"
|
||||
description: "Using repository devcontainer metadata with Fabro environments"
|
||||
---
|
||||
|
||||
Fabro can use your project's [devcontainer](https://containers.dev/) configuration to set up sandbox environments. When enabled, Fabro resolves `devcontainer.json` from the repository, uses its Dockerfile to build the Daytona sandbox snapshot, runs lifecycle hooks inside the sandbox, and merges devcontainer environment variables into the sandbox environment.
|
||||
Named environments are the supported configuration surface for run execution. Define reusable environments under `[environments.<slug>]` and select one with `[run.environment] id = "..."`.
|
||||
|
||||
## Enabling devcontainer support
|
||||
Devcontainer-specific run configuration (`[run.sandbox] devcontainer = true`) has been removed with the named environments configuration break. To use a devcontainer-style image today, build or reference it through an environment image:
|
||||
|
||||
Set `devcontainer = true` in the `[run.sandbox]` section of your run config:
|
||||
```toml
|
||||
[run.environment]
|
||||
id = "dev"
|
||||
|
||||
```toml title="run.toml"
|
||||
_version = 1
|
||||
[environments.dev]
|
||||
provider = "docker"
|
||||
|
||||
[workflow]
|
||||
graph = "workflow.fabro"
|
||||
|
||||
[run.sandbox]
|
||||
provider = "daytona"
|
||||
devcontainer = true
|
||||
[environments.dev.image]
|
||||
ref = "ghcr.io/acme/project-devcontainer:latest"
|
||||
```
|
||||
|
||||
Fabro looks for `.devcontainer/devcontainer.json` in the repository root. If found, it extracts the Dockerfile, lifecycle commands, and environment variables from the configuration.
|
||||
For Daytona snapshot creation, provide a Dockerfile path on the selected environment:
|
||||
|
||||
## What Fabro uses from devcontainer.json
|
||||
```toml
|
||||
[run.environment]
|
||||
id = "cloud-dev"
|
||||
|
||||
| Field | How Fabro uses it |
|
||||
|---|---|
|
||||
| `build.dockerfile` | Used as the Dockerfile for the Daytona sandbox snapshot. A deterministic snapshot name is generated from a hash of the Dockerfile content. |
|
||||
| `onCreateCommand` | Runs inside the sandbox after it's created |
|
||||
| `postCreateCommand` | Runs after `onCreateCommand` completes |
|
||||
| `postStartCommand` | Runs after the sandbox starts |
|
||||
| `containerEnv` | Merged into sandbox environment variables (TOML `[run.sandbox.env]` values take precedence on key collisions) |
|
||||
[environments.cloud-dev]
|
||||
provider = "daytona"
|
||||
|
||||
Lifecycle commands (`onCreateCommand`, `postCreateCommand`, `postStartCommand`) execute sequentially inside the sandbox. If any command fails, the run aborts before the workflow starts.
|
||||
|
||||
## Dockerfile limitations
|
||||
|
||||
Fabro detects and reports unsupported `COPY` and `ADD` instructions in devcontainer base Dockerfiles. These instructions reference files from the build context, which isn't available when building Daytona snapshots. If your Dockerfile uses `COPY` or `ADD`, you'll need to restructure it to use `RUN` commands that fetch files at build time (e.g., via `curl` or `wget`).
|
||||
|
||||
## Interaction with other sandbox settings
|
||||
|
||||
When `devcontainer = true`, the devcontainer Dockerfile overrides any `snapshot.dockerfile` setting in `[run.sandbox.daytona]`. Other Daytona settings (`cpu`, `memory`, `disk`, `auto_stop_interval`, `labels`) still apply.
|
||||
|
||||
Environment variables from `containerEnv` in the devcontainer config are merged with `[run.sandbox.env]` from the TOML config. On key collisions, the TOML config wins.
|
||||
[environments.cloud-dev.image]
|
||||
ref = "project-dev"
|
||||
dockerfile = { path = ".devcontainer/Dockerfile" }
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,265 +1,197 @@
|
|||
---
|
||||
title: "Environments"
|
||||
description: "Sandbox providers for workflow execution"
|
||||
description: "Reusable named execution environments for workflow runs"
|
||||
---
|
||||
|
||||
When an agent runs a shell command, edits a file, or searches code, it does so inside a **sandbox**. The sandbox is the execution environment for all tool operations — it controls where commands run, which files are visible, and how much isolation exists between the agent and the host.
|
||||
Fabro separates **environments** from **sandboxes**:
|
||||
|
||||
Fabro supports three sandbox providers. Each one implements the same interface (file I/O, command execution, grep, glob), so workflows run identically regardless of which provider you choose. The difference is in where and how the tools execute.
|
||||
- An **environment** is reusable desired configuration: provider, image, resources, network, lifecycle, labels, volumes, and environment variables.
|
||||
- A **sandbox** is the concrete runtime instance Fabro creates for a run from the selected environment.
|
||||
|
||||
| Provider | Runs on | Use case | Status |
|
||||
|---|---|---|---|
|
||||
| `local` | Host machine | Development, trusted workflows | Available |
|
||||
| `docker` | Docker container | Reproducible environments, untrusted code | Available |
|
||||
| `daytona` | Cloud VM | CI/CD, team-shared runs, SSH debugging | Available |
|
||||
Runs select environments by slug:
|
||||
|
||||
## Choosing a provider
|
||||
|
||||
Set the sandbox provider via CLI flag, [run config TOML](/execution/run-configuration), or server defaults:
|
||||
|
||||
```bash
|
||||
# CLI flag
|
||||
fabro run workflow.fabro --sandbox local
|
||||
fabro run workflow.fabro --sandbox docker
|
||||
fabro run workflow.fabro --sandbox daytona
|
||||
```toml title="workflow.toml"
|
||||
[run.environment]
|
||||
id = "fabro-dev"
|
||||
```
|
||||
|
||||
```toml title="run.toml"
|
||||
# Run config TOML
|
||||
[run.sandbox]
|
||||
provider = "daytona"
|
||||
Environment catalogs can live in `settings.toml`, `.fabro/project.toml`, or `workflow.toml`, and merge through the normal settings precedence. The built-in default is `default`, a Docker environment using `buildpack-deps:noble`.
|
||||
|
||||
## Defining environments
|
||||
|
||||
```toml title="workflow.toml"
|
||||
[run.environment]
|
||||
id = "fabro-dev"
|
||||
|
||||
[environments.fabro-dev]
|
||||
provider = "daytona" # local | docker | daytona
|
||||
|
||||
[environments.fabro-dev.image]
|
||||
ref = "fabro-v11" # Docker image or Daytona snapshot name
|
||||
dockerfile = { path = "Dockerfile" }
|
||||
|
||||
[environments.fabro-dev.resources]
|
||||
cpu = 8
|
||||
memory = "16GB"
|
||||
disk = "20GB"
|
||||
|
||||
[environments.fabro-dev.network]
|
||||
mode = "cidr_allow_list" # allow_all | block | cidr_allow_list
|
||||
allow = ["10.0.0.0/8"]
|
||||
|
||||
[environments.fabro-dev.lifecycle]
|
||||
preserve = false
|
||||
stop_on_terminal = true
|
||||
auto_stop = "30m"
|
||||
|
||||
[environments.fabro-dev.labels]
|
||||
repo = "fabro-sh/fabro"
|
||||
|
||||
[[environments.fabro-dev.volumes]]
|
||||
id = "vol-agent-state"
|
||||
mount_path = "/home/daytona/agent-state"
|
||||
subpath = "auth"
|
||||
|
||||
[environments.fabro-dev.env]
|
||||
NODE_ENV = "development"
|
||||
```
|
||||
|
||||
The precedence order is: CLI flag > run config TOML > server defaults > built-in default (`docker`).
|
||||
Run-level overrides are sparse and apply only to the selected environment:
|
||||
|
||||
## Local
|
||||
```toml title="workflow.toml"
|
||||
[run.environment]
|
||||
id = "fabro-dev"
|
||||
|
||||
The local sandbox runs all tool operations directly on the host machine. Use it for trusted workflows, local development, or runs that must operate directly on the current working tree.
|
||||
[run.environment.resources]
|
||||
memory = "32GB"
|
||||
|
||||
### How it works
|
||||
|
||||
- **Working directory** — Set to the current directory (or `directory` from the run config). Fabro creates it if it doesn't exist.
|
||||
- **Commands** — Executed via `/bin/bash -c` in the working directory.
|
||||
- **File operations** — Read and write directly to the host filesystem. Relative paths resolve against the working directory.
|
||||
- **Cleanup** — No-op. Local sandbox doesn't create or destroy anything on cleanup.
|
||||
|
||||
### Environment variable filtering
|
||||
|
||||
The local sandbox filters sensitive environment variables before passing them to commands. Variables ending in `_API_KEY`, `_SECRET`, `_TOKEN`, `_PASSWORD`, or `_CREDENTIAL` are stripped. A safelist of common variables (`PATH`, `HOME`, `USER`, `SHELL`, `LANG`, `TERM`, `TMPDIR`, `GOPATH`, `CARGO_HOME`, `NVM_DIR`) is always passed through.
|
||||
|
||||
<Note>
|
||||
The local sandbox offers no isolation. Agents can read and modify any file on the host. Use `docker` or `daytona` when running untrusted workflows or when you need a reproducible environment.
|
||||
</Note>
|
||||
|
||||
## Docker
|
||||
|
||||
The Docker sandbox runs all tool operations inside a Docker container. Docker is the built-in default runtime provider. Docker runs use a provider-owned workspace in the container; when the run has a GitHub origin, Fabro clones that repository into the workspace.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker Engine running on the host
|
||||
- The configured image available locally, or a Docker client that can pull it
|
||||
- GitHub credentials configured when cloning private repositories
|
||||
|
||||
### How it works
|
||||
|
||||
- **Container lifecycle** — On `initialize()`, Fabro pulls the image (if needed), creates a container with `sleep infinity`, and starts it. On `cleanup()`, Fabro stops and removes the container.
|
||||
- **Working directory** — Docker runs use a provider-owned workspace inside the container. When a run has a GitHub origin, Fabro clones that repository into the workspace instead of bind-mounting the host source tree.
|
||||
- **Git clone** — Docker and Daytona accept GitHub origins for automatic cloning. If the run has a present non-GitHub origin, set `[run.clone] enabled = false` or switch to `local`.
|
||||
- **Commands** — Executed via `docker exec` with `/bin/bash -c` inside the container. Timeout and cancellation are supported.
|
||||
- **File writes** — Use the Docker API's tar upload to avoid shell escaping issues with special characters.
|
||||
- **Platform detection** — The container's `uname -r` is cached at startup.
|
||||
|
||||
### Configuration
|
||||
|
||||
Configure Docker through `[run.sandbox.docker]`:
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox]
|
||||
provider = "docker"
|
||||
|
||||
[run.sandbox.docker]
|
||||
image = "buildpack-deps:noble"
|
||||
network_mode = "bridge"
|
||||
memory_limit = "4GB"
|
||||
cpu_quota = 200000
|
||||
```
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---|---|---|
|
||||
| `image` | `buildpack-deps:noble` | Docker image to use |
|
||||
| `network_mode` | `bridge` | Docker network mode |
|
||||
| `memory_limit` | `4GB` | Memory limit |
|
||||
| `cpu_quota` | `200000` | CPU quota (microseconds per 100ms period) |
|
||||
|
||||
Set `[run.clone] enabled = false` to start with an empty provider workspace instead of cloning the run's GitHub origin. Use [prepare steps](/execution/run-configuration#runprepare) to clone or create any files the workflow needs.
|
||||
|
||||
### Preserving the container
|
||||
|
||||
By default, the container is destroyed when the run finishes. To keep it alive for debugging:
|
||||
|
||||
```bash
|
||||
fabro run workflow.fabro --sandbox docker --preserve-sandbox
|
||||
```
|
||||
|
||||
Or in the run config:
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox]
|
||||
provider = "docker"
|
||||
[run.environment.lifecycle]
|
||||
preserve = true
|
||||
```
|
||||
|
||||
When preserved, Fabro prints the container ID so you can reconnect with `docker exec -it <id> bash`.
|
||||
`env` and `labels` merge by key. `volumes` replace as a whole list when set at a higher-precedence layer.
|
||||
|
||||
## Selecting an environment from the CLI
|
||||
|
||||
Use `--environment` with an environment slug:
|
||||
|
||||
```bash
|
||||
fabro run workflow.fabro --environment fabro-dev
|
||||
fabro preflight workflow.fabro --environment ci
|
||||
fabro server start --environment default
|
||||
```
|
||||
|
||||
`--preserve-sandbox` still controls the concrete runtime instance lifecycle for a run. Runtime commands such as `fabro sandbox ssh` keep the word "sandbox" because they operate on an already-created runtime instance.
|
||||
|
||||
## Built-in default
|
||||
|
||||
```toml
|
||||
[run.environment]
|
||||
id = "default"
|
||||
|
||||
[environments.default]
|
||||
provider = "docker"
|
||||
|
||||
[environments.default.image]
|
||||
ref = "buildpack-deps:noble"
|
||||
|
||||
[environments.default.resources]
|
||||
cpu = 2
|
||||
memory = "4GB"
|
||||
|
||||
[environments.default.lifecycle]
|
||||
preserve = false
|
||||
stop_on_terminal = true
|
||||
```
|
||||
|
||||
## Provider mappings
|
||||
|
||||
| Environment field | Local | Docker | Daytona |
|
||||
|---|---|---|---|
|
||||
| `image.ref` | Ignored | Docker image | Snapshot name |
|
||||
| `image.dockerfile` | Ignored | Warning; ignored | Snapshot Dockerfile; requires `image.ref` |
|
||||
| `resources.cpu` | Warning; ignored | `cpu_quota = cpu * 100000` | Snapshot CPU |
|
||||
| `resources.memory` | Warning; ignored | Container memory limit | Snapshot memory |
|
||||
| `resources.disk` | Warning; ignored | Warning; ignored | Snapshot disk |
|
||||
| `network.mode = "allow_all"` | Host network | Docker default bridge | Daytona allow-all |
|
||||
| `network.mode = "block"` | Error | Docker `none` network | Daytona block |
|
||||
| `network.mode = "cidr_allow_list"` | Error | Error | Daytona CIDR allow-list |
|
||||
| `labels` | Warning; ignored | Warning; ignored | Daytona labels |
|
||||
| `volumes` | Warning; ignored | Warning; ignored | Daytona volume mounts |
|
||||
| `lifecycle.auto_stop` | Warning; ignored | Warning; ignored | Daytona auto-stop |
|
||||
| `env` | Process environment overlay | Container environment | Sandbox environment |
|
||||
|
||||
## Local
|
||||
|
||||
`local` runs tools directly in the resolved working directory. It offers no filesystem or network isolation, so use it only for trusted workflows.
|
||||
|
||||
```toml
|
||||
[run.environment]
|
||||
id = "host"
|
||||
|
||||
[environments.host]
|
||||
provider = "local"
|
||||
```
|
||||
|
||||
Fabro hard-errors if a local environment asks for blocked or CIDR-restricted networking because the provider cannot enforce it.
|
||||
|
||||
## Docker
|
||||
|
||||
Docker runs tools inside a container created from `image.ref`. Docker is the built-in default provider.
|
||||
|
||||
```toml
|
||||
[run.environment]
|
||||
id = "ci"
|
||||
|
||||
[environments.ci]
|
||||
provider = "docker"
|
||||
|
||||
[environments.ci.image]
|
||||
ref = "buildpack-deps:noble"
|
||||
|
||||
[environments.ci.resources]
|
||||
cpu = 2
|
||||
memory = "4GB"
|
||||
|
||||
[environments.ci.network]
|
||||
mode = "block"
|
||||
```
|
||||
|
||||
Docker and Daytona are clone-based providers. When a run has a GitHub origin, Fabro clones it into the provider workspace. Set `[run.clone] enabled = false` to start with an empty workspace.
|
||||
|
||||
## Daytona
|
||||
|
||||
The Daytona sandbox runs all tool operations inside a cloud-hosted VM managed by [Daytona](https://daytona.io). It provides full machine-level isolation, automatic git cloning, and SSH access for debugging.
|
||||
Daytona runs tools in a cloud sandbox. `image.ref` is the snapshot name. If `image.dockerfile` is set and the snapshot does not exist, Fabro creates the snapshot; `image.ref` is required so the snapshot has a name.
|
||||
|
||||
### Prerequisites
|
||||
```toml
|
||||
[run.environment]
|
||||
id = "cloud"
|
||||
|
||||
- A `DAYTONA_API_KEY` environment variable
|
||||
- GitHub access configured via `fabro install` or `gh auth login` (for private repository cloning)
|
||||
|
||||
The Daytona API key must include `write:snapshots`, `delete:snapshots`, `write:sandboxes`, and `delete:sandboxes`. `fabro install`, `fabro secret set DAYTONA_API_KEY`, and `fabro doctor` validate these scopes before the first run reaches sandbox creation.
|
||||
|
||||
### How it works
|
||||
|
||||
- **Sandbox lifecycle** — On `initialize()`, Fabro creates a Daytona sandbox (from an image or a snapshot), clones the run's GitHub origin into it when one is available, and waits until it's ready. On `cleanup()`, the sandbox is deleted.
|
||||
- **Working directory** — Fixed at `/home/daytona/workspace`. GitHub-origin runs clone the repository there automatically.
|
||||
- **Git clone** — Fabro detects the run manifest's GitHub origin URL and branch, converts SSH URLs to HTTPS, and clones into the sandbox. For private repositories, Fabro uses a GitHub App Installation Access Token scoped to the specific repository. Public repositories are cloned without credentials. Set `[run.clone] enabled = false` to create an empty workspace instead.
|
||||
- **Commands** — Executed via the Daytona process API. Commands are base64-encoded and piped through `sh` to support pipes, environment variables, and other shell features.
|
||||
- **Ephemeral** — Sandboxes are created with `ephemeral: true` and a unique timestamped name (e.g. `fabro-20260305-142301-a3f2`).
|
||||
|
||||
### Snapshots
|
||||
|
||||
Snapshots let you pre-build an environment image so each run starts with dependencies already installed. If the named snapshot doesn't exist and a `dockerfile` is provided, Fabro creates it automatically and polls until it's ready (up to 10 minutes).
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox]
|
||||
[environments.cloud]
|
||||
provider = "daytona"
|
||||
|
||||
[run.sandbox.daytona]
|
||||
auto_stop_interval = 60
|
||||
[environments.cloud.image]
|
||||
ref = "rust-dev"
|
||||
dockerfile = { path = "Dockerfile" }
|
||||
|
||||
[run.sandbox.daytona.snapshot]
|
||||
name = "rust-dev"
|
||||
[environments.cloud.resources]
|
||||
cpu = 4
|
||||
memory = "8GB"
|
||||
disk = "20GB"
|
||||
dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update && apt-get install -y git ripgrep"
|
||||
# Or reference a Dockerfile beside this TOML file:
|
||||
# dockerfile = { path = "./Dockerfile" }
|
||||
|
||||
[environments.cloud.lifecycle]
|
||||
auto_stop = "30m"
|
||||
|
||||
[environments.cloud.network]
|
||||
mode = "cidr_allow_list"
|
||||
allow = ["208.80.154.232/32", "10.0.0.0/8"]
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `name` | Snapshot identifier. Reused across runs if it already exists. |
|
||||
| `cpu` | CPU cores for the snapshot VM. |
|
||||
| `memory` | Memory in GB. |
|
||||
| `disk` | Disk in GB. |
|
||||
| `dockerfile` | Dockerfile content or `{ path = "..." }` reference for building the snapshot. Required when creating a new snapshot. Paths resolve relative to the TOML file that declares them. |
|
||||
Daytona volumes reference existing provider-managed volumes:
|
||||
|
||||
If the snapshot already exists and is in `Active` state, Fabro uses it directly. If it's in `Building` or `Pending` state, Fabro polls with exponential backoff until it's ready.
|
||||
|
||||
### Volumes
|
||||
|
||||
Mount existing Daytona volumes into each sandbox at creation time:
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox]
|
||||
provider = "daytona"
|
||||
|
||||
[[run.sandbox.daytona.volumes]]
|
||||
volume_id = "vol-agent-state"
|
||||
```toml
|
||||
[[environments.cloud.volumes]]
|
||||
id = "vol-agent-state"
|
||||
mount_path = "/home/daytona/agent-state"
|
||||
subpath = "agent-auth"
|
||||
```
|
||||
|
||||
Fabro does not create or manage Daytona volumes. Create the volume in Daytona first, then reference its `volume_id` from the run config. Use `subpath` when a shared volume should expose only one prefix to the sandbox.
|
||||
|
||||
### Labels
|
||||
|
||||
Attach key-value labels to sandboxes for filtering and identification in the Daytona dashboard:
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox.daytona.labels]
|
||||
project = "fabro"
|
||||
env = "ci"
|
||||
team = "platform"
|
||||
```
|
||||
|
||||
When using server defaults, labels are merged — run config labels override default labels on key collisions.
|
||||
|
||||
### SSH access
|
||||
|
||||
Connect to a running Daytona sandbox via SSH for live debugging:
|
||||
|
||||
```bash
|
||||
fabro sandbox ssh <run-id>
|
||||
```
|
||||
|
||||
This creates temporary SSH credentials (valid for 60 minutes) and connects directly.
|
||||
|
||||
### Preserving the sandbox
|
||||
|
||||
Like Docker, Daytona sandboxes are destroyed on cleanup by default. Use `--preserve-sandbox` to keep them alive:
|
||||
|
||||
```bash
|
||||
fabro run workflow.fabro --sandbox daytona --preserve-sandbox
|
||||
```
|
||||
|
||||
Fabro prints the sandbox name so you can find it in the [Daytona dashboard](https://app.daytona.io/dashboard/sandboxes).
|
||||
|
||||
### Auto-stop
|
||||
|
||||
The `auto_stop_interval` setting (in minutes) tells Daytona to stop the sandbox after a period of inactivity. This saves costs for long-running sandboxes that may sit idle:
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox.daytona]
|
||||
auto_stop_interval = 30
|
||||
```
|
||||
|
||||
## Sandboxing
|
||||
|
||||
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 and providing a reproducible environment for each run.
|
||||
|
||||
### Filesystem
|
||||
|
||||
Each provider offers a different level of filesystem isolation:
|
||||
|
||||
| Provider | Isolation | What agents can access |
|
||||
|---|---|---|
|
||||
| `local` | None | The entire host filesystem. Agents can read and modify any file. |
|
||||
| `docker` | Container-level | The provider-owned workspace (`/workspace` by default) and whatever is in the container image. Host files are not bind-mounted into the container. |
|
||||
| `daytona` | Full machine | A cloud VM with the repository cloned into `/home/daytona/workspace`. The host filesystem is completely inaccessible. |
|
||||
|
||||
For `local`, Fabro filters sensitive environment variables (those ending in `_API_KEY`, `_SECRET`, `_TOKEN`, `_PASSWORD`, or `_CREDENTIAL`) but does not restrict file access. Use `docker` or `daytona` when running untrusted workflows.
|
||||
|
||||
### Network
|
||||
|
||||
Each provider handles outbound network access differently:
|
||||
|
||||
| Provider | Default | Controls |
|
||||
|---|---|---|
|
||||
| `local` | Full access | No network isolation. Agents have the same network access as the host. |
|
||||
| `docker` | Bridge network | Set via the `network_mode` config option. Supports all Docker network modes (`bridge`, `none`, `host`, etc.). |
|
||||
| `daytona` | Full access | Configurable via the `network` setting with three modes: `"allow_all"`, `"block"`, or CIDR-based allow lists. |
|
||||
|
||||
For Daytona, network access is configured in the `[run.sandbox.daytona]` section:
|
||||
|
||||
```toml title="run.toml"
|
||||
# Block all egress
|
||||
[run.sandbox.daytona]
|
||||
network = "block"
|
||||
|
||||
# Allow only specific CIDRs
|
||||
[run.sandbox.daytona]
|
||||
network = { allow_list = ["208.80.154.232/32", "10.0.0.0/8"] }
|
||||
```
|
||||
|
||||
When using server defaults, the run config `network` overrides the server default. If neither specifies `network`, Daytona's own default (full access) applies.
|
||||
|
||||
## Safety guardrails
|
||||
|
||||
Regardless of which provider you use, Fabro applies a **read-before-write** guardrail. Agents must read a file (via `read_file` or `grep`) before they can modify it with `write_file` or `delete_file`. Writing to new files that don't yet exist is always allowed. This prevents agents from blindly overwriting files they haven't inspected.
|
||||
|
|
|
|||
|
|
@ -56,25 +56,30 @@ script = "git clone https://github.com/fabro-sh/fabro repo"
|
|||
[[run.prepare.steps]]
|
||||
script = "cd repo && npm install"
|
||||
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "cloud"
|
||||
|
||||
[environments.cloud]
|
||||
provider = "daytona"
|
||||
|
||||
[environments.cloud.lifecycle]
|
||||
preserve = false
|
||||
auto_stop = "60m"
|
||||
|
||||
[run.sandbox.daytona]
|
||||
auto_stop_interval = 60
|
||||
|
||||
[run.sandbox.daytona.labels]
|
||||
[environments.cloud.labels]
|
||||
project = "fabro"
|
||||
env = "ci"
|
||||
|
||||
[run.sandbox.daytona.snapshot]
|
||||
name = "node-20"
|
||||
[environments.cloud.image]
|
||||
ref = "node-20"
|
||||
dockerfile = "FROM node:20-slim\nRUN apt-get update && apt-get install -y git"
|
||||
|
||||
[environments.cloud.resources]
|
||||
cpu = 4
|
||||
memory = "8GB"
|
||||
disk = "20GB"
|
||||
dockerfile = "FROM node:20-slim\nRUN apt-get update && apt-get install -y git"
|
||||
|
||||
[run.sandbox.env]
|
||||
[environments.cloud.env]
|
||||
API_KEY = "{{ env.MY_API_KEY }}"
|
||||
NODE_ENV = "production"
|
||||
|
||||
|
|
@ -222,94 +227,66 @@ push = true
|
|||
| `enabled` | When `false`, Fabro skips metadata branch snapshots. |
|
||||
| `push` | When `false`, Fabro writes metadata snapshots locally but does not push `fabro/meta/<id>` to the remote. |
|
||||
|
||||
### `[run.sandbox]`
|
||||
### `[run.environment]` and `[environments.<slug>]`
|
||||
|
||||
Configure how agent tools (bash, file edits) are executed.
|
||||
Runs select a reusable named environment by slug. Environment catalogs can be
|
||||
defined in `settings.toml`, `.fabro/project.toml`, or `workflow.toml`.
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox]
|
||||
provider = "docker"
|
||||
[run.environment]
|
||||
id = "ci"
|
||||
|
||||
[environments.ci]
|
||||
provider = "docker" # local | docker | daytona
|
||||
|
||||
[environments.ci.image]
|
||||
ref = "buildpack-deps:noble"
|
||||
|
||||
[environments.ci.resources]
|
||||
cpu = 2
|
||||
memory = "4GB"
|
||||
|
||||
[environments.ci.lifecycle]
|
||||
preserve = true
|
||||
stop_on_terminal = true
|
||||
|
||||
[environments.ci.env]
|
||||
NODE_ENV = "production"
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `provider` | Sandbox mode: `docker` (default), `local`, or `daytona`. |
|
||||
| `preserve` | When `true`, keep the sandbox alive after the run finishes. Useful for debugging. |
|
||||
| `devcontainer` | When `true`, use the repo's `devcontainer.json` to configure the sandbox. See [Devcontainers](/execution/devcontainers). |
|
||||
Sparse run-level overrides live under `[run.environment.*]` and apply to the
|
||||
selected environment only:
|
||||
|
||||
#### `[run.sandbox.docker]`
|
||||
|
||||
Additional settings when using the Docker sandbox:
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox]
|
||||
provider = "docker"
|
||||
|
||||
[run.sandbox.docker]
|
||||
image = "buildpack-deps:noble"
|
||||
network_mode = "bridge"
|
||||
memory_limit = "4GB"
|
||||
cpu_quota = 200000
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `image` | Docker image used for the run container. Defaults to `buildpack-deps:noble`. |
|
||||
| `network_mode` | Docker network mode. Defaults to `bridge`. |
|
||||
| `memory_limit` | Memory limit using human-readable units such as `"4GB"` or `"512MiB"`. |
|
||||
| `cpu_quota` | Docker CPU quota in microseconds per 100ms period. Defaults to `200000`. |
|
||||
| `env_vars` | Provider-level environment variables passed to the Docker container. For workflow tool execution, prefer `[run.sandbox.env]`. |
|
||||
|
||||
#### `[run.sandbox.daytona]`
|
||||
|
||||
Additional settings when using the Daytona cloud sandbox:
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox.daytona]
|
||||
auto_stop_interval = 60
|
||||
|
||||
[run.sandbox.daytona.labels]
|
||||
project = "fabro"
|
||||
env = "staging"
|
||||
|
||||
[[run.sandbox.daytona.volumes]]
|
||||
volume_id = "vol-agent-state"
|
||||
mount_path = "/home/daytona/agent-state"
|
||||
subpath = "agent-auth"
|
||||
|
||||
[run.sandbox.daytona.snapshot]
|
||||
name = "my-snapshot"
|
||||
cpu = 4
|
||||
```toml
|
||||
[run.environment.resources]
|
||||
memory = "8GB"
|
||||
disk = "20GB"
|
||||
dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update"
|
||||
# Or reference an external Dockerfile:
|
||||
# dockerfile = { path = "./Dockerfile" }
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `auto_stop_interval` | Minutes of inactivity before the sandbox auto-stops. |
|
||||
| `labels` | Key-value labels attached to the sandbox for filtering and identification. Labels merge across layers (sticky merge-by-key). |
|
||||
| `volumes` | Existing Daytona volumes to mount at sandbox creation. Each entry requires `volume_id` and `mount_path`; `subpath` is optional. Fabro does not create or manage volume lifecycle. |
|
||||
| `snapshot.name` | Snapshot name to create or use for the sandbox. |
|
||||
| `snapshot.cpu` | CPU cores for the snapshot (integer). |
|
||||
| `snapshot.memory` | Memory size using human-readable units: `"8GB"`, `"16GiB"`, or bare integers that default to GB. |
|
||||
| `snapshot.disk` | Disk size using the same units as `memory`. |
|
||||
| `snapshot.dockerfile` | Dockerfile content (inline string) or path (`{ path = "..." }`) for building the snapshot image. Paths are resolved relative to the TOML file's directory. |
|
||||
| `network` | Network access mode: `"allow_all"` (default), `"block"`, or `{ allow_list = ["..."] }`. See [Sandboxing](/administration/sandboxing#network-access-control). |
|
||||
| `run.environment.id` | Environment slug to select. Defaults to `default`. |
|
||||
| `environments.<slug>.provider` | Required provider: `local`, `docker`, or `daytona`. |
|
||||
| `image.ref` | Docker image or Daytona snapshot name. |
|
||||
| `image.dockerfile` | Inline Dockerfile or `{ path = "Dockerfile" }`; for Daytona, requires `image.ref`. |
|
||||
| `resources.cpu` / `memory` / `disk` | Best-effort resource hints. Unsupported provider fields warn and continue. |
|
||||
| `network.mode` | `allow_all`, `block`, or `cidr_allow_list`. Local cannot enforce blocked/CIDR networking; Docker cannot enforce CIDR allow-lists. |
|
||||
| `network.allow` | CIDRs for `cidr_allow_list`; entries are validated as CIDRs. |
|
||||
| `lifecycle.preserve` | Keep the created sandbox after the run finishes. |
|
||||
| `lifecycle.stop_on_terminal` | Stop the sandbox when the run reaches a terminal state. |
|
||||
| `lifecycle.auto_stop` | Daytona auto-stop duration, such as `"30m"`. |
|
||||
| `labels` | Provider labels. Merge by key across layers. |
|
||||
| `volumes` | Provider volume hints. Lists replace wholesale across layers. |
|
||||
| `env` | Environment variables passed to command and agent execution. Merge by key across layers. |
|
||||
|
||||
#### Local sandbox
|
||||
When `provider = "local"`, Fabro runs directly in the resolved working
|
||||
directory. If you want local isolation, create or enter a separate clone or Git
|
||||
worktree yourself.
|
||||
|
||||
When `run.sandbox.provider = "local"`, Fabro runs directly in the resolved working directory. If you want local isolation, create or enter a separate clone or Git worktree yourself, then run with `--sandbox local`.
|
||||
|
||||
#### `[run.sandbox.env]`
|
||||
|
||||
Pass environment variables into sandbox command and agent execution. Values can be literal strings or host environment references using `{{ env.VARNAME }}` syntax:
|
||||
Environment variable values can be literal strings or host environment
|
||||
references using `{{ env.VARNAME }}` syntax:
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox.env]
|
||||
[environments.ci.env]
|
||||
API_KEY = "{{ env.MY_API_KEY }}"
|
||||
NODE_ENV = "production"
|
||||
SERVICE_URL = "https://api.{{ env.REGION }}.example.com"
|
||||
|
|
@ -321,7 +298,7 @@ SERVICE_URL = "https://api.{{ env.REGION }}.example.com"
|
|||
| `"{{ env.VARNAME }}"` | Whole-value reference resolved from the host environment at consumption time |
|
||||
| `"prefix-{{ env.X }}-suffix"` | Substring interpolation; multiple tokens per string are supported |
|
||||
|
||||
Missing host variables produce a hard error pointing at the specific field and unresolved token. `run.sandbox.env` is a sticky merge-by-key map: entries from all layers combine, with higher-precedence layers overriding individual keys.
|
||||
Missing host variables produce a hard error pointing at the specific field and unresolved token.
|
||||
|
||||
### `[run.integrations.github.permissions]`
|
||||
|
||||
|
|
@ -514,7 +491,7 @@ Settings can come from multiple sources. Fabro resolves them in this order (firs
|
|||
| Source | Priority |
|
||||
|---|---|
|
||||
| Node-level [stylesheet](/workflows/stylesheets) | Highest |
|
||||
| CLI flags (`--model`, `--provider`, `--sandbox`) | |
|
||||
| CLI flags (`--model`, `--provider`, `--environment`) | |
|
||||
| Run config TOML (`workflow.toml` or equivalent) | |
|
||||
| Project defaults (`.fabro/project.toml`) | |
|
||||
| Machine defaults (`~/.fabro/settings.toml`) | |
|
||||
|
|
@ -535,14 +512,17 @@ _version = 1
|
|||
[run.model]
|
||||
name = "claude-sonnet-4-5"
|
||||
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "cloud"
|
||||
|
||||
[environments.cloud]
|
||||
provider = "daytona"
|
||||
|
||||
[run.sandbox.daytona.snapshot]
|
||||
name = "my-project-snapshot"
|
||||
[environments.cloud.image]
|
||||
ref = "my-project-snapshot"
|
||||
```
|
||||
|
||||
Project defaults and workflow config values merge per the normative merge matrix: most fields merge by field (higher-precedence wins per key), TOML `run.inputs` tables replace wholesale, CLI input flags merge per key at highest precedence, `run.sandbox.env` sticky-merges by key, and `run.prepare.steps` replaces whole-list.
|
||||
Project defaults and workflow config values merge per the normative merge matrix: most fields merge by field (higher-precedence wins per key), TOML `run.inputs` tables replace wholesale, CLI input flags merge per key at highest precedence, environment `env` and `labels` merge by key, environment `volumes` replace as a whole list, and `run.prepare.steps` replaces whole-list.
|
||||
|
||||
### Machine defaults
|
||||
|
||||
|
|
|
|||
|
|
@ -29,20 +29,23 @@ fabro sandbox ssh <run-id> --ttl 120
|
|||
By default, Daytona sandboxes are destroyed when the workflow finishes. To keep the sandbox running after the workflow completes — so you can continue debugging — pass `--preserve-sandbox`:
|
||||
|
||||
```bash
|
||||
fabro run workflow.fabro --sandbox daytona --preserve-sandbox
|
||||
fabro run workflow.fabro --environment cloud --preserve-sandbox
|
||||
```
|
||||
|
||||
Without `--preserve-sandbox`, the SSH session is terminated when the run ends and the sandbox is cleaned up.
|
||||
|
||||
You can also set `auto_stop_interval` in your run config to control how long an idle sandbox stays alive:
|
||||
You can also set `lifecycle.auto_stop` in your environment to control how long an idle sandbox stays alive:
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox]
|
||||
provider = "daytona"
|
||||
preserve = true
|
||||
[run.environment]
|
||||
id = "cloud"
|
||||
|
||||
[run.sandbox.daytona]
|
||||
auto_stop_interval = 60
|
||||
[environments.cloud]
|
||||
provider = "daytona"
|
||||
|
||||
[environments.cloud.lifecycle]
|
||||
preserve = true
|
||||
auto_stop = "60m"
|
||||
```
|
||||
|
||||
## What you can do over SSH
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ VS Code remote access requires [SSH access](/human-tools/ssh-access), which is o
|
|||
1. Start a workflow with a preserved Daytona sandbox:
|
||||
|
||||
```bash
|
||||
fabro run workflow.fabro --sandbox daytona --preserve-sandbox
|
||||
fabro run workflow.fabro --environment cloud --preserve-sandbox
|
||||
```
|
||||
|
||||
2. Use `fabro sandbox ssh` to get the connection command:
|
||||
|
|
|
|||
|
|
@ -24,28 +24,34 @@ The Daytona key must include the snapshot and sandbox scopes Fabro uses to creat
|
|||
|
||||
## Configuration
|
||||
|
||||
Set the sandbox provider in your run config TOML or via CLI flag:
|
||||
Select a Daytona environment in your run config TOML or via CLI flag:
|
||||
|
||||
```bash
|
||||
fabro run workflow.fabro --sandbox daytona
|
||||
fabro run workflow.fabro --environment cloud
|
||||
```
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "cloud"
|
||||
|
||||
[environments.cloud]
|
||||
provider = "daytona"
|
||||
```
|
||||
|
||||
A full configuration example with all Daytona-specific options:
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "cloud"
|
||||
|
||||
[environments.cloud]
|
||||
provider = "daytona"
|
||||
|
||||
[environments.cloud.lifecycle]
|
||||
preserve = false
|
||||
auto_stop = "60m"
|
||||
|
||||
[run.sandbox.daytona]
|
||||
auto_stop_interval = 60
|
||||
|
||||
[run.sandbox.daytona.labels]
|
||||
[environments.cloud.labels]
|
||||
project = "fabro"
|
||||
env = "staging"
|
||||
team = "platform"
|
||||
|
|
@ -58,17 +64,19 @@ back to Fabro-managed runs. User-provided values for these reserved keys are
|
|||
overwritten.
|
||||
</Note>
|
||||
|
||||
[run.sandbox.daytona.snapshot]
|
||||
name = "rust-dev"
|
||||
cpu = 4
|
||||
memory = "8GB"
|
||||
disk = "20GB"
|
||||
[environments.cloud.image]
|
||||
ref = "rust-dev"
|
||||
dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update && apt-get install -y git ripgrep"
|
||||
# Or keep the Dockerfile next to this TOML file:
|
||||
# dockerfile = { path = "./Dockerfile" }
|
||||
|
||||
[environments.cloud.resources]
|
||||
cpu = 4
|
||||
memory = "8GB"
|
||||
disk = "20GB"
|
||||
```
|
||||
|
||||
See [Server Configuration](/administration/server-configuration) for the full reference on all sandbox fields and server defaults.
|
||||
See [Environments](/execution/environments) for the full reference on all environment fields and server defaults.
|
||||
|
||||
### Network access control
|
||||
|
||||
|
|
@ -76,16 +84,17 @@ Control outbound network access with the `network` field. Three modes are availa
|
|||
|
||||
```toml title="run.toml"
|
||||
# Full access (default)
|
||||
[run.sandbox.daytona]
|
||||
network = "allow_all"
|
||||
[environments.cloud.network]
|
||||
mode = "allow_all"
|
||||
|
||||
# Block all egress
|
||||
[run.sandbox.daytona]
|
||||
network = "block"
|
||||
[environments.cloud.network]
|
||||
mode = "block"
|
||||
|
||||
# CIDR-based allow list
|
||||
[run.sandbox.daytona]
|
||||
network = { allow_list = ["208.80.154.232/32", "10.0.0.0/8"] }
|
||||
[environments.cloud.network]
|
||||
mode = "cidr_allow_list"
|
||||
allow = ["208.80.154.232/32", "10.0.0.0/8"]
|
||||
```
|
||||
|
||||
Use `"block"` or a CIDR allow list when running untrusted or generated code to prevent agents from making arbitrary network requests.
|
||||
|
|
@ -95,12 +104,14 @@ Use `"block"` or a CIDR allow list when running untrusted or generated code to p
|
|||
Snapshots let you pre-build an environment image so each run starts with dependencies already installed rather than installing them in setup commands every time.
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox.daytona.snapshot]
|
||||
name = "my-snapshot"
|
||||
[environments.cloud.image]
|
||||
ref = "my-snapshot"
|
||||
dockerfile = "FROM node:20-slim\nRUN apt-get update && apt-get install -y git"
|
||||
|
||||
[environments.cloud.resources]
|
||||
cpu = 4
|
||||
memory = 8
|
||||
disk = 20
|
||||
dockerfile = "FROM node:20-slim\nRUN apt-get update && apt-get install -y git"
|
||||
```
|
||||
|
||||
When a run starts with a snapshot configured, Fabro looks up the snapshot by name. If it doesn't exist and a `dockerfile` is provided, Fabro creates it automatically and polls until it reaches `Active` state (up to 10 minutes). `dockerfile` can be inline content or `{ path = "..." }`; paths are resolved relative to the TOML file that declares them and are bundled into run manifests. If the snapshot already exists, it's reused immediately.
|
||||
|
|
@ -116,7 +127,10 @@ Fabro automatically clones the run's GitHub origin into the sandbox at `/home/da
|
|||
Set `[run.clone] enabled = false` when a workflow should start with an empty Daytona workspace instead of cloning the run origin:
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "cloud"
|
||||
|
||||
[environments.cloud]
|
||||
provider = "daytona"
|
||||
|
||||
[run.clone]
|
||||
|
|
@ -153,14 +167,19 @@ Each sandbox gets a unique timestamped name (e.g. `fabro-20260307-143022-a3f2`)
|
|||
To keep a sandbox alive for debugging:
|
||||
|
||||
```bash
|
||||
fabro run workflow.fabro --sandbox daytona --preserve-sandbox
|
||||
fabro run workflow.fabro --environment cloud --preserve-sandbox
|
||||
```
|
||||
|
||||
Or in the run config:
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "cloud"
|
||||
|
||||
[environments.cloud]
|
||||
provider = "daytona"
|
||||
|
||||
[environments.cloud.lifecycle]
|
||||
preserve = true
|
||||
```
|
||||
|
||||
|
|
@ -168,11 +187,11 @@ When preserved, Fabro prints the sandbox name so you can find it in the [Daytona
|
|||
|
||||
### Auto-stop
|
||||
|
||||
The `auto_stop_interval` setting tells Daytona to stop the sandbox after a period of inactivity, saving costs for preserved or long-running sandboxes:
|
||||
The `lifecycle.auto_stop` setting tells Daytona to stop the sandbox after a period of inactivity, saving costs for preserved or long-running sandboxes:
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox.daytona]
|
||||
auto_stop_interval = 30
|
||||
[environments.cloud.lifecycle]
|
||||
auto_stop = "30m"
|
||||
```
|
||||
|
||||
## Server defaults
|
||||
|
|
@ -191,7 +210,7 @@ If doctor reports missing scopes, regenerate the Daytona key with `write:snapsho
|
|||
|
||||
### "Snapshot does not exist and no dockerfile provided"
|
||||
|
||||
The run config references a snapshot name that doesn't exist on Daytona, and no `dockerfile` is provided to create it. Either create the snapshot manually in the Daytona dashboard or add a `dockerfile` field to `[run.sandbox.daytona.snapshot]`.
|
||||
The run config references a snapshot name that doesn't exist on Daytona, and no `dockerfile` is provided to create it. Either create the snapshot manually in the Daytona dashboard or add `image.dockerfile` under the selected `[environments.<slug>.image]`.
|
||||
|
||||
### "Timed out waiting for snapshot to become active"
|
||||
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ fabro create [OPTIONS] <WORKFLOW>
|
|||
| `--parent <run>` | Link this run to an existing orchestration parent run |
|
||||
| `--preserve-sandbox` | Keep the sandbox alive after the run finishes (for debugging) |
|
||||
| `--provider <provider>` | Override default LLM provider |
|
||||
| `--sandbox <sandbox>` | Sandbox for agent tools<br />Values: `local`, `docker`, `daytona` |
|
||||
| `--environment <environment>` | Named environment slug for agent tools |
|
||||
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
|
||||
| `-I, --input <key=value>` | Override a workflow input value (repeatable, format: KEY=VALUE) |
|
||||
| `-v, --verbose` | Enable verbose output |
|
||||
|
|
@ -873,7 +873,7 @@ fabro preflight [OPTIONS] <WORKFLOW>
|
|||
| `--goal-file <goal_file>` | Read the workflow goal from a file |
|
||||
| `--model <model>` | Override default LLM model |
|
||||
| `--provider <provider>` | Override default LLM provider |
|
||||
| `--sandbox <sandbox>` | Sandbox for agent tools<br />Values: `local`, `docker`, `daytona` |
|
||||
| `--environment <environment>` | Named environment slug for agent tools |
|
||||
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
|
||||
| `-I, --input <key=value>` | Override a workflow input value (repeatable, format: KEY=VALUE) |
|
||||
| `-v, --verbose` | Enable verbose output |
|
||||
|
|
@ -1037,7 +1037,7 @@ fabro run [OPTIONS] <WORKFLOW>
|
|||
| `--parent <run>` | Link this run to an existing orchestration parent run |
|
||||
| `--preserve-sandbox` | Keep the sandbox alive after the run finishes (for debugging) |
|
||||
| `--provider <provider>` | Override default LLM provider |
|
||||
| `--sandbox <sandbox>` | Sandbox for agent tools<br />Values: `local`, `docker`, `daytona` |
|
||||
| `--environment <environment>` | Named environment slug for agent tools |
|
||||
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
|
||||
| `-I, --input <key=value>` | Override a workflow input value (repeatable, format: KEY=VALUE) |
|
||||
| `-v, --verbose` | Enable verbose output |
|
||||
|
|
@ -1229,7 +1229,7 @@ fabro server restart [OPTIONS]
|
|||
| `--model <model>` | Override default LLM model |
|
||||
| `--no-web` | Disable the embedded web UI, browser auth routes, and web-only helper endpoints |
|
||||
| `--provider <provider>` | Override default LLM provider |
|
||||
| `--sandbox <sandbox>` | Sandbox for agent tools |
|
||||
| `--environment <environment>` | Named environment slug for agent tools |
|
||||
| `--storage-dir <storage_dir>` | Local storage directory (default: ~/.fabro/storage) |
|
||||
| `--timeout <timeout>` | Seconds to wait for graceful shutdown before SIGKILL<br />Default: `10` |
|
||||
| `--watch-web` | Run `bun run dev` in apps/fabro-web to watch/recompile web assets (debug only) |
|
||||
|
|
@ -1254,7 +1254,7 @@ fabro server start [OPTIONS]
|
|||
| `--model <model>` | Override default LLM model |
|
||||
| `--no-web` | Disable the embedded web UI, browser auth routes, and web-only helper endpoints |
|
||||
| `--provider <provider>` | Override default LLM provider |
|
||||
| `--sandbox <sandbox>` | Sandbox for agent tools |
|
||||
| `--environment <environment>` | Named environment slug for agent tools |
|
||||
| `--storage-dir <storage_dir>` | Local storage directory (default: ~/.fabro/storage) |
|
||||
| `--watch-web` | Run `bun run dev` in apps/fabro-web to watch/recompile web assets (debug only) |
|
||||
| `--web` | Enable the embedded web UI and browser auth routes |
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ Common flags:
|
|||
|---|---|---|
|
||||
| `--bind` | `~/.fabro/fabro.sock` | Address to bind: `IP` or `IP:port` for TCP, or a path for Unix socket |
|
||||
| `--model` | — | Override default LLM model |
|
||||
| `--sandbox` | — | Override default sandbox provider |
|
||||
| `--environment` | — | Override default environment slug |
|
||||
| `--max-concurrent-runs` | `5` | Maximum concurrent run executions |
|
||||
|
||||
See [Server Configuration](/administration/server-configuration) for the full `settings.toml` reference.
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ Files that omit `_version` are treated as version `1`. The legacy top-level `ver
|
|||
| Scope | Examples |
|
||||
|---|---|
|
||||
| CLI-only | `[cli.target]`, `[cli.auth]`, `[cli.exec]`, `[cli.output]`, `[cli.updates]`, `[cli.logging]` |
|
||||
| Shared run defaults | `[run.model]`, `[run.sandbox]`, `[run.checkpoint]`, `[run.inputs]`, `[run.prepare]`, `[run.pull_request]`, `[run.integrations.github.permissions]`, `[run.hooks]`, `[run.agent.mcps]` |
|
||||
| Shared run defaults | `[run.model]`, `[run.environment]`, `[environments.<slug>]`, `[run.checkpoint]`, `[run.inputs]`, `[run.prepare]`, `[run.pull_request]`, `[run.integrations.github.permissions]`, `[run.hooks]`, `[run.agent.mcps]` |
|
||||
| Shared LLM catalog | `[llm.providers.<id>]`, `[llm.models.<id>]`, model limits, features, controls, and costs |
|
||||
| Server-only | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.storage]`, `[server.artifacts]`, `[server.slatedb]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` |
|
||||
|
||||
|
|
|
|||
|
|
@ -218,33 +218,6 @@ pub(crate) struct InputOverrideArgs {
|
|||
pub(crate) values: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub(crate) enum CliSandboxProvider {
|
||||
Local,
|
||||
Docker,
|
||||
Daytona,
|
||||
}
|
||||
|
||||
impl From<CliSandboxProvider> for fabro_sandbox::SandboxProvider {
|
||||
fn from(value: CliSandboxProvider) -> Self {
|
||||
match value {
|
||||
CliSandboxProvider::Local => Self::Local,
|
||||
CliSandboxProvider::Docker => Self::Docker,
|
||||
CliSandboxProvider::Daytona => Self::Daytona,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<fabro_sandbox::SandboxProvider> for CliSandboxProvider {
|
||||
fn from(value: fabro_sandbox::SandboxProvider) -> Self {
|
||||
match value {
|
||||
fabro_sandbox::SandboxProvider::Local => Self::Local,
|
||||
fabro_sandbox::SandboxProvider::Docker => Self::Docker,
|
||||
fabro_sandbox::SandboxProvider::Daytona => Self::Daytona,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub(crate) struct RunArgs {
|
||||
#[command(flatten)]
|
||||
|
|
@ -285,9 +258,9 @@ pub(crate) struct RunArgs {
|
|||
#[arg(short, long)]
|
||||
pub(crate) verbose: bool,
|
||||
|
||||
/// Sandbox for agent tools
|
||||
#[arg(long, value_enum)]
|
||||
pub(crate) sandbox: Option<CliSandboxProvider>,
|
||||
/// Named environment for agent tools
|
||||
#[arg(long)]
|
||||
pub(crate) environment: Option<String>,
|
||||
|
||||
/// Attach a label to this run (repeatable, format: KEY=VALUE)
|
||||
#[arg(long = "label", value_name = "KEY=VALUE")]
|
||||
|
|
@ -341,9 +314,9 @@ pub(crate) struct PreflightArgs {
|
|||
#[arg(short, long)]
|
||||
pub(crate) verbose: bool,
|
||||
|
||||
/// Sandbox for agent tools
|
||||
#[arg(long, value_enum)]
|
||||
pub(crate) sandbox: Option<CliSandboxProvider>,
|
||||
/// Named environment for agent tools
|
||||
#[arg(long)]
|
||||
pub(crate) environment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ draft = true
|
|||
let toml_path = workflow_dir.join("workflow.toml");
|
||||
std::fs::write(
|
||||
&toml_path,
|
||||
"_version = 1\n\n[workflow]\ngraph = \"workflow.fabro\"\n\n[run.sandbox]\nprovider = \"local\"\n",
|
||||
"_version = 1\n\n[workflow]\ngraph = \"workflow.fabro\"\n\n[run.environment]\nid = \"local\"\n\n[environments.local]\nprovider = \"local\"\n",
|
||||
)
|
||||
.with_context(|| format!("failed to write {}", toml_path.display()))?;
|
||||
created.push(".fabro/workflows/hello/workflow.toml".to_string());
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use fabro_config::{
|
|||
CliLayer, CliOutputLayer, RunGoalLayer, RunLayer, parse_input_overrides, parse_labels,
|
||||
};
|
||||
use fabro_manifest::{RunOverrideInput, build_run_overrides};
|
||||
use fabro_sandbox::SandboxProvider;
|
||||
use fabro_types::settings::cli::OutputVerbosity;
|
||||
use fabro_types::settings::interp::InterpString;
|
||||
|
||||
|
|
@ -71,13 +70,11 @@ fn current_dir_or_dot() -> PathBuf {
|
|||
pub(crate) fn run_args_overrides(args: &RunArgs) -> Result<ManifestSettingsOverrides> {
|
||||
let cwd = current_dir_or_dot();
|
||||
let goal = goal_layer_from_args(args.goal.as_deref(), args.goal_file.as_deref(), &cwd)?;
|
||||
let sandbox = args.sandbox.map(SandboxProvider::from);
|
||||
let sandbox_provider = sandbox.as_ref().map(ToString::to_string);
|
||||
let mut run = build_run_overrides(RunOverrideInput {
|
||||
goal: None,
|
||||
model: args.model.as_deref(),
|
||||
provider: args.provider.as_deref(),
|
||||
sandbox: sandbox_provider.as_deref(),
|
||||
environment: args.environment.as_deref(),
|
||||
docker_image: None,
|
||||
preserve_sandbox: sparse_flag(args.preserve_sandbox),
|
||||
dry_run: sparse_flag(args.dry_run),
|
||||
|
|
@ -96,14 +93,11 @@ pub(crate) fn run_args_overrides(args: &RunArgs) -> Result<ManifestSettingsOverr
|
|||
pub(crate) fn preflight_args_overrides(args: &PreflightArgs) -> Result<ManifestSettingsOverrides> {
|
||||
let cwd = current_dir_or_dot();
|
||||
let goal = goal_layer_from_args(args.goal.as_deref(), args.goal_file.as_deref(), &cwd)?;
|
||||
let sandbox_provider = args
|
||||
.sandbox
|
||||
.map(|sandbox| SandboxProvider::from(sandbox).to_string());
|
||||
let mut run = build_run_overrides(RunOverrideInput {
|
||||
goal: None,
|
||||
model: args.model.as_deref(),
|
||||
provider: args.provider.as_deref(),
|
||||
sandbox: sandbox_provider.as_deref(),
|
||||
environment: args.environment.as_deref(),
|
||||
docker_image: None,
|
||||
preserve_sandbox: None,
|
||||
dry_run: None,
|
||||
|
|
|
|||
|
|
@ -708,7 +708,7 @@ fn requires_github_credentials(run: &RunNamespace) -> bool {
|
|||
return true;
|
||||
}
|
||||
run.execution.mode != RunMode::DryRun
|
||||
&& clone_sandbox_requires_github_credentials(&run.sandbox.provider)
|
||||
&& clone_sandbox_requires_github_credentials(&run.environment.provider.to_string())
|
||||
}
|
||||
|
||||
fn clone_sandbox_requires_github_credentials(provider: &str) -> bool {
|
||||
|
|
@ -1147,8 +1147,8 @@ mod tests {
|
|||
|
||||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::run::{
|
||||
RunIntegrationsGithubSettings, RunIntegrationsSettings, RunMode, RunNamespace,
|
||||
RunSandboxSettings,
|
||||
EnvironmentProvider, RunIntegrationsGithubSettings, RunIntegrationsSettings, RunMode,
|
||||
RunNamespace,
|
||||
};
|
||||
|
||||
use super::super::requires_github_credentials;
|
||||
|
|
@ -1160,10 +1160,9 @@ mod tests {
|
|||
) -> RunNamespace {
|
||||
let mut run = RunNamespace::default();
|
||||
run.execution.mode = mode;
|
||||
run.sandbox = RunSandboxSettings {
|
||||
provider: provider.to_string(),
|
||||
..RunSandboxSettings::default()
|
||||
};
|
||||
run.environment.provider = provider
|
||||
.parse::<EnvironmentProvider>()
|
||||
.expect("test provider should parse");
|
||||
run.integrations = RunIntegrationsSettings {
|
||||
github: RunIntegrationsGithubSettings { permissions },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ async fn ensure_server_running_with_bind(
|
|||
no_web: false,
|
||||
model: None,
|
||||
provider: None,
|
||||
sandbox: None,
|
||||
environment: None,
|
||||
max_concurrent_runs: server_max_concurrent_runs_override(),
|
||||
config: Some(config_path.to_path_buf()),
|
||||
#[cfg(debug_assertions)]
|
||||
|
|
@ -322,8 +322,8 @@ async fn execute_daemon(
|
|||
if serve_args.no_web {
|
||||
cmd.arg("--no-web");
|
||||
}
|
||||
if let Some(ref sandbox) = serve_args.sandbox {
|
||||
cmd.args(["--sandbox", &sandbox.to_string()]);
|
||||
if let Some(ref environment) = serve_args.environment {
|
||||
cmd.args(["--environment", environment]);
|
||||
}
|
||||
if let Some(max) = serve_args.max_concurrent_runs {
|
||||
cmd.args(["--max-concurrent-runs", &max.to_string()]);
|
||||
|
|
@ -589,7 +589,7 @@ destination = "{destination}"
|
|||
no_web: false,
|
||||
model: None,
|
||||
provider: None,
|
||||
sandbox: None,
|
||||
environment: None,
|
||||
max_concurrent_runs: None,
|
||||
config: Some(config_path.to_path_buf()),
|
||||
#[cfg(debug_assertions)]
|
||||
|
|
|
|||
|
|
@ -10,9 +10,7 @@ pub(crate) fn run_manifest_args(args: &RunArgs) -> Option<types::ManifestArgs> {
|
|||
model: args.model.clone(),
|
||||
preserve_sandbox: args.preserve_sandbox.then_some(true),
|
||||
provider: args.provider.clone(),
|
||||
sandbox: args
|
||||
.sandbox
|
||||
.map(|provider| fabro_sandbox::SandboxProvider::from(provider).to_string()),
|
||||
environment: args.environment.clone(),
|
||||
docker_image: None,
|
||||
input: args.inputs.values.clone(),
|
||||
verbose: args.verbose.then_some(true),
|
||||
|
|
@ -28,9 +26,7 @@ pub(crate) fn preflight_manifest_args(args: &PreflightArgs) -> Option<types::Man
|
|||
model: args.model.clone(),
|
||||
preserve_sandbox: None,
|
||||
provider: args.provider.clone(),
|
||||
sandbox: args
|
||||
.sandbox
|
||||
.map(|provider| fabro_sandbox::SandboxProvider::from(provider).to_string()),
|
||||
environment: args.environment.clone(),
|
||||
docker_image: None,
|
||||
input: args.inputs.values.clone(),
|
||||
verbose: args.verbose.then_some(true),
|
||||
|
|
|
|||
|
|
@ -363,8 +363,8 @@ fn create_persists_requested_overrides_into_store() {
|
|||
"gpt-5",
|
||||
"--provider",
|
||||
"openai",
|
||||
"--sandbox",
|
||||
"local",
|
||||
"--environment",
|
||||
"default",
|
||||
"--label",
|
||||
"env=dev",
|
||||
"--label",
|
||||
|
|
@ -410,9 +410,10 @@ fn create_persists_requested_overrides_into_store() {
|
|||
"model": resolved_run.model.name.as_ref().map(fabro_types::settings::InterpString::as_source),
|
||||
"provider": resolved_run.model.provider.as_ref().map(fabro_types::settings::InterpString::as_source),
|
||||
},
|
||||
"sandbox": {
|
||||
"provider": resolved_run.sandbox.provider,
|
||||
"preserve": resolved_run.sandbox.preserve,
|
||||
"environment": {
|
||||
"id": resolved_run.environment.id,
|
||||
"provider": resolved_run.environment.provider.to_string(),
|
||||
"preserve": resolved_run.environment.lifecycle.preserve,
|
||||
},
|
||||
},
|
||||
"labels": labels,
|
||||
|
|
@ -429,8 +430,9 @@ fn create_persists_requested_overrides_into_store() {
|
|||
"model": "gpt-5",
|
||||
"provider": "openai"
|
||||
},
|
||||
"sandbox": {
|
||||
"provider": "local",
|
||||
"environment": {
|
||||
"id": "default",
|
||||
"provider": "docker",
|
||||
"preserve": true
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ use fabro_util::error::SharedError;
|
|||
use crate::defaults::DEFAULTS_LAYER;
|
||||
use crate::load::load_settings_path;
|
||||
use crate::resolve::{
|
||||
ResolveError, resolve_cli, resolve_project, resolve_run, resolve_server, resolve_workflow,
|
||||
ResolveError, resolve_cli, resolve_environments, resolve_project, resolve_run, resolve_server,
|
||||
resolve_workflow,
|
||||
};
|
||||
use crate::user::load_settings_config;
|
||||
use crate::{
|
||||
CliLayer, Combine, CostRates, Error, LlmLayer, LlmModelFeatures, LlmModelLimits, ModelControls,
|
||||
ModelCostTable, ModelSettings, ProviderSettings, Result, RunLayer, ServerLayer, SettingsLayer,
|
||||
run,
|
||||
CliLayer, Combine, CostRates, EnvironmentLayer, Error, LlmLayer, LlmModelFeatures,
|
||||
LlmModelLimits, MergeMap, ModelControls, ModelCostTable, ModelSettings, ProviderSettings,
|
||||
Result, RunLayer, ServerLayer, SettingsLayer, run,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -190,7 +191,12 @@ impl RunSettingsBuilder {
|
|||
pub(crate) fn from_layer(layer: &SettingsLayer) -> Result<RunNamespace> {
|
||||
let layer = layer.clone().combine(DEFAULTS_LAYER.clone());
|
||||
let mut errors = Vec::new();
|
||||
let run = resolve_run(&layer.run.clone().unwrap_or_default(), &mut errors);
|
||||
let environments = resolve_environments(&layer.environments, &mut errors);
|
||||
let run = resolve_run(
|
||||
&layer.run.clone().unwrap_or_default(),
|
||||
&environments,
|
||||
&mut errors,
|
||||
);
|
||||
finish_result(run, "failed to resolve run settings", errors)
|
||||
}
|
||||
|
||||
|
|
@ -204,10 +210,11 @@ impl RunSettingsBuilder {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct ServerRuntimeSettings {
|
||||
pub server_settings: ServerSettings,
|
||||
pub manifest_run_defaults: RunLayer,
|
||||
pub manifest_run_settings: std::result::Result<RunNamespace, SharedError>,
|
||||
pub llm_catalog_settings: model_catalog::LlmCatalogSettings,
|
||||
pub server_settings: ServerSettings,
|
||||
pub manifest_run_defaults: RunLayer,
|
||||
pub manifest_environment_defaults: crate::MergeMap<crate::EnvironmentLayer>,
|
||||
pub manifest_run_settings: std::result::Result<RunNamespace, SharedError>,
|
||||
pub llm_catalog_settings: model_catalog::LlmCatalogSettings,
|
||||
}
|
||||
|
||||
pub fn load_server_runtime_settings(
|
||||
|
|
@ -263,12 +270,18 @@ fn resolve_server_runtime_settings(
|
|||
}
|
||||
|
||||
let manifest_run_defaults = layer.run.clone().unwrap_or_default();
|
||||
let manifest_environment_defaults = layer.environments.clone();
|
||||
let llm_catalog_settings = llm_catalog_settings_from_layer(&layer);
|
||||
Ok(ServerRuntimeSettings {
|
||||
server_settings: ServerSettingsBuilder::from_layer(&layer)?,
|
||||
manifest_run_settings: RunSettingsBuilder::from_run_layer(&manifest_run_defaults)
|
||||
.map_err(|err| SharedError::new(anyhow::Error::new(err))),
|
||||
manifest_run_settings: RunSettingsBuilder::from_layer(&SettingsLayer {
|
||||
run: Some(manifest_run_defaults.clone()),
|
||||
environments: manifest_environment_defaults.clone(),
|
||||
..SettingsLayer::default()
|
||||
})
|
||||
.map_err(|err| SharedError::new(anyhow::Error::new(err))),
|
||||
manifest_run_defaults,
|
||||
manifest_environment_defaults,
|
||||
llm_catalog_settings,
|
||||
})
|
||||
}
|
||||
|
|
@ -429,7 +442,7 @@ impl WorkflowSettingsBuilder {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn workflow_layer(mut self, layer: SettingsLayer) -> Self {
|
||||
pub fn workflow_layer(mut self, layer: SettingsLayer) -> Self {
|
||||
self.workflow = layer;
|
||||
self
|
||||
}
|
||||
|
|
@ -462,7 +475,7 @@ impl WorkflowSettingsBuilder {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn project_layer(mut self, layer: SettingsLayer) -> Self {
|
||||
pub fn project_layer(mut self, layer: SettingsLayer) -> Self {
|
||||
self.project = layer;
|
||||
self
|
||||
}
|
||||
|
|
@ -517,6 +530,19 @@ impl WorkflowSettingsBuilder {
|
|||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn server_manifest_defaults(
|
||||
self,
|
||||
run: RunLayer,
|
||||
environments: MergeMap<EnvironmentLayer>,
|
||||
) -> Self {
|
||||
self.server_layer(SettingsLayer {
|
||||
run: Some(run),
|
||||
environments,
|
||||
..SettingsLayer::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn run_overrides(self, run: RunLayer) -> Self {
|
||||
self.args_layer(SettingsLayer {
|
||||
|
|
@ -538,6 +564,7 @@ impl WorkflowSettingsBuilder {
|
|||
let server_defaults = SettingsLayer {
|
||||
version: self.server.version,
|
||||
run: self.server.run,
|
||||
environments: self.server.environments,
|
||||
..SettingsLayer::default()
|
||||
};
|
||||
let mut layer = self
|
||||
|
|
@ -563,11 +590,17 @@ impl WorkflowSettingsBuilder {
|
|||
let mut errors = Vec::new();
|
||||
let project = resolve_project(&layer.project.clone().unwrap_or_default(), &mut errors);
|
||||
let workflow = resolve_workflow(&layer.workflow.clone().unwrap_or_default(), &mut errors);
|
||||
let run = resolve_run(&layer.run.clone().unwrap_or_default(), &mut errors);
|
||||
let environments = resolve_environments(&layer.environments, &mut errors);
|
||||
let run = resolve_run(
|
||||
&layer.run.clone().unwrap_or_default(),
|
||||
&environments,
|
||||
&mut errors,
|
||||
);
|
||||
finish_dense_result(
|
||||
WorkflowSettings {
|
||||
project,
|
||||
workflow,
|
||||
environments,
|
||||
run,
|
||||
},
|
||||
errors,
|
||||
|
|
|
|||
|
|
@ -23,16 +23,42 @@ push = true
|
|||
enabled = true
|
||||
push = true
|
||||
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "default"
|
||||
|
||||
[environments.default]
|
||||
provider = "docker"
|
||||
|
||||
[environments.default.image]
|
||||
ref = "buildpack-deps:noble"
|
||||
|
||||
[environments.default.resources]
|
||||
cpu = 2
|
||||
memory = "4GB"
|
||||
|
||||
[environments.default.lifecycle]
|
||||
preserve = false
|
||||
stop_on_terminal = true
|
||||
devcontainer = false
|
||||
|
||||
[run.sandbox.docker]
|
||||
image = "buildpack-deps:noble"
|
||||
memory_limit = "4GB"
|
||||
cpu_quota = 200000
|
||||
[environments.local]
|
||||
provider = "local"
|
||||
|
||||
[environments.docker]
|
||||
provider = "docker"
|
||||
|
||||
[environments.docker.image]
|
||||
ref = "buildpack-deps:noble"
|
||||
|
||||
[environments.docker.resources]
|
||||
cpu = 2
|
||||
memory = "4GB"
|
||||
|
||||
[environments.docker.lifecycle]
|
||||
preserve = false
|
||||
stop_on_terminal = true
|
||||
|
||||
[environments.daytona]
|
||||
provider = "daytona"
|
||||
|
||||
[cli.output]
|
||||
format = "text"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ use std::collections::{BTreeMap, HashMap};
|
|||
use fabro_model::{AgentProfileKind, BillingPolicy, ProviderAuthConfig};
|
||||
use fabro_types::settings::cli::{CliAuthStrategy, OutputFormat, OutputVerbosity};
|
||||
use fabro_types::settings::run::{
|
||||
AgentPermissions, ApprovalMode, DaytonaNetworkLayer, MergeStrategy, RunMode,
|
||||
AgentPermissions, ApprovalMode, EnvironmentNetworkMode, EnvironmentProvider, MergeStrategy,
|
||||
RunMode,
|
||||
};
|
||||
use fabro_types::settings::server::{
|
||||
GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, ServerAuthMethod,
|
||||
|
|
@ -13,11 +14,12 @@ use fabro_types::settings::{Duration, InterpString, Size};
|
|||
|
||||
use super::LogFilter;
|
||||
use super::cli::{CliAuthLayer, CliLoggingLayer, CliTargetLayer};
|
||||
use super::environment::{EnvironmentDockerfileLayer, EnvironmentVolumeLayer};
|
||||
use super::llm::{CostRates, CredentialRef, HeaderValueRef, ReasoningEffortFeature};
|
||||
use super::run::{
|
||||
DaytonaSnapshotLayer, DaytonaVolumeLayer, HookAgentMarker, HookEntry, HookTlsMode,
|
||||
InterviewProviderLayer, ModelRefOrSplice, NotificationProviderLayer, RunArtifactsLayer,
|
||||
RunCheckpointLayer, RunGoalLayer, RunPrepareLayer, ScmGitHubLayer, StringOrSplice,
|
||||
HookAgentMarker, HookEntry, HookTlsMode, InterviewProviderLayer, ModelRefOrSplice,
|
||||
NotificationProviderLayer, RunArtifactsLayer, RunCheckpointLayer, RunGoalLayer,
|
||||
RunPrepareLayer, ScmGitHubLayer, StringOrSplice,
|
||||
};
|
||||
use super::server::{
|
||||
ObjectStoreLocalLayer, ObjectStoreS3Layer, ServerApiLayer, ServerAuthGithubLayer,
|
||||
|
|
@ -43,7 +45,7 @@ impl<T: Combine> Combine for Option<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Combine for Option<Vec<DaytonaVolumeLayer>> {
|
||||
impl Combine for Option<Vec<EnvironmentVolumeLayer>> {
|
||||
fn combine(self, other: Self) -> Self {
|
||||
self.or(other)
|
||||
}
|
||||
|
|
@ -147,8 +149,9 @@ impl_combine_self!(
|
|||
CliAuthLayer,
|
||||
CliLoggingLayer,
|
||||
CliTargetLayer,
|
||||
DaytonaNetworkLayer,
|
||||
DaytonaSnapshotLayer,
|
||||
EnvironmentNetworkMode,
|
||||
EnvironmentProvider,
|
||||
EnvironmentDockerfileLayer,
|
||||
InterviewProviderLayer,
|
||||
NotificationProviderLayer,
|
||||
RunArtifactsLayer,
|
||||
|
|
|
|||
134
lib/crates/fabro-config/src/layers/environment.rs
Normal file
134
lib/crates/fabro-config/src/layers/environment.rs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
//! Sparse top-level `[environments.<slug>]` settings layer definitions.
|
||||
|
||||
use fabro_types::settings::{Duration, InterpString, Size};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::combine::Combine;
|
||||
use super::maps::StickyMap;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EnvironmentLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub image: Option<EnvironmentImageLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub resources: Option<EnvironmentResourcesLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub network: Option<EnvironmentNetworkLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub lifecycle: Option<EnvironmentLifecycleLayer>,
|
||||
#[serde(default, skip_serializing_if = "StickyMap::is_empty")]
|
||||
pub labels: StickyMap<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub volumes: Option<Vec<EnvironmentVolumeLayer>>,
|
||||
#[serde(default, skip_serializing_if = "StickyMap::is_empty")]
|
||||
pub env: StickyMap<InterpString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RunEnvironmentLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub image: Option<EnvironmentImageLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub resources: Option<EnvironmentResourcesLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub network: Option<EnvironmentNetworkLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub lifecycle: Option<EnvironmentLifecycleLayer>,
|
||||
#[serde(default, skip_serializing_if = "StickyMap::is_empty")]
|
||||
pub labels: StickyMap<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub volumes: Option<Vec<EnvironmentVolumeLayer>>,
|
||||
#[serde(default, skip_serializing_if = "StickyMap::is_empty")]
|
||||
pub env: StickyMap<InterpString>,
|
||||
}
|
||||
|
||||
impl RunEnvironmentLayer {
|
||||
#[must_use]
|
||||
pub fn into_environment_override(self) -> EnvironmentLayer {
|
||||
EnvironmentLayer {
|
||||
provider: None,
|
||||
image: self.image,
|
||||
resources: self.resources,
|
||||
network: self.network,
|
||||
lifecycle: self.lifecycle,
|
||||
labels: self.labels,
|
||||
volumes: self.volumes,
|
||||
env: self.env,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EnvironmentImageLayer {
|
||||
#[serde(default, rename = "ref", skip_serializing_if = "Option::is_none")]
|
||||
pub reference: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub dockerfile: Option<EnvironmentDockerfileLayer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EnvironmentResourcesLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cpu: Option<i32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub memory: Option<Size>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub disk: Option<Size>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EnvironmentNetworkLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mode: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub allow: Vec<String>,
|
||||
}
|
||||
|
||||
impl Combine for EnvironmentNetworkLayer {
|
||||
fn combine(self, other: Self) -> Self {
|
||||
Self {
|
||||
mode: self.mode.or(other.mode),
|
||||
allow: if self.allow.is_empty() {
|
||||
other.allow
|
||||
} else {
|
||||
self.allow
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EnvironmentLifecycleLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub preserve: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub stop_on_terminal: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub auto_stop: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EnvironmentVolumeLayer {
|
||||
pub id: String,
|
||||
pub mount_path: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub subpath: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
pub enum EnvironmentDockerfileLayer {
|
||||
Inline(String),
|
||||
Path { path: String },
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
mod cli;
|
||||
mod combine;
|
||||
mod environment;
|
||||
mod llm;
|
||||
mod log_filter;
|
||||
mod maps;
|
||||
|
|
@ -15,6 +16,11 @@ pub use cli::{
|
|||
CliOutputLayer, CliTargetLayer, CliUpdatesLayer,
|
||||
};
|
||||
pub(crate) use combine::Combine;
|
||||
pub use environment::{
|
||||
EnvironmentDockerfileLayer, EnvironmentImageLayer, EnvironmentLayer, EnvironmentLifecycleLayer,
|
||||
EnvironmentNetworkLayer, EnvironmentResourcesLayer, EnvironmentVolumeLayer,
|
||||
RunEnvironmentLayer,
|
||||
};
|
||||
pub use llm::{
|
||||
CostRates, CredentialRef, CredentialRefParseError, HeaderValueRef, LlmLayer, ModelControls,
|
||||
ModelCostTable, ModelFeatures as LlmModelFeatures, ModelLimits as LlmModelLimits,
|
||||
|
|
@ -24,14 +30,13 @@ pub use log_filter::LogFilter;
|
|||
pub use maps::{MergeMap, ReplaceMap, StickyMap};
|
||||
pub use project::ProjectLayer;
|
||||
pub use run::{
|
||||
DaytonaDockerfileLayer, DaytonaSandboxLayer, DaytonaSnapshotLayer, DaytonaVolumeLayer,
|
||||
DockerSandboxLayer, GitAuthorLayer, HookAgentMarker, HookEntry, HookTlsMode,
|
||||
InterviewProviderLayer, InterviewsLayer, McpEntryLayer, ModelRefOrSplice,
|
||||
NotificationProviderLayer, NotificationRouteLayer, PrepareStep, RunAgentLayer,
|
||||
RunArtifactsLayer, RunCheckpointLayer, RunCloneLayer, RunExecutionLayer, RunGitLayer,
|
||||
RunGoalLayer, RunIntegrationsGithubLayer, RunIntegrationsLayer, RunLayer, RunMetaBranchLayer,
|
||||
RunModelControlsLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer,
|
||||
RunSandboxLayer, RunScmLayer, ScmGitHubLayer, StringOrSplice,
|
||||
GitAuthorLayer, HookAgentMarker, HookEntry, HookTlsMode, InterviewProviderLayer,
|
||||
InterviewsLayer, McpEntryLayer, ModelRefOrSplice, NotificationProviderLayer,
|
||||
NotificationRouteLayer, PrepareStep, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer,
|
||||
RunCloneLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer,
|
||||
RunIntegrationsLayer, RunLayer, RunMetaBranchLayer, RunModelControlsLayer, RunModelLayer,
|
||||
RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer, RunScmLayer, ScmGitHubLayer,
|
||||
StringOrSplice,
|
||||
};
|
||||
pub use server::{
|
||||
GithubIntegrationLayer, IntegrationWebhooksLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer,
|
||||
|
|
@ -40,5 +45,5 @@ pub use server::{
|
|||
ServerListenLayer, ServerLoggingLayer, ServerSchedulerLayer, ServerSlateDbLayer,
|
||||
ServerStorageLayer, ServerWebLayer, SlackIntegrationLayer,
|
||||
};
|
||||
pub(crate) use settings::SettingsLayer;
|
||||
pub use settings::SettingsLayer;
|
||||
pub use workflow::WorkflowLayer;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use fabro_types::settings::run::{
|
||||
AgentPermissions, ApprovalMode, DaytonaNetworkLayer, HookEvent, MergeStrategy, RunMode,
|
||||
AgentPermissions, ApprovalMode, HookEvent, MergeStrategy, RunMode,
|
||||
};
|
||||
use fabro_types::settings::{Duration, InterpString, ModelRef, Size};
|
||||
use fabro_types::settings::{Duration, InterpString, ModelRef};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::combine::Combine;
|
||||
use super::environment::RunEnvironmentLayer;
|
||||
use super::maps::{MergeMap, ReplaceMap, StickyMap};
|
||||
use super::splice_array::SPLICE_MARKER;
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ pub struct RunLayer {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub meta_branch: Option<RunMetaBranchLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sandbox: Option<RunSandboxLayer>,
|
||||
pub environment: Option<RunEnvironmentLayer>,
|
||||
#[serde(default, skip_serializing_if = "MergeMap::is_empty")]
|
||||
pub notifications: MergeMap<NotificationRouteLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -314,90 +315,6 @@ pub struct RunMetaBranchLayer {
|
|||
pub push: Option<bool>,
|
||||
}
|
||||
|
||||
/// `[run.sandbox]` — sandbox selection and execution-environment surface.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RunSandboxLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub preserve: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub stop_on_terminal: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub devcontainer: Option<bool>,
|
||||
/// Sticky merge-by-key across layers.
|
||||
#[serde(default, skip_serializing_if = "StickyMap::is_empty")]
|
||||
pub env: StickyMap<InterpString>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub docker: Option<DockerSandboxLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub daytona: Option<DaytonaSandboxLayer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DockerSandboxLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub image: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub network_mode: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub memory_limit: Option<Size>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cpu_quota: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "StickyMap::is_empty")]
|
||||
pub env_vars: StickyMap<InterpString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DaytonaSandboxLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub auto_stop_interval: Option<i32>,
|
||||
/// Sticky merge-by-key (provider-native labels).
|
||||
#[serde(default, skip_serializing_if = "StickyMap::is_empty")]
|
||||
pub labels: StickyMap<String>,
|
||||
/// Existing Daytona volumes to mount when creating the sandbox.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub volumes: Option<Vec<DaytonaVolumeLayer>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub snapshot: Option<DaytonaSnapshotLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub network: Option<DaytonaNetworkLayer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DaytonaVolumeLayer {
|
||||
pub volume_id: String,
|
||||
pub mount_path: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub subpath: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DaytonaSnapshotLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cpu: Option<i32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub memory: Option<Size>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub disk: Option<Size>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub dockerfile: Option<DaytonaDockerfileLayer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
pub enum DaytonaDockerfileLayer {
|
||||
Inline(String),
|
||||
Path { path: String },
|
||||
}
|
||||
|
||||
/// `[run.notifications.<name>]` — a keyed notification route.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ use std::str::FromStr;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::cli::CliLayer;
|
||||
use super::environment::EnvironmentLayer;
|
||||
use super::llm::LlmLayer;
|
||||
use super::maps::MergeMap;
|
||||
use super::project::ProjectLayer;
|
||||
use super::run::RunLayer;
|
||||
use super::server::ServerLayer;
|
||||
|
|
@ -19,21 +21,23 @@ use crate::parse::{ParseError, parse_settings};
|
|||
|
||||
/// A sparse settings layer before merge/resolve.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
pub(crate) struct SettingsLayer {
|
||||
pub struct SettingsLayer {
|
||||
#[serde(default, rename = "_version", skip_serializing_if = "Option::is_none")]
|
||||
pub version: Option<u32>,
|
||||
pub version: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub project: Option<ProjectLayer>,
|
||||
pub project: Option<ProjectLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub workflow: Option<WorkflowLayer>,
|
||||
pub workflow: Option<WorkflowLayer>,
|
||||
#[serde(default, skip_serializing_if = "MergeMap::is_empty")]
|
||||
pub environments: MergeMap<EnvironmentLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub run: Option<RunLayer>,
|
||||
pub run: Option<RunLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cli: Option<CliLayer>,
|
||||
pub cli: Option<CliLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub server: Option<ServerLayer>,
|
||||
pub server: Option<ServerLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub llm: Option<LlmLayer>,
|
||||
pub llm: Option<LlmLayer>,
|
||||
}
|
||||
|
||||
impl FromStr for SettingsLayer {
|
||||
|
|
|
|||
|
|
@ -38,26 +38,27 @@ pub use error::{Error, Result};
|
|||
pub use fabro_util::path::expand_tilde;
|
||||
pub use home::Home;
|
||||
pub use input_overrides::{InputOverrideParseError, parse_input_overrides, parse_labels};
|
||||
pub(crate) use layers::Combine;
|
||||
pub use layers::{
|
||||
CliAuthLayer, CliExecAgentLayer, CliExecLayer, CliExecModelLayer, CliLayer, CliLoggingLayer,
|
||||
CliOutputLayer, CliTargetLayer, CliUpdatesLayer, CostRates, CredentialRef,
|
||||
CredentialRefParseError, DaytonaDockerfileLayer, DaytonaSandboxLayer, DaytonaSnapshotLayer,
|
||||
DaytonaVolumeLayer, DockerSandboxLayer, GitAuthorLayer, GithubIntegrationLayer, HeaderValueRef,
|
||||
CredentialRefParseError, EnvironmentDockerfileLayer, EnvironmentImageLayer, EnvironmentLayer,
|
||||
EnvironmentLifecycleLayer, EnvironmentNetworkLayer, EnvironmentResourcesLayer,
|
||||
EnvironmentVolumeLayer, GitAuthorLayer, GithubIntegrationLayer, HeaderValueRef,
|
||||
HookAgentMarker, HookEntry, HookTlsMode, IntegrationWebhooksLayer, InterviewProviderLayer,
|
||||
InterviewsLayer, LlmLayer, LlmModelFeatures, LlmModelLimits, LogFilter, McpEntryLayer,
|
||||
MergeMap, ModelControls, ModelCostTable, ModelRefOrSplice, ModelSettings,
|
||||
NotificationProviderLayer, NotificationRouteLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer,
|
||||
PrepareStep, ProjectLayer, ProviderSettings, ReasoningEffortFeature, ReplaceMap, RunAgentLayer,
|
||||
RunArtifactsLayer, RunCheckpointLayer, RunCloneLayer, RunExecutionLayer, RunGitLayer,
|
||||
RunGoalLayer, RunIntegrationsGithubLayer, RunIntegrationsLayer, RunLayer, RunMetaBranchLayer,
|
||||
RunModelControlsLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer,
|
||||
RunSandboxLayer, RunScmLayer, ScmGitHubLayer, ServerApiLayer, ServerArtifactsLayer,
|
||||
RunArtifactsLayer, RunCheckpointLayer, RunCloneLayer, RunEnvironmentLayer, RunExecutionLayer,
|
||||
RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer, RunIntegrationsLayer, RunLayer,
|
||||
RunMetaBranchLayer, RunModelControlsLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer,
|
||||
RunRunBranchLayer, RunScmLayer, ScmGitHubLayer, ServerApiLayer, ServerArtifactsLayer,
|
||||
ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerIpAllowlistLayer,
|
||||
ServerIpAllowlistOverrideLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer,
|
||||
ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer,
|
||||
ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer, SettingsLayer,
|
||||
SlackIntegrationLayer, StickyMap, StringOrSplice, WorkflowLayer,
|
||||
};
|
||||
pub(crate) use layers::{Combine, SettingsLayer};
|
||||
pub use logging::{resolve_log_destination, resolve_log_destination_with_env};
|
||||
pub use parse::ParseError;
|
||||
pub use resolve::{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,14 @@ use crate::SettingsLayer;
|
|||
const CURRENT_VERSION: u32 = 1;
|
||||
|
||||
const ALLOWED_TOP_LEVEL_KEYS: &[&str] = &[
|
||||
"_version", "project", "workflow", "run", "cli", "server", "llm",
|
||||
"_version",
|
||||
"project",
|
||||
"workflow",
|
||||
"environments",
|
||||
"run",
|
||||
"cli",
|
||||
"server",
|
||||
"llm",
|
||||
];
|
||||
|
||||
/// Legacy `[llm]` keys that pre-date the settings-driven catalog plan and
|
||||
|
|
@ -38,7 +45,7 @@ impl fmt::Display for ParseError {
|
|||
} else {
|
||||
write!(
|
||||
f,
|
||||
"unknown top-level settings key `{key}`: expected one of `_version`, `project`, `workflow`, `run`, `cli`, `server`, `llm`"
|
||||
"unknown top-level settings key `{key}`: expected one of `_version`, `project`, `workflow`, `environments`, `run`, `cli`, `server`, `llm`"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -128,7 +135,7 @@ fn rename_hint(key: &str) -> Option<String> {
|
|||
"llm" => "rename to `[run.model]`",
|
||||
"vars" => "rename to `[run.inputs]`",
|
||||
"setup" => "rename to `[run.prepare]`",
|
||||
"sandbox" => "move under `[run.sandbox]`",
|
||||
"sandbox" => "rename to `[run.environment]` and `[environments.<slug>]`",
|
||||
"checkpoint" => "move under `[run.checkpoint]`",
|
||||
"pull_request" => "move under `[run.pull_request]`",
|
||||
"artifacts" => "move under `[run.artifacts]`",
|
||||
|
|
|
|||
335
lib/crates/fabro-config/src/resolve/environment.rs
Normal file
335
lib/crates/fabro-config/src/resolve/environment.rs
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use fabro_types::settings::run::{
|
||||
DockerfileSource, EnvironmentImageSettings, EnvironmentLifecycleSettings,
|
||||
EnvironmentNetworkMode, EnvironmentNetworkSettings, EnvironmentProvider,
|
||||
EnvironmentResourcesSettings, EnvironmentSettings, EnvironmentVolumeSettings,
|
||||
RunEnvironmentSettings,
|
||||
};
|
||||
|
||||
use super::ResolveError;
|
||||
use crate::{
|
||||
EnvironmentDockerfileLayer, EnvironmentImageLayer, EnvironmentLayer, EnvironmentLifecycleLayer,
|
||||
EnvironmentNetworkLayer, EnvironmentResourcesLayer, EnvironmentVolumeLayer, MergeMap,
|
||||
RunEnvironmentLayer,
|
||||
};
|
||||
|
||||
pub(crate) fn resolve_environments(
|
||||
layers: &MergeMap<EnvironmentLayer>,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) -> HashMap<String, EnvironmentSettings> {
|
||||
layers
|
||||
.iter()
|
||||
.map(|(slug, layer)| {
|
||||
let path = format!("environments.{slug}");
|
||||
(
|
||||
slug.clone(),
|
||||
resolve_environment_layer(layer, &path, errors),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_run_environment(
|
||||
layer: Option<&RunEnvironmentLayer>,
|
||||
environments: &HashMap<String, EnvironmentSettings>,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) -> RunEnvironmentSettings {
|
||||
let layer = layer.expect("defaults.toml should provide run.environment defaults");
|
||||
let id = layer.id.clone().unwrap_or_else(|| {
|
||||
errors.push(ResolveError::Missing {
|
||||
path: "run.environment.id".to_string(),
|
||||
});
|
||||
"default".to_string()
|
||||
});
|
||||
|
||||
let Some(base) = environments.get(&id) else {
|
||||
errors.push(ResolveError::Invalid {
|
||||
path: "run.environment.id".to_string(),
|
||||
reason: format!("unknown environment: {id}"),
|
||||
});
|
||||
return RunEnvironmentSettings::from_environment(id, EnvironmentSettings::default());
|
||||
};
|
||||
|
||||
let mut environment = base.clone();
|
||||
apply_run_environment_overrides(&mut environment, layer, errors);
|
||||
validate_provider_capabilities(&environment, "run.environment", errors);
|
||||
RunEnvironmentSettings::from_environment(id, environment)
|
||||
}
|
||||
|
||||
fn resolve_environment_layer(
|
||||
layer: &EnvironmentLayer,
|
||||
path: &str,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) -> EnvironmentSettings {
|
||||
let provider = if let Some(raw) = layer.provider.as_deref() {
|
||||
parse_provider(raw, &format!("{path}.provider"), errors)
|
||||
} else {
|
||||
errors.push(ResolveError::Missing {
|
||||
path: format!("{path}.provider"),
|
||||
});
|
||||
EnvironmentProvider::Local
|
||||
};
|
||||
|
||||
let environment = EnvironmentSettings {
|
||||
provider,
|
||||
image: resolve_image(layer.image.as_ref()),
|
||||
resources: resolve_resources(layer.resources.as_ref()),
|
||||
network: resolve_network(layer.network.as_ref(), &format!("{path}.network"), errors),
|
||||
lifecycle: resolve_lifecycle(layer.lifecycle.as_ref()),
|
||||
labels: layer.labels.clone().into_inner(),
|
||||
volumes: resolve_volumes(layer.volumes.as_deref()),
|
||||
env: layer.env.clone().into_inner(),
|
||||
};
|
||||
validate_daytona_snapshot_name(&environment, path, errors);
|
||||
environment
|
||||
}
|
||||
|
||||
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 resolve_image(layer: Option<&EnvironmentImageLayer>) -> EnvironmentImageSettings {
|
||||
let Some(layer) = layer else {
|
||||
return EnvironmentImageSettings::default();
|
||||
};
|
||||
EnvironmentImageSettings {
|
||||
reference: layer.reference.clone(),
|
||||
dockerfile: layer.dockerfile.as_ref().map(dockerfile_source),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_resources(layer: Option<&EnvironmentResourcesLayer>) -> EnvironmentResourcesSettings {
|
||||
let Some(layer) = layer else {
|
||||
return EnvironmentResourcesSettings::default();
|
||||
};
|
||||
EnvironmentResourcesSettings {
|
||||
cpu: layer.cpu,
|
||||
memory: layer.memory,
|
||||
disk: layer.disk,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_network(
|
||||
layer: Option<&EnvironmentNetworkLayer>,
|
||||
path: &str,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) -> EnvironmentNetworkSettings {
|
||||
let Some(layer) = layer else {
|
||||
return EnvironmentNetworkSettings::default();
|
||||
};
|
||||
|
||||
for (index, cidr) in layer.allow.iter().enumerate() {
|
||||
if cidr.parse::<ipnet::IpNet>().is_err() {
|
||||
errors.push(ResolveError::Invalid {
|
||||
path: format!("{path}.allow[{index}]"),
|
||||
reason: format!("invalid CIDR: {cidr}"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mode = match layer.mode.as_deref() {
|
||||
Some(raw) => parse_network_mode(raw, &format!("{path}.mode"), errors),
|
||||
None if layer.allow.is_empty() => EnvironmentNetworkMode::AllowAll,
|
||||
None => EnvironmentNetworkMode::CidrAllowList,
|
||||
};
|
||||
|
||||
EnvironmentNetworkSettings {
|
||||
mode,
|
||||
allow: layer.allow.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_network_mode(
|
||||
raw: &str,
|
||||
path: &str,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) -> EnvironmentNetworkMode {
|
||||
if let Ok(mode) = raw.parse::<EnvironmentNetworkMode>() {
|
||||
mode
|
||||
} else {
|
||||
errors.push(ResolveError::Invalid {
|
||||
path: path.to_string(),
|
||||
reason: format!("unknown environment network mode: {raw}"),
|
||||
});
|
||||
EnvironmentNetworkMode::AllowAll
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_lifecycle(layer: Option<&EnvironmentLifecycleLayer>) -> EnvironmentLifecycleSettings {
|
||||
let Some(layer) = layer else {
|
||||
return EnvironmentLifecycleSettings::default();
|
||||
};
|
||||
EnvironmentLifecycleSettings {
|
||||
preserve: layer.preserve.unwrap_or(false),
|
||||
stop_on_terminal: layer.stop_on_terminal.unwrap_or(true),
|
||||
auto_stop: layer.auto_stop,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_volumes(layers: Option<&[EnvironmentVolumeLayer]>) -> Vec<EnvironmentVolumeSettings> {
|
||||
layers
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.map(|volume| EnvironmentVolumeSettings {
|
||||
id: volume.id.clone(),
|
||||
mount_path: volume.mount_path.clone(),
|
||||
subpath: volume.subpath.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn apply_run_environment_overrides(
|
||||
environment: &mut EnvironmentSettings,
|
||||
layer: &RunEnvironmentLayer,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) {
|
||||
if let Some(image) = layer.image.as_ref() {
|
||||
apply_image_override(&mut environment.image, image);
|
||||
}
|
||||
if let Some(resources) = layer.resources.as_ref() {
|
||||
apply_resources_override(&mut environment.resources, resources);
|
||||
}
|
||||
if let Some(network) = layer.network.as_ref() {
|
||||
apply_network_override(
|
||||
&mut environment.network,
|
||||
network,
|
||||
"run.environment.network",
|
||||
errors,
|
||||
);
|
||||
}
|
||||
if let Some(lifecycle) = layer.lifecycle.as_ref() {
|
||||
apply_lifecycle_override(&mut environment.lifecycle, lifecycle);
|
||||
}
|
||||
environment.labels.extend(layer.labels.clone().into_inner());
|
||||
if let Some(volumes) = layer.volumes.as_deref() {
|
||||
environment.volumes = resolve_volumes(Some(volumes));
|
||||
}
|
||||
environment.env.extend(layer.env.clone().into_inner());
|
||||
}
|
||||
|
||||
fn apply_image_override(target: &mut EnvironmentImageSettings, layer: &EnvironmentImageLayer) {
|
||||
if let Some(reference) = layer.reference.as_ref() {
|
||||
target.reference = Some(reference.clone());
|
||||
}
|
||||
if let Some(dockerfile) = layer.dockerfile.as_ref() {
|
||||
target.dockerfile = Some(dockerfile_source(dockerfile));
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_resources_override(
|
||||
target: &mut EnvironmentResourcesSettings,
|
||||
layer: &EnvironmentResourcesLayer,
|
||||
) {
|
||||
if layer.cpu.is_some() {
|
||||
target.cpu = layer.cpu;
|
||||
}
|
||||
if layer.memory.is_some() {
|
||||
target.memory = layer.memory;
|
||||
}
|
||||
if layer.disk.is_some() {
|
||||
target.disk = layer.disk;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_network_override(
|
||||
target: &mut EnvironmentNetworkSettings,
|
||||
layer: &EnvironmentNetworkLayer,
|
||||
path: &str,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) {
|
||||
for (index, cidr) in layer.allow.iter().enumerate() {
|
||||
if cidr.parse::<ipnet::IpNet>().is_err() {
|
||||
errors.push(ResolveError::Invalid {
|
||||
path: format!("{path}.allow[{index}]"),
|
||||
reason: format!("invalid CIDR: {cidr}"),
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(raw) = layer.mode.as_deref() {
|
||||
target.mode = parse_network_mode(raw, &format!("{path}.mode"), errors);
|
||||
}
|
||||
if !layer.allow.is_empty() {
|
||||
target.allow.clone_from(&layer.allow);
|
||||
}
|
||||
}
|
||||
|
||||
fn dockerfile_source(dockerfile: &EnvironmentDockerfileLayer) -> DockerfileSource {
|
||||
match dockerfile {
|
||||
EnvironmentDockerfileLayer::Inline(text) => DockerfileSource::Inline(text.clone()),
|
||||
EnvironmentDockerfileLayer::Path { path } => DockerfileSource::Path { path: path.clone() },
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_lifecycle_override(
|
||||
target: &mut EnvironmentLifecycleSettings,
|
||||
layer: &EnvironmentLifecycleLayer,
|
||||
) {
|
||||
if let Some(preserve) = layer.preserve {
|
||||
target.preserve = preserve;
|
||||
}
|
||||
if let Some(stop_on_terminal) = layer.stop_on_terminal {
|
||||
target.stop_on_terminal = stop_on_terminal;
|
||||
}
|
||||
if layer.auto_stop.is_some() {
|
||||
target.auto_stop = layer.auto_stop;
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_daytona_snapshot_name(
|
||||
environment: &EnvironmentSettings,
|
||||
path: &str,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) {
|
||||
if environment.provider == EnvironmentProvider::Daytona
|
||||
&& environment.image.dockerfile.is_some()
|
||||
&& environment.image.reference.is_none()
|
||||
{
|
||||
errors.push(ResolveError::Invalid {
|
||||
path: format!("{path}.image"),
|
||||
reason: "daytona environments with image.dockerfile must also set image.ref"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_provider_capabilities(
|
||||
environment: &EnvironmentSettings,
|
||||
path: &str,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) {
|
||||
validate_daytona_snapshot_name(environment, path, errors);
|
||||
match environment.provider {
|
||||
EnvironmentProvider::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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
EnvironmentProvider::Daytona => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
mod cli;
|
||||
mod environment;
|
||||
mod error;
|
||||
mod project;
|
||||
mod run;
|
||||
|
|
@ -6,6 +7,7 @@ mod server;
|
|||
mod workflow;
|
||||
|
||||
pub use cli::resolve_cli;
|
||||
pub(crate) use environment::{resolve_environments, resolve_run_environment};
|
||||
pub use error::ResolveError;
|
||||
use fabro_types::settings::InterpString;
|
||||
pub use project::resolve_project;
|
||||
|
|
|
|||
|
|
@ -1,26 +1,28 @@
|
|||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::run::{
|
||||
ArtifactsSettings, DaytonaSettings, DaytonaSnapshotSettings, DaytonaVolumeSettings,
|
||||
DockerSettings, DockerfileSource, GitAuthorSettings, HookDefinition, HookType,
|
||||
ArtifactsSettings, EnvironmentSettings, GitAuthorSettings, HookDefinition, HookType,
|
||||
InterviewProviderSettings, McpServerSettings, McpTransport, MergeStrategy,
|
||||
NotificationProviderSettings, NotificationRouteSettings, PullRequestSettings, RunAgentSettings,
|
||||
RunBranchSettings, RunCheckpointSettings, RunCloneSettings, RunExecutionSettings,
|
||||
RunGitSettings, RunGoal, RunIntegrationsGithubSettings, RunIntegrationsSettings,
|
||||
RunInterviewsSettings, RunMetaBranchSettings, RunModelControls, RunModelSettings, RunNamespace,
|
||||
RunPrepareSettings, RunSandboxSettings, RunScmSettings, ScmGitHubSettings, TlsMode,
|
||||
RunPrepareSettings, RunScmSettings, ScmGitHubSettings, TlsMode,
|
||||
};
|
||||
|
||||
use super::ResolveError;
|
||||
use super::{ResolveError, resolve_run_environment};
|
||||
use crate::{
|
||||
DaytonaDockerfileLayer, DaytonaSandboxLayer, HookAgentMarker, HookEntry, HookTlsMode,
|
||||
InterviewProviderLayer, InterviewsLayer, McpEntryLayer, ModelRefOrSplice,
|
||||
NotificationProviderLayer, NotificationRouteLayer, RunAgentLayer, RunArtifactsLayer,
|
||||
RunCheckpointLayer, RunCloneLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer,
|
||||
RunIntegrationsLayer, RunLayer, RunMetaBranchLayer, RunModelLayer, RunPrepareLayer,
|
||||
RunPullRequestLayer, RunRunBranchLayer, RunSandboxLayer, RunScmLayer, StringOrSplice,
|
||||
HookAgentMarker, HookEntry, HookTlsMode, InterviewProviderLayer, InterviewsLayer,
|
||||
McpEntryLayer, ModelRefOrSplice, NotificationProviderLayer, NotificationRouteLayer,
|
||||
RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer, RunCloneLayer, RunExecutionLayer,
|
||||
RunGitLayer, RunGoalLayer, RunIntegrationsLayer, RunLayer, RunMetaBranchLayer, RunModelLayer,
|
||||
RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer, RunScmLayer, StringOrSplice,
|
||||
};
|
||||
|
||||
pub fn resolve_run(layer: &RunLayer, errors: &mut Vec<ResolveError>) -> RunNamespace {
|
||||
pub fn resolve_run(
|
||||
layer: &RunLayer,
|
||||
environments: &std::collections::HashMap<String, EnvironmentSettings>,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) -> RunNamespace {
|
||||
let clone = resolve_clone(layer.clone.as_ref());
|
||||
let run_branch = resolve_run_branch(layer.run_branch.as_ref());
|
||||
let mut meta_branch = resolve_meta_branch(layer.meta_branch.as_ref());
|
||||
|
|
@ -59,7 +61,7 @@ pub fn resolve_run(layer: &RunLayer, errors: &mut Vec<ResolveError>) -> RunNames
|
|||
clone,
|
||||
run_branch,
|
||||
meta_branch,
|
||||
sandbox: resolve_sandbox(layer.sandbox.as_ref(), errors),
|
||||
environment: resolve_run_environment(layer.environment.as_ref(), environments, errors),
|
||||
notifications: layer
|
||||
.notifications
|
||||
.iter()
|
||||
|
|
@ -221,91 +223,6 @@ fn resolve_meta_branch(meta_branch: Option<&RunMetaBranchLayer>) -> RunMetaBranc
|
|||
}
|
||||
}
|
||||
|
||||
fn resolve_sandbox(
|
||||
sandbox: Option<&RunSandboxLayer>,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) -> RunSandboxSettings {
|
||||
let sandbox = sandbox.expect("defaults.toml should provide run.sandbox defaults");
|
||||
|
||||
let provider = sandbox
|
||||
.provider
|
||||
.clone()
|
||||
.expect("defaults.toml should provide run.sandbox.provider");
|
||||
match provider.as_str() {
|
||||
"local" | "docker" | "daytona" => {}
|
||||
other => errors.push(ResolveError::Invalid {
|
||||
path: "run.sandbox.provider".to_string(),
|
||||
reason: format!("unknown sandbox provider: {other}"),
|
||||
}),
|
||||
}
|
||||
|
||||
RunSandboxSettings {
|
||||
provider,
|
||||
preserve: sandbox
|
||||
.preserve
|
||||
.expect("defaults.toml should provide run.sandbox.preserve"),
|
||||
stop_on_terminal: sandbox
|
||||
.stop_on_terminal
|
||||
.expect("defaults.toml should provide run.sandbox.stop_on_terminal"),
|
||||
devcontainer: sandbox
|
||||
.devcontainer
|
||||
.expect("defaults.toml should provide run.sandbox.devcontainer"),
|
||||
env: sandbox.env.clone().into_inner(),
|
||||
docker: sandbox.docker.as_ref().map(resolve_docker),
|
||||
daytona: sandbox.daytona.as_ref().map(resolve_daytona),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_docker(docker: &crate::DockerSandboxLayer) -> DockerSettings {
|
||||
DockerSettings {
|
||||
image: docker.image.clone().unwrap_or_default(),
|
||||
network_mode: docker.network_mode.clone(),
|
||||
memory_limit: docker
|
||||
.memory_limit
|
||||
.and_then(|size| i64::try_from(size.as_bytes()).ok()),
|
||||
cpu_quota: docker.cpu_quota,
|
||||
env_vars: docker.env_vars.clone().into_inner(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_daytona(daytona: &DaytonaSandboxLayer) -> DaytonaSettings {
|
||||
DaytonaSettings {
|
||||
auto_stop_interval: daytona.auto_stop_interval,
|
||||
labels: daytona.labels.clone().into_inner(),
|
||||
volumes: daytona
|
||||
.volumes
|
||||
.as_deref()
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.map(|volume| DaytonaVolumeSettings {
|
||||
volume_id: volume.volume_id.clone(),
|
||||
mount_path: volume.mount_path.clone(),
|
||||
subpath: volume.subpath.clone(),
|
||||
})
|
||||
.collect(),
|
||||
snapshot: daytona.snapshot.as_ref().and_then(|snapshot| {
|
||||
snapshot.name.as_ref().map(|name| DaytonaSnapshotSettings {
|
||||
name: name.clone(),
|
||||
cpu: snapshot.cpu,
|
||||
memory_gb: snapshot.memory.map(|size| size_to_gb_i32(size.as_bytes())),
|
||||
disk_gb: snapshot.disk.map(|size| size_to_gb_i32(size.as_bytes())),
|
||||
dockerfile: snapshot
|
||||
.dockerfile
|
||||
.as_ref()
|
||||
.map(|dockerfile| match dockerfile {
|
||||
DaytonaDockerfileLayer::Inline(text) => {
|
||||
DockerfileSource::Inline(text.clone())
|
||||
}
|
||||
DaytonaDockerfileLayer::Path { path } => {
|
||||
DockerfileSource::Path { path: path.clone() }
|
||||
}
|
||||
}),
|
||||
})
|
||||
}),
|
||||
network: daytona.network.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_notification_route(route: &NotificationRouteLayer) -> NotificationRouteSettings {
|
||||
NotificationRouteSettings {
|
||||
enabled: route.enabled.unwrap_or(false),
|
||||
|
|
@ -570,8 +487,3 @@ fn resolve_artifacts(artifacts: Option<&RunArtifactsLayer>) -> ArtifactsSettings
|
|||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn size_to_gb_i32(bytes: u64) -> i32 {
|
||||
let gb = bytes / 1_000_000_000;
|
||||
i32::try_from(gb).unwrap_or(i32::MAX)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,30 +33,38 @@ a = "higher"
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn run_sandbox_env_merges_sticky() {
|
||||
fn run_environment_env_merges_sticky() {
|
||||
let lower = parse(
|
||||
r#"
|
||||
[run.sandbox.env]
|
||||
[run.environment.env]
|
||||
A = "lower-a"
|
||||
B = "lower-b"
|
||||
"#,
|
||||
);
|
||||
let higher = parse(
|
||||
r#"
|
||||
[run.sandbox.env]
|
||||
[run.environment.env]
|
||||
A = "higher-a"
|
||||
C = "higher-c"
|
||||
"#,
|
||||
);
|
||||
let merged = higher.combine(lower);
|
||||
let sandbox = merged.run.unwrap().sandbox.unwrap();
|
||||
assert_eq!(sandbox.env.len(), 3);
|
||||
let environment = merged.run.unwrap().environment.unwrap();
|
||||
assert_eq!(environment.env.len(), 3);
|
||||
assert_eq!(
|
||||
sandbox.env.get("A").map(InterpString::as_source).as_deref(),
|
||||
environment
|
||||
.env
|
||||
.get("A")
|
||||
.map(InterpString::as_source)
|
||||
.as_deref(),
|
||||
Some("higher-a")
|
||||
);
|
||||
assert_eq!(
|
||||
sandbox.env.get("B").map(InterpString::as_source).as_deref(),
|
||||
environment
|
||||
.env
|
||||
.get("B")
|
||||
.map(InterpString::as_source)
|
||||
.as_deref(),
|
||||
Some("lower-b")
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,8 +58,15 @@ fn apply_builtin_defaults_materializes_expected_layer() {
|
|||
layer
|
||||
.run
|
||||
.as_ref()
|
||||
.and_then(|run| run.sandbox.as_ref())
|
||||
.and_then(|sandbox| sandbox.provider.as_deref()),
|
||||
.and_then(|run| run.environment.as_ref())
|
||||
.and_then(|environment| environment.id.as_deref()),
|
||||
Some("default")
|
||||
);
|
||||
assert_eq!(
|
||||
layer
|
||||
.environments
|
||||
.get("default")
|
||||
.and_then(|environment| environment.provider.as_deref()),
|
||||
Some("docker")
|
||||
);
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -35,7 +35,10 @@ methods = ["github"]
|
|||
[server.auth.github]
|
||||
allowed_usernames = []
|
||||
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "bad"
|
||||
|
||||
[environments.bad]
|
||||
provider = "not-a-provider"
|
||||
"#;
|
||||
|
||||
|
|
@ -64,7 +67,7 @@ provider = "not-a-provider"
|
|||
|
||||
assert!(rendered.contains("server.listen.address"));
|
||||
assert!(rendered.contains("server.auth.github.allowed_usernames"));
|
||||
assert!(rendered.contains("run.sandbox.provider"));
|
||||
assert!(rendered.contains("environments.bad.provider"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -165,12 +168,15 @@ shared = "run"
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn workflow_settings_report_invalid_run_sandbox_provider() {
|
||||
fn workflow_settings_report_invalid_environment_provider() {
|
||||
let errors = match fabro_config::WorkflowSettingsBuilder::from_toml(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "bad"
|
||||
|
||||
[environments.bad]
|
||||
provider = "not-a-provider"
|
||||
"#,
|
||||
)
|
||||
|
|
@ -183,7 +189,7 @@ provider = "not-a-provider"
|
|||
assert!(errors.iter().any(|error| {
|
||||
matches!(
|
||||
error,
|
||||
fabro_config::ResolveError::Invalid { path, .. } if path == "run.sandbox.provider"
|
||||
fabro_config::ResolveError::Invalid { path, .. } if path == "environments.bad.provider"
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
|
@ -194,7 +200,10 @@ fn workflow_settings_accumulate_multiple_run_errors() {
|
|||
r#"
|
||||
_version = 1
|
||||
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "bad"
|
||||
|
||||
[environments.bad]
|
||||
provider = "not-a-provider"
|
||||
|
||||
[[run.prepare.steps]]
|
||||
|
|
@ -205,6 +214,6 @@ command = ["echo", "hi"]
|
|||
.expect_err("invalid workflow settings should fail")
|
||||
.to_string();
|
||||
|
||||
assert!(rendered.contains("run.sandbox.provider"));
|
||||
assert!(rendered.contains("environments.bad.provider"));
|
||||
assert!(rendered.contains("run.prepare.steps[0]"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::run::{ApprovalMode, RunGoal, RunMode};
|
||||
use fabro_types::settings::run::{
|
||||
ApprovalMode, EnvironmentNetworkMode, EnvironmentProvider, RunGoal, RunMode,
|
||||
};
|
||||
|
||||
use crate::{SettingsLayer, WorkflowSettingsBuilder};
|
||||
|
||||
|
|
@ -43,16 +45,23 @@ fn resolves_run_defaults_from_empty_settings() {
|
|||
assert_eq!(settings.execution.mode, RunMode::Normal);
|
||||
assert_eq!(settings.execution.approval, ApprovalMode::Prompt);
|
||||
assert_eq!(settings.prepare.timeout_ms, 300_000);
|
||||
assert_eq!(settings.sandbox.provider, "docker");
|
||||
assert!(settings.sandbox.stop_on_terminal);
|
||||
let docker = settings
|
||||
.sandbox
|
||||
.docker
|
||||
.as_ref()
|
||||
.expect("defaults should provide docker settings");
|
||||
assert_eq!(docker.image, "buildpack-deps:noble");
|
||||
assert_eq!(docker.memory_limit, Some(4_000_000_000));
|
||||
assert_eq!(docker.cpu_quota, Some(200_000));
|
||||
assert_eq!(settings.environment.id, "default");
|
||||
assert_eq!(settings.environment.provider, EnvironmentProvider::Docker);
|
||||
assert_eq!(
|
||||
settings.environment.image.reference.as_deref(),
|
||||
Some("buildpack-deps:noble")
|
||||
);
|
||||
assert_eq!(settings.environment.resources.cpu, Some(2));
|
||||
assert_eq!(
|
||||
settings
|
||||
.environment
|
||||
.resources
|
||||
.memory
|
||||
.map(|size| size.as_bytes()),
|
||||
Some(4_000_000_000)
|
||||
);
|
||||
assert!(!settings.environment.lifecycle.preserve);
|
||||
assert!(settings.environment.lifecycle.stop_on_terminal);
|
||||
assert!(settings.clone.enabled);
|
||||
assert!(settings.run_branch.enabled);
|
||||
assert!(settings.run_branch.push);
|
||||
|
|
@ -62,33 +71,98 @@ fn resolves_run_defaults_from_empty_settings() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_daytona_volume_mounts() {
|
||||
fn resolves_named_daytona_environment_and_run_overrides() {
|
||||
let settings = WorkflowSettingsBuilder::from_toml(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "fabro-dev"
|
||||
|
||||
[run.environment.resources]
|
||||
memory = "32GB"
|
||||
|
||||
[run.environment.lifecycle]
|
||||
preserve = true
|
||||
|
||||
[environments.fabro-dev]
|
||||
provider = "daytona"
|
||||
|
||||
[[run.sandbox.daytona.volumes]]
|
||||
volume_id = "vol_auth"
|
||||
[environments.fabro-dev.image]
|
||||
ref = "fabro-v11"
|
||||
|
||||
[environments.fabro-dev.resources]
|
||||
cpu = 8
|
||||
memory = "16GB"
|
||||
disk = "20GB"
|
||||
|
||||
[environments.fabro-dev.network]
|
||||
mode = "cidr_allow_list"
|
||||
allow = ["10.0.0.0/8"]
|
||||
|
||||
[environments.fabro-dev.lifecycle]
|
||||
preserve = false
|
||||
stop_on_terminal = true
|
||||
auto_stop = "30m"
|
||||
|
||||
[environments.fabro-dev.labels]
|
||||
repo = "fabro-sh/fabro"
|
||||
|
||||
[[environments.fabro-dev.volumes]]
|
||||
id = "vol_auth"
|
||||
mount_path = "/home/daytona/.config"
|
||||
subpath = "agents"
|
||||
|
||||
[environments.fabro-dev.env]
|
||||
NODE_ENV = "development"
|
||||
"#,
|
||||
)
|
||||
.expect("daytona volume mount should resolve")
|
||||
.run;
|
||||
.expect("daytona environment should resolve");
|
||||
|
||||
let daytona = settings
|
||||
.sandbox
|
||||
.daytona
|
||||
.as_ref()
|
||||
.expect("daytona settings should resolve");
|
||||
assert!(settings.environments.contains_key("fabro-dev"));
|
||||
let environment = settings.run.environment;
|
||||
|
||||
assert_eq!(daytona.volumes.len(), 1);
|
||||
assert_eq!(daytona.volumes[0].volume_id, "vol_auth");
|
||||
assert_eq!(daytona.volumes[0].mount_path, "/home/daytona/.config");
|
||||
assert_eq!(daytona.volumes[0].subpath.as_deref(), Some("agents"));
|
||||
assert_eq!(environment.id, "fabro-dev");
|
||||
assert_eq!(environment.provider, EnvironmentProvider::Daytona);
|
||||
assert_eq!(environment.image.reference.as_deref(), Some("fabro-v11"));
|
||||
assert_eq!(environment.resources.cpu, Some(8));
|
||||
assert_eq!(
|
||||
environment.resources.memory.map(|size| size.as_bytes()),
|
||||
Some(32_000_000_000)
|
||||
);
|
||||
assert_eq!(
|
||||
environment.resources.disk.map(|size| size.as_bytes()),
|
||||
Some(20_000_000_000)
|
||||
);
|
||||
assert_eq!(
|
||||
environment.network.mode,
|
||||
EnvironmentNetworkMode::CidrAllowList
|
||||
);
|
||||
assert_eq!(environment.network.allow, vec!["10.0.0.0/8"]);
|
||||
assert!(environment.lifecycle.preserve);
|
||||
assert_eq!(
|
||||
environment
|
||||
.lifecycle
|
||||
.auto_stop
|
||||
.map(|duration| duration.as_std().as_secs()),
|
||||
Some(1800)
|
||||
);
|
||||
assert_eq!(
|
||||
environment.labels.get("repo").map(String::as_str),
|
||||
Some("fabro-sh/fabro")
|
||||
);
|
||||
assert_eq!(environment.volumes.len(), 1);
|
||||
assert_eq!(environment.volumes[0].id, "vol_auth");
|
||||
assert_eq!(environment.volumes[0].mount_path, "/home/daytona/.config");
|
||||
assert_eq!(environment.volumes[0].subpath.as_deref(), Some("agents"));
|
||||
assert_eq!(
|
||||
environment
|
||||
.env
|
||||
.get("NODE_ENV")
|
||||
.map(InterpString::as_source)
|
||||
.as_deref(),
|
||||
Some("development")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -181,19 +255,19 @@ enabled = true
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn provider_skip_clone_is_rejected() {
|
||||
let err = r"
|
||||
fn legacy_run_sandbox_is_rejected() {
|
||||
let err = r#"
|
||||
_version = 1
|
||||
|
||||
[run.sandbox.docker]
|
||||
skip_clone = true
|
||||
"
|
||||
[run.sandbox]
|
||||
provider = "local"
|
||||
"#
|
||||
.parse::<SettingsLayer>()
|
||||
.expect_err("provider-level skip_clone should be unknown");
|
||||
.expect_err("legacy run.sandbox should be unknown");
|
||||
let message = err.to_string();
|
||||
assert!(
|
||||
message.contains("skip_clone") || message.contains("unknown field"),
|
||||
"expected unknown-field error mentioning skip_clone, got: {message}"
|
||||
message.contains("sandbox") || message.contains("unknown field"),
|
||||
"expected unknown-field error mentioning sandbox, got: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -289,31 +363,131 @@ fn resolves_explicit_stop_on_terminal_false() {
|
|||
r"
|
||||
_version = 1
|
||||
|
||||
[run.sandbox]
|
||||
[run.environment.lifecycle]
|
||||
stop_on_terminal = false
|
||||
",
|
||||
)
|
||||
.expect("sandbox stop_on_terminal setting should resolve")
|
||||
.expect("environment stop_on_terminal setting should resolve")
|
||||
.run;
|
||||
|
||||
assert!(!settings.sandbox.stop_on_terminal);
|
||||
assert!(!settings.environment.lifecycle.stop_on_terminal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_minimal_local_provider_without_docker_table() {
|
||||
fn resolves_minimal_local_environment() {
|
||||
let settings = WorkflowSettingsBuilder::from_toml(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "host"
|
||||
|
||||
[environments.host]
|
||||
provider = "local"
|
||||
"#,
|
||||
)
|
||||
.expect("minimal local sandbox settings should resolve")
|
||||
.expect("minimal local environment settings should resolve")
|
||||
.run;
|
||||
|
||||
assert_eq!(settings.sandbox.provider, "local");
|
||||
assert!(settings.sandbox.docker.is_some());
|
||||
assert_eq!(settings.environment.id, "host");
|
||||
assert_eq!(settings.environment.provider, EnvironmentProvider::Local);
|
||||
assert!(settings.environment.image.reference.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_environment_slug_errors() {
|
||||
let err = WorkflowSettingsBuilder::from_toml(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[run.environment]
|
||||
id = "missing"
|
||||
"#,
|
||||
)
|
||||
.expect_err("missing selected environment should error");
|
||||
|
||||
let message = err.to_string();
|
||||
assert!(
|
||||
message.contains("run.environment.id") && message.contains("missing"),
|
||||
"expected missing environment diagnostic, got: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn docker_cidr_allow_list_errors() {
|
||||
let err = WorkflowSettingsBuilder::from_toml(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[run.environment]
|
||||
id = "locked"
|
||||
|
||||
[environments.locked]
|
||||
provider = "docker"
|
||||
|
||||
[environments.locked.network]
|
||||
mode = "cidr_allow_list"
|
||||
allow = ["10.0.0.0/8"]
|
||||
"#,
|
||||
)
|
||||
.expect_err("docker cannot enforce cidr allow list");
|
||||
|
||||
let message = err.to_string();
|
||||
assert!(
|
||||
message.contains("run.environment.network.mode") && message.contains("CIDR allow-list"),
|
||||
"expected docker CIDR capability diagnostic, got: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_blocked_network_errors() {
|
||||
let err = WorkflowSettingsBuilder::from_toml(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[run.environment]
|
||||
id = "host"
|
||||
|
||||
[environments.host]
|
||||
provider = "local"
|
||||
|
||||
[environments.host.network]
|
||||
mode = "block"
|
||||
"#,
|
||||
)
|
||||
.expect_err("local cannot enforce blocked networking");
|
||||
|
||||
let message = err.to_string();
|
||||
assert!(
|
||||
message.contains("run.environment.network.mode")
|
||||
&& message.contains("local environments cannot enforce"),
|
||||
"expected local blocked-network diagnostic, got: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn daytona_dockerfile_without_image_ref_errors() {
|
||||
let err = WorkflowSettingsBuilder::from_toml(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[run.environment]
|
||||
id = "cloud"
|
||||
|
||||
[environments.cloud]
|
||||
provider = "daytona"
|
||||
|
||||
[environments.cloud.image]
|
||||
dockerfile = { path = "Dockerfile" }
|
||||
"#,
|
||||
)
|
||||
.expect_err("daytona dockerfile needs a snapshot name");
|
||||
|
||||
let message = err.to_string();
|
||||
assert!(
|
||||
message.contains("image.ref"),
|
||||
"expected daytona dockerfile/image.ref diagnostic, got: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ use fabro_api::types;
|
|||
use fabro_config::project::{self, WorkflowLocation, discover_project_config};
|
||||
use fabro_config::run::{resolve_run_goal_from_layer, resolve_run_goal_from_namespace};
|
||||
use fabro_config::{
|
||||
CliLayer, DaytonaDockerfileLayer, DockerSandboxLayer, ReplaceMap, RunExecutionLayer,
|
||||
RunGoalLayer, RunLayer, RunModelLayer, RunSandboxLayer, WorkflowSettingsBuilder,
|
||||
CliLayer, EnvironmentDockerfileLayer, EnvironmentImageLayer, EnvironmentLifecycleLayer,
|
||||
ReplaceMap, RunEnvironmentLayer, RunExecutionLayer, RunGoalLayer, RunLayer, RunModelLayer,
|
||||
SettingsLayer, WorkflowSettingsBuilder,
|
||||
};
|
||||
use fabro_graphviz::graph::AttrValue;
|
||||
use fabro_graphviz::parser;
|
||||
|
|
@ -58,7 +59,7 @@ pub struct RunOverrideInput<'a> {
|
|||
pub goal: Option<&'a str>,
|
||||
pub model: Option<&'a str>,
|
||||
pub provider: Option<&'a str>,
|
||||
pub sandbox: Option<&'a str>,
|
||||
pub environment: Option<&'a str>,
|
||||
pub docker_image: Option<&'a str>,
|
||||
pub preserve_sandbox: Option<bool>,
|
||||
pub dry_run: Option<bool>,
|
||||
|
|
@ -77,17 +78,22 @@ pub fn build_run_overrides(input: RunOverrideInput<'_>) -> RunLayer {
|
|||
fallbacks: Vec::new(),
|
||||
controls: None,
|
||||
});
|
||||
let sandbox = (input.sandbox.is_some()
|
||||
let environment = (input.environment.is_some()
|
||||
|| input.docker_image.is_some()
|
||||
|| input.preserve_sandbox.is_some())
|
||||
.then(|| RunSandboxLayer {
|
||||
provider: input.sandbox.map(ToOwned::to_owned),
|
||||
docker: input.docker_image.map(|image| DockerSandboxLayer {
|
||||
image: Some(image.to_string()),
|
||||
..DockerSandboxLayer::default()
|
||||
.then(|| RunEnvironmentLayer {
|
||||
id: input.environment.map(ToOwned::to_owned),
|
||||
image: input.docker_image.map(|image| EnvironmentImageLayer {
|
||||
reference: Some(image.to_string()),
|
||||
..EnvironmentImageLayer::default()
|
||||
}),
|
||||
preserve: input.preserve_sandbox,
|
||||
..RunSandboxLayer::default()
|
||||
lifecycle: input
|
||||
.preserve_sandbox
|
||||
.map(|preserve| EnvironmentLifecycleLayer {
|
||||
preserve: Some(preserve),
|
||||
..EnvironmentLifecycleLayer::default()
|
||||
}),
|
||||
..RunEnvironmentLayer::default()
|
||||
});
|
||||
let execution =
|
||||
(input.dry_run.is_some() || input.auto_approve.is_some()).then(|| RunExecutionLayer {
|
||||
|
|
@ -111,7 +117,7 @@ pub fn build_run_overrides(input: RunOverrideInput<'_>) -> RunLayer {
|
|||
goal,
|
||||
metadata: ReplaceMap::from(input.labels),
|
||||
model,
|
||||
sandbox,
|
||||
environment,
|
||||
execution,
|
||||
..RunLayer::default()
|
||||
}
|
||||
|
|
@ -123,7 +129,7 @@ pub fn build_sparse_run_overrides(input: RunOverrideInput<'_>) -> Option<RunLaye
|
|||
(run.goal.is_some()
|
||||
|| !run.metadata.is_empty()
|
||||
|| run.model.is_some()
|
||||
|| run.sandbox.is_some()
|
||||
|| run.environment.is_some()
|
||||
|| run.execution.is_some())
|
||||
.then_some(run)
|
||||
}
|
||||
|
|
@ -558,29 +564,49 @@ fn collect_config_dockerfile(
|
|||
source: &str,
|
||||
files: &mut HashMap<String, types::ManifestFileEntry>,
|
||||
) -> Result<()> {
|
||||
let mut document: toml::Table = source.parse().context("Failed to parse run config TOML")?;
|
||||
let run = document
|
||||
.remove("run")
|
||||
.map(toml::Value::try_into::<RunLayer>)
|
||||
.transpose()
|
||||
.context("Failed to parse run config TOML")?
|
||||
.unwrap_or_default();
|
||||
let dockerfile = run
|
||||
.sandbox
|
||||
.as_ref()
|
||||
.and_then(|sandbox| sandbox.daytona.as_ref())
|
||||
.and_then(|daytona| daytona.snapshot.as_ref())
|
||||
.and_then(|snapshot| snapshot.dockerfile.as_ref());
|
||||
let layer = source
|
||||
.parse::<SettingsLayer>()
|
||||
.context("Failed to parse run config TOML")?;
|
||||
let absolute_config_path = cwd.join(config_path.as_path());
|
||||
let base_dir = absolute_config_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new("."));
|
||||
|
||||
let Some(DaytonaDockerfileLayer::Path { path }) = dockerfile else {
|
||||
for environment in layer.environments.values() {
|
||||
collect_environment_dockerfile(
|
||||
files,
|
||||
base_dir,
|
||||
cwd,
|
||||
config_path,
|
||||
environment.image.as_ref(),
|
||||
)?;
|
||||
}
|
||||
if let Some(run_environment) = layer.run.as_ref().and_then(|run| run.environment.as_ref()) {
|
||||
collect_environment_dockerfile(
|
||||
files,
|
||||
base_dir,
|
||||
cwd,
|
||||
config_path,
|
||||
run_environment.image.as_ref(),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_environment_dockerfile(
|
||||
files: &mut HashMap<String, types::ManifestFileEntry>,
|
||||
base_dir: &Path,
|
||||
cwd: &Path,
|
||||
config_path: &ManifestPath,
|
||||
image: Option<&EnvironmentImageLayer>,
|
||||
) -> Result<()> {
|
||||
let dockerfile = image.and_then(|image| image.dockerfile.as_ref());
|
||||
let Some(EnvironmentDockerfileLayer::Path { path }) = dockerfile else {
|
||||
return Ok(());
|
||||
};
|
||||
let absolute_config_path = cwd.join(config_path.as_path());
|
||||
collect_bundled_file(
|
||||
files,
|
||||
absolute_config_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new(".")),
|
||||
base_dir,
|
||||
cwd,
|
||||
path,
|
||||
types::ManifestFileRefType::Dockerfile,
|
||||
|
|
@ -849,7 +875,7 @@ pub fn manifest_args_is_empty(args: &types::ManifestArgs) -> bool {
|
|||
&& args.model.is_none()
|
||||
&& args.preserve_sandbox.is_none()
|
||||
&& args.provider.is_none()
|
||||
&& args.sandbox.is_none()
|
||||
&& args.environment.is_none()
|
||||
&& args.docker_image.is_none()
|
||||
&& args.input.is_empty()
|
||||
&& args.verbose.is_none()
|
||||
|
|
@ -865,7 +891,7 @@ mod tests {
|
|||
goal: Some("ship it"),
|
||||
model: Some("gpt-5.4-mini"),
|
||||
provider: Some("openai"),
|
||||
sandbox: Some("local"),
|
||||
environment: Some("local"),
|
||||
docker_image: None,
|
||||
preserve_sandbox: Some(true),
|
||||
dry_run: Some(true),
|
||||
|
|
@ -900,10 +926,20 @@ mod tests {
|
|||
"openai"
|
||||
);
|
||||
assert_eq!(
|
||||
overrides.sandbox.as_ref().unwrap().provider.as_deref(),
|
||||
overrides.environment.as_ref().unwrap().id.as_deref(),
|
||||
Some("local")
|
||||
);
|
||||
assert_eq!(overrides.sandbox.as_ref().unwrap().preserve, Some(true));
|
||||
assert_eq!(
|
||||
overrides
|
||||
.environment
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.lifecycle
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.preserve,
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
overrides.execution.as_ref().unwrap().mode,
|
||||
Some(RunMode::DryRun)
|
||||
|
|
|
|||
|
|
@ -1041,8 +1041,9 @@ mod runs {
|
|||
|
||||
use fabro_api::types::*;
|
||||
use fabro_types::settings::run::{
|
||||
DaytonaSettings, DaytonaSnapshotSettings, RunGoal, RunModelSettings, RunNamespace,
|
||||
RunPrepareSettings, RunSandboxSettings,
|
||||
EnvironmentImageSettings, EnvironmentLifecycleSettings, EnvironmentProvider,
|
||||
EnvironmentResourcesSettings, EnvironmentSettings, RunEnvironmentSettings, RunGoal,
|
||||
RunModelSettings, RunNamespace, RunPrepareSettings,
|
||||
};
|
||||
use fabro_types::settings::{InterpString, ProjectNamespace, WorkflowNamespace};
|
||||
use fabro_types::{
|
||||
|
|
@ -1714,12 +1715,33 @@ mod runs {
|
|||
|
||||
pub(super) fn settings() -> serde_json::Value {
|
||||
let settings = WorkflowSettings {
|
||||
project: ProjectNamespace::default(),
|
||||
workflow: WorkflowNamespace {
|
||||
project: ProjectNamespace::default(),
|
||||
workflow: WorkflowNamespace {
|
||||
graph: "workflow.fabro".into(),
|
||||
..WorkflowNamespace::default()
|
||||
},
|
||||
run: RunNamespace {
|
||||
environments: HashMap::from([("api-server".to_string(), EnvironmentSettings {
|
||||
provider: EnvironmentProvider::Daytona,
|
||||
image: EnvironmentImageSettings {
|
||||
reference: Some("api-server-dev".into()),
|
||||
dockerfile: None,
|
||||
},
|
||||
resources: EnvironmentResourcesSettings {
|
||||
cpu: Some(4),
|
||||
memory: Some(fabro_types::settings::Size::from_gigabytes(8)),
|
||||
disk: Some(fabro_types::settings::Size::from_gigabytes(10)),
|
||||
},
|
||||
lifecycle: EnvironmentLifecycleSettings {
|
||||
preserve: false,
|
||||
stop_on_terminal: true,
|
||||
auto_stop: Some(
|
||||
"60m".parse().expect("hardcoded demo duration should parse"),
|
||||
),
|
||||
},
|
||||
labels: HashMap::from([("project".to_string(), "api-server".to_string())]),
|
||||
..EnvironmentSettings::default()
|
||||
})]),
|
||||
run: RunNamespace {
|
||||
goal: Some(RunGoal::Inline(InterpString::parse(
|
||||
"Add rate limiting to auth endpoints",
|
||||
))),
|
||||
|
|
@ -1733,30 +1755,30 @@ mod runs {
|
|||
commands: vec!["bun install".into(), "bun run typecheck".into()],
|
||||
timeout_ms: 120_000,
|
||||
},
|
||||
sandbox: RunSandboxSettings {
|
||||
provider: "daytona".into(),
|
||||
preserve: false,
|
||||
stop_on_terminal: true,
|
||||
devcontainer: false,
|
||||
env: HashMap::new(),
|
||||
docker: None,
|
||||
daytona: Some(DaytonaSettings {
|
||||
auto_stop_interval: Some(60),
|
||||
labels: HashMap::from([(
|
||||
"project".to_string(),
|
||||
"api-server".to_string(),
|
||||
)]),
|
||||
volumes: Vec::new(),
|
||||
snapshot: Some(DaytonaSnapshotSettings {
|
||||
name: "api-server-dev".into(),
|
||||
cpu: Some(4),
|
||||
memory_gb: Some(8),
|
||||
disk_gb: Some(10),
|
||||
environment: RunEnvironmentSettings::from_environment(
|
||||
"api-server".to_string(),
|
||||
EnvironmentSettings {
|
||||
provider: EnvironmentProvider::Daytona,
|
||||
image: EnvironmentImageSettings {
|
||||
reference: Some("api-server-dev".into()),
|
||||
dockerfile: None,
|
||||
}),
|
||||
network: None,
|
||||
}),
|
||||
},
|
||||
},
|
||||
resources: EnvironmentResourcesSettings {
|
||||
cpu: Some(4),
|
||||
memory: Some(fabro_types::settings::Size::from_gigabytes(8)),
|
||||
disk: Some(fabro_types::settings::Size::from_gigabytes(10)),
|
||||
},
|
||||
lifecycle: EnvironmentLifecycleSettings {
|
||||
preserve: false,
|
||||
stop_on_terminal: true,
|
||||
auto_stop: Some(
|
||||
"60m".parse().expect("hardcoded demo duration should parse"),
|
||||
),
|
||||
},
|
||||
labels: HashMap::from([("project".to_string(), "api-server".to_string())]),
|
||||
..EnvironmentSettings::default()
|
||||
},
|
||||
),
|
||||
..RunNamespace::default()
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ use std::time::Duration;
|
|||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use fabro_api::types;
|
||||
use fabro_auth::auth_issue_message;
|
||||
use fabro_config::run::parse_run_layer_from_settings_toml;
|
||||
use fabro_config::{
|
||||
CliLayer, CliOutputLayer, DaytonaDockerfileLayer, RunLayer, WorkflowSettingsBuilder,
|
||||
parse_input_overrides, parse_labels,
|
||||
CliLayer, CliOutputLayer, EnvironmentDockerfileLayer, EnvironmentImageLayer, EnvironmentLayer,
|
||||
MergeMap, RunLayer, SettingsLayer, WorkflowSettingsBuilder, parse_input_overrides,
|
||||
parse_labels,
|
||||
};
|
||||
use fabro_graphviz::graph::{Graph, is_llm_handler_type};
|
||||
use fabro_graphviz::render::apply_direction;
|
||||
|
|
@ -27,8 +27,8 @@ use fabro_static::EnvVars;
|
|||
use fabro_types::settings::cli::OutputVerbosity;
|
||||
use fabro_types::settings::interp::InterpString;
|
||||
use fabro_types::settings::run::{
|
||||
DaytonaNetworkLayer, DaytonaSettings, DockerSettings, DockerfileSource, RunGoal, RunMode,
|
||||
RunNamespace,
|
||||
DockerfileSource, EnvironmentNetworkMode, EnvironmentProvider, RunEnvironmentSettings, RunGoal,
|
||||
RunMode, RunNamespace,
|
||||
};
|
||||
use fabro_types::{ManifestPath, RunId, WorkflowSettings};
|
||||
use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus};
|
||||
|
|
@ -76,6 +76,18 @@ pub(crate) fn manifest_run_defaults(run: Option<&RunLayer>) -> RunLayer {
|
|||
pub(crate) fn prepare_manifest(
|
||||
manifest_run_defaults: &RunLayer,
|
||||
manifest: &types::RunManifest,
|
||||
) -> Result<PreparedManifest> {
|
||||
prepare_manifest_with_environment_defaults(
|
||||
manifest_run_defaults,
|
||||
&MergeMap::default(),
|
||||
manifest,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_manifest_with_environment_defaults(
|
||||
manifest_run_defaults: &RunLayer,
|
||||
manifest_environment_defaults: &MergeMap<EnvironmentLayer>,
|
||||
manifest: &types::RunManifest,
|
||||
) -> Result<PreparedManifest> {
|
||||
if manifest.version != 1 {
|
||||
bail!("unsupported manifest version {}", manifest.version);
|
||||
|
|
@ -93,8 +105,10 @@ pub(crate) fn prepare_manifest(
|
|||
|
||||
let args_overrides =
|
||||
manifest_args_overrides(manifest.args.as_ref()).context("failed to parse manifest args")?;
|
||||
let mut workflow_settings_builder =
|
||||
WorkflowSettingsBuilder::new().server_run_defaults(manifest_run_defaults.clone());
|
||||
let mut workflow_settings_builder = WorkflowSettingsBuilder::new().server_manifest_defaults(
|
||||
manifest_run_defaults.clone(),
|
||||
manifest_environment_defaults.clone(),
|
||||
);
|
||||
if let Some(run) = args_overrides.run {
|
||||
workflow_settings_builder = workflow_settings_builder.run_overrides(run);
|
||||
}
|
||||
|
|
@ -102,9 +116,12 @@ pub(crate) fn prepare_manifest(
|
|||
workflow_settings_builder = workflow_settings_builder.cli_overrides(cli);
|
||||
}
|
||||
if let Some(config) = workflow_input.config.as_ref() {
|
||||
let workflow_run_layer = workflow_run_layer_with_resolved_dockerfile(&workflow_input)?;
|
||||
workflow_settings_builder = workflow_settings_builder
|
||||
.workflow_toml_with_run_layer(&config.source, workflow_run_layer)?;
|
||||
let layer = settings_layer_with_resolved_dockerfiles(
|
||||
&config.source,
|
||||
&config.path,
|
||||
&workflow_input.files,
|
||||
)?;
|
||||
workflow_settings_builder = workflow_settings_builder.workflow_layer(layer);
|
||||
}
|
||||
for config in manifest
|
||||
.configs
|
||||
|
|
@ -112,16 +129,13 @@ pub(crate) fn prepare_manifest(
|
|||
.filter(|config| config.type_ == types::ManifestConfigType::Project)
|
||||
{
|
||||
if let Some(source) = config.source.as_deref() {
|
||||
let mut run = parse_run_layer_from_settings_toml(source)
|
||||
.context("Failed to parse project config TOML")?;
|
||||
if run_has_path_dockerfile(&run) {
|
||||
let config_path = manifest_project_config_path(config, &cwd)?;
|
||||
resolve_manifest_dockerfile(&mut run, &config_path, &workflow_input.files)?;
|
||||
workflow_settings_builder =
|
||||
workflow_settings_builder.project_toml_with_run_layer(source, run)?;
|
||||
} else {
|
||||
workflow_settings_builder = workflow_settings_builder.project_toml(source)?;
|
||||
}
|
||||
let config_path = manifest_project_config_path(config, &cwd)?;
|
||||
let layer = settings_layer_with_resolved_dockerfiles(
|
||||
source,
|
||||
&config_path,
|
||||
&workflow_input.files,
|
||||
)?;
|
||||
workflow_settings_builder = workflow_settings_builder.project_layer(layer);
|
||||
}
|
||||
}
|
||||
for config in manifest
|
||||
|
|
@ -316,21 +330,19 @@ fn workflow_files_from_manifest(
|
|||
Ok(bundled)
|
||||
}
|
||||
|
||||
fn workflow_run_layer_with_resolved_dockerfile(workflow: &BundledWorkflow) -> Result<RunLayer> {
|
||||
let config = workflow
|
||||
.config
|
||||
.as_ref()
|
||||
.expect("workflow config should exist before resolving its run layer");
|
||||
fn settings_layer_with_resolved_dockerfiles(
|
||||
source: &str,
|
||||
config_path: &ManifestPath,
|
||||
files: &HashMap<ManifestPath, String>,
|
||||
) -> Result<SettingsLayer> {
|
||||
// Parse via `SettingsLayer` so unknown nested keys (like a stale
|
||||
// `[server.integrations.github.permissions]` after the move to
|
||||
// `[run.integrations.github.permissions]`) trip
|
||||
// `deny_unknown_fields`. Other valid top-level domains
|
||||
// (`_version`, `[workflow]`, `[server.*]`) parse cleanly and the
|
||||
// builder ignores everything outside `[run]` for this code path.
|
||||
let mut run = parse_run_layer_from_settings_toml(&config.source)
|
||||
// `[run.integrations.github.permissions]`) trip `deny_unknown_fields`.
|
||||
let mut layer = source
|
||||
.parse::<SettingsLayer>()
|
||||
.context("Failed to parse run config TOML")?;
|
||||
resolve_manifest_dockerfile(&mut run, &config.path, &workflow.files)?;
|
||||
Ok(run)
|
||||
resolve_manifest_dockerfiles(&mut layer, config_path, files)?;
|
||||
Ok(layer)
|
||||
}
|
||||
|
||||
fn manifest_args_overrides(
|
||||
|
|
@ -344,7 +356,7 @@ fn manifest_args_overrides(
|
|||
goal: None,
|
||||
model: args.model.as_deref(),
|
||||
provider: args.provider.as_deref(),
|
||||
sandbox: args.sandbox.as_deref(),
|
||||
environment: args.environment.as_deref(),
|
||||
docker_image: args.docker_image.as_deref(),
|
||||
preserve_sandbox: args.preserve_sandbox,
|
||||
dry_run: args.dry_run,
|
||||
|
|
@ -401,21 +413,37 @@ fn process_env_var(name: &str) -> Option<String> {
|
|||
std::env::var(name).ok()
|
||||
}
|
||||
|
||||
fn resolve_manifest_dockerfile(
|
||||
run: &mut RunLayer,
|
||||
fn resolve_manifest_dockerfiles(
|
||||
layer: &mut SettingsLayer,
|
||||
config_path: &ManifestPath,
|
||||
files: &HashMap<ManifestPath, String>,
|
||||
) -> Result<()> {
|
||||
let source = run
|
||||
.sandbox
|
||||
if let Some(image) = layer
|
||||
.run
|
||||
.as_mut()
|
||||
.and_then(|sandbox| sandbox.daytona.as_mut())
|
||||
.and_then(|daytona| daytona.snapshot.as_mut())
|
||||
.and_then(|snapshot| snapshot.dockerfile.as_mut());
|
||||
.and_then(|run| run.environment.as_mut())
|
||||
.and_then(|environment| environment.image.as_mut())
|
||||
{
|
||||
resolve_manifest_dockerfile(image, config_path, files)?;
|
||||
}
|
||||
for environment in layer.environments.values_mut() {
|
||||
if let Some(image) = environment.image.as_mut() {
|
||||
resolve_manifest_dockerfile(image, config_path, files)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_manifest_dockerfile(
|
||||
image: &mut EnvironmentImageLayer,
|
||||
config_path: &ManifestPath,
|
||||
files: &HashMap<ManifestPath, String>,
|
||||
) -> Result<()> {
|
||||
let source = image.dockerfile.as_mut();
|
||||
let Some(source) = source else {
|
||||
return Ok(());
|
||||
};
|
||||
let DaytonaDockerfileLayer::Path { path } = &*source else {
|
||||
let EnvironmentDockerfileLayer::Path { path } = &*source else {
|
||||
return Ok(());
|
||||
};
|
||||
let path_owned = path.clone();
|
||||
|
|
@ -425,21 +453,10 @@ fn resolve_manifest_dockerfile(
|
|||
.get(&manifest_path)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("missing bundled dockerfile: {manifest_path}"))?;
|
||||
*source = DaytonaDockerfileLayer::Inline(content);
|
||||
*source = EnvironmentDockerfileLayer::Inline(content);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_has_path_dockerfile(run: &RunLayer) -> bool {
|
||||
matches!(
|
||||
run.sandbox
|
||||
.as_ref()
|
||||
.and_then(|sandbox| sandbox.daytona.as_ref())
|
||||
.and_then(|daytona| daytona.snapshot.as_ref())
|
||||
.and_then(|snapshot| snapshot.dockerfile.as_ref()),
|
||||
Some(DaytonaDockerfileLayer::Path { .. })
|
||||
)
|
||||
}
|
||||
|
||||
fn manifest_project_config_path(
|
||||
config: &types::ManifestConfig,
|
||||
cwd: &Path,
|
||||
|
|
@ -495,13 +512,14 @@ async fn build_preflight_report(
|
|||
let resolved_run = materialized.run;
|
||||
let server_settings = state.server_settings();
|
||||
let github_integration = &server_settings.server.integrations.github;
|
||||
let sandbox_provider = resolve_sandbox_provider(&resolved_run)?;
|
||||
let sandbox_provider = resolve_sandbox_provider(&resolved_run);
|
||||
let sandbox_provider =
|
||||
if resolved_run.execution.mode == RunMode::DryRun && !sandbox_provider.is_local() {
|
||||
SandboxProvider::Local
|
||||
} else {
|
||||
sandbox_provider
|
||||
};
|
||||
run_environment_capability_check(&mut checks, &resolved_run);
|
||||
let needs_github_credentials =
|
||||
sandbox_provider.is_clone_based() || resolved_run.integrations.github.is_token_requested();
|
||||
let github_app = if needs_github_credentials {
|
||||
|
|
@ -609,33 +627,18 @@ fn base_preflight_checks(prepared: &PreparedManifest, graph: &Graph) -> Vec<Chec
|
|||
]
|
||||
}
|
||||
|
||||
fn resolve_sandbox_provider(settings: &RunNamespace) -> Result<SandboxProvider> {
|
||||
Ok(Some(str::parse::<SandboxProvider>(
|
||||
settings.sandbox.provider.as_str(),
|
||||
))
|
||||
.transpose()
|
||||
.context("Invalid sandbox provider")?
|
||||
.unwrap_or_default())
|
||||
fn resolve_sandbox_provider(settings: &RunNamespace) -> SandboxProvider {
|
||||
SandboxProvider::from(settings.environment.provider)
|
||||
}
|
||||
|
||||
fn resolve_daytona_config(settings: &RunNamespace) -> DaytonaConfig {
|
||||
let mut config = settings
|
||||
.sandbox
|
||||
.daytona
|
||||
.as_ref()
|
||||
.map(|daytona| runtime_daytona_config(daytona, !settings.clone.enabled))
|
||||
.unwrap_or_default();
|
||||
let mut config = runtime_daytona_config(&settings.environment, !settings.clone.enabled);
|
||||
config.skip_clone = !settings.clone.enabled;
|
||||
config
|
||||
}
|
||||
|
||||
fn resolve_docker_config(settings: &RunNamespace) -> DockerSandboxOptions {
|
||||
let mut config = settings
|
||||
.sandbox
|
||||
.docker
|
||||
.as_ref()
|
||||
.map(|docker| runtime_docker_config(docker, !settings.clone.enabled))
|
||||
.unwrap_or_default();
|
||||
let mut config = runtime_docker_config(&settings.environment, !settings.clone.enabled);
|
||||
config.skip_clone = !settings.clone.enabled;
|
||||
config
|
||||
}
|
||||
|
|
@ -653,6 +656,66 @@ fn clone_disabled_for_provider(provider: SandboxProvider, resolved_run: &RunName
|
|||
}
|
||||
}
|
||||
|
||||
fn run_environment_capability_check(checks: &mut Vec<CheckResult>, resolved_run: &RunNamespace) {
|
||||
let warnings = environment_capability_warnings(resolved_run);
|
||||
if warnings.is_empty() {
|
||||
return;
|
||||
}
|
||||
checks.push(CheckResult {
|
||||
name: "Environment Capabilities".into(),
|
||||
status: CheckStatus::Warning,
|
||||
summary: format!("{} unsupported hint(s) ignored", warnings.len()),
|
||||
details: warnings
|
||||
.into_iter()
|
||||
.map(|text| CheckDetail { text, warn: true })
|
||||
.collect(),
|
||||
remediation: None,
|
||||
});
|
||||
}
|
||||
|
||||
fn environment_capability_warnings(resolved_run: &RunNamespace) -> Vec<String> {
|
||||
let environment = &resolved_run.environment;
|
||||
let mut warnings = Vec::new();
|
||||
match environment.provider {
|
||||
EnvironmentProvider::Local => {
|
||||
if environment.resources.cpu.is_some()
|
||||
|| environment.resources.memory.is_some()
|
||||
|| environment.resources.disk.is_some()
|
||||
{
|
||||
warnings.push("local provider ignores resource limits".to_string());
|
||||
}
|
||||
if !environment.volumes.is_empty() {
|
||||
warnings.push("local provider ignores volume mounts".to_string());
|
||||
}
|
||||
if !environment.labels.is_empty() {
|
||||
warnings.push("local provider ignores labels".to_string());
|
||||
}
|
||||
if environment.lifecycle.auto_stop.is_some() {
|
||||
warnings.push("local provider ignores lifecycle.auto_stop".to_string());
|
||||
}
|
||||
}
|
||||
EnvironmentProvider::Docker => {
|
||||
if environment.resources.disk.is_some() {
|
||||
warnings.push("docker provider ignores disk resource limits".to_string());
|
||||
}
|
||||
if !environment.volumes.is_empty() {
|
||||
warnings.push("docker provider ignores volume mounts".to_string());
|
||||
}
|
||||
if !environment.labels.is_empty() {
|
||||
warnings.push("docker provider ignores labels".to_string());
|
||||
}
|
||||
if environment.lifecycle.auto_stop.is_some() {
|
||||
warnings.push("docker provider ignores lifecycle.auto_stop".to_string());
|
||||
}
|
||||
if environment.image.dockerfile.is_some() {
|
||||
warnings.push("docker provider ignores image.dockerfile".to_string());
|
||||
}
|
||||
}
|
||||
EnvironmentProvider::Daytona => {}
|
||||
}
|
||||
warnings
|
||||
}
|
||||
|
||||
fn repository_access_details(request: &GitRemoteRefCheck) -> Vec<CheckDetail> {
|
||||
let mut details = vec![CheckDetail::new(format!("Origin: {}", request.origin_url))];
|
||||
if let Some(branch) = request.branch.as_ref() {
|
||||
|
|
@ -1121,28 +1184,39 @@ fn resolve_model_provider(
|
|||
}
|
||||
}
|
||||
|
||||
fn runtime_daytona_config(settings: &DaytonaSettings, skip_clone: bool) -> DaytonaConfig {
|
||||
fn runtime_daytona_config(settings: &RunEnvironmentSettings, skip_clone: bool) -> DaytonaConfig {
|
||||
DaytonaConfig {
|
||||
auto_stop_interval: settings.auto_stop_interval,
|
||||
auto_stop_interval: settings
|
||||
.lifecycle
|
||||
.auto_stop
|
||||
.map(|duration| duration_to_minutes_i32(duration.as_std())),
|
||||
labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()),
|
||||
volumes: settings
|
||||
.volumes
|
||||
.iter()
|
||||
.map(|volume| DaytonaVolumeMount {
|
||||
volume_id: volume.volume_id.clone(),
|
||||
volume_id: volume.id.clone(),
|
||||
mount_path: volume.mount_path.clone(),
|
||||
subpath: volume.subpath.clone(),
|
||||
})
|
||||
.collect(),
|
||||
snapshot: settings
|
||||
.snapshot
|
||||
.image
|
||||
.reference
|
||||
.as_ref()
|
||||
.map(|snapshot| DaytonaSnapshotSettings {
|
||||
name: snapshot.name.clone(),
|
||||
cpu: snapshot.cpu,
|
||||
memory: snapshot.memory_gb,
|
||||
disk: snapshot.disk_gb,
|
||||
dockerfile: snapshot
|
||||
.map(|name| DaytonaSnapshotSettings {
|
||||
name: name.clone(),
|
||||
cpu: settings.resources.cpu,
|
||||
memory: settings
|
||||
.resources
|
||||
.memory
|
||||
.map(|size| size_to_gb_i32(size.as_bytes())),
|
||||
disk: settings
|
||||
.resources
|
||||
.disk
|
||||
.map(|size| size_to_gb_i32(size.as_bytes())),
|
||||
dockerfile: settings
|
||||
.image
|
||||
.dockerfile
|
||||
.as_ref()
|
||||
.map(|dockerfile| match dockerfile {
|
||||
|
|
@ -1154,36 +1228,65 @@ fn runtime_daytona_config(settings: &DaytonaSettings, skip_clone: bool) -> Dayto
|
|||
}
|
||||
}),
|
||||
}),
|
||||
network: settings.network.as_ref().map(|network| match network {
|
||||
DaytonaNetworkLayer::Block => DaytonaNetwork::Block,
|
||||
DaytonaNetworkLayer::AllowAll => DaytonaNetwork::AllowAll,
|
||||
DaytonaNetworkLayer::AllowList { allow_list } => {
|
||||
DaytonaNetwork::AllowList(allow_list.clone())
|
||||
network: Some(match settings.network.mode {
|
||||
EnvironmentNetworkMode::Block => DaytonaNetwork::Block,
|
||||
EnvironmentNetworkMode::AllowAll => DaytonaNetwork::AllowAll,
|
||||
EnvironmentNetworkMode::CidrAllowList => {
|
||||
DaytonaNetwork::AllowList(settings.network.allow.clone())
|
||||
}
|
||||
}),
|
||||
skip_clone,
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_docker_config(settings: &DockerSettings, skip_clone: bool) -> DockerSandboxOptions {
|
||||
fn runtime_docker_config(
|
||||
settings: &RunEnvironmentSettings,
|
||||
skip_clone: bool,
|
||||
) -> DockerSandboxOptions {
|
||||
let mut env_vars = settings
|
||||
.env_vars
|
||||
.env
|
||||
.iter()
|
||||
.map(|(key, value)| format!("{key}={}", resolve_interp(value)))
|
||||
.collect::<Vec<_>>();
|
||||
env_vars.sort();
|
||||
let default_options = DockerSandboxOptions::default();
|
||||
|
||||
DockerSandboxOptions {
|
||||
image: settings.image.clone(),
|
||||
network_mode: settings.network_mode.clone(),
|
||||
memory_limit: settings.memory_limit,
|
||||
cpu_quota: settings.cpu_quota,
|
||||
image: settings
|
||||
.image
|
||||
.reference
|
||||
.clone()
|
||||
.unwrap_or(default_options.image),
|
||||
network_mode: match settings.network.mode {
|
||||
EnvironmentNetworkMode::Block => Some("none".to_string()),
|
||||
EnvironmentNetworkMode::AllowAll | EnvironmentNetworkMode::CidrAllowList => {
|
||||
default_options.network_mode
|
||||
}
|
||||
},
|
||||
memory_limit: settings
|
||||
.resources
|
||||
.memory
|
||||
.and_then(|size| i64::try_from(size.as_bytes()).ok()),
|
||||
cpu_quota: settings
|
||||
.resources
|
||||
.cpu
|
||||
.map(|cpu| i64::from(cpu).saturating_mul(100_000)),
|
||||
env_vars,
|
||||
skip_clone,
|
||||
..DockerSandboxOptions::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn duration_to_minutes_i32(duration: Duration) -> i32 {
|
||||
let minutes = duration.as_secs() / 60;
|
||||
i32::try_from(minutes).unwrap_or(i32::MAX)
|
||||
}
|
||||
|
||||
fn size_to_gb_i32(bytes: u64) -> i32 {
|
||||
let gb = bytes / 1_000_000_000;
|
||||
i32::try_from(gb).unwrap_or(i32::MAX)
|
||||
}
|
||||
|
||||
async fn run_github_token_check(
|
||||
checks: &mut Vec<CheckResult>,
|
||||
prepared: &PreparedManifest,
|
||||
|
|
@ -1464,7 +1567,10 @@ mod tests {
|
|||
r#"
|
||||
_version = 1
|
||||
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "selected"
|
||||
|
||||
[environments.selected]
|
||||
provider = "{provider}"
|
||||
|
||||
[run.clone]
|
||||
|
|
@ -1493,14 +1599,17 @@ enabled = {clone_enabled}
|
|||
|
||||
#[test]
|
||||
fn runtime_daytona_config_preserves_volume_mounts() {
|
||||
let settings = DaytonaSettings {
|
||||
volumes: vec![fabro_types::settings::run::DaytonaVolumeSettings {
|
||||
volume_id: "vol_auth".to_string(),
|
||||
mount_path: "/home/daytona/.config".to_string(),
|
||||
subpath: Some("agents".to_string()),
|
||||
}],
|
||||
..DaytonaSettings::default()
|
||||
};
|
||||
let settings = RunEnvironmentSettings::from_environment(
|
||||
"cloud".to_string(),
|
||||
fabro_types::settings::run::EnvironmentSettings {
|
||||
volumes: vec![fabro_types::settings::run::EnvironmentVolumeSettings {
|
||||
id: "vol_auth".to_string(),
|
||||
mount_path: "/home/daytona/.config".to_string(),
|
||||
subpath: Some("agents".to_string()),
|
||||
}],
|
||||
..fabro_types::settings::run::EnvironmentSettings::default()
|
||||
},
|
||||
);
|
||||
|
||||
let config = runtime_daytona_config(&settings, false);
|
||||
|
||||
|
|
@ -1509,7 +1618,6 @@ enabled = {clone_enabled}
|
|||
assert_eq!(config.volumes[0].mount_path, "/home/daytona/.config");
|
||||
assert_eq!(config.volumes[0].subpath.as_deref(), Some("agents"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_manifest_inlines_project_config_daytona_dockerfile_from_bundle() {
|
||||
let mut manifest = minimal_manifest();
|
||||
|
|
@ -1518,11 +1626,14 @@ enabled = {clone_enabled}
|
|||
source: Some(
|
||||
r#"_version = 1
|
||||
|
||||
[run.sandbox]
|
||||
[run.environment]
|
||||
id = "cloud"
|
||||
|
||||
[environments.cloud]
|
||||
provider = "daytona"
|
||||
|
||||
[run.sandbox.daytona.snapshot]
|
||||
name = "fabro-test"
|
||||
[environments.cloud.image]
|
||||
ref = "fabro-test"
|
||||
dockerfile = { path = "Dockerfile" }
|
||||
"#
|
||||
.to_string(),
|
||||
|
|
@ -1552,11 +1663,10 @@ dockerfile = { path = "Dockerfile" }
|
|||
let dockerfile = prepared
|
||||
.settings
|
||||
.run
|
||||
.sandbox
|
||||
.daytona
|
||||
.environment
|
||||
.image
|
||||
.dockerfile
|
||||
.as_ref()
|
||||
.and_then(|daytona| daytona.snapshot.as_ref())
|
||||
.and_then(|snapshot| snapshot.dockerfile.as_ref())
|
||||
.expect("project Dockerfile should resolve");
|
||||
match dockerfile {
|
||||
DockerfileSource::Inline(value) => assert_eq!(value, "FROM ubuntu:24.04\n"),
|
||||
|
|
@ -1574,8 +1684,14 @@ dockerfile = { path = "Dockerfile" }
|
|||
source: Some(
|
||||
r#"_version = 1
|
||||
|
||||
[run.sandbox.daytona.snapshot]
|
||||
name = "fabro-test"
|
||||
[run.environment]
|
||||
id = "cloud"
|
||||
|
||||
[environments.cloud]
|
||||
provider = "daytona"
|
||||
|
||||
[environments.cloud.image]
|
||||
ref = "fabro-test"
|
||||
dockerfile = { path = "Dockerfile" }
|
||||
"#
|
||||
.to_string(),
|
||||
|
|
@ -1844,7 +1960,7 @@ root = "/srv/fabro"
|
|||
model: None,
|
||||
preserve_sandbox: None,
|
||||
provider: None,
|
||||
sandbox: None,
|
||||
environment: None,
|
||||
docker_image: None,
|
||||
input: Vec::new(),
|
||||
verbose: None,
|
||||
|
|
@ -1877,7 +1993,7 @@ override = "server"
|
|||
model: None,
|
||||
preserve_sandbox: None,
|
||||
provider: None,
|
||||
sandbox: None,
|
||||
environment: None,
|
||||
docker_image: None,
|
||||
input: vec!["override=cli".to_string()],
|
||||
verbose: None,
|
||||
|
|
@ -1972,8 +2088,8 @@ description = "Move the feature through review"
|
|||
team = "platform"
|
||||
priority = "high"
|
||||
|
||||
[run.sandbox]
|
||||
provider = "local"
|
||||
[run.environment]
|
||||
id = "local"
|
||||
"#
|
||||
.to_string(),
|
||||
});
|
||||
|
|
@ -2114,8 +2230,8 @@ name = "Control Plane"
|
|||
path: "workflow.toml".to_string(),
|
||||
source: r#"_version = 1
|
||||
|
||||
[run.sandbox]
|
||||
provider = "local"
|
||||
[run.environment]
|
||||
id = "local"
|
||||
|
||||
[run.integrations.github.permissions]
|
||||
issues = "read"
|
||||
|
|
@ -2163,8 +2279,8 @@ _version = 1
|
|||
[run.pull_request]
|
||||
enabled = true
|
||||
|
||||
[run.sandbox]
|
||||
provider = "local"
|
||||
[run.environment]
|
||||
id = "local"
|
||||
"#
|
||||
.to_string(),
|
||||
),
|
||||
|
|
@ -2272,8 +2388,8 @@ name = "Project Config Name"
|
|||
r#"
|
||||
_version = 1
|
||||
|
||||
[run.sandbox]
|
||||
provider = "daytona"
|
||||
[run.environment]
|
||||
id = "daytona"
|
||||
"#
|
||||
.to_string(),
|
||||
),
|
||||
|
|
@ -2492,8 +2608,8 @@ digraph Demo {
|
|||
);
|
||||
}
|
||||
|
||||
mod workflow_run_layer_with_resolved_dockerfile_tests {
|
||||
//! `workflow_run_layer_with_resolved_dockerfile` parses bundled
|
||||
mod settings_layer_with_resolved_dockerfiles_tests {
|
||||
//! `settings_layer_with_resolved_dockerfiles` parses bundled
|
||||
//! workflow.toml through the strict `SettingsLayer` schema, so
|
||||
//! unknown fields anywhere in the document trip
|
||||
//! `deny_unknown_fields`.
|
||||
|
|
@ -2501,7 +2617,7 @@ digraph Demo {
|
|||
use fabro_types::ManifestPath;
|
||||
use fabro_workflow::workflow_bundle::{BundledWorkflow, ParsedWorkflowConfig};
|
||||
|
||||
use super::super::workflow_run_layer_with_resolved_dockerfile;
|
||||
use super::super::settings_layer_with_resolved_dockerfiles;
|
||||
|
||||
fn workflow_with_config(source: &str) -> BundledWorkflow {
|
||||
BundledWorkflow {
|
||||
|
|
@ -2526,8 +2642,13 @@ issues = "read"
|
|||
"#,
|
||||
);
|
||||
|
||||
let run = workflow_run_layer_with_resolved_dockerfile(&workflow)
|
||||
.expect("workflow.toml should parse");
|
||||
let layer = settings_layer_with_resolved_dockerfiles(
|
||||
&workflow.config.as_ref().unwrap().source,
|
||||
&workflow.config.as_ref().unwrap().path,
|
||||
&workflow.files,
|
||||
)
|
||||
.expect("workflow.toml should parse");
|
||||
let run = layer.run.expect("run layer should be present");
|
||||
let github = run
|
||||
.integrations
|
||||
.as_ref()
|
||||
|
|
@ -2551,8 +2672,12 @@ issues = "read"
|
|||
"#,
|
||||
);
|
||||
|
||||
let err = workflow_run_layer_with_resolved_dockerfile(&workflow)
|
||||
.expect_err("stale [server.integrations.github.permissions] should be rejected");
|
||||
let err = settings_layer_with_resolved_dockerfiles(
|
||||
&workflow.config.as_ref().unwrap().source,
|
||||
&workflow.config.as_ref().unwrap().path,
|
||||
&workflow.files,
|
||||
)
|
||||
.expect_err("stale [server.integrations.github.permissions] should be rejected");
|
||||
let message = format!("{err:#}");
|
||||
assert!(
|
||||
message.contains("permissions") || message.contains("unknown field"),
|
||||
|
|
@ -2573,8 +2698,13 @@ contents = "read"
|
|||
"#,
|
||||
);
|
||||
|
||||
let run = workflow_run_layer_with_resolved_dockerfile(&workflow)
|
||||
.expect("workflow + run blocks should parse");
|
||||
let layer = settings_layer_with_resolved_dockerfiles(
|
||||
&workflow.config.as_ref().unwrap().source,
|
||||
&workflow.config.as_ref().unwrap().path,
|
||||
&workflow.files,
|
||||
)
|
||||
.expect("workflow + run blocks should parse");
|
||||
let run = layer.run.expect("run layer should be present");
|
||||
assert!(run.integrations.is_some());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ pub fn run_tool_manifest_args(spec: &ValidatedCreateRunSpec) -> Option<types::Ma
|
|||
model: spec.model.clone(),
|
||||
preserve_sandbox: spec.preserve_sandbox.filter(|value| *value),
|
||||
provider: spec.provider.clone(),
|
||||
sandbox: spec.sandbox.clone(),
|
||||
environment: spec.environment.clone(),
|
||||
verbose: None,
|
||||
};
|
||||
(!fabro_manifest::manifest_args_is_empty(&payload)).then_some(payload)
|
||||
|
|
@ -73,7 +73,7 @@ pub fn run_tool_run_overrides(spec: &ValidatedCreateRunSpec) -> Option<RunLayer>
|
|||
goal: spec.goal.as_deref(),
|
||||
model: spec.model.as_deref(),
|
||||
provider: spec.provider.as_deref(),
|
||||
sandbox: spec.sandbox.as_deref(),
|
||||
environment: spec.environment.as_deref(),
|
||||
docker_image: None,
|
||||
preserve_sandbox: spec.preserve_sandbox,
|
||||
dry_run: spec.dry_run,
|
||||
|
|
@ -88,7 +88,7 @@ pub fn run_tool_run_overrides(spec: &ValidatedCreateRunSpec) -> Option<RunLayer>
|
|||
(run.goal.is_some()
|
||||
|| !run.metadata.is_empty()
|
||||
|| run.model.is_some()
|
||||
|| run.sandbox.is_some()
|
||||
|| run.environment.is_some()
|
||||
|| run.execution.is_some())
|
||||
.then_some(run)
|
||||
}
|
||||
|
|
@ -118,7 +118,7 @@ mod tests {
|
|||
labels: HashMap::new(),
|
||||
model: None,
|
||||
provider: None,
|
||||
sandbox: None,
|
||||
environment: None,
|
||||
dry_run: None,
|
||||
auto_approve: None,
|
||||
preserve_sandbox: None,
|
||||
|
|
@ -143,7 +143,7 @@ mod tests {
|
|||
labels: HashMap::new(),
|
||||
model: None,
|
||||
provider: None,
|
||||
sandbox: None,
|
||||
environment: None,
|
||||
dry_run: None,
|
||||
auto_approve: None,
|
||||
preserve_sandbox: None,
|
||||
|
|
|
|||
|
|
@ -9,11 +9,10 @@ use clap::Args;
|
|||
use fabro_config::bind::{self, Bind, BindRequest};
|
||||
use fabro_config::user::active_settings_path;
|
||||
use fabro_config::{
|
||||
RunLayer, RunModelLayer, RunSandboxLayer, ServerLayer, ServerWebLayer, Storage,
|
||||
RunEnvironmentLayer, RunLayer, RunModelLayer, ServerLayer, ServerWebLayer, Storage,
|
||||
load_config_file, load_server_runtime_settings,
|
||||
};
|
||||
use fabro_install::{OBJECT_STORE_ACCESS_KEY_ID_ENV, OBJECT_STORE_SECRET_ACCESS_KEY_ENV};
|
||||
use fabro_sandbox::SandboxProvider;
|
||||
use fabro_static::EnvVars;
|
||||
use fabro_types::ServerSettings;
|
||||
use fabro_types::settings::server::{GithubIntegrationStrategy, LogDestination, WebhookStrategy};
|
||||
|
|
@ -205,9 +204,9 @@ pub struct ServeArgs {
|
|||
#[arg(long)]
|
||||
pub provider: Option<String>,
|
||||
|
||||
/// Sandbox for agent tools
|
||||
#[arg(long, value_enum)]
|
||||
pub sandbox: Option<SandboxProvider>,
|
||||
/// Named environment for agent tools
|
||||
#[arg(long)]
|
||||
pub environment: Option<String>,
|
||||
|
||||
/// Maximum number of concurrent run executions
|
||||
#[arg(long)]
|
||||
|
|
@ -240,9 +239,11 @@ fn serve_overrides(args: &ServeArgs) -> (Option<RunLayer>, Option<ServerLayer>)
|
|||
let model_layer = run.model.get_or_insert_with(RunModelLayer::default);
|
||||
model_layer.provider = Some(InterpString::parse(provider));
|
||||
}
|
||||
if let Some(sandbox) = args.sandbox {
|
||||
let sandbox_layer = run.sandbox.get_or_insert_with(RunSandboxLayer::default);
|
||||
sandbox_layer.provider = Some(sandbox.to_string());
|
||||
if let Some(environment) = args.environment.as_ref() {
|
||||
let environment_layer = run
|
||||
.environment
|
||||
.get_or_insert_with(RunEnvironmentLayer::default);
|
||||
environment_layer.id = Some(environment.clone());
|
||||
}
|
||||
(
|
||||
(run != RunLayer::default()).then_some(run),
|
||||
|
|
@ -722,10 +723,11 @@ where
|
|||
effective_log_destination,
|
||||
);
|
||||
let resolved_app_settings = ResolvedAppStateSettings {
|
||||
server_settings: runtime_settings.server_settings,
|
||||
manifest_run_defaults: runtime_settings.manifest_run_defaults,
|
||||
manifest_run_settings: runtime_settings.manifest_run_settings,
|
||||
llm_catalog_settings: runtime_settings.llm_catalog_settings,
|
||||
server_settings: runtime_settings.server_settings,
|
||||
manifest_run_defaults: runtime_settings.manifest_run_defaults,
|
||||
manifest_environment_defaults: runtime_settings.manifest_environment_defaults,
|
||||
manifest_run_settings: runtime_settings.manifest_run_settings,
|
||||
llm_catalog_settings: runtime_settings.llm_catalog_settings,
|
||||
};
|
||||
let resolved_server_settings = resolved_app_settings.server_settings.server.clone();
|
||||
let (auth_mode, server_secrets) = resolve_startup(
|
||||
|
|
@ -893,10 +895,12 @@ where
|
|||
.server_settings
|
||||
.with_storage_override(&data_dir_for_poll);
|
||||
ResolvedAppStateSettings {
|
||||
server_settings: resolved.server_settings,
|
||||
manifest_run_defaults: resolved.manifest_run_defaults,
|
||||
manifest_run_settings: resolved.manifest_run_settings,
|
||||
llm_catalog_settings: resolved.llm_catalog_settings,
|
||||
server_settings: resolved.server_settings,
|
||||
manifest_run_defaults: resolved.manifest_run_defaults,
|
||||
manifest_environment_defaults: resolved
|
||||
.manifest_environment_defaults,
|
||||
manifest_run_settings: resolved.manifest_run_settings,
|
||||
llm_catalog_settings: resolved.llm_catalog_settings,
|
||||
}
|
||||
});
|
||||
match resolved {
|
||||
|
|
@ -1276,6 +1280,7 @@ mod tests {
|
|||
manifest_run_settings: RunSettingsBuilder::from_run_layer(&manifest_run_defaults)
|
||||
.map_err(|err| fabro_util::error::SharedError::new(anyhow::Error::new(err))),
|
||||
manifest_run_defaults,
|
||||
manifest_environment_defaults: fabro_config::MergeMap::default(),
|
||||
server_settings: server_settings(source),
|
||||
llm_catalog_settings: fabro_model::catalog::LlmCatalogSettings::default(),
|
||||
}
|
||||
|
|
@ -1468,7 +1473,7 @@ destination = "file"
|
|||
bind: None,
|
||||
model: None,
|
||||
provider: None,
|
||||
sandbox: None,
|
||||
environment: None,
|
||||
web: true,
|
||||
no_web: false,
|
||||
max_concurrent_runs: None,
|
||||
|
|
@ -1494,7 +1499,7 @@ destination = "file"
|
|||
bind: None,
|
||||
model: None,
|
||||
provider: None,
|
||||
sandbox: None,
|
||||
environment: None,
|
||||
web: false,
|
||||
no_web: true,
|
||||
max_concurrent_runs: None,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ use fabro_auth::{CredentialSource, VaultCredentialSource, auth_issue_message};
|
|||
#[cfg(test)]
|
||||
use fabro_config::RunSettingsBuilder;
|
||||
use fabro_config::daemon::ServerDaemon;
|
||||
use fabro_config::{RunLayer, Storage};
|
||||
use fabro_config::{EnvironmentLayer, MergeMap, RunLayer, Storage};
|
||||
use fabro_interview::{
|
||||
Answer, AnswerSubmission, ControlInterviewer, Interviewer, Question, WorkerControlEnvelope,
|
||||
};
|
||||
|
|
@ -623,6 +623,7 @@ pub struct AppState {
|
|||
pub(super) server_secrets: ServerSecrets,
|
||||
pub(crate) llm_source: Arc<dyn CredentialSource>,
|
||||
manifest_run_defaults: RwLock<Arc<RunLayer>>,
|
||||
manifest_environment_defaults: RwLock<Arc<MergeMap<EnvironmentLayer>>>,
|
||||
manifest_run_settings: RwLock<std::result::Result<RunNamespace, SharedError>>,
|
||||
pub(crate) server_settings: RwLock<Arc<ServerSettings>>,
|
||||
catalog: RwLock<Arc<Catalog>>,
|
||||
|
|
@ -734,10 +735,11 @@ pub(crate) struct AppStateConfig {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ResolvedAppStateSettings {
|
||||
pub(crate) server_settings: ServerSettings,
|
||||
pub(crate) manifest_run_defaults: RunLayer,
|
||||
pub(crate) manifest_run_settings: std::result::Result<RunNamespace, SharedError>,
|
||||
pub(crate) llm_catalog_settings: LlmCatalogSettings,
|
||||
pub(crate) server_settings: ServerSettings,
|
||||
pub(crate) manifest_run_defaults: RunLayer,
|
||||
pub(crate) manifest_environment_defaults: MergeMap<EnvironmentLayer>,
|
||||
pub(crate) manifest_run_settings: std::result::Result<RunNamespace, SharedError>,
|
||||
pub(crate) llm_catalog_settings: LlmCatalogSettings,
|
||||
}
|
||||
|
||||
fn accumulate_billing_rollup(
|
||||
|
|
@ -784,6 +786,15 @@ impl AppState {
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) fn manifest_environment_defaults(&self) -> Arc<MergeMap<EnvironmentLayer>> {
|
||||
Arc::clone(
|
||||
&self
|
||||
.manifest_environment_defaults
|
||||
.read()
|
||||
.expect("manifest environment defaults lock poisoned"),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn server_settings(&self) -> Arc<ServerSettings> {
|
||||
Arc::clone(
|
||||
&self
|
||||
|
|
@ -1031,11 +1042,13 @@ impl AppState {
|
|||
let ResolvedAppStateSettings {
|
||||
server_settings,
|
||||
manifest_run_defaults,
|
||||
manifest_environment_defaults,
|
||||
manifest_run_settings,
|
||||
llm_catalog_settings,
|
||||
} = resolved_settings;
|
||||
let server_settings = Arc::new(server_settings);
|
||||
let manifest_run_defaults = Arc::new(manifest_run_defaults);
|
||||
let manifest_environment_defaults = Arc::new(manifest_environment_defaults);
|
||||
let catalog = Arc::new(
|
||||
Catalog::from_builtin_with_overrides(&llm_catalog_settings)
|
||||
.context("building LLM model catalog")?,
|
||||
|
|
@ -1047,6 +1060,10 @@ impl AppState {
|
|||
.manifest_run_defaults
|
||||
.write()
|
||||
.expect("manifest run defaults lock poisoned") = manifest_run_defaults;
|
||||
*self
|
||||
.manifest_environment_defaults
|
||||
.write()
|
||||
.expect("manifest environment defaults lock poisoned") = manifest_environment_defaults;
|
||||
*self
|
||||
.manifest_run_settings
|
||||
.write()
|
||||
|
|
@ -1663,7 +1680,7 @@ fn system_sandbox_provider(
|
|||
) -> String {
|
||||
manifest_run_settings.as_ref().map_or_else(
|
||||
|_| SandboxProvider::default().to_string(),
|
||||
|settings| settings.sandbox.provider.clone(),
|
||||
|settings| settings.environment.provider.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1766,6 +1783,8 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
|
|||
let (global_event_tx, _) = broadcast::channel(4096);
|
||||
let current_server_settings = Arc::new(resolved_settings.server_settings);
|
||||
let current_manifest_run_defaults = Arc::new(resolved_settings.manifest_run_defaults);
|
||||
let current_manifest_environment_defaults =
|
||||
Arc::new(resolved_settings.manifest_environment_defaults);
|
||||
let current_manifest_run_settings = resolved_settings.manifest_run_settings;
|
||||
let current_catalog = Arc::new(
|
||||
Catalog::from_builtin_with_overrides(&resolved_settings.llm_catalog_settings)
|
||||
|
|
@ -1816,6 +1835,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
|
|||
server_secrets,
|
||||
llm_source,
|
||||
manifest_run_defaults: RwLock::new(current_manifest_run_defaults),
|
||||
manifest_environment_defaults: RwLock::new(current_manifest_environment_defaults),
|
||||
manifest_run_settings: RwLock::new(current_manifest_run_settings),
|
||||
server_settings: RwLock::new(current_server_settings),
|
||||
catalog: RwLock::new(current_catalog),
|
||||
|
|
@ -1956,7 +1976,13 @@ async fn delete_run_sandbox_resource(
|
|||
})?;
|
||||
}
|
||||
|
||||
let preserve = projection.spec().settings.run.sandbox.preserve;
|
||||
let preserve = projection
|
||||
.spec()
|
||||
.settings
|
||||
.run
|
||||
.environment
|
||||
.lifecycle
|
||||
.preserve;
|
||||
let Some(record) = projection.sandbox else {
|
||||
return Ok(DeleteRunOutcome::NoContent);
|
||||
};
|
||||
|
|
@ -3221,7 +3247,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
|
||||
&& clone_sandbox_can_use_github_credentials(&settings.sandbox.provider)
|
||||
&& clone_sandbox_can_use_github_credentials(&settings.environment.provider.to_string())
|
||||
&& run_spec
|
||||
.repo_origin_url()
|
||||
.is_some_and(|origin| !origin.trim().is_empty());
|
||||
|
|
|
|||
|
|
@ -41,11 +41,15 @@ async fn render_graph_from_manifest(
|
|||
Json(req): Json<RenderWorkflowGraphRequest>,
|
||||
) -> Response {
|
||||
let manifest_run_defaults = state.manifest_run_defaults();
|
||||
let prepared =
|
||||
match run_manifest::prepare_manifest(manifest_run_defaults.as_ref(), &req.manifest) {
|
||||
Ok(prepared) => prepared,
|
||||
Err(err) => return ApiError::bad_request(err.to_string()).into_response(),
|
||||
};
|
||||
let manifest_environment_defaults = state.manifest_environment_defaults();
|
||||
let prepared = match run_manifest::prepare_manifest_with_environment_defaults(
|
||||
manifest_run_defaults.as_ref(),
|
||||
manifest_environment_defaults.as_ref(),
|
||||
&req.manifest,
|
||||
) {
|
||||
Ok(prepared) => prepared,
|
||||
Err(err) => return ApiError::bad_request(err.to_string()).into_response(),
|
||||
};
|
||||
let validated = match run_manifest::validate_prepared_manifest(&prepared, state.catalog()) {
|
||||
Ok(validated) => validated,
|
||||
Err(err) => return ApiError::bad_request(err.to_string()).into_response(),
|
||||
|
|
|
|||
|
|
@ -547,7 +547,12 @@ async fn create_run(
|
|||
Err(err) => return ApiError::bad_request(err.to_string()).into_response(),
|
||||
};
|
||||
let manifest_run_defaults = state.manifest_run_defaults();
|
||||
let prepared = match run_manifest::prepare_manifest(manifest_run_defaults.as_ref(), &req) {
|
||||
let manifest_environment_defaults = state.manifest_environment_defaults();
|
||||
let prepared = match run_manifest::prepare_manifest_with_environment_defaults(
|
||||
manifest_run_defaults.as_ref(),
|
||||
manifest_environment_defaults.as_ref(),
|
||||
&req,
|
||||
) {
|
||||
Ok(prepared) => prepared,
|
||||
Err(err) => return ApiError::bad_request(err.to_string()).into_response(),
|
||||
};
|
||||
|
|
@ -676,7 +681,12 @@ async fn run_preflight(
|
|||
Json(req): Json<RunManifest>,
|
||||
) -> Response {
|
||||
let manifest_run_defaults = state.manifest_run_defaults();
|
||||
let prepared = match run_manifest::prepare_manifest(manifest_run_defaults.as_ref(), &req) {
|
||||
let manifest_environment_defaults = state.manifest_environment_defaults();
|
||||
let prepared = match run_manifest::prepare_manifest_with_environment_defaults(
|
||||
manifest_run_defaults.as_ref(),
|
||||
manifest_environment_defaults.as_ref(),
|
||||
&req,
|
||||
) {
|
||||
Ok(prepared) => prepared,
|
||||
Err(err) => return ApiError::bad_request(err.to_string()).into_response(),
|
||||
};
|
||||
|
|
@ -704,7 +714,12 @@ async fn validate_run_manifest(
|
|||
Json(req): Json<RunManifest>,
|
||||
) -> Response {
|
||||
let manifest_run_defaults = state.manifest_run_defaults();
|
||||
let prepared = match run_manifest::prepare_manifest(manifest_run_defaults.as_ref(), &req) {
|
||||
let manifest_environment_defaults = state.manifest_environment_defaults();
|
||||
let prepared = match run_manifest::prepare_manifest_with_environment_defaults(
|
||||
manifest_run_defaults.as_ref(),
|
||||
manifest_environment_defaults.as_ref(),
|
||||
&req,
|
||||
) {
|
||||
Ok(prepared) => prepared,
|
||||
Err(err) => return ApiError::bad_request(err.to_string()).into_response(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -855,8 +855,8 @@ methods = ["dev-token"]
|
|||
[server.web]
|
||||
url = "http://new.example.com"
|
||||
|
||||
[run.sandbox]
|
||||
provider = "invalid-provider"
|
||||
[run.environment]
|
||||
id = "missing"
|
||||
"#;
|
||||
|
||||
state
|
||||
|
|
@ -875,8 +875,8 @@ fn system_sandbox_provider_uses_manifest_defaults() {
|
|||
let source = r#"
|
||||
_version = 1
|
||||
|
||||
[run.sandbox]
|
||||
provider = "daytona"
|
||||
[run.environment]
|
||||
id = "daytona"
|
||||
"#;
|
||||
let manifest_run_settings = resolve_manifest_run_settings(
|
||||
&run_manifest::manifest_run_defaults(Some(&manifest_run_defaults_from_toml(source))),
|
||||
|
|
@ -890,8 +890,8 @@ fn system_sandbox_provider_defaults_when_manifest_run_settings_do_not_resolve()
|
|||
let source = r#"
|
||||
_version = 1
|
||||
|
||||
[run.sandbox]
|
||||
provider = "invalid-provider"
|
||||
[run.environment]
|
||||
id = "missing"
|
||||
"#;
|
||||
let manifest_run_settings = resolve_manifest_run_settings(
|
||||
&run_manifest::manifest_run_defaults(Some(&manifest_run_defaults_from_toml(source))),
|
||||
|
|
@ -9022,7 +9022,7 @@ async fn delete_run_with_preserved_sandbox_returns_handoff() {
|
|||
let app = crate::test_support::build_test_router(Arc::clone(&state));
|
||||
let run_id = RunId::new();
|
||||
let mut settings = fabro_types::WorkflowSettings::default();
|
||||
settings.run.sandbox.preserve = true;
|
||||
settings.run.environment.lifecycle.preserve = true;
|
||||
let graph = Graph::new("test");
|
||||
|
||||
create_durable_run_with_events(&state, run_id, &[
|
||||
|
|
@ -9488,8 +9488,8 @@ mode = "dry_run"
|
|||
provider = "anthropic"
|
||||
name = "claude-sonnet-4-5"
|
||||
|
||||
[run.sandbox]
|
||||
provider = "local"
|
||||
[run.environment]
|
||||
id = "local"
|
||||
|
||||
[[run.hooks]]
|
||||
name = "snapshot-hook"
|
||||
|
|
@ -10095,8 +10095,8 @@ script = "sleep 5"
|
|||
[run.prepare]
|
||||
timeout = "30s"
|
||||
|
||||
[run.sandbox]
|
||||
provider = "local"
|
||||
[run.environment]
|
||||
id = "local"
|
||||
"#;
|
||||
let state = test_app_state_with_settings_and_registry_factory(
|
||||
server_settings_from_toml(source),
|
||||
|
|
|
|||
|
|
@ -269,10 +269,12 @@ pub(crate) fn resolved_runtime_settings_for_tests(
|
|||
manifest_run_defaults: RunLayer,
|
||||
llm_catalog_settings: LlmCatalogSettings,
|
||||
) -> ResolvedAppStateSettings {
|
||||
let manifest_environment_defaults = fabro_config::MergeMap::default();
|
||||
ResolvedAppStateSettings {
|
||||
manifest_run_settings: RunSettingsBuilder::from_run_layer(&manifest_run_defaults)
|
||||
.map_err(|err| SharedError::new(anyhow::Error::new(err))),
|
||||
manifest_run_defaults,
|
||||
manifest_environment_defaults,
|
||||
server_settings,
|
||||
llm_catalog_settings,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ async fn spawn_served_listener(
|
|||
no_web: true,
|
||||
model: None,
|
||||
provider: None,
|
||||
sandbox: None,
|
||||
environment: None,
|
||||
max_concurrent_runs: None,
|
||||
config: Some(config_path),
|
||||
#[cfg(debug_assertions)]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use std::time::Duration;
|
|||
|
||||
use axum::body::{Body, to_bytes};
|
||||
use axum::http::{Request, StatusCode};
|
||||
use fabro_config::{RunLayer, RunSandboxLayer, ServerSettingsBuilder};
|
||||
use fabro_config::{RunEnvironmentLayer, RunLayer, ServerSettingsBuilder};
|
||||
use fabro_server::server::{AppState, spawn_scheduler};
|
||||
use fabro_server::test_support::{
|
||||
TestAppStateBuilder, build_test_router, llm_catalog_settings_with_provider_base_url,
|
||||
|
|
@ -94,9 +94,9 @@ pub(crate) fn test_app_state_with_options(
|
|||
pub(crate) fn test_settings() -> TestAppSettings {
|
||||
TestAppSettings {
|
||||
manifest_run_defaults: RunLayer {
|
||||
sandbox: Some(RunSandboxLayer {
|
||||
provider: Some("local".to_string()),
|
||||
..RunSandboxLayer::default()
|
||||
environment: Some(RunEnvironmentLayer {
|
||||
id: Some("default".to_string()),
|
||||
..RunEnvironmentLayer::default()
|
||||
}),
|
||||
..RunLayer::default()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use fabro_types::run_event::{
|
|||
RunFailedProps, StageCompletedProps, StagePromptProps, TodoCreatedProps, TodoDeletedProps,
|
||||
TodoUpdatedProps,
|
||||
};
|
||||
use fabro_types::settings::run::RunSandboxSettings;
|
||||
use fabro_types::settings::run::{EnvironmentProvider, RunEnvironmentSettings};
|
||||
use fabro_types::{
|
||||
AgentBackend, AskFabro, BilledModelUsage, Checkpoint, CheckpointRecord, CommandTermination,
|
||||
Conclusion, EventBody, FailureSignature, InterviewQuestionRecord, Outcome,
|
||||
|
|
@ -535,27 +535,21 @@ fn projection_from_created(event: &EventEnvelope) -> Result<RunProjection> {
|
|||
let mut projection = RunProjection::new(title, spec, stored.ts);
|
||||
projection.parent_id = props.parent_id;
|
||||
projection.web_url.clone_from(&props.web_url);
|
||||
projection.sandbox = Some(planned_sandbox(&projection.spec.settings.run.sandbox));
|
||||
projection.sandbox = Some(planned_sandbox(&projection.spec.settings.run.environment));
|
||||
Ok(projection)
|
||||
}
|
||||
|
||||
fn planned_sandbox(settings: &RunSandboxSettings) -> RunSandbox {
|
||||
let provider = settings
|
||||
.provider
|
||||
.parse::<SandboxProvider>()
|
||||
.unwrap_or(SandboxProvider::Local);
|
||||
fn planned_sandbox(settings: &RunEnvironmentSettings) -> RunSandbox {
|
||||
let provider = SandboxProvider::from(settings.provider);
|
||||
RunSandbox {
|
||||
provider,
|
||||
image: settings
|
||||
.docker
|
||||
.as_ref()
|
||||
.map(|docker| docker.image.clone())
|
||||
image: (settings.provider == EnvironmentProvider::Docker)
|
||||
.then(|| settings.image.reference.clone())
|
||||
.flatten()
|
||||
.filter(|image| !image.is_empty()),
|
||||
snapshot: settings
|
||||
.daytona
|
||||
.as_ref()
|
||||
.and_then(|daytona| daytona.snapshot.as_ref())
|
||||
.map(|snapshot| snapshot.name.clone()),
|
||||
snapshot: (settings.provider == EnvironmentProvider::Daytona)
|
||||
.then(|| settings.image.reference.clone())
|
||||
.flatten(),
|
||||
runtime: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,12 +166,12 @@ impl JsonSchema for CreateRunSpecInput {
|
|||
],
|
||||
"description": "Provider override for the run."
|
||||
},
|
||||
"sandbox": {
|
||||
"environment": {
|
||||
"anyOf": [
|
||||
{ "type": "string" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"description": "Sandbox provider override for the run."
|
||||
"description": "Named environment slug override for the run."
|
||||
},
|
||||
"preserve_sandbox": {
|
||||
"anyOf": [
|
||||
|
|
@ -210,7 +210,7 @@ pub struct CreateRunSpec {
|
|||
pub auto_approve: Option<bool>,
|
||||
pub model: Option<String>,
|
||||
pub provider: Option<String>,
|
||||
pub sandbox: Option<String>,
|
||||
pub environment: Option<String>,
|
||||
pub preserve_sandbox: Option<bool>,
|
||||
pub start: Option<bool>,
|
||||
}
|
||||
|
|
@ -272,7 +272,7 @@ pub struct ValidatedCreateRunSpec {
|
|||
pub auto_approve: Option<bool>,
|
||||
pub model: Option<String>,
|
||||
pub provider: Option<String>,
|
||||
pub sandbox: Option<String>,
|
||||
pub environment: Option<String>,
|
||||
pub preserve_sandbox: Option<bool>,
|
||||
pub start: Option<bool>,
|
||||
}
|
||||
|
|
@ -314,7 +314,7 @@ impl TryFrom<CreateRunSpecInput> for ValidatedCreateRunSpec {
|
|||
auto_approve: None,
|
||||
model: None,
|
||||
provider: None,
|
||||
sandbox: None,
|
||||
environment: None,
|
||||
preserve_sandbox: None,
|
||||
start: None,
|
||||
})
|
||||
|
|
@ -378,7 +378,7 @@ impl TryFrom<CreateRunSpec> for ValidatedCreateRunSpec {
|
|||
auto_approve: spec.auto_approve,
|
||||
model: spec.model,
|
||||
provider: spec.provider,
|
||||
sandbox: spec.sandbox,
|
||||
environment: spec.environment,
|
||||
preserve_sandbox: spec.preserve_sandbox,
|
||||
start: spec.start,
|
||||
})
|
||||
|
|
@ -547,7 +547,7 @@ mod tests {
|
|||
auto_approve: None,
|
||||
model: None,
|
||||
provider: None,
|
||||
sandbox: None,
|
||||
environment: None,
|
||||
preserve_sandbox: None,
|
||||
start: None,
|
||||
})
|
||||
|
|
@ -692,7 +692,7 @@ mod tests {
|
|||
auto_approve: Some(true),
|
||||
model: None,
|
||||
provider: None,
|
||||
sandbox: None,
|
||||
environment: None,
|
||||
preserve_sandbox: None,
|
||||
start: Some(false),
|
||||
}
|
||||
|
|
@ -742,7 +742,7 @@ mod tests {
|
|||
auto_approve: Some(true),
|
||||
model: None,
|
||||
provider: None,
|
||||
sandbox: None,
|
||||
environment: None,
|
||||
preserve_sandbox: None,
|
||||
start: Some(false),
|
||||
})
|
||||
|
|
@ -791,7 +791,7 @@ mod tests {
|
|||
auto_approve: Some(true),
|
||||
model: None,
|
||||
provider: None,
|
||||
sandbox: None,
|
||||
environment: None,
|
||||
preserve_sandbox: None,
|
||||
start: Some(false),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ use std::path::Path;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::settings::{
|
||||
CliNamespace, InterpString, ObjectStoreSettings, ProjectNamespace, RunNamespace,
|
||||
ServerNamespace, WorkflowNamespace,
|
||||
CliNamespace, EnvironmentSettings, InterpString, ObjectStoreSettings, ProjectNamespace,
|
||||
RunNamespace, ServerNamespace, WorkflowNamespace,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -47,9 +47,10 @@ pub struct UserSettings {
|
|||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WorkflowSettings {
|
||||
pub project: ProjectNamespace,
|
||||
pub workflow: WorkflowNamespace,
|
||||
pub run: RunNamespace,
|
||||
pub project: ProjectNamespace,
|
||||
pub workflow: WorkflowNamespace,
|
||||
pub environments: HashMap<String, EnvironmentSettings>,
|
||||
pub run: RunNamespace,
|
||||
}
|
||||
|
||||
impl WorkflowSettings {
|
||||
|
|
|
|||
|
|
@ -34,12 +34,14 @@ pub use public_url::{
|
|||
is_wildcard_host, replace_wildcard_host, validate_public_url, validate_public_url_with_label,
|
||||
};
|
||||
pub use run::{
|
||||
ArtifactsSettings, DaytonaSettings, DaytonaSnapshotSettings, DockerSettings, DockerfileSource,
|
||||
ArtifactsSettings, DockerfileSource, EnvironmentImageSettings, EnvironmentLifecycleSettings,
|
||||
EnvironmentNetworkMode, EnvironmentNetworkSettings, EnvironmentProvider,
|
||||
EnvironmentResourcesSettings, EnvironmentSettings, EnvironmentVolumeSettings,
|
||||
GitAuthorSettings, HookDefinition, HookType, InterviewProviderSettings, McpServerSettings,
|
||||
McpTransport, NotificationProviderSettings, NotificationRouteSettings, PullRequestSettings,
|
||||
RunAgentSettings, RunCheckpointSettings, RunExecutionSettings, RunGitSettings, RunGoal,
|
||||
RunIntegrationsGithubSettings, RunIntegrationsSettings, RunInterviewsSettings,
|
||||
RunModelControls, RunModelSettings, RunNamespace, RunPrepareSettings, RunSandboxSettings,
|
||||
RunAgentSettings, RunCheckpointSettings, RunEnvironmentSettings, RunExecutionSettings,
|
||||
RunGitSettings, RunGoal, RunIntegrationsGithubSettings, RunIntegrationsSettings,
|
||||
RunInterviewsSettings, RunModelControls, RunModelSettings, RunNamespace, RunPrepareSettings,
|
||||
RunScmSettings, ScmGitHubSettings, TlsMode,
|
||||
};
|
||||
pub use server::{
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ use std::time::Duration as StdDuration;
|
|||
use serde::ser::SerializeStruct;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::duration::Duration;
|
||||
use super::interp::InterpString;
|
||||
use super::model_ref::ModelRef;
|
||||
use super::size::Size;
|
||||
|
||||
/// A structurally resolved `[run]` view for consumers.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -31,7 +33,7 @@ pub struct RunNamespace {
|
|||
pub clone: RunCloneSettings,
|
||||
pub run_branch: RunBranchSettings,
|
||||
pub meta_branch: RunMetaBranchSettings,
|
||||
pub sandbox: RunSandboxSettings,
|
||||
pub environment: RunEnvironmentSettings,
|
||||
pub notifications: HashMap<String, NotificationRouteSettings>,
|
||||
pub interviews: RunInterviewsSettings,
|
||||
pub agent: RunAgentSettings,
|
||||
|
|
@ -61,7 +63,7 @@ impl Default for RunNamespace {
|
|||
clone: RunCloneSettings::default(),
|
||||
run_branch: RunBranchSettings::default(),
|
||||
meta_branch: RunMetaBranchSettings::default(),
|
||||
sandbox: RunSandboxSettings::default(),
|
||||
environment: RunEnvironmentSettings::default(),
|
||||
notifications: HashMap::new(),
|
||||
interviews: RunInterviewsSettings::default(),
|
||||
agent: RunAgentSettings::default(),
|
||||
|
|
@ -287,62 +289,193 @@ 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::SandboxProvider {
|
||||
fn from(value: EnvironmentProvider) -> Self {
|
||||
match value {
|
||||
EnvironmentProvider::Local => Self::Local,
|
||||
EnvironmentProvider::Docker => Self::Docker,
|
||||
EnvironmentProvider::Daytona => Self::Daytona,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
strum::Display,
|
||||
strum::EnumString,
|
||||
strum::IntoStaticStr,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
|
||||
pub enum EnvironmentNetworkMode {
|
||||
#[default]
|
||||
AllowAll,
|
||||
Block,
|
||||
CidrAllowList,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct EnvironmentImageSettings {
|
||||
#[serde(rename = "ref")]
|
||||
pub reference: Option<String>,
|
||||
pub dockerfile: Option<DockerfileSource>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct EnvironmentResourcesSettings {
|
||||
pub cpu: Option<i32>,
|
||||
pub memory: Option<Size>,
|
||||
pub disk: Option<Size>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunSandboxSettings {
|
||||
pub provider: String,
|
||||
pub struct EnvironmentNetworkSettings {
|
||||
pub mode: EnvironmentNetworkMode,
|
||||
pub allow: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for EnvironmentNetworkSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode: EnvironmentNetworkMode::AllowAll,
|
||||
allow: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct EnvironmentLifecycleSettings {
|
||||
pub preserve: bool,
|
||||
#[serde(default = "default_stop_on_terminal")]
|
||||
pub stop_on_terminal: bool,
|
||||
pub devcontainer: bool,
|
||||
pub env: HashMap<String, InterpString>,
|
||||
pub docker: Option<DockerSettings>,
|
||||
pub daytona: Option<DaytonaSettings>,
|
||||
pub auto_stop: Option<Duration>,
|
||||
}
|
||||
|
||||
fn default_stop_on_terminal() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for RunSandboxSettings {
|
||||
impl Default for EnvironmentLifecycleSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: "local".to_string(),
|
||||
preserve: false,
|
||||
stop_on_terminal: true,
|
||||
devcontainer: false,
|
||||
env: HashMap::new(),
|
||||
docker: None,
|
||||
daytona: None,
|
||||
auto_stop: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DockerSettings {
|
||||
pub image: String,
|
||||
pub network_mode: Option<String>,
|
||||
pub memory_limit: Option<i64>,
|
||||
pub cpu_quota: Option<i64>,
|
||||
pub env_vars: HashMap<String, InterpString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DaytonaSettings {
|
||||
pub auto_stop_interval: Option<i32>,
|
||||
pub labels: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub volumes: Vec<DaytonaVolumeSettings>,
|
||||
pub snapshot: Option<DaytonaSnapshotSettings>,
|
||||
pub network: Option<DaytonaNetworkLayer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DaytonaVolumeSettings {
|
||||
pub volume_id: String,
|
||||
pub struct EnvironmentVolumeSettings {
|
||||
pub id: String,
|
||||
pub mount_path: String,
|
||||
pub subpath: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct EnvironmentSettings {
|
||||
pub provider: EnvironmentProvider,
|
||||
pub image: EnvironmentImageSettings,
|
||||
pub resources: EnvironmentResourcesSettings,
|
||||
pub network: EnvironmentNetworkSettings,
|
||||
pub lifecycle: EnvironmentLifecycleSettings,
|
||||
pub labels: HashMap<String, String>,
|
||||
pub volumes: Vec<EnvironmentVolumeSettings>,
|
||||
pub env: HashMap<String, InterpString>,
|
||||
}
|
||||
|
||||
impl Default for EnvironmentSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: EnvironmentProvider::Local,
|
||||
image: EnvironmentImageSettings::default(),
|
||||
resources: EnvironmentResourcesSettings::default(),
|
||||
network: EnvironmentNetworkSettings::default(),
|
||||
lifecycle: EnvironmentLifecycleSettings::default(),
|
||||
labels: HashMap::new(),
|
||||
volumes: Vec::new(),
|
||||
env: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunEnvironmentSettings {
|
||||
pub id: String,
|
||||
pub provider: EnvironmentProvider,
|
||||
pub image: EnvironmentImageSettings,
|
||||
pub resources: EnvironmentResourcesSettings,
|
||||
pub network: EnvironmentNetworkSettings,
|
||||
pub lifecycle: EnvironmentLifecycleSettings,
|
||||
pub labels: HashMap<String, String>,
|
||||
pub volumes: Vec<EnvironmentVolumeSettings>,
|
||||
pub env: HashMap<String, InterpString>,
|
||||
}
|
||||
|
||||
impl RunEnvironmentSettings {
|
||||
#[must_use]
|
||||
pub fn from_environment(id: String, environment: EnvironmentSettings) -> Self {
|
||||
Self {
|
||||
id,
|
||||
provider: environment.provider,
|
||||
image: environment.image,
|
||||
resources: environment.resources,
|
||||
network: environment.network,
|
||||
lifecycle: environment.lifecycle,
|
||||
labels: environment.labels,
|
||||
volumes: environment.volumes,
|
||||
env: environment.env,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RunEnvironmentSettings {
|
||||
fn default() -> Self {
|
||||
Self::from_environment("default".to_string(), EnvironmentSettings::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum DockerfileSource {
|
||||
Inline(String),
|
||||
|
|
@ -388,15 +521,6 @@ impl<'de> Deserialize<'de> for DockerfileSource {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DaytonaSnapshotSettings {
|
||||
pub name: String,
|
||||
pub cpu: Option<i32>,
|
||||
pub memory_gb: Option<i32>,
|
||||
pub disk_gb: Option<i32>,
|
||||
pub dockerfile: Option<DockerfileSource>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct NotificationRouteSettings {
|
||||
pub enabled: bool,
|
||||
|
|
@ -707,14 +831,6 @@ pub enum ApprovalMode {
|
|||
Auto,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum DaytonaNetworkLayer {
|
||||
Block,
|
||||
AllowAll,
|
||||
AllowList { allow_list: Vec<String> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum AgentPermissions {
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ use std::sync::Arc;
|
|||
use fabro_config::Storage;
|
||||
use fabro_graphviz::graph::{AttrValue, Graph};
|
||||
use fabro_model::{Catalog, ProviderId};
|
||||
use fabro_sandbox::SandboxProvider;
|
||||
use fabro_store::Database;
|
||||
use fabro_types::settings::run::{RunMode, RunNamespace};
|
||||
use fabro_types::{
|
||||
ForkSourceRef, GitContext, ManifestPath, RunId, RunProvenance, WorkflowSettings,
|
||||
};
|
||||
|
|
@ -91,9 +89,6 @@ pub async fn create(
|
|||
cwd: request.cwd,
|
||||
})
|
||||
.map_err(|err| Error::Parse(err.to_string()))?;
|
||||
if resolved.settings.run.execution.mode != RunMode::DryRun {
|
||||
validate_sandbox_provider(&resolved.settings.run)?;
|
||||
}
|
||||
let labels = resolved.settings.combined_labels();
|
||||
let settings = resolved.settings.clone();
|
||||
|
||||
|
|
@ -277,15 +272,6 @@ fn store_error(err: impl std::fmt::Display) -> Error {
|
|||
Error::engine(err.to_string())
|
||||
}
|
||||
|
||||
fn validate_sandbox_provider(run: &RunNamespace) -> Result<(), Error> {
|
||||
run.sandbox
|
||||
.provider
|
||||
.parse::<SandboxProvider>()
|
||||
.map_err(|err| Error::Precondition(format!("Invalid sandbox provider: {err}")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_from_source(
|
||||
dot_source: &str,
|
||||
options: PersistCreateOptions,
|
||||
|
|
@ -1130,53 +1116,6 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_reports_workflow_settings_errors_with_rendered_message() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let storage_root = dir.path().join("storage");
|
||||
let store = memory_store();
|
||||
let err = create(
|
||||
&store,
|
||||
CreateRunInput {
|
||||
workflow: WorkflowInput::DotSource {
|
||||
source: MINIMAL_DOT.to_string(),
|
||||
base_dir: None,
|
||||
},
|
||||
settings: {
|
||||
let mut settings = WorkflowSettings::default();
|
||||
settings.run.execution.mode = RunMode::Normal;
|
||||
settings.run.sandbox.provider = "not-a-provider".to_string();
|
||||
settings
|
||||
},
|
||||
cwd: dir.path().to_path_buf(),
|
||||
workflow_slug: None,
|
||||
workflow_path: None,
|
||||
workflow_bundle: None,
|
||||
submitted_manifest_bytes: None,
|
||||
run_id: None,
|
||||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
},
|
||||
storage_root,
|
||||
test_catalog(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
match err {
|
||||
Error::Precondition(message) => {
|
||||
assert!(message.contains("Invalid sandbox provider"));
|
||||
assert!(!message.contains('\n'));
|
||||
}
|
||||
other => panic!("expected Precondition, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_persists_normalized_config_and_initial_state() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ use fabro_sandbox::daytona::DaytonaConfig;
|
|||
use fabro_sandbox::{DockerSandboxOptions, SandboxProvider, SandboxSpec};
|
||||
use fabro_static::EnvVars;
|
||||
use fabro_types::settings::run::{
|
||||
ApprovalMode, DaytonaNetworkLayer, DaytonaSettings, DockerSettings,
|
||||
DockerfileSource as ResolvedDockerfileSource, HookDefinition as ResolvedHookDefinition,
|
||||
HookEvent as ResolvedHookEvent, HookType as ResolvedHookType,
|
||||
McpServerSettings as ResolvedMcpServerSettings, McpTransport as ResolvedMcpTransport,
|
||||
PullRequestSettings, RunMode, RunModelSettings as ResolvedRunModelSettings,
|
||||
RunNamespace as ResolvedRunSettings, TlsMode as ResolvedTlsMode,
|
||||
ApprovalMode, DockerfileSource as ResolvedDockerfileSource, EnvironmentNetworkMode,
|
||||
HookDefinition as ResolvedHookDefinition, HookEvent as ResolvedHookEvent,
|
||||
HookType as ResolvedHookType, McpServerSettings as ResolvedMcpServerSettings,
|
||||
McpTransport as ResolvedMcpTransport, PullRequestSettings, RunEnvironmentSettings, RunMode,
|
||||
RunModelSettings as ResolvedRunModelSettings, RunNamespace as ResolvedRunSettings,
|
||||
TlsMode as ResolvedTlsMode,
|
||||
};
|
||||
use fabro_types::settings::{InterpString, ModelRegistry, ResolvedModelRef};
|
||||
use fabro_types::{ManifestPath, RunId};
|
||||
|
|
@ -316,7 +316,7 @@ impl RunSession {
|
|||
|
||||
let resolved = &settings.run;
|
||||
|
||||
let sandbox_provider = resolve_sandbox_provider(resolved)?;
|
||||
let sandbox_provider = resolve_sandbox_provider(resolved);
|
||||
let sandbox_provider =
|
||||
if resolved.execution.mode == RunMode::DryRun && !sandbox_provider.is_local() {
|
||||
SandboxProvider::Local
|
||||
|
|
@ -366,7 +366,7 @@ impl RunSession {
|
|||
};
|
||||
|
||||
let toml_env: HashMap<String, String> = resolved
|
||||
.sandbox
|
||||
.environment
|
||||
.env
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), resolve_interp(v)))
|
||||
|
|
@ -380,10 +380,7 @@ impl RunSession {
|
|||
origin_url: record.repo_origin_url().map(str::to_string),
|
||||
};
|
||||
|
||||
let devcontainer = resolved.sandbox.devcontainer.then(|| DevcontainerSpec {
|
||||
enabled: true,
|
||||
resolve_dir: working_directory.clone(),
|
||||
});
|
||||
let devcontainer = None;
|
||||
|
||||
let interviewer: Arc<dyn Interviewer> = if resolved.execution.approval == ApprovalMode::Auto
|
||||
{
|
||||
|
|
@ -427,8 +424,8 @@ impl RunSession {
|
|||
git,
|
||||
github_app: services.github_app.clone(),
|
||||
registry_override: services.registry_override,
|
||||
preserve_sandbox: resolved.sandbox.preserve,
|
||||
stop_on_terminal: resolved.sandbox.stop_on_terminal,
|
||||
preserve_sandbox: resolved.environment.lifecycle.preserve,
|
||||
stop_on_terminal: resolved.environment.lifecycle.stop_on_terminal,
|
||||
pr_config,
|
||||
pr_github_app: services.github_app,
|
||||
pr_origin_url: record.repo_origin_url().map(str::to_string),
|
||||
|
|
@ -515,33 +512,18 @@ async fn load_accepted_run_definition(
|
|||
serde_json::from_slice(&bytes).map_err(|err| Error::Parse(err.to_string()))
|
||||
}
|
||||
|
||||
fn resolve_sandbox_provider(settings: &ResolvedRunSettings) -> Result<SandboxProvider, Error> {
|
||||
Some(str::parse::<SandboxProvider>(
|
||||
settings.sandbox.provider.as_str(),
|
||||
))
|
||||
.transpose()
|
||||
.map_err(|err| Error::Precondition(format!("Invalid sandbox provider: {err}")))?
|
||||
.map_or_else(|| Ok(SandboxProvider::default()), Ok)
|
||||
fn resolve_sandbox_provider(settings: &ResolvedRunSettings) -> SandboxProvider {
|
||||
SandboxProvider::from(settings.environment.provider)
|
||||
}
|
||||
|
||||
fn resolve_daytona_config(settings: &ResolvedRunSettings) -> DaytonaConfig {
|
||||
let mut config = settings
|
||||
.sandbox
|
||||
.daytona
|
||||
.as_ref()
|
||||
.map(|daytona| runtime_daytona_config(daytona, !settings.clone.enabled))
|
||||
.unwrap_or_default();
|
||||
let mut config = runtime_daytona_config(&settings.environment, !settings.clone.enabled);
|
||||
config.skip_clone = !settings.clone.enabled;
|
||||
config
|
||||
}
|
||||
|
||||
fn resolve_docker_config(settings: &ResolvedRunSettings) -> DockerSandboxOptions {
|
||||
let mut config = settings
|
||||
.sandbox
|
||||
.docker
|
||||
.as_ref()
|
||||
.map(|docker| runtime_docker_config(docker, !settings.clone.enabled))
|
||||
.unwrap_or_default();
|
||||
let mut config = runtime_docker_config(&settings.environment, !settings.clone.enabled);
|
||||
config.skip_clone = !settings.clone.enabled;
|
||||
config
|
||||
}
|
||||
|
|
@ -676,28 +658,39 @@ fn runtime_mcp_server(settings: &ResolvedMcpServerSettings) -> McpServerSettings
|
|||
}
|
||||
}
|
||||
|
||||
fn runtime_daytona_config(settings: &DaytonaSettings, skip_clone: bool) -> DaytonaConfig {
|
||||
fn runtime_daytona_config(settings: &RunEnvironmentSettings, skip_clone: bool) -> DaytonaConfig {
|
||||
DaytonaConfig {
|
||||
auto_stop_interval: settings.auto_stop_interval,
|
||||
auto_stop_interval: settings
|
||||
.lifecycle
|
||||
.auto_stop
|
||||
.map(|duration| duration_to_minutes_i32(duration.as_std())),
|
||||
labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()),
|
||||
volumes: settings
|
||||
.volumes
|
||||
.iter()
|
||||
.map(|volume| DaytonaVolumeMount {
|
||||
volume_id: volume.volume_id.clone(),
|
||||
volume_id: volume.id.clone(),
|
||||
mount_path: volume.mount_path.clone(),
|
||||
subpath: volume.subpath.clone(),
|
||||
})
|
||||
.collect(),
|
||||
snapshot: settings
|
||||
.snapshot
|
||||
.image
|
||||
.reference
|
||||
.as_ref()
|
||||
.map(|snapshot| DaytonaSnapshotSettings {
|
||||
name: snapshot.name.clone(),
|
||||
cpu: snapshot.cpu,
|
||||
memory: snapshot.memory_gb,
|
||||
disk: snapshot.disk_gb,
|
||||
dockerfile: snapshot
|
||||
.map(|name| DaytonaSnapshotSettings {
|
||||
name: name.clone(),
|
||||
cpu: settings.resources.cpu,
|
||||
memory: settings
|
||||
.resources
|
||||
.memory
|
||||
.map(|size| size_to_gb_i32(size.as_bytes())),
|
||||
disk: settings
|
||||
.resources
|
||||
.disk
|
||||
.map(|size| size_to_gb_i32(size.as_bytes())),
|
||||
dockerfile: settings
|
||||
.image
|
||||
.dockerfile
|
||||
.as_ref()
|
||||
.map(|dockerfile| match dockerfile {
|
||||
|
|
@ -709,36 +702,65 @@ fn runtime_daytona_config(settings: &DaytonaSettings, skip_clone: bool) -> Dayto
|
|||
}
|
||||
}),
|
||||
}),
|
||||
network: settings.network.as_ref().map(|network| match network {
|
||||
DaytonaNetworkLayer::Block => DaytonaNetwork::Block,
|
||||
DaytonaNetworkLayer::AllowAll => DaytonaNetwork::AllowAll,
|
||||
DaytonaNetworkLayer::AllowList { allow_list } => {
|
||||
DaytonaNetwork::AllowList(allow_list.clone())
|
||||
network: Some(match settings.network.mode {
|
||||
EnvironmentNetworkMode::Block => DaytonaNetwork::Block,
|
||||
EnvironmentNetworkMode::AllowAll => DaytonaNetwork::AllowAll,
|
||||
EnvironmentNetworkMode::CidrAllowList => {
|
||||
DaytonaNetwork::AllowList(settings.network.allow.clone())
|
||||
}
|
||||
}),
|
||||
skip_clone,
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_docker_config(settings: &DockerSettings, skip_clone: bool) -> DockerSandboxOptions {
|
||||
fn runtime_docker_config(
|
||||
settings: &RunEnvironmentSettings,
|
||||
skip_clone: bool,
|
||||
) -> DockerSandboxOptions {
|
||||
let mut env_vars = settings
|
||||
.env_vars
|
||||
.env
|
||||
.iter()
|
||||
.map(|(key, value)| format!("{key}={}", resolve_interp(value)))
|
||||
.collect::<Vec<_>>();
|
||||
env_vars.sort();
|
||||
let default_options = DockerSandboxOptions::default();
|
||||
|
||||
DockerSandboxOptions {
|
||||
image: settings.image.clone(),
|
||||
network_mode: settings.network_mode.clone(),
|
||||
memory_limit: settings.memory_limit,
|
||||
cpu_quota: settings.cpu_quota,
|
||||
image: settings
|
||||
.image
|
||||
.reference
|
||||
.clone()
|
||||
.unwrap_or(default_options.image),
|
||||
network_mode: match settings.network.mode {
|
||||
EnvironmentNetworkMode::Block => Some("none".to_string()),
|
||||
EnvironmentNetworkMode::AllowAll | EnvironmentNetworkMode::CidrAllowList => {
|
||||
default_options.network_mode
|
||||
}
|
||||
},
|
||||
memory_limit: settings
|
||||
.resources
|
||||
.memory
|
||||
.and_then(|size| i64::try_from(size.as_bytes()).ok()),
|
||||
cpu_quota: settings
|
||||
.resources
|
||||
.cpu
|
||||
.map(|cpu| i64::from(cpu).saturating_mul(100_000)),
|
||||
env_vars,
|
||||
skip_clone,
|
||||
..DockerSandboxOptions::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn duration_to_minutes_i32(duration: Duration) -> i32 {
|
||||
let minutes = duration.as_secs() / 60;
|
||||
i32::try_from(minutes).unwrap_or(i32::MAX)
|
||||
}
|
||||
|
||||
fn size_to_gb_i32(bytes: u64) -> i32 {
|
||||
let gb = bytes / 1_000_000_000;
|
||||
i32::try_from(gb).unwrap_or(i32::MAX)
|
||||
}
|
||||
|
||||
fn runtime_hook_definition(definition: &ResolvedHookDefinition) -> fabro_hooks::HookDefinition {
|
||||
fabro_hooks::HookDefinition {
|
||||
name: definition.name.clone(),
|
||||
|
|
@ -1135,12 +1157,13 @@ mod tests {
|
|||
|
||||
use chrono::Utc;
|
||||
use fabro_config::{
|
||||
DaytonaSandboxLayer, DaytonaVolumeLayer, RunCloneLayer, RunExecutionLayer, RunLayer,
|
||||
RunSandboxLayer, WorkflowSettingsBuilder,
|
||||
EnvironmentImageLayer, EnvironmentNetworkLayer, EnvironmentResourcesLayer,
|
||||
EnvironmentVolumeLayer, RunCloneLayer, RunEnvironmentLayer, RunExecutionLayer, RunLayer,
|
||||
StickyMap, WorkflowSettingsBuilder,
|
||||
};
|
||||
use fabro_store::Database;
|
||||
use fabro_types::settings::ModelRef;
|
||||
use fabro_types::settings::run::RunMode;
|
||||
use fabro_types::settings::{InterpString, ModelRef};
|
||||
use fabro_types::{ManifestPath, WorkflowSettings, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
|
|
@ -1284,19 +1307,51 @@ reasoning = false
|
|||
assert!(resolve_daytona_config(&settings.run).skip_clone);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_docker_config_maps_environment_hints() {
|
||||
let settings = settings_from_run_layer(RunLayer {
|
||||
environment: Some(RunEnvironmentLayer {
|
||||
image: Some(EnvironmentImageLayer {
|
||||
reference: Some("ubuntu:24.04".to_string()),
|
||||
..EnvironmentImageLayer::default()
|
||||
}),
|
||||
resources: Some(EnvironmentResourcesLayer {
|
||||
cpu: Some(4),
|
||||
memory: Some("2GB".parse().unwrap()),
|
||||
disk: None,
|
||||
}),
|
||||
network: Some(EnvironmentNetworkLayer {
|
||||
mode: Some("block".to_string()),
|
||||
allow: Vec::new(),
|
||||
}),
|
||||
env: StickyMap::from(HashMap::from([(
|
||||
"NODE_ENV".to_string(),
|
||||
InterpString::parse("test"),
|
||||
)])),
|
||||
..RunEnvironmentLayer::default()
|
||||
}),
|
||||
..RunLayer::default()
|
||||
});
|
||||
|
||||
let config = resolve_docker_config(&settings.run);
|
||||
|
||||
assert_eq!(config.image, "ubuntu:24.04");
|
||||
assert_eq!(config.cpu_quota, Some(400_000));
|
||||
assert_eq!(config.memory_limit, Some(2_000_000_000));
|
||||
assert_eq!(config.network_mode.as_deref(), Some("none"));
|
||||
assert_eq!(config.env_vars, vec!["NODE_ENV=test"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_daytona_config_preserves_volume_mounts() {
|
||||
let settings = settings_from_run_layer(RunLayer {
|
||||
sandbox: Some(RunSandboxLayer {
|
||||
daytona: Some(DaytonaSandboxLayer {
|
||||
volumes: Some(vec![DaytonaVolumeLayer {
|
||||
volume_id: "vol_auth".to_string(),
|
||||
mount_path: "/home/daytona/.config".to_string(),
|
||||
subpath: Some("agents".to_string()),
|
||||
}]),
|
||||
..DaytonaSandboxLayer::default()
|
||||
}),
|
||||
..RunSandboxLayer::default()
|
||||
environment: Some(RunEnvironmentLayer {
|
||||
volumes: Some(vec![EnvironmentVolumeLayer {
|
||||
id: "vol_auth".to_string(),
|
||||
mount_path: "/home/daytona/.config".to_string(),
|
||||
subpath: Some("agents".to_string()),
|
||||
}]),
|
||||
..RunEnvironmentLayer::default()
|
||||
}),
|
||||
..RunLayer::default()
|
||||
});
|
||||
|
|
|
|||
|
|
@ -67,11 +67,6 @@ models/create-completion-request.ts
|
|||
models/create-run-pull-request-request.ts
|
||||
models/create-run-session-request.ts
|
||||
models/create-secret-request.ts
|
||||
models/daytona-network-layer-one-of-allow-list.ts
|
||||
models/daytona-network-layer-one-of.ts
|
||||
models/daytona-network-layer.ts
|
||||
models/daytona-settings.ts
|
||||
models/daytona-snapshot-settings.ts
|
||||
models/delete-run-response.ts
|
||||
models/delete-run-sandbox.ts
|
||||
models/delete-secret-request.ts
|
||||
|
|
@ -90,10 +85,17 @@ models/dirty-status.ts
|
|||
models/disk-usage-response.ts
|
||||
models/disk-usage-run-row.ts
|
||||
models/disk-usage-summary-row.ts
|
||||
models/docker-settings.ts
|
||||
models/dockerfile-source-inline.ts
|
||||
models/dockerfile-source-path.ts
|
||||
models/dockerfile-source.ts
|
||||
models/environment-image-settings.ts
|
||||
models/environment-lifecycle-settings.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-volume-settings.ts
|
||||
models/error-response-entry.ts
|
||||
models/error-response.ts
|
||||
models/event-envelope.ts
|
||||
|
|
@ -280,6 +282,7 @@ models/run-commit.ts
|
|||
models/run-commits-meta.ts
|
||||
models/run-control-action.ts
|
||||
models/run-diff.ts
|
||||
models/run-environment-settings.ts
|
||||
models/run-error.ts
|
||||
models/run-event-detail-response-content.ts
|
||||
models/run-event-detail-response-event.ts
|
||||
|
|
@ -311,7 +314,6 @@ models/run-provenance.ts
|
|||
models/run-question.ts
|
||||
models/run-reference.ts
|
||||
models/run-sandbox-runtime.ts
|
||||
models/run-sandbox-settings.ts
|
||||
models/run-sandbox.ts
|
||||
models/run-scm-settings.ts
|
||||
models/run-server-provenance.ts
|
||||
|
|
@ -410,6 +412,10 @@ models/system-resources-response.ts
|
|||
models/system-run-counts.ts
|
||||
models/timeline-entry-response.ts
|
||||
models/tls-mode.ts
|
||||
models/todo-list-kind.ts
|
||||
models/todo-list-projection.ts
|
||||
models/todo-projection.ts
|
||||
models/todo-status.ts
|
||||
models/update-run-parent-request.ts
|
||||
models/update-run-request.ts
|
||||
models/user-response.ts
|
||||
|
|
@ -425,4 +431,4 @@ models/workflow-ref.ts
|
|||
models/workflow-reference.ts
|
||||
models/workflow-schedule-summary.ts
|
||||
models/workflow-settings.ts
|
||||
models/write-blob-response.ts
|
||||
models/write-blob-response.ts
|
||||
|
|
|
|||
23
lib/packages/fabro-api-client/src/models/environment-image-settings.ts
generated
Normal file
23
lib/packages/fabro-api-client/src/models/environment-image-settings.ts
generated
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { DockerfileSource } from './dockerfile-source';
|
||||
|
||||
export interface EnvironmentImageSettings {
|
||||
'ref': string | null;
|
||||
'dockerfile': DockerfileSource | null;
|
||||
}
|
||||
21
lib/packages/fabro-api-client/src/models/environment-lifecycle-settings.ts
generated
Normal file
21
lib/packages/fabro-api-client/src/models/environment-lifecycle-settings.ts
generated
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export interface EnvironmentLifecycleSettings {
|
||||
'preserve': boolean;
|
||||
'stop_on_terminal': boolean;
|
||||
'auto_stop': string | null;
|
||||
}
|
||||
24
lib/packages/fabro-api-client/src/models/environment-network-mode.ts
generated
Normal file
24
lib/packages/fabro-api-client/src/models/environment-network-mode.ts
generated
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
export const EnvironmentNetworkMode = {
|
||||
ALLOW_ALL: 'allow_all',
|
||||
BLOCK: 'block',
|
||||
CIDR_ALLOW_LIST: 'cidr_allow_list'
|
||||
} as const;
|
||||
|
||||
export type EnvironmentNetworkMode = typeof EnvironmentNetworkMode[keyof typeof EnvironmentNetworkMode];
|
||||
23
lib/packages/fabro-api-client/src/models/environment-network-settings.ts
generated
Normal file
23
lib/packages/fabro-api-client/src/models/environment-network-settings.ts
generated
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { EnvironmentNetworkMode } from './environment-network-mode';
|
||||
|
||||
export interface EnvironmentNetworkSettings {
|
||||
'mode': EnvironmentNetworkMode;
|
||||
'allow': Array<string>;
|
||||
}
|
||||
27
lib/packages/fabro-api-client/src/models/environment-provider.ts
generated
Normal file
27
lib/packages/fabro-api-client/src/models/environment-provider.ts
generated
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Desired environment provider.
|
||||
*/
|
||||
|
||||
export const EnvironmentProvider = {
|
||||
LOCAL: 'local',
|
||||
DOCKER: 'docker',
|
||||
DAYTONA: 'daytona'
|
||||
} as const;
|
||||
|
||||
export type EnvironmentProvider = typeof EnvironmentProvider[keyof typeof EnvironmentProvider];
|
||||
21
lib/packages/fabro-api-client/src/models/environment-resources-settings.ts
generated
Normal file
21
lib/packages/fabro-api-client/src/models/environment-resources-settings.ts
generated
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export interface EnvironmentResourcesSettings {
|
||||
'cpu': number | null;
|
||||
'memory': string | null;
|
||||
'disk': string | null;
|
||||
}
|
||||
44
lib/packages/fabro-api-client/src/models/environment-settings.ts
generated
Normal file
44
lib/packages/fabro-api-client/src/models/environment-settings.ts
generated
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { EnvironmentImageSettings } from './environment-image-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { EnvironmentLifecycleSettings } from './environment-lifecycle-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
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';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { EnvironmentVolumeSettings } from './environment-volume-settings';
|
||||
|
||||
export interface EnvironmentSettings {
|
||||
'provider': EnvironmentProvider;
|
||||
'image': EnvironmentImageSettings;
|
||||
'resources': EnvironmentResourcesSettings;
|
||||
'network': EnvironmentNetworkSettings;
|
||||
'lifecycle': EnvironmentLifecycleSettings;
|
||||
'labels': { [key: string]: string; };
|
||||
'volumes': Array<EnvironmentVolumeSettings>;
|
||||
'env': { [key: string]: string; };
|
||||
}
|
||||
21
lib/packages/fabro-api-client/src/models/environment-volume-settings.ts
generated
Normal file
21
lib/packages/fabro-api-client/src/models/environment-volume-settings.ts
generated
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export interface EnvironmentVolumeSettings {
|
||||
'id': string;
|
||||
'mount_path': string;
|
||||
'subpath': string | null;
|
||||
}
|
||||
18
lib/packages/fabro-api-client/src/models/index.ts
generated
18
lib/packages/fabro-api-client/src/models/index.ts
generated
|
|
@ -44,11 +44,6 @@ export * from './create-completion-request';
|
|||
export * from './create-run-pull-request-request';
|
||||
export * from './create-run-session-request';
|
||||
export * from './create-secret-request';
|
||||
export * from './daytona-network-layer';
|
||||
export * from './daytona-network-layer-one-of';
|
||||
export * from './daytona-network-layer-one-of-allow-list';
|
||||
export * from './daytona-settings';
|
||||
export * from './daytona-snapshot-settings';
|
||||
export * from './delete-run-response';
|
||||
export * from './delete-run-sandbox';
|
||||
export * from './delete-secret-request';
|
||||
|
|
@ -67,10 +62,17 @@ export * from './dirty-status';
|
|||
export * from './disk-usage-response';
|
||||
export * from './disk-usage-run-row';
|
||||
export * from './disk-usage-summary-row';
|
||||
export * from './docker-settings';
|
||||
export * from './dockerfile-source';
|
||||
export * from './dockerfile-source-inline';
|
||||
export * from './dockerfile-source-path';
|
||||
export * from './environment-image-settings';
|
||||
export * from './environment-lifecycle-settings';
|
||||
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 './environment-volume-settings';
|
||||
export * from './error-response';
|
||||
export * from './error-response-entry';
|
||||
export * from './event-envelope';
|
||||
|
|
@ -257,6 +259,7 @@ export * from './run-commit-person';
|
|||
export * from './run-commits-meta';
|
||||
export * from './run-control-action';
|
||||
export * from './run-diff';
|
||||
export * from './run-environment-settings';
|
||||
export * from './run-error';
|
||||
export * from './run-event';
|
||||
export * from './run-event-detail-response';
|
||||
|
|
@ -289,7 +292,6 @@ export * from './run-question';
|
|||
export * from './run-reference';
|
||||
export * from './run-sandbox';
|
||||
export * from './run-sandbox-runtime';
|
||||
export * from './run-sandbox-settings';
|
||||
export * from './run-scm-settings';
|
||||
export * from './run-server-provenance';
|
||||
export * from './run-spec';
|
||||
|
|
@ -405,4 +407,4 @@ export * from './workflow-ref';
|
|||
export * from './workflow-reference';
|
||||
export * from './workflow-schedule-summary';
|
||||
export * from './workflow-settings';
|
||||
export * from './write-blob-response';
|
||||
export * from './write-blob-response';
|
||||
|
|
|
|||
|
|
@ -20,9 +20,12 @@
|
|||
export interface ManifestArgs {
|
||||
'model'?: string;
|
||||
'provider'?: string;
|
||||
'sandbox'?: string;
|
||||
/**
|
||||
* Per-run Docker sandbox image override.
|
||||
* Named environment slug to select for the run.
|
||||
*/
|
||||
'environment'?: string;
|
||||
/**
|
||||
* Per-run environment image override.
|
||||
*/
|
||||
'docker_image'?: string;
|
||||
'verbose'?: boolean;
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ export interface RunCheckpointSettings {
|
|||
'exclude_globs': Array<string>;
|
||||
/**
|
||||
* When true, Fabro-managed run-branch checkpoint commits bypass local Git commit hooks. Does not affect Fabro `[[run.hooks]]` or metadata-branch snapshots. Defaults to false.
|
||||
* @type {boolean}
|
||||
* @memberof RunCheckpointSettings
|
||||
*/
|
||||
'skip_git_hooks': boolean;
|
||||
}
|
||||
|
|
|
|||
45
lib/packages/fabro-api-client/src/models/run-environment-settings.ts
generated
Normal file
45
lib/packages/fabro-api-client/src/models/run-environment-settings.ts
generated
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { EnvironmentImageSettings } from './environment-image-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { EnvironmentLifecycleSettings } from './environment-lifecycle-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
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';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { EnvironmentVolumeSettings } from './environment-volume-settings';
|
||||
|
||||
export interface RunEnvironmentSettings {
|
||||
'id': string;
|
||||
'provider': EnvironmentProvider;
|
||||
'image': EnvironmentImageSettings;
|
||||
'resources': EnvironmentResourcesSettings;
|
||||
'network': EnvironmentNetworkSettings;
|
||||
'lifecycle': EnvironmentLifecycleSettings;
|
||||
'labels': { [key: string]: string; };
|
||||
'volumes': Array<EnvironmentVolumeSettings>;
|
||||
'env': { [key: string]: string; };
|
||||
}
|
||||
|
|
@ -39,6 +39,9 @@ import type { RunCheckpointSettings } from './run-checkpoint-settings';
|
|||
import type { RunCloneSettings } from './run-clone-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunEnvironmentSettings } from './run-environment-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunExecutionSettings } from './run-execution-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
|
|
@ -63,9 +66,6 @@ import type { RunModelSettings } from './run-model-settings';
|
|||
import type { RunPrepareSettings } from './run-prepare-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunSandboxSettings } from './run-sandbox-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunScmSettings } from './run-scm-settings';
|
||||
|
||||
export interface RunNamespace {
|
||||
|
|
@ -84,7 +84,7 @@ export interface RunNamespace {
|
|||
'clone': RunCloneSettings;
|
||||
'run_branch': RunBranchSettings;
|
||||
'meta_branch': RunMetaBranchSettings;
|
||||
'sandbox': RunSandboxSettings;
|
||||
'environment': RunEnvironmentSettings;
|
||||
'notifications': { [key: string]: NotificationRouteSettings; };
|
||||
'interviews': RunInterviewsSettings;
|
||||
'agent': RunAgentSettings;
|
||||
|
|
|
|||
|
|
@ -87,4 +87,4 @@ export interface RunProjection {
|
|||
* Map from StageId (`node_id@visit`) to stage projection data.
|
||||
*/
|
||||
'stages': { [key: string]: StageProjection; };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { EnvironmentSettings } from './environment-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { ProjectNamespace } from './project-namespace';
|
||||
|
|
@ -29,5 +32,6 @@ import type { WorkflowNamespace } from './workflow-namespace';
|
|||
export interface WorkflowSettings {
|
||||
'project': ProjectNamespace;
|
||||
'workflow': WorkflowNamespace;
|
||||
'environments': { [key: string]: EnvironmentSettings; };
|
||||
'run': RunNamespace;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue