SandboxProviderKind is now a validated string newtype instead of a
closed enum. The bundled kinds (local, docker, daytona) keep their
constants and a BundledProvider enum for the code paths that still
dispatch on them; any other well-formed sandbox-driver kind name is
accepted and names a plugin executable. EnvironmentProvider is gone:
environment settings carry SandboxProviderKind directly, and
is_clone_based is replaced by a workspace policy where local runs in a
designated directory and every other provider clones.
Server sandbox policy is keyed by kind. [server.sandbox.providers.<kind>]
accepts the bundled kinds with `enabled` and any plugin kind with its
launch settings (path, sha256, dev, args, env, inherit_env); bundled
kinds reject the plugin keys and a kind with no entry is disabled. The
OpenAPI schema, generated Rust and TypeScript clients, web settings
pages, and docs follow. The environments table drops its provider CHECK
enumeration in favour of the kind name rules so a plugin environment
can be stored.
Bundled-only code paths (run start, preflight, reconnect, terminal,
details) now fail with an explicit message for a plugin kind until the
driver construction function lands in the next step.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The process environment is no longer a configuration source. `{{ vars.NAME }}`
(non-sensitive, server-stored) and `{{ secrets.NAME }}` (vault-backed) cover
both cases, and reading the worker's ambient environment made a run's inputs
depend on how its process happened to be launched.
`Namespace::Env` is kept but wired to nothing, so `{{ env.NAME }}` still
parses and fails with a message naming its replacement rather than reaching
a consumer as literal text. `ResolveCtx::with_env` is gone, so no call site
can opt back in.
Two long-standing warts were env-only and go with it:
- `InterpString::resolve_or_source`, the "fall back to the raw template
source on failure" path, which let an unresolved token reach a sandbox or
the GitHub API as literal `{{ ... }}` text. Its own comment noted it was
slated for hard-error semantics.
- `RunEnvironmentSettings::resolve_env`'s matching source fallback for
env-only values.
Both carried `#[expect(clippy::disallowed_methods)]` escape hatches. Every
run-boundary resolver — sandbox env, prepare steps, MCP transports, GitHub
permissions, Slack channels, run goal files, provider extra_headers — now
fails closed instead.
Hooks lose their `allowed_env_vars` allowlist, `resolve_header`, and
`HeaderResolveError` along with the `E: Env` generic threaded through the
executor. They keep `{{ vars.* }}`, which `RunSettings::substitute_variables`
already substitutes server-side at run creation.
`allowed_env_vars` is removed from the OpenAPI spec and the generated
TypeScript client. The docs example showing `{{ env.* }}` in
`[server.slatedb.s3].bucket` was already wrong — that field is a plain
String and never interpolated — and is now a literal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
## Summary
Move server-managed environments from sibling TOML files into SQLite,
matching the storage model already used by variables and secrets.
This adds:
- an `environments` SQLite table with DB-level validation for IDs,
revisions, providers, network modes, booleans, and JSON fields
- a SQLite-backed `EnvironmentStore` with cached synchronous reads,
transactional create/replace/delete, synthetic unpersisted `local`, and
`default` as an ordinary seeded row users can delete
- one-time legacy import from `environments/*.toml` next to the active
server `settings.toml`, including relative Dockerfile path inlining and
backup rename to `environments.imported-<timestamp>.bak`
- install/test/CLI seeding of `default` directly into SQLite instead of
writing `environments/default.toml`
- docs updates for API/SQLite-managed server environments and legacy
import behavior
The REST API shape is unchanged; path Dockerfile sources remain rejected
over the environments API.
## Testing
- `cargo nextest run -p fabro-db -p fabro-environment` - 15 passed
- `cargo nextest run -p fabro-server --features test-support
environments` - 16 passed
- `cargo nextest run -p fabro-server --features test-support install` -
60 passed
- `cargo nextest run -p fabro-server --features test-support
create_run_rejects_disabled_sandbox_provider` - 1 passed
- `cargo nextest run -p fabro-server --features test-support
system_sandbox_provider` - 2 passed
- `cargo nextest run -p fabro-cli install` - 132 passed
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
## 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>
## What
Adds a CRUD interface for **server-managed Environments** at
`/settings/environments`, driven by the `/api/v1/environments` REST API
(list / create / retrieve / replace / delete), and reshapes how built-in
environments are provisioned and protected.
The page lives in the **Workflows** settings nav section (also
introduced in this branch), positioned before Variables.
## Why
The Environments REST API shipped (#453) but had no UI — environments
could only be managed via the API/CLI. This gives operators a web UI
alongside Variables and Secrets, and along the way tightens the model:
environments are seeded at install time (not silently re-created on
every boot), and the `default` fallback is an ordinary, deletable
environment.
## Web UI
**Pages & component**
- `settings-environments.tsx` — list view: provider badge,
image/resource summary, row actions (Edit/Delete). **"New environment"
is a dropdown** of the enabled sandbox providers; the chosen provider is
fixed for the environment's lifetime.
- `settings-environments-new.tsx` / `settings-environments-edit.tsx` —
create/edit flows; create reads the provider from a query param.
- `environment-form.tsx` — shared form, reorganized:
- **General** panel (merged identity + image): id, and an **image-source
selector** (Image reference *vs* inline Dockerfile) that shows,
requires, and sends only the selected, mutually-exclusive source.
- **Resources**: CPU / memory / disk as **range sliders** (CPU 1–8,
memory 1–16 GB, disk 1–20 GB), each always writing a concrete value.
- **Environment variables** key/value editor.
- **Advanced** progressive-disclosure section holding **Network** (a
single "Block all network access" toggle — allow-all vs block) and
**Lifecycle** (preserve / stop-on-terminal / auto-stop). Opens by
default when any advanced value is non-default.
- The in-form **provider control and the Labels editor were removed** —
labels remain API-managed and are round-tripped untouched so UI edits
never clear them.
**Data layer**: `environmentsApi` client, `queryKeys.environments`,
`useEnvironments` / `useEnvironment` SWR hooks.
**Nav & routing**: "Environments" item in the Workflows section before
Variables; routes registered in `router.tsx`.
## Backend: seed at install, deletable `default`
- **Seeding moved to install time.** The server no longer seeds
built-ins on startup; `EnvironmentStore::load_or_seed` → `load`
(load-only). A new public `seed_environments(dir)` (idempotent,
preserves operator edits) is called by both the web installer and the
CLI installer. An uninstalled instance therefore has no managed
environments, and a run selecting an absent environment fails explicitly
(`unknown environment: default`) rather than resurrecting a built-in.
- **`default` is no longer protected.** The delete guard and the
`Protected` error variant are gone; deleting `default` succeeds (204)
and removes the run fallback on purpose — forcing an explicit choice.
`local` is unchanged (reserved, in-memory).
- **`volumes` removed** from environment settings across the OpenAPI
spec, generated Rust + TS clients, config layers,
sandbox/server/workflow plumbing, docs, and tests.
## API contract details honored
- Edit sends the environment `revision` as `If-Match`; 409 conflicts
surface a "changed since you opened it" message.
- The REST API accepts inline Dockerfiles only — the form never sends a
Dockerfile path.
## Verification
- Rust: `cargo build` (touched crates) ✅, `cargo nextest -p
fabro-environment` 21/21 ✅, server env unit + `tests/it` integration 2/2
+ 15/15 ✅, `clippy` (nightly, touched crates, all targets) clean ✅, `fmt
--check` clean ✅. Full `--workspace` suite not run here — worth a CI
pass.
- Web: `bun run typecheck` ✅, `bun run build` ✅,
`environment-form.test.ts` 5/5 ✅. Web suite: 512 pass / 1 unrelated
pre-existing `RunDetail` failure.
- **Not visually verified in-browser** — the local app is login-gated
and automated loads redirect to `/login`; rendering of the form, the
New-environment dropdown, and `default` delete should be confirmed in a
logged-in session.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: fabro-sh-0530[bot] <281434857+fabro-sh-0530[bot]@users.noreply.github.com>
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Release Repro <release-repro@example.com>
Changelog: add entries for 2026-05-28 through 2026-06-09, regenerate
the 2026-05-26/27 entries to cover their full days, and add the
missing 2026-05-27 navigation entry.
Product docs: scope workflow templating docs to prompt + goal (#474),
document the server-managed environments directory and seeded
built-ins (#446/#453), add a new Automations page, and list the
Automations/Environments/Variables endpoints in the API reference nav.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## Summary
Secures Daytona custom snapshot creation by removing user-controlled
snapshot/image references and replacing them with deterministic names
Fabro computes internally. Docker image selection now uses
`image.docker`, while Daytona only accepts `image.dockerfile` for custom
snapshots and continues to use `daytona-medium` when no Dockerfile is
configured.
## Changes
- Replaces public `image.ref` config/API shape with Docker-specific
`image.docker` across Rust settings, OpenAPI, generated TypeScript
client, docs, defaults, examples, and web samples.
- Adds Daytona snapshot identity generation using HMAC-SHA256 over a
canonical manifest keyed by the Daytona API key, producing
`fabro-<uuid>` snapshot names without exposing Dockerfile text or key
material.
- Routes Daytona custom Dockerfiles, including devcontainer-generated
Dockerfiles, through the same computed identity path before calling
Daytona snapshot APIs.
- Updates sandbox initialization events and store projections so
initialized run state can show the resolved image and computed Daytona
snapshot after startup.
- Updates legacy config migration behavior so Docker image refs map to
`image.docker`, while Daytona legacy snapshot names are not preserved.
## Breaking Changes
- `image.ref` is no longer accepted in new environment config.
- Docker environments should use `image.docker` for image selection.
- Daytona environments reject `image.docker`; use `image.dockerfile` to
request a custom computed snapshot.
## Verification
- `cargo build -p fabro-api`
- `cd lib/packages/fabro-api-client && bun run generate`
- `cd lib/packages/fabro-api-client && bun run typecheck`
- `cd apps/fabro-web && bun run typecheck`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `ulimit -n 4096 && cargo nextest run --no-fail-fast -p fabro-cli -p
fabro-config -p fabro-sandbox -p fabro-workflow -p fabro-store -p
fabro-server -p fabro-api`
- `cargo insta pending-snapshots`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
## Summary
This adds run-configuration support for mounting existing Daytona
volumes into Fabro-managed Daytona sandboxes.
Concretely, this PR:
- adds `[[run.sandbox.daytona.volumes]]` with `volume_id`, `mount_path`,
and optional `subpath`
- resolves that config through the layer/settings/runtime pipeline
- forwards configured mounts to `daytona_sdk::SandboxBaseParams.volumes`
when creating the sandbox
- documents the configuration surface in the Daytona environment and run
configuration docs
## Motivation
Daytona already supports attaching volumes when a sandbox is created,
but Fabro currently owns that sandbox creation call. That means users
cannot attach a pre-created Daytona volume to a Fabro-managed sandbox
from run config.
The intended use is persistent, provider-owned state such as agent
credentials, caches, datasets, or other files that should survive
ephemeral sandbox lifecycles.
## Scope
This is intentionally a narrow passthrough. Fabro does not create,
delete, list, wait on, or otherwise manage Daytona volume lifecycle.
Users create the volume in Daytona first, then reference its `volume_id`
from Fabro run config.
`volumes` defaults to an empty list in resolved settings for backwards
compatibility with existing serialized settings.
## Testing
- `cargo test -p fabro-server
runtime_daytona_config_preserves_volume_mounts`
- `cargo test -p fabro-config resolves_daytona_volume_mounts`
- `cargo test -p fabro-sandbox --features daytona volume_mounts`
- `cargo test -p fabro-workflow
runtime_daytona_config_preserves_volume_mounts`
- `cargo check -p fabro-server`
---
_Re-opened from #262 (originally by @kimprobably) to land a rustfmt fix
— the original PR came from an org-owned fork, which blocks maintainer
pushes. Branch is now on the base repo. Original commit preserved; one
additional commit fixes rustfmt formatting._
Co-authored-by: Tim Keen <tim@keen.digital>
## Summary
Adds run-level controls for clone behavior, managed run branch
setup/pushes, and metadata branch writes/pushes so workflows can opt out
of Fabro-managed Git behavior without relying on provider-specific
`skip_clone` settings. This closesfabro-sh/fabro#240.
## What Changed
- Introduced `[run.clone]`, `[run.run_branch]`, and `[run.meta_branch]`
settings with defaults that preserve current behavior.
- Removed user-facing `skip_clone` from Docker/Daytona config while
mapping the new run-level clone setting into the internal sandbox
runtime options.
- Gated run branch setup/push, metadata branch writer creation/push, and
PR branch output on the new settings.
- Enforced invalid combinations: pull requests require an enabled pushed
run branch, and disabling the run branch also disables metadata branch
behavior.
- Updated OpenAPI, the generated TypeScript API client, frontend fixture
data, and docs for the new configuration shape.
## Testing
- `cargo nextest run -p fabro-config -p fabro-types -p fabro-workflow -p
fabro-server`
- `cargo build -p fabro-api`
- `cd lib/packages/fabro-api-client && bun run generate`
- `cd lib/packages/fabro-api-client && bun run typecheck`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test`
- `cargo build --workspace`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo insta pending-snapshots`
- `git diff --check`
## Post-Deploy Monitoring & Validation
- Validation window: first 24 hours after release; owner: release
owner/on-call engineer.
- Log queries/search terms: `run_branch`, `meta_branch`,
`clone.enabled`, `skip_clone`, `pull request requires an enabled pushed
run branch`, `metadata branch`.
- Healthy signals: runs without custom branch config continue creating
and pushing run/meta branches; runs with `[run.clone] enabled = false`
start provider sandboxes without cloning; runs with branch pushes
disabled complete without Git push errors.
- Failure signals: increased run startup failures for Docker/Daytona,
unexpected PR creation conflicts, missing metadata for default-config
runs, or validation errors for configurations that previously used
default settings.
- Mitigation trigger: if default-config runs stop producing expected
branch/metadata artifacts or sandbox startup failures increase, roll
back the release or temporarily restore previous defaults while
investigating the run-level setting resolution path.
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
Co-authored-by: Haroldo Olivieri <6575718+haroldolivieri@users.noreply.github.com>
Reflect Docker as the default sandbox provider, add `skip_clone` for
clone-based providers, document the `[run.sandbox.docker]` config
table, and update tutorial command lines from `files-internal/...` to
`docs/internal/...`. Bump the docs skill watermark to the latest synced
commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ensure local runs use the worktree checkpoint path by default, expose source and sandbox paths in API/web surfaces, and remove dead fork/rewind push controls. Update docs for clone-based sandboxes and durable checkpoint timelines.
Invert the docs convention so the Mintlify-published site lives under
docs/public/ and internal artifacts (strategy docs, brainstorms, plans,
etc.) sit at docs/ root or docs/internal/. Tools that default to writing
into docs/ now land in the catch-all instead of leaking into the
published tree.
- Move Mintlify content (administration/, agents/, api-reference/,
changelog/, core-concepts/, examples/, execution/, getting-started/,
human-tools/, integrations/, languages/, reference/, tutorials/,
workflows/, images/, logo/, docs.json, favicon.svg, dot-highlight.js)
into docs/public/.
- Collapse docs-internal/ into docs/internal/.
- Update Rust path references (fabro-api/build.rs, fabro-server,
fabro-dev), TypeScript generator arg, CI path filters, clippy.toml
reasons, AGENTS.md/CLAUDE.md, and README.md image refs.
Mintlify dashboard project root must be updated to docs/public/ in a
follow-up. .mintignore move/trim and .claude/skills/ updates land in a
separate commit.
2026-04-27 07:21:13 -07:00
Renamed from docs/execution/environments.mdx (Browse further)