fabro/docs/execution/environments.mdx
brynary-fabro[bot] 165e2495bf Extract fabro resume subcommand (#137)
This PR extracts the `fabro resume` subcommand from `fabro run`,
replacing the `--resume` and `--run-branch` flags with a dedicated, more
ergonomic interface. Users can now run `fabro resume <RUN_ID>` instead
of constructing `fabro run --run-branch fabro/run/<RUN_ID>` manually,
and the command also accepts run ID prefixes (matching the pattern
established by `fabro rewind` and `fabro fork`). Checkpoint-file-based
resumption is also supported via `fabro resume --checkpoint
path/to/checkpoint.json --workflow workflow.fabro`.

The implementation moves the ~315-line `run_from_branch()` function out
of `run.rs` and into a new `commands/resume.rs` module, splitting it
into two preparation paths (`prepare_from_checkpoint` and
`prepare_from_branch`) that converge on a shared `run_resumed()` tail.
Several previously private helpers in `run.rs` are widened to
`pub(crate)` to allow sharing: `local_sandbox_with_callback`,
`resolve_ssh_config`, `resolve_ssh_clone_params`,
`resolve_preserve_sandbox`, `generate_retro`, `write_finalize_commit`,
`print_final_output`, `print_assets`, and the new `default_run_dir`
helper extracted from duplicated inline logic. The `RunArgs` struct
loses its `resume` and `run_branch` fields along with their
`conflicts_with` annotations, and `RunSpec` drops the corresponding
fields with `#[serde(default)]` for backward compatibility.

Documentation across `docs/reference/cli.mdx`,
`docs/execution/checkpoints.mdx`, and
`docs/core-concepts/how-fabro-works.mdx` is updated to reflect the new
interface, and the `rewind`/`fork` commands now hint `fabro resume
<short-prefix>` instead of the full branch name.

### Fabro Details

<details>
<summary>Ran 9 stages in 30m 25s for $6.70</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 10s | – | 0 |
| preflight_lint | 13s | – | 0 |
| implement | 18m 30s | $3.95 | 0 |
| simplify_opus | 9m 38s | $2.75 | 0 |
| simplify_gpt | 0s | – | 0 |
| verify | 19s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **30m 25s** | **$6.70** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-6; }
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:45:06 -04:00

360 lines
17 KiB
Text

---
title: "Environments"
description: "Sandbox providers for workflow execution"
---
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 supports six 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.
| 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 |
| `ssh` | Any SSH host | Existing remote machines, dev servers, NAS boxes | Available |
| `exe` | Cloud VM ([exe.dev](https://exe.dev)) | Fast ephemeral VMs, lightweight cloud sandboxing | Experimental |
| `sprites` | Cloud VM | Managed cloud sandboxes | In Development |
## 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
fabro run workflow.fabro --sandbox ssh
fabro run workflow.fabro --sandbox exe
```
```toml title="run.toml"
# Run config TOML
[sandbox]
provider = "daytona"
```
The precedence order is: CLI flag > run config TOML > server defaults > built-in default (`local`).
## Local
The local sandbox runs all tool operations directly on the host machine. It's the default and the simplest option — no setup required beyond the Fabro binary itself.
### 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`, `daytona`, or `exe` when running untrusted workflows or when you need a reproducible environment.
</Note>
## Docker
The Docker sandbox runs all tool operations inside a Docker container. The host working directory is bind-mounted into the container, so file changes are visible on both sides.
### Prerequisites
- Docker Engine running on the host
- The configured image available locally (or `auto_pull` enabled)
### 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** — The host working directory is bind-mounted at `/workspace` inside the container. All relative paths resolve against this mount point.
- **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
The Docker sandbox is configured through the `DockerSandboxConfig`:
| Setting | Default | Description |
|---|---|---|
| `image` | `fabro-agent:latest` | Docker image to use |
| `container_mount_point` | `/workspace` | Mount point inside the container |
| `network_mode` | `bridge` | Docker network mode |
| `extra_mounts` | `[]` | Additional `host:container` bind mounts |
| `memory_limit` | unlimited | Memory limit in bytes |
| `cpu_quota` | unlimited | CPU quota (microseconds per 100ms period) |
| `auto_pull` | `true` | Pull the image if not found locally |
| `env_vars` | `[]` | Additional `KEY=VALUE` environment variables |
### 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"
[sandbox]
provider = "docker"
preserve = true
```
When preserved, Fabro prints the container ID so you can reconnect with `docker exec -it <id> bash`.
## 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.
### Prerequisites
- A `DAYTONA_API_KEY` environment variable
- A GitHub App configured via `fabro install` (for private repository cloning)
### How it works
- **Sandbox lifecycle** — On `initialize()`, Fabro creates a Daytona sandbox (from an image or a snapshot), clones the current git repository into it, and waits until it's ready. On `cleanup()`, the sandbox is deleted.
- **Working directory** — Fixed at `/home/daytona/workspace`. The current repository is cloned there automatically.
- **Git clone** — Fabro detects the local `origin` remote URL and current branch, converts SSH URLs to HTTPS, and clones into the sandbox. For private repositories, Fabro uses a GitHub App Installation Access Token scoped to `contents: read` on the specific repository. Public repositories are cloned without credentials. If no git repo is detected, the working directory is created empty.
- **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"
[sandbox]
provider = "daytona"
[sandbox.daytona]
auto_stop_interval = 60
[sandbox.daytona.snapshot]
name = "rust-dev"
cpu = 4
memory = 8
disk = 20
dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update && apt-get install -y git ripgrep"
```
| 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 for building the snapshot. Required when creating a new snapshot. |
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.
### Labels
Attach key-value labels to sandboxes for filtering and identification in the Daytona dashboard:
```toml title="run.toml"
[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 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"
[sandbox.daytona]
auto_stop_interval = 30
```
## SSH
The SSH sandbox runs all tool operations on an existing remote machine over SSH. Unlike cloud providers (Daytona, Exe), there is no VM lifecycle management — the host must already be running and reachable. This makes it ideal for persistent dev servers, NAS boxes, or any machine you SSH into regularly.
### Prerequisites
- SSH access to the target machine (key-based auth recommended)
- The remote working directory must exist, or the agent must be able to create it
- A GitHub App configured via `fabro install` (for private repository cloning)
### How it works
- **No lifecycle management** — Fabro does not create or destroy the remote host. It connects, runs operations, and disconnects. `cleanup()` is a no-op.
- **Working directory** — Set explicitly in the run config. Relative paths for all file operations resolve against this directory.
- **Git clone** — Fabro detects the local `origin` remote URL and current branch, converts SSH URLs to HTTPS, and clones into the working directory. For private repositories, Fabro uses a GitHub App Installation Access Token. Public repositories are cloned without credentials. If no git repo is detected, the working directory is used as-is.
- **Commands** — Executed via SSH. Commands are base64-encoded and piped through `sh` to support pipes, environment variables, and other shell features.
- **File I/O** — Reads use `cat` over SSH. Writes upload content via SCP. Parent directories are created automatically.
### Configuration
```toml title="run.toml"
[sandbox]
provider = "ssh"
[sandbox.ssh]
destination = "user@myserver"
working_directory = "/home/user/workspace"
```
| Field | Required | Description |
|---|---|---|
| `destination` | Yes | SSH destination — `user@host`, a hostname, or an SSH alias from `~/.ssh/config`. |
| `working_directory` | Yes | Absolute path to the working directory on the remote host. |
| `config_file` | No | Path to a custom SSH config file. Useful when using a non-default key or jump host. |
| `preview_url_base` | No | Base URL for port previews (e.g. `"http://myserver"`). When set, preview URLs are returned as `{preview_url_base}:{port}` instead of falling back to `localhost`. |
### SSH config file
Use `config_file` to point Fabro at a non-default SSH config — useful when the remote host requires a specific key, ProxyJump, or port:
```toml title="run.toml"
[sandbox.ssh]
destination = "devbox"
working_directory = "/home/user/projects/myapp"
config_file = "/home/user/.ssh/fabro_config"
```
```ssh-config title="~/.ssh/fabro_config"
Host devbox
HostName 192.168.1.42
User alice
IdentityFile ~/.ssh/id_ed25519_devbox
Port 2222
```
### Port previews
When a workflow stage starts a local server on a port, Fabro calls `get_preview_url(port)` to produce a clickable URL. For SSH sandboxes, this requires knowing the host's reachable address — Fabro can't infer it automatically.
Set `preview_url_base` to enable preview URLs:
```toml title="run.toml"
[sandbox.ssh]
destination = "alice@devbox"
working_directory = "/home/alice/projects/myapp"
preview_url_base = "http://devbox"
```
With this config, port 3000 on the remote host yields `http://devbox:3000`.
## Exe
<Warning>
The exe.dev sandbox provider is **under development** and requires building Fabro with the `exedev` feature flag. The API and configuration may change.
</Warning>
The Exe sandbox runs all tool operations inside a cloud VM managed by [exe.dev](https://exe.dev). It provides full machine-level isolation with fast VM startup via SSH.
### Prerequisites
- SSH keys configured for `exe.dev` (added via the exe.dev dashboard)
- A GitHub App configured via `fabro install` (for private repository cloning)
### How it works
- **VM lifecycle** — On `initialize()`, Fabro connects to the exe.dev management plane via SSH and runs `new --json` to create a VM. The response contains the VM name and an SSH destination (e.g. `my-vm.exe.xyz`). On `cleanup()`, Fabro runs `rm <vm_name>` to destroy the VM. Cancelling a run cleanly tears down the VM.
- **Working directory** — Fixed at `/home/exedev`. Relative paths resolve against this directory.
- **Git clone** — Fabro detects the local `origin` remote URL and current branch, converts SSH URLs to HTTPS, and clones into the VM. For private repositories, Fabro uses a GitHub App Installation Access Token. Public repositories are cloned without credentials. If no git repo is detected, the working directory is created empty.
- **Commands** — Executed via SSH on the data plane (`<vmname>.exe.xyz`). Commands are base64-encoded and piped through `sh` to handle pipes, environment variables, and other shell features. Environment variables and working directory overrides are supported.
- **File I/O** — Reads use `cat` over SSH. Writes use SCP upload. Parent directories are created automatically.
- **Git checkpointing** — Between stages, Fabro commits agent changes and pushes them to a remote branch, the same mechanism used by Daytona sandboxes.
- **Ephemeral** — Each run gets a fresh VM that is destroyed on cleanup.
### Configuration
```toml title="run.toml"
[sandbox]
provider = "exe"
[sandbox.exe]
image = "my-custom-image:latest"
```
| Field | Description |
|---|---|
| `image` | Custom container image for the VM. Optional — uses the exe.dev default when omitted. |
### SSH access
Connect to a running exe.dev sandbox via SSH for live debugging:
```bash
fabro ssh <run-id>
```
This prints the SSH connection command so you can connect to the VM while the workflow runs.
## 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 | Only the bind-mounted working directory (`/workspace` by default) and whatever is in the container image. Host files outside the mount are inaccessible. |
| `daytona` | Full machine | A cloud VM with the repository cloned into `/home/daytona/workspace`. The host filesystem is completely inaccessible. |
| `ssh` | Remote host | The remote machine's filesystem, rooted at the configured working directory. No isolation from other users or processes on that host. |
| `exe` | Full machine | A cloud VM with the repository cloned into `/home/exedev`. 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`, `daytona`, or `exe` 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. |
| `ssh` | Remote host's access | No network controls. Agents have the same outbound access as the remote host. |
| `exe` | Full access | No network isolation controls. VMs have full outbound access. |
For Daytona, network access is configured in the `[sandbox.daytona]` section:
```toml title="run.toml"
# Block all egress
[sandbox.daytona]
network = "block"
# Allow only specific CIDRs
[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.