fabro/docs/public/execution/environments.mdx
Adrian Muraru a769336c39
Some checks failed
Rust / Format (push) Has been cancelled
Rust / Clippy (push) Has been cancelled
Rust / Generated Docs (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
Rust / Test (Linux) (push) Has been cancelled
Rust / Test (macOS) (push) Has been cancelled
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
feat(workflow): support overriding cwd for local sandbox provider (#467)
## Problem

The `local` sandbox uses the run's `source_directory` (the CLI's cwd at
invocation time) as its working directory and `create_dir_all`s it on
the server (`LocalSandbox::initialize` in `fabro-sandbox`). That is
correct when the CLI and the server share a host — the agent operates
directly on the user's project tree.

When the server is **remote** from the CLI — e.g. `fabro serve` running
in a container in Kubernetes, driven over HTTP with the `local` sandbox
— the client's cwd (e.g. `/Users/alice/project`) does not exist on the
server. The sandbox then tries to create that path as the (often
unprivileged) server user and fails at init:

```
sandbox.failed provider="local" error="Failed to create working directory" causes=["Permission denied (os error 13)"]
```

and the run dies with `workflow_error` before the agent starts.

## Fix

When `source_directory` is absent or does not exist on the server, fall
back to a server-writable `workspace` directory under the run's scratch
dir instead of recreating the client path. **Same-host behavior is
unchanged**: an existing `source_directory` is still used as-is.

The selection is extracted into a small pure helper,
`local_working_directory(source_directory, run_dir)`, so it can be
unit-tested directly.

## Testing

- `cargo test -p fabro-workflow local_working_directory` — 3 new tests
(existing source dir → used; absent → fallback;
present-but-missing-on-server → fallback)
- `cargo check -p fabro-workflow`

🤖 Generated with [Claude Code](https://claude.com/claude-code)


Thanks for fabro @brynary!

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 12:32:32 -04:00

228 lines
7.7 KiB
Text

---
title: "Environments"
description: "Reusable named execution environments for workflow runs"
---
Fabro separates **environments** from **sandboxes**:
- An **environment** is reusable desired configuration: provider, image, resources, network, lifecycle, labels, and environment variables.
- A **sandbox** is the concrete runtime instance Fabro creates for a run from the selected environment.
<Warning>
Older pre-v1.0 config files that still use `[run.sandbox]` are temporarily auto-migrated when Fabro loads them from disk. Fabro writes a sibling `*.legacy-sandbox-migration.bak` file, rewrites the config to `[run.environment]` plus an environment definition, and then continues startup.
Similarly, `[environments.*]` tables in the server's active `settings.toml` are auto-migrated on startup: each entry is extracted into a sibling `environments/<id>.toml` file, with a `.settings-environments-migration.bak` backup written first.
These compatibility rewrites only handle direct field mappings. Unsupported legacy fields fail with a migration message that lists the keys to edit manually. The rewrite paths will be removed before v1.0.
</Warning>
Runs select environments by slug:
```toml title="workflow.toml"
[run.environment]
id = "fabro-dev"
```
Environments are server-managed. The server keeps one TOML file per environment in an `environments/` directory next to its `settings.toml`, seeded on first startup with built-in `default`, `local`, `docker`, and `daytona` environments. Manage them by editing those files or through the `/api/v1/environments` REST API. Workflow and project TOML can additionally define `[environments.<slug>]` catalog entries that merge with the server catalog through the normal settings precedence. The built-in default is `default`, a Docker environment using `buildpack-deps:noble`.
## Defining environments
Each server-managed environment is a file whose name is its slug:
```toml title="environments/fabro-dev.toml"
provider = "daytona" # local | docker | daytona
[image]
dockerfile = { path = "Dockerfile" }
[resources]
cpu = 8
memory = "16GB"
disk = "20GB"
[network]
mode = "cidr_allow_list" # allow_all | block | cidr_allow_list
allow = ["10.0.0.0/8"]
[lifecycle]
preserve = false
stop_on_terminal = true
auto_stop = "30m"
[labels]
repo = "fabro-sh/fabro"
[env]
NODE_ENV = "development"
```
Server-managed local environments can also set `cwd`, an optional runtime
command working directory:
```toml title="environments/host.toml"
provider = "local"
cwd = "/srv/fabro/workspaces/team-a"
```
`cwd` is owned by the server environment and is only honored by the `local`
provider. It is not a replacement for `run.working_dir`. Docker and Daytona
ignore `cwd` and report a preflight warning because those clone-based providers
own their workspace layout. Workflow, project, user, and direct-run
`[environments.<slug>]` catalogs cannot set `cwd`; configure it in the
server-managed environment file or through the environments API.
The same fields nest under `[environments.<slug>]` when defined in workflow or project TOML instead:
```toml title="workflow.toml"
[run.environment]
id = "fabro-dev"
[environments.fabro-dev]
provider = "daytona"
[environments.fabro-dev.image]
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.env]
NODE_ENV = "development"
```
Run-level overrides are sparse and apply only to the selected environment:
```toml title="workflow.toml"
[run.environment]
id = "fabro-dev"
[run.environment.resources]
memory = "32GB"
[run.environment.lifecycle]
preserve = true
```
`env` and `labels` merge by key.
## 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 environments
The server seeds four built-in environments on first startup: `default`, `local`, `docker`, and `daytona`. Missing files are re-seeded, so editing a seeded file customizes it while deleting it restores the built-in definition on the next restart. The seeded default:
```toml title="environments/default.toml"
provider = "docker"
[image]
docker = "buildpack-deps:noble"
[resources]
cpu = 2
memory = "4GB"
[lifecycle]
preserve = false
stop_on_terminal = true
```
## Provider mappings
| Environment field | Local | Docker | Daytona |
|---|---|---|---|
| `image.docker` | Ignored | Docker image | Error |
| `image.dockerfile` | Ignored | Warning; ignored | Snapshot Dockerfile; Fabro computes the snapshot name |
| `resources.cpu` | Warning; ignored | `cpu_quota = cpu * 100000` | Snapshot CPU |
| `resources.memory` | Warning; ignored | Container memory limit | Snapshot memory |
| `resources.disk` | Warning; ignored | Warning; ignored | Snapshot disk |
| `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 |
| `lifecycle.auto_stop` | Warning; ignored | Warning; ignored | Daytona auto-stop |
| `env` | Process environment overlay | Container environment | Sandbox environment |
| `cwd` | Server-side command working directory | Warning; ignored | Warning; ignored |
## 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 title="environments/host.toml"
provider = "local"
cwd = "/srv/fabro/workspaces/team-a"
```
When `cwd` is set, local runs execute commands from that absolute server-side
path. When it is unset, Fabro keeps same-host compatibility by using the
submitted source directory only if that path exists on the server. If neither is
available, the run fails before execution with a remediation to configure
`cwd`.
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.docker`. Docker is the built-in default provider.
```toml title="environments/ci.toml"
provider = "docker"
[image]
docker = "buildpack-deps:noble"
[resources]
cpu = 2
memory = "4GB"
[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. Docker and Daytona ignore `cwd`; use the provider-owned workspace layout and `run.working_dir` for repository-relative commands.
## Daytona
Daytona runs tools in a cloud sandbox. Without `image.dockerfile`, Fabro uses Daytona's built-in `daytona-medium` snapshot. With `image.dockerfile`, Fabro computes a deterministic internal snapshot name from the Dockerfile, resource hints, a single-tenant scope, and the Daytona API key.
```toml title="environments/cloud.toml"
provider = "daytona"
[image]
dockerfile = { path = "Dockerfile" }
[resources]
cpu = 4
memory = "8GB"
disk = "20GB"
[lifecycle]
auto_stop = "30m"
[network]
mode = "cidr_allow_list"
allow = ["208.80.154.232/32", "10.0.0.0/8"]
```