mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-05 08:10:39 +00:00
Fabro advertised Bash while its three backends implemented three different contracts: Daytona evaluated commands through `sh`, and Docker's streaming, stdio, and setup paths used a login shell. Bash-only syntax silently misbehaved depending on provider and code path, and login profiles could change PATH and command behavior per image. Make `bash -c` the enforced interpreter for every command string the Unix sandbox API accepts, on every production backend and through both buffered and streaming execution. This selects the interpreter only — no `errexit`, no `pipefail`, no login mode — so `false | true` still succeeds and a workflow that wants other semantics writes them into its own command. Local resolves `bash` through the worker's PATH (NixOS has no /bin/bash) and reuses that one executable across all three command paths. Docker and Daytona require /bin/bash with no `sh` fallback. Fresh initialization and resume/start now verify Bash through a shared marker-validating probe before reporting the sandbox usable, so a missing or non-Bash interpreter fails at the lifecycle boundary with provider-specific remediation instead of on the first command. The probe also rejects Bash in POSIX mode, which an image whose `bash` is really `sh` would otherwise pass. Sandbox MCP scripts and the detached launch wrapper move under the same contract; host-side stdio MCP scripts, hooks, and interactive terminals are separate executors and keep their existing `sh` behavior. The `shell` tool's name and JSON schema are unchanged across providers; only its prose now identifies `command` as Bash source. BREAKING CHANGE: sandbox commands no longer load login-shell profiles, so environment set in /etc/profile.d/*.sh, ~/.bash_profile, or nvm/rbenv/sdkman initializers is gone. Move those exports into the Dockerfile's ENV or the Daytona snapshot image. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
287 lines
10 KiB
Text
287 lines
10 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. Existing sibling `environments/*.toml` files are then treated as a legacy import source: Fabro imports missing environment IDs into SQLite once and renames the directory to `environments.imported-<timestamp>.bak`.
|
|
|
|
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"
|
|
```
|
|
|
|
Server-managed environments are stored in the server SQLite database. Manage them through the web UI or the `/api/v1/environments` REST API. Install seeds `default` as an ordinary persisted environment; users can replace or delete it. `local` is reserved, synthetic, and unpersisted: it appears only when the local sandbox provider is enabled, and it cannot be created, replaced, or deleted through the environments API. Workflow and project TOML can additionally define `[environments.<slug>]` catalog entries that merge with the server catalog through the normal settings precedence.
|
|
|
|
## Defining Server Environments
|
|
|
|
Create server-managed environments through the REST API. Stored environments use inline Dockerfile content; local Dockerfile paths are rejected by the API because the server cannot safely resolve client-side paths.
|
|
|
|
```json title="POST /api/v1/environments"
|
|
{
|
|
"id": "fabro-dev",
|
|
"provider": "daytona",
|
|
"cwd": null,
|
|
"image": {
|
|
"docker": null,
|
|
"dockerfile": {
|
|
"type": "inline",
|
|
"value": "FROM buildpack-deps:noble\n"
|
|
}
|
|
},
|
|
"resources": {
|
|
"cpu": 8,
|
|
"memory": "16GB",
|
|
"disk": "20GB"
|
|
},
|
|
"network": {
|
|
"mode": "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-provider environments can also set `cwd`, an optional runtime
|
|
command working directory:
|
|
|
|
```json title="POST /api/v1/environments"
|
|
{
|
|
"id": "host",
|
|
"provider": "local",
|
|
"cwd": "/srv/fabro/workspaces/team-a",
|
|
"image": {
|
|
"docker": null,
|
|
"dockerfile": null
|
|
},
|
|
"resources": {
|
|
"cpu": null,
|
|
"memory": null,
|
|
"disk": null
|
|
},
|
|
"network": {
|
|
"mode": "allow_all",
|
|
"allow": []
|
|
},
|
|
"lifecycle": {
|
|
"preserve": false,
|
|
"stop_on_terminal": true,
|
|
"auto_stop": null
|
|
},
|
|
"labels": {},
|
|
"env": {}
|
|
}
|
|
```
|
|
|
|
`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 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.
|
|
|
|
## Environment value interpolation
|
|
|
|
Environment `env` values can mix literal text with `{{ vars.NAME }}`, `{{ env.NAME }}`, and `{{ secrets.NAME }}` tokens:
|
|
|
|
```toml title="workflow.toml"
|
|
[environments.fabro-dev.env]
|
|
DEPLOY_ENV = "{{ vars.DEPLOY_ENV }}"
|
|
SERVICE_URL = "https://api.{{ env.REGION }}.example.com"
|
|
SERVICE_TOKEN = "{{ secrets.SERVICE_TOKEN }}"
|
|
```
|
|
|
|
Server-managed variables resolve when the run is created. Worker environment variables and token secrets resolve immediately before the sandbox starts, so resolved secret values are not persisted in the run definition. A missing or non-token secret fails closed. For backward compatibility, a value containing only missing `{{ env.* }}` references is passed through in source form.
|
|
|
|
## 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.
|
|
|
|
## Seeded Environments
|
|
|
|
Install seeds a `default` environment into SQLite. It is a normal persisted environment, so deleting it removes the default run target until you recreate it. The standard Docker default is:
|
|
|
|
```json title="GET /api/v1/environments/default"
|
|
{
|
|
"id": "default",
|
|
"provider": "docker",
|
|
"image": {
|
|
"docker": "buildpack-deps:noble",
|
|
"dockerfile": null
|
|
},
|
|
"resources": {
|
|
"cpu": 2,
|
|
"memory": "4GB",
|
|
"disk": null
|
|
},
|
|
"network": {
|
|
"mode": "allow_all",
|
|
"allow": []
|
|
},
|
|
"lifecycle": {
|
|
"preserve": false,
|
|
"stop_on_terminal": true,
|
|
"auto_stop": null
|
|
},
|
|
"labels": {},
|
|
"env": {}
|
|
}
|
|
```
|
|
|
|
`local` is not stored in SQLite. It is synthesized at runtime when the local sandbox provider is enabled.
|
|
|
|
## 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.
|
|
|
|
Create a server-managed local-provider environment through the environments API when you need a host `cwd`.
|
|
|
|
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="workflow.toml"
|
|
[environments.ci]
|
|
provider = "docker"
|
|
|
|
[environments.ci.image]
|
|
docker = "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. Docker and Daytona ignore `cwd`; use the provider-owned workspace layout and `run.working_dir` for repository-relative commands.
|
|
|
|
The image must provide `/bin/bash`; Fabro evaluates every sandbox command with it and has no `sh` fallback. Commands run in a **non-login** shell, so login profiles (`/etc/profile.d/*.sh`, `~/.bash_profile`, and `nvm`/`rbenv`/`sdkman` initializers) are not sourced — put anything they set into the Dockerfile's `ENV` instead. Fabro verifies Bash during initialization and again on resume, and fails with remediation rather than reporting the sandbox ready.
|
|
|
|
## 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="workflow.toml"
|
|
[environments.cloud]
|
|
provider = "daytona"
|
|
|
|
[environments.cloud.image]
|
|
dockerfile = { path = "Dockerfile" }
|
|
|
|
[environments.cloud.resources]
|
|
cpu = 4
|
|
memory = "8GB"
|
|
disk = "20GB"
|
|
|
|
[environments.cloud.lifecycle]
|
|
auto_stop = "30m"
|
|
|
|
[environments.cloud.network]
|
|
mode = "cidr_allow_list"
|
|
allow = ["208.80.154.232/32", "10.0.0.0/8"]
|
|
```
|