diff --git a/docs/administration/server-configuration.mdx b/docs/administration/server-configuration.mdx index 9156ecc02..3c0f575ee 100644 --- a/docs/administration/server-configuration.mdx +++ b/docs/administration/server-configuration.mdx @@ -7,7 +7,7 @@ description: "Server-owned settings.toml sections, CLI overrides, and environmen `fabro server start` reads `~/.fabro/settings.toml` by default. This is the same file schema used by the CLI. -On a same-machine setup, the CLI and server share one `settings.toml`. On a remote deployment, the server machine has its own `settings.toml`, and the client machine keeps a separate local `settings.toml` for CLI-only values such as `[server].target`. +On a same-machine setup, the CLI and server share one `settings.toml`. On a remote deployment, the server machine has its own `settings.toml`, and the client machine keeps a separate local `settings.toml` for CLI-only values such as `[cli.target]`. Legacy `server.toml`, `user.toml`, and `cli.toml` are ignored with a warning. Rename them to `settings.toml`. @@ -17,84 +17,88 @@ Legacy `server.toml`, `user.toml`, and `cli.toml` are ignored with a warning. Re | Scope | Examples | |---|---| -| Server-owned | `storage_dir`, `max_concurrent_runs`, `[web]`, `[api]`, `[features]` | -| Shared with CLI and workflow defaults | `[llm]`, `[log]`, `[git]`, `[setup]`, `[sandbox]`, `[checkpoint]`, `[vars]`, `[pull_request]` | +| 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]`, `[features]` | +| Shared run defaults (layered through `fabro.toml`/`workflow.toml`) | `[run.model]`, `[run.prepare]`, `[run.sandbox]`, `[run.checkpoint]`, `[run.inputs]`, `[run.pull_request]`, `[run.git]`, `[run.hooks]`, `[run.agent]` | -The CLI-only `[server]` section still belongs in the client machine's `settings.toml`. It tells CLI commands where to find a server. The server process does not read `[server].target` for its own binding or routing. +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. ### Full reference ```toml title="settings.toml" -# Maximum concurrent workflow runs (default: 5) -max_concurrent_runs = 8 +_version = 1 -# Override the default data directory (default: ~/.fabro) -data_dir = "/var/lib/fabro" +[server.listen] +type = "tcp" +address = "0.0.0.0:3000" -[api] -base_url = "https://fabro.example.com/api/v1" - -[api.tls] +[server.listen.tls] cert = "/etc/fabro/tls/cert.pem" key = "/etc/fabro/tls/key.pem" ca = "/etc/fabro/tls/ca.pem" -# Authentication strategies (array of Jwt or Mtls) -[[api.authentication_strategies]] -type = "Jwt" +[server.api] +url = "https://fabro.example.com/api/v1" -[web] +[server.auth.api.jwt] +enabled = true + +[server.web] enabled = true url = "https://fabro-web.example.com" -[web.auth] -provider = "Github" +[server.auth.web] allowed_usernames = ["alice", "bob"] -[git] -provider = "Github" +[server.auth.web.providers.github] +enabled = true +client_id = "Iv1.abc123" + +[server.integrations.github] app_id = "123456" client_id = "Iv1.abc123" -[log] +[server.integrations.github.webhooks] +strategy = "tailscale_funnel" + +[server.storage] +root = "/var/lib/fabro" + +[server.scheduler] +max_concurrent_runs = 8 + +[server.logging] level = "info" -[git.author] +# Run defaults — applied to every run unless overridden by workflow/project config +[run.model] +name = "claude-sonnet-4-5" +provider = "anthropic" +fallbacks = ["gemini", "openai"] + +[[run.prepare.steps]] +script = "npm install" + +[run.sandbox] +provider = "daytona" + +[run.sandbox.daytona] +auto_stop_interval = 60 + +[run.sandbox.daytona.labels] +team = "platform" + +[run.checkpoint] +exclude_globs = ["**/node_modules/**", "**/.cache/**"] + +[run.inputs] +default_branch = "main" + +[run.git.author] name = "fabro-bot" email = "fabro-bot@company.com" -[git.webhooks] -strategy = "tailscale_funnel" - -# Run defaults — applied to every run unless overridden by workflow/project config -[llm] -model = "claude-sonnet-4-5" -provider = "anthropic" - -[llm.fallbacks] -anthropic = ["gemini", "openai"] - -[setup] -commands = ["npm install"] -timeout_ms = 120000 - -[sandbox] -provider = "daytona" - -[sandbox.daytona] -auto_stop_interval = 60 - -[sandbox.daytona.labels] -team = "platform" - [features] -retros = true - -[checkpoint] -exclude_globs = ["**/node_modules/**", "**/.cache/**"] - -[vars] -default_branch = "main" +session_sandboxes = true ``` ### CLI overrides @@ -116,36 +120,38 @@ Several `settings.toml` settings can be overridden via `fabro server start` flag CLI flags take precedence over `settings.toml` values. See [Run Configuration — Precedence](/execution/run-configuration#precedence) for the full resolution order. -### `[web]` section +### `[server.web]` section Control the embedded SPA and browser-oriented routes. | Key | Description | Default | |---|---|---| | `enabled` | Serve the embedded SPA, `/auth/*`, and the web-only helper endpoints under `/api/v1` | `true` | -| `url` | External web UI URL used for OAuth redirects | `http://localhost:3000` | +| `url` | External web UI URL used for OAuth redirects | none (no implicit derivation from `server.listen`) | When `enabled = false`, the server still exposes the machine API and `/health`, but `/`, `/auth/*`, SPA client routes, `/api/v1/auth/me`, `/api/v1/setup/*`, and `/api/v1/demo/toggle` all return `404`. ### Run defaults -The `[llm]`, `[setup]`, `[sandbox]`, `[checkpoint]`, and `[vars]` sections in `settings.toml` act as defaults for every run. +The `[run.*]` sections in `settings.toml` act as defaults for every run. -On a same-machine setup, `settings.toml` is the shared machine-default layer under `workflow.toml` / `run.toml` and `fabro.toml`. +On a same-machine setup, `settings.toml` is the shared machine-default layer under `workflow.toml` and `fabro.toml`. -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 `storage_dir`, `[api]`, `[web]`, `[features]`, and `max_concurrent_runs` always come from the server machine's own `settings.toml` or `fabro server start` flags. +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]`, `[features]`, and `[server.scheduler]` always come from the server machine's own `settings.toml` or `fabro server start` flags. -For `[vars]`, Daytona labels, and checkpoint exclude globs, values are **merged** and the more specific layer wins on key collisions. All other fields use "first non-empty wins" precedence. +Merge rules follow the normative matrix: `[run.inputs]` replaces wholesale, `[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. -### `[log]` section +### `[server.logging]` section -Configure the default log level without environment variables. Precedence: `FABRO_LOG` env var > `--debug` flag > `[log]` level > `"info"`. +Configure the default server log level. Precedence: `FABRO_LOG` env var > `--debug` flag > `[server.logging].level` > `"info"`. | Key | Description | Default | |---|---|---| | `level` | Log level: `error`, `warn`, `info`, `debug`, `trace` | `"info"` | -### `[git.author]` section +The CLI has its own `[cli.logging]` section. + +### `[run.git.author]` section Customize the git author identity used for checkpoint commits. When not set, defaults to `fabro` / `fabro@local`. @@ -154,19 +160,23 @@ Customize the git author identity used for checkpoint commits. When not set, def | `name` | Git author name | `"fabro"` | | `email` | Git author email | `"fabro@local"` | -On same-machine setups, the CLI and server read the same `[git.author]`. On remote setups, the server uses its local `settings.toml`. +### `[server.integrations.github]` section -### `[git.webhooks]` section +Configure a GitHub App integration. Required fields include `app_id`, `client_id`, and `slug`. Webhook delivery is configured under `[server.integrations.github.webhooks]`: -Enable automatic GitHub webhook delivery via Tailscale funnel. When configured, `fabro server start` binds a local HTTP listener, exposes it through `tailscale funnel`, and updates the GitHub App's webhook URL on startup. Incoming webhooks are verified with HMAC-SHA256. +```toml title="settings.toml" +[server.integrations.github] +app_id = "123456" +client_id = "Iv1.abc123" +slug = "fabro-app" -| Key | Description | Values | -|---|---|---| -| `strategy` | Webhook delivery method | `"tailscale_funnel"` | +[server.integrations.github.webhooks] +strategy = "tailscale_funnel" +``` -Requires a configured GitHub App (`[git]` section with `app_id` and `client_id`) and the `GITHUB_APP_WEBHOOK_SECRET` environment variable. +When `webhooks.strategy = "tailscale_funnel"` is configured, `fabro server start` binds a local HTTP listener, exposes it through `tailscale funnel`, and updates the GitHub App's webhook URL on startup. Incoming webhooks are verified with HMAC-SHA256. Requires the `GITHUB_APP_WEBHOOK_SECRET` environment variable. -### `[checkpoint]` section +### `[run.checkpoint]` section Configure checkpoint behavior for all runs. @@ -174,7 +184,7 @@ Configure checkpoint behavior for all runs. |---|---| | `exclude_globs` | Glob patterns for files to exclude from checkpoint commits (for example, `["**/node_modules/**"]`) | -Exclude globs from `settings.toml` and run configs are merged (union, deduplicated). See [Run Configuration — Checkpoint](/execution/run-configuration#checkpoint) for per-run configuration. +`exclude_globs` replaces across layers — the highest-precedence layer wins wholesale. See [Run Configuration — Checkpoint](/execution/run-configuration#runcheckpoint) for per-run configuration. ### `[features]` section @@ -182,7 +192,6 @@ Toggle experimental or opt-in features. All features default to `false`. | Key | Description | |---|---| -| `retros` | Enable automatic [retro](/execution/retros) generation after workflow runs (experimental) | | `session_sandboxes` | Enable session sandboxes in the web UI | The same `[features]` section can be set in `fabro.toml` (project-level) to enable features per-project. diff --git a/docs/agents/mcp.mdx b/docs/agents/mcp.mdx index 885564db4..f6d74d057 100644 --- a/docs/agents/mcp.mdx +++ b/docs/agents/mcp.mdx @@ -152,25 +152,29 @@ If the server marks the result as an error (`is_error: true`), the tool result i A workflow that uses Playwright MCP to automate a browser inside a Daytona sandbox: ```toml title="run.toml" -version = 1 -goal = "Test the login page" +_version = 1 + +[workflow] graph = "workflow.fabro" -[sandbox] +[run] +goal = "Test the login page" + +[run.sandbox] provider = "daytona" -[sandbox.daytona.snapshot] +[run.sandbox.daytona.snapshot] name = "daytona-medium" -[artifacts] +[run.artifacts] include = ["screenshots/**"] -[mcp_servers.playwright] +[run.agent.mcps.playwright] type = "sandbox" command = ["npx", "@playwright/mcp@latest", "--port", "3100", "--headless", "--browser", "chromium"] port = 3100 -startup_timeout_secs = 60 -tool_timeout_secs = 120 +startup_timeout = "60s" +tool_timeout = "2m" ``` After startup, the agent sees 22 Playwright tools including: diff --git a/docs/api-reference/overview.mdx b/docs/api-reference/overview.mdx index e3ab95f15..45d0b12cb 100644 --- a/docs/api-reference/overview.mdx +++ b/docs/api-reference/overview.mdx @@ -20,8 +20,8 @@ http://localhost:3000/api/v1 The base URL is configurable via `settings.toml`: ```toml title="settings.toml" -[api] -base_url = "https://fabro.example.com/api/v1" +[server.api] +url = "https://fabro.example.com/api/v1" ``` ## Authentication @@ -29,8 +29,8 @@ base_url = "https://fabro.example.com/api/v1" The API supports two authentication strategies, configured in `settings.toml`: ```toml title="settings.toml" -[api] -authentication_strategies = ["jwt"] +[server.auth.api.jwt] +enabled = true ``` ### JWT (Bearer Token) @@ -56,13 +56,17 @@ Set the verification key via the `FABRO_JWT_PUBLIC_KEY` environment variable (PE ### mTLS (Mutual TLS) -With mTLS, the client authenticates using a TLS client certificate. Configure both the strategy and TLS paths: +With mTLS, the client authenticates using a TLS client certificate. Configure the strategy and shared listener TLS: ```toml title="settings.toml" -[api] -authentication_strategies = ["mtls"] +[server.auth.api.mtls] +enabled = true -[api.tls] +[server.listen] +type = "tcp" +address = "0.0.0.0:3000" + +[server.listen.tls] cert = "~/.fabro/certs/server.crt" key = "~/.fabro/certs/server.key" ca = "~/.fabro/certs/ca.crt" @@ -72,11 +76,14 @@ The Common Name (CN) from the client certificate identifies the user. ### Multiple Strategies -You can configure both strategies. They are tried in order — the first successful match wins: +You can enable both strategies. They are tried in order — the first successful match wins: ```toml title="settings.toml" -[api] -authentication_strategies = ["jwt", "mtls"] +[server.auth.api.jwt] +enabled = true + +[server.auth.api.mtls] +enabled = true ``` ## Errors diff --git a/docs/core-concepts/models.mdx b/docs/core-concepts/models.mdx index 5ad270cd6..7ea0f25f5 100644 --- a/docs/core-concepts/models.mdx +++ b/docs/core-concepts/models.mdx @@ -92,16 +92,17 @@ These flags set the default model for all nodes that don't have an explicit mode For repeatable runs, set the model in a run config file: ```toml title="run.toml" -version = 1 -goal = "Implement the feature" +_version = 1 + +[workflow] graph = "implement.fabro" -[llm] -model = "claude-sonnet-4-5" +[run] +goal = "Implement the feature" -[llm.fallbacks] -anthropic = ["gemini", "openai"] -gemini = ["anthropic", "openai"] +[run.model] +name = "claude-sonnet-4-5" +fallbacks = ["gemini", "openai"] ``` Then launch with: @@ -110,7 +111,7 @@ Then launch with: fabro run run.toml ``` -The `[llm.fallbacks]` table is optional. It maps each provider to an ordered list of fallback providers to try when the primary is unavailable. +The `fallbacks` array is optional. Each entry may be a bare provider token (like `"gemini"`), a bare model alias (like `"gpt-5.4"`), or a qualified `"provider/model"` reference. Fabro tries them in order when the primary provider is unavailable. The precedence order is: node-level stylesheet > run config TOML > CLI flags > server defaults. More specific settings always win. diff --git a/docs/execution/devcontainers.mdx b/docs/execution/devcontainers.mdx index 6db34fb0f..c98e8bbf3 100644 --- a/docs/execution/devcontainers.mdx +++ b/docs/execution/devcontainers.mdx @@ -7,13 +7,15 @@ Fabro can use your project's [devcontainer](https://containers.dev/) configurati ## Enabling devcontainer support -Set `devcontainer = true` in the `[sandbox]` section of your run config: +Set `devcontainer = true` in the `[run.sandbox]` section of your run config: ```toml title="run.toml" -version = 1 +_version = 1 + +[workflow] graph = "workflow.fabro" -[sandbox] +[run.sandbox] provider = "daytona" devcontainer = true ``` diff --git a/docs/execution/environments.mdx b/docs/execution/environments.mdx index f58adc377..ca70264e2 100644 --- a/docs/execution/environments.mdx +++ b/docs/execution/environments.mdx @@ -26,7 +26,7 @@ fabro run workflow.fabro --sandbox daytona ```toml title="run.toml" # Run config TOML -[sandbox] +[run.sandbox] provider = "daytona" ``` @@ -94,7 +94,7 @@ fabro run workflow.fabro --sandbox docker --preserve-sandbox Or in the run config: ```toml title="run.toml" -[sandbox] +[run.sandbox] provider = "docker" preserve = true ``` @@ -123,17 +123,17 @@ The Daytona sandbox runs all tool operations inside a cloud-hosted VM managed by 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] +[run.sandbox] provider = "daytona" -[sandbox.daytona] +[run.sandbox.daytona] auto_stop_interval = 60 -[sandbox.daytona.snapshot] +[run.sandbox.daytona.snapshot] name = "rust-dev" cpu = 4 -memory = 8 -disk = 20 +memory = "8GB" +disk = "20GB" dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update && apt-get install -y git ripgrep" ``` @@ -152,7 +152,7 @@ If the snapshot already exists and is in `Active` state, Fabro uses it directly. Attach key-value labels to sandboxes for filtering and identification in the Daytona dashboard: ```toml title="run.toml" -[sandbox.daytona.labels] +[run.sandbox.daytona.labels] project = "fabro" env = "ci" team = "platform" @@ -185,7 +185,7 @@ Fabro prints the sandbox name so you can find it in the [Daytona dashboard](http 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] +[run.sandbox.daytona] auto_stop_interval = 30 ``` @@ -215,15 +215,15 @@ Each provider handles outbound network access differently: | `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 `[sandbox.daytona]` section: +For Daytona, network access is configured in the `[run.sandbox.daytona]` section: ```toml title="run.toml" # Block all egress -[sandbox.daytona] +[run.sandbox.daytona] network = "block" # Allow only specific CIDRs -[sandbox.daytona] +[run.sandbox.daytona] network = { allow_list = ["208.80.154.232/32", "10.0.0.0/8"] } ``` diff --git a/docs/execution/failures.mdx b/docs/execution/failures.mdx index d820951f7..5a219da33 100644 --- a/docs/execution/failures.mdx +++ b/docs/execution/failures.mdx @@ -115,16 +115,13 @@ When a handler returns a `Retry` status instead of `Fail`, retries always procee When a model provider fails with a transient error or quota exhaustion, Fabro can automatically switch to a different provider. Configure fallback chains in your [run configuration](/execution/run-configuration): ```toml title="run.toml" -[llm] -model = "claude-opus-4-6" +[run.model] +name = "claude-opus-4-6" provider = "anthropic" - -[llm.fallbacks] -anthropic = ["gemini", "openai"] -gemini = ["anthropic", "openai"] +fallbacks = ["gemini", "openai"] ``` -When Anthropic is unavailable, Fabro tries Gemini first, then OpenAI. For each fallback provider, Fabro selects the closest model by matching required capabilities (tool use, vision, reasoning) and minimizing cost difference. +When Anthropic is unavailable, Fabro tries Gemini first, then OpenAI. Each fallback entry may be a bare provider token (like `"gemini"`), a bare model alias (like `"gpt-5.4"`), or a qualified `"provider/model"` reference. For each fallback provider, Fabro selects the closest model by matching required capabilities (tool use, vision, reasoning) and minimizing cost difference. ### What triggers failover diff --git a/docs/execution/retros.mdx b/docs/execution/retros.mdx index 44471a74a..e52a6c355 100644 --- a/docs/execution/retros.mdx +++ b/docs/execution/retros.mdx @@ -4,7 +4,7 @@ description: "Automatic retrospectives that analyze every workflow run" --- -**Experimental feature.** Retros are disabled by default. Enable them with `[features] retros = true` in your project config or server config. +**Experimental feature.** Retros are disabled by default. Enable them by setting `retros = true` under `[run.execution]` in your project or workflow config. After every workflow run, Fabro can generate a **retro** — a structured retrospective that captures what happened, what went well, and what didn't. Retros combine deterministic metrics extracted from the run's checkpoint with a qualitative narrative produced by an LLM agent that analyzes the full event stream. @@ -113,12 +113,12 @@ Both phases run automatically at the end of every CLI run. The API server derive ### CLI -To enable retros for your project, set `retros = true` in the `[features]` section of your `fabro.toml`: +To enable retros for your project, set `retros = true` under `[run.execution]` in your `fabro.toml`: ```toml title="fabro.toml" -version = 1 +_version = 1 -[features] +[run.execution] retros = true ``` @@ -131,7 +131,9 @@ fabro run workflow.fabro --no-retro Retros can also be enabled server-wide in `settings.toml`: ```toml title="settings.toml" -[features] +_version = 1 + +[run.execution] retros = true ``` diff --git a/docs/execution/run-configuration.mdx b/docs/execution/run-configuration.mdx index 9e6c9e1f2..3f855f8fb 100644 --- a/docs/execution/run-configuration.mdx +++ b/docs/execution/run-configuration.mdx @@ -3,7 +3,7 @@ title: "Run Configuration" description: "Configure workflow runs with TOML files" --- -A run config is a TOML file that bundles a workflow graph with all the settings needed to execute it — the goal, model, sandbox, setup commands, variables, and hooks. Instead of passing a dozen CLI flags, you check a `.toml` file into version control and launch with a single command: +A run config is a TOML file that bundles a workflow graph with all the settings needed to execute it — the goal, model, sandbox, prepare steps, inputs, and hooks. Instead of passing a dozen CLI flags, you check a `.toml` file into version control and launch with a single command: ```bash fabro run run.toml @@ -11,142 +11,154 @@ fabro run run.toml ## Minimal example -A run config requires two fields: +A run config needs at minimum a schema version and a goal: ```toml title="run.toml" -version = 1 +_version = 1 + +[workflow] graph = "workflow.fabro" + +[run] goal = "Implement the login feature" ``` | Field | Required | Description | |---|---|---| -| `version` | Yes | Config format version. Must be `1`. | -| `graph` | Yes | Path to the Graphviz workflow file, resolved relative to the TOML file's directory. | -| `goal` | No | What the workflow should accomplish. Passed to agents and used in retrospectives. Can also be provided via `--goal` CLI flag or Graphviz graph `goal` attribute. | +| `_version` | No (defaults to `1`) | Schema version. Must be `1` in the first pass. | +| `[workflow].graph` | No | Path to the Graphviz workflow file, relative to the TOML file's directory. Defaults to `workflow.fabro`. | +| `[run].goal` | No | What the workflow should accomplish. Passed to agents and used in retrospectives. Can also be provided via `--goal` CLI flag or Graphviz graph `goal` attribute. | -Goal precedence: CLI `--goal` > TOML `goal` > Graphviz graph attribute. +Goal precedence: CLI `--goal` > `[run].goal` > Graphviz graph attribute. ## Full example ```toml title="run.toml" -version = 1 -goal = "Run the CI pipeline for $repo_name" +_version = 1 + +[workflow] graph = "fabro/workflows/ci.fabro" -directory = "/tmp/workdir" -[llm] -model = "claude-sonnet-4-5" +[run] +goal = "Run the CI pipeline for $repo_name" +working_dir = "/tmp/workdir" -[llm.fallbacks] -anthropic = ["gemini", "openai"] -gemini = ["anthropic", "openai"] +[run.model] +name = "claude-sonnet-4-5" +fallbacks = ["openai", "gemini"] -[setup] -commands = ["git clone $repo_url repo", "cd repo && npm install"] -timeout_ms = 120000 +[[run.prepare.steps]] +script = "git clone $repo_url repo" -[sandbox] +[[run.prepare.steps]] +script = "cd repo && npm install" + +[run.sandbox] provider = "daytona" preserve = false -[sandbox.daytona] +[run.sandbox.daytona] auto_stop_interval = 60 -[sandbox.daytona.labels] +[run.sandbox.daytona.labels] project = "fabro" env = "ci" -[sandbox.daytona.snapshot] +[run.sandbox.daytona.snapshot] name = "node-20" cpu = 4 -memory = 8 -disk = 20 +memory = "8GB" +disk = "20GB" dockerfile = "FROM node:20-slim\nRUN apt-get update && apt-get install -y git" -[sandbox.env] +[run.sandbox.env] API_KEY = "${env.MY_API_KEY}" NODE_ENV = "production" -[checkpoint] +[run.checkpoint] exclude_globs = ["**/node_modules/**", "**/.cache/**"] -[vars] +[run.inputs] repo_name = "fabro" repo_url = "https://github.com/fabro-sh/fabro" -[artifacts] +[run.artifacts] include = ["test-results/**", "playwright-report/**"] -[mcp_servers.playwright] +[run.agent.mcps.playwright] type = "sandbox" command = ["npx", "@playwright/mcp@latest", "--port", "3100", "--headless"] port = 3100 -[pull_request] +[run.pull_request] enabled = true draft = false -[[hooks]] +[[run.hooks]] +id = "pre-check" event = "stage_start" -command = "./scripts/pre-check.sh" +script = "./scripts/pre-check.sh" blocking = true sandbox = false -[[hooks]] +[[run.hooks]] event = "run_complete" -command = "echo done" +script = "echo done" ``` ## Sections -### `[llm]` +### `[run.model]` Override the default model and provider for all nodes that don't have an explicit model assigned via a [stylesheet](/workflows/stylesheets). ```toml title="run.toml" -[llm] -model = "claude-sonnet-4-5" +[run.model] +name = "claude-sonnet-4-5" ``` | Field | Description | |---|---| -| `model` | Model ID or alias (e.g. `claude-sonnet-4-5`, `opus`, `gemini-pro`). See [Models](/core-concepts/models). | +| `name` | Model ID or alias (e.g. `claude-sonnet-4-5`, `opus`, `gemini-pro`). See [Models](/core-concepts/models). | | `provider` | Provider name (optional — auto-inferred from the model catalog). Only needed for models not in the catalog or to force a specific provider. | +| `fallbacks` | Ordered list of model references to try when the primary is unavailable. Entries can be bare provider tokens (`"openai"`), bare model aliases, or qualified `"provider/model"` references. | -#### `[llm.fallbacks]` +#### Fallbacks with splice -Map each provider to an ordered list of fallback providers. When the primary provider is unavailable, Fabro tries the fallbacks in order: +Use the reserved `"..."` marker in `fallbacks` to splice in the inherited list from lower-precedence layers: ```toml title="run.toml" -[llm.fallbacks] -anthropic = ["gemini", "openai"] -gemini = ["anthropic", "openai"] +[run.model] +# Prepend "anthropic" to whatever fallbacks the project config already defines. +fallbacks = ["anthropic", "..."] ``` -### `[setup]` +### `[run.prepare]` -Shell commands to run before the workflow starts. Use this to clone repositories, install dependencies, or prepare the environment. +Ordered list of steps to run before the workflow starts. Use this to clone repositories, install dependencies, or prepare the environment. ```toml title="run.toml" -[setup] -commands = ["pip install -r requirements.txt", "npm install"] -timeout_ms = 60000 +[[run.prepare.steps]] +script = "pip install -r requirements.txt" + +[[run.prepare.steps]] +script = "npm install" ``` | Field | Description | |---|---| -| `commands` | List of shell commands, executed sequentially via `sh -c`. | -| `timeout_ms` | Per-command timeout in milliseconds. Default: `300000` (5 minutes). | +| `script` | Shell-evaluated command (runs through `sh -c`). | +| `command` | Argv-style command, mutually exclusive with `script`. | +| `env` | Additional environment variables for this step. | -Each command must exit with status 0. If any command fails or times out, the run aborts before the workflow starts. +Each step must exit with status 0. If any step fails, the run aborts before the workflow starts. Prepare steps replace across layers — the higher-precedence layer wins wholesale. -### `[sandbox]` +### `[run.sandbox]` Configure how agent tools (bash, file edits) are executed. ```toml title="run.toml" -[sandbox] +[run.sandbox] provider = "docker" preserve = true ``` @@ -157,23 +169,23 @@ preserve = true | `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). | -#### `[sandbox.daytona]` +#### `[run.sandbox.daytona]` Additional settings when using the Daytona cloud sandbox: ```toml title="run.toml" -[sandbox.daytona] +[run.sandbox.daytona] auto_stop_interval = 60 -[sandbox.daytona.labels] +[run.sandbox.daytona.labels] project = "fabro" env = "staging" -[sandbox.daytona.snapshot] +[run.sandbox.daytona.snapshot] name = "my-snapshot" cpu = 4 -memory = 8 -disk = 20 +memory = "8GB" +disk = "20GB" dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update" # Or reference an external Dockerfile: # dockerfile = { path = "./Dockerfile" } @@ -182,20 +194,20 @@ dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update" | 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` | Key-value labels attached to the sandbox for filtering and identification. Labels merge across layers (sticky merge-by-key). | | `snapshot.name` | Snapshot name to create or use for the sandbox. | -| `snapshot.cpu` | CPU cores for the snapshot. | -| `snapshot.memory` | Memory in GB for the snapshot. | -| `snapshot.disk` | Disk in GB for the snapshot. | +| `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). | -#### `[sandbox.local]` +#### `[run.sandbox.local]` Additional settings when using the local sandbox: ```toml title="run.toml" -[sandbox.local] +[run.sandbox.local] worktree_mode = "always" ``` @@ -203,29 +215,31 @@ worktree_mode = "always" |---|---| | `worktree_mode` | When to create a git worktree for the run: `always`, `clean` (default — only when the working tree is clean), `dirty` (also when dirty), or `never`. | -#### `[sandbox.env]` +#### `[run.sandbox.env]` -Pass environment variables into sandbox command and agent execution. Values can be literal strings or host environment passthrough using `${env.VARNAME}` syntax: +Pass environment variables into sandbox command and agent execution. Values can be literal strings or host environment references using `${env.VARNAME}` syntax: ```toml title="run.toml" -[sandbox.env] +[run.sandbox.env] API_KEY = "${env.MY_API_KEY}" NODE_ENV = "production" +SERVICE_URL = "https://api.${env.REGION}.example.com" ``` | Syntax | Description | |---|---| | `"literal"` | Static value passed as-is | -| `"${env.VARNAME}"` | Resolved from the host environment at load time. Missing vars produce a hard error. | +| `"${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 | -Host env references must be whole-value only — partial interpolation like `"prefix-${env.X}"` is not supported. Sandbox env vars from `settings.toml` defaults and the run config are merged, with the run config winning on key collisions. +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. -### `[checkpoint]` +### `[run.checkpoint]` Configure how git checkpoint commits behave. ```toml title="run.toml" -[checkpoint] +[run.checkpoint] exclude_globs = ["**/node_modules/**", "**/.cache/**", "**/dist/**"] ``` @@ -233,20 +247,20 @@ exclude_globs = ["**/node_modules/**", "**/.cache/**", "**/dist/**"] |---|---| | `exclude_globs` | Glob patterns for files to exclude from checkpoint commits. Uses git pathspec `:(glob,exclude)` syntax. | -Exclude globs from `settings.toml` defaults and the run config are merged (union, deduplicated). +`exclude_globs` replaces across layers — the higher-precedence layer wins wholesale. -### `[vars]` +### `[run.inputs]` -Define variables that are expanded into the Graphviz source before the graph is parsed. See [Variables](/workflows/variables) for the full reference. +Define inputs that are expanded into the Graphviz source before the graph is parsed. See [Variables](/workflows/variables) for the full reference. ```toml title="run.toml" -[vars] +[run.inputs] repo_name = "fabro" repo_url = "https://github.com/fabro-sh/fabro" language = "rust" ``` -Variables can be used anywhere in the Graphviz file with `$name` syntax: +Inputs can be used anywhere in the Graphviz file with `$name` syntax: ```dot title="c-i.fabro" digraph CI { @@ -256,14 +270,16 @@ digraph CI { } ``` -If a `$variable` in the Graphviz file has no matching entry in `[vars]`, Fabro raises an error immediately. A bare `$` not followed by an identifier (e.g. `costs $5`) is left as-is. +If a `$variable` in the Graphviz file has no matching entry in `[run.inputs]`, Fabro raises an error immediately. A bare `$` not followed by an identifier (e.g. `costs $5`) is left as-is. -### `[artifacts]` +`[run.inputs]` replaces wholesale across layers. Unlike labels, inputs do not merge by key — the highest-precedence layer that sets `inputs` wins its entire map. + +### `[run.artifacts]` Configure automatic collection of test artifacts (Playwright reports, JUnit XML, screenshots, etc.) from the execution environment after each stage. ```toml title="run.toml" -[artifacts] +[run.artifacts] include = ["test-results/**", "playwright-report/**", "*.trace.zip"] ``` @@ -271,40 +287,41 @@ include = ["test-results/**", "playwright-report/**", "*.trace.zip"] |---|---| | `include` | Glob patterns for files to collect as assets. Matched against the working directory after each stage completes. | -Artifact collection is opt-in — when no `[artifacts]` section is present, no file scanning occurs. This avoids the overhead of scanning large working directories when assets aren't needed. +Artifact collection is opt-in — when no `[run.artifacts]` section is present, no file scanning occurs. -### `[mcp_servers]` +### `[run.agent.mcps]` -Configure [MCP servers](/agents/mcp) available to agent stages during the workflow run. Each server is a named TOML table. All three transport types are supported: `stdio`, `http`, and `sandbox`. +Configure [MCP servers](/agents/mcp) available to agent stages during the workflow run. Each server is a named TOML table under `[run.agent.mcps]`. All three transport types are supported: `stdio`, `http`, and `sandbox`. ```toml title="run.toml" -[mcp_servers.playwright] +[run.agent.mcps.playwright] type = "sandbox" command = ["npx", "@playwright/mcp@latest", "--port", "3100", "--headless", "--browser", "chromium"] port = 3100 -startup_timeout_secs = 60 -tool_timeout_secs = 120 +startup_timeout = "60s" +tool_timeout = "2m" ``` | Field | Description | Default | |---|---|---| | `type` | Transport type: `"stdio"`, `"http"`, or `"sandbox"`. | — | -| `command` | (stdio, sandbox) Array: executable + arguments. | — | +| `script` | (stdio, sandbox) Shell-evaluated startup command, mutually exclusive with `command`. | — | +| `command` | (stdio, sandbox) Argv array: executable + arguments. | — | | `port` | (sandbox) Port the server listens on inside the sandbox. | — | | `url` | (http) The MCP server endpoint URL. | — | | `env` | (stdio, sandbox) Additional environment variables. | `{}` | | `headers` | (http) Optional HTTP headers for authentication. | `{}` | -| `startup_timeout_secs` | Max seconds for server startup + MCP handshake. | `10` | -| `tool_timeout_secs` | Max seconds for a single tool call. | `60` | +| `startup_timeout` | Max duration for server startup + MCP handshake (e.g. `"10s"`, `"1m"`). | `"10s"` | +| `tool_timeout` | Max duration for a single tool call. | `"60s"` | The `sandbox` transport runs the MCP server inside the workflow's sandbox. This is useful for tools that need access to the sandbox environment, such as browser automation with Playwright. See [MCP](/agents/mcp#sandbox) for details. -### `[pull_request]` +### `[run.pull_request]` Automatically open a GitHub pull request when the workflow run completes successfully. Requires a [GitHub App](/integrations/github) to be configured. ```toml title="run.toml" -[pull_request] +[run.pull_request] enabled = true draft = true auto_merge = false @@ -318,64 +335,46 @@ merge_strategy = "squash" | `auto_merge` | When `true`, enables GitHub auto-merge on the created PR. Implies `draft = false` since GitHub doesn't allow auto-merge on draft PRs. The repository must have auto-merge enabled in GitHub settings. Default: `false`. | | `merge_strategy` | Merge method when `auto_merge` is enabled: `squash` (default), `merge`, or `rebase`. | -### `[github]` - -Request a scoped GitHub Installation Access Token and inject it into the sandbox as `GITHUB_TOKEN`. The token is minted from the configured [GitHub App](/integrations/github) with only the permissions you specify. - -```toml title="run.toml" -[github] -permissions = { contents = "write", pull_requests = "read" } -``` - -| Field | Description | -|---|---| -| `permissions` | Map of GitHub API permission names to access levels (`"read"` or `"write"`). Only the listed permissions are requested. | - -This requires a GitHub App to be configured. If the app is missing or the repository doesn't have an installation, the run logs a warning and continues without injecting the token. - -### `[[hooks]]` +### `[[run.hooks]]` Define hooks that run in response to lifecycle events. Each hook is a TOML array entry: ```toml title="run.toml" -[[hooks]] -name = "pre-check" +[[run.hooks]] +id = "pre-check" +name = "Pre-check script" event = "stage_start" -command = "./scripts/pre-check.sh" +script = "./scripts/pre-check.sh" matcher = "agent_loop" blocking = true -timeout_ms = 30000 +timeout = "30s" sandbox = false ``` | Field | Description | |---|---| +| `id` | Optional merge identity. Hooks with the same `id` replace each other across layers. | | `name` | Optional display name for the hook. | -| `event` | Lifecycle event: `run_start`, `run_complete`, `stage_start`, `stage_complete`. | -| `command` | Shell command to execute (shorthand for `type = "command"`). | +| `event` | Lifecycle event: `run_start`, `run_complete`, `stage_start`, `stage_complete`, etc. | +| `script` | Shell-evaluated command (equivalent to the old `type = "command"` shorthand). | +| `command` | Argv-style command (alternative to `script`). | | `matcher` | Regex matched against node ID or handler type. Limits which stages trigger this hook. | | `blocking` | Whether the hook must complete before execution continues. Defaults vary by event. | -| `timeout_ms` | Hook timeout in milliseconds. Default: `60000` (60s). | +| `timeout` | Human-readable hook timeout (e.g. `"30s"`, `"1m"`). Default: `"60s"`. | | `sandbox` | Run inside the sandbox (`true`, default) or on the host (`false`). | -See [Hooks](/agents/hooks) for hook types beyond simple commands (HTTP, prompt, agent). +Hook merge semantics: hooks with matching `id` values replace in place. Hooks without an `id` from a higher-precedence layer append after the fully merged inherited hook list. -## Top-level fields - -In addition to the sections above, two optional top-level fields are available: - -| Field | Description | -|---|---| -| `directory` | Working directory for the run. Defaults to the current directory. | +See [Hooks](/agents/hooks) for hook types beyond scripts (HTTP, prompt, agent). ## Graph path resolution -The `graph` path is resolved relative to the TOML file's parent directory, not the current working directory. This means a run config and its workflow can live side by side: +The `[workflow].graph` path is resolved relative to the TOML file's parent directory, not the current working directory. This means a run config and its workflow can live side by side: ``` project/ runs/ - ci.toml # graph = "ci.fabro" + ci.toml # [workflow] graph = "ci.fabro" ci.fabro ``` @@ -388,67 +387,50 @@ Settings can come from multiple sources. Fabro resolves them in this order (firs | Source | Priority | |---|---| | Node-level [stylesheet](/workflows/stylesheets) | Highest | -| Run config TOML | | | CLI flags (`--model`, `--provider`, `--sandbox`) | | +| Run config TOML (`workflow.toml` or equivalent) | | | Project defaults (`fabro.toml`) | | -| Server defaults (`~/.fabro/settings.toml`) | | +| Machine defaults (`~/.fabro/settings.toml`) | | | Graphviz graph attributes (`default_model`, `default_provider`) | | | Built-in defaults | Lowest | -For model and provider specifically, the precedence is: CLI flags > TOML config > project defaults > server defaults > Graphviz graph attributes > built-in defaults. Stylesheet rules on individual nodes always take priority over all of these. +Stylesheet rules on individual nodes always take priority over run config values. ### Project defaults (`fabro.toml`) -The `fabro.toml` project config can set default values for `[llm]`, `[setup]`, `[sandbox]`, `[vars]`, `[checkpoint]`, `[pull_request]`, `[github]`, `[artifacts]`, `[[hooks]]`, and `[mcp_servers]`. These defaults apply to all runs in the project unless the run config overrides them: +The `fabro.toml` project config can set default values for any of the `[run.*]` sections described above. These defaults apply to all runs in the project unless the workflow config overrides them: ```toml title="fabro.toml" -version = 1 +_version = 1 -[llm] -model = "claude-sonnet-4-5" +[project] +directory = "fabro/" -[sandbox] +[run.model] +name = "claude-sonnet-4-5" + +[run.sandbox] provider = "daytona" -[sandbox.daytona.snapshot] +[run.sandbox.daytona.snapshot] name = "my-project-snapshot" - -[github] -permissions = { contents = "write" } ``` -Project defaults are merged with run config values using the same rules as server defaults — run config wins on key collisions. +Project defaults and workflow config values merge per the normative merge matrix: most fields merge by field (higher-precedence wins per key), `run.inputs` replaces wholesale, `run.sandbox.env` sticky-merges by key, and `run.prepare.steps` replaces whole-list. -### Server defaults +### Machine defaults -When running via `fabro server start`, the server config at `~/.fabro/settings.toml` can set default values for `[llm]`, `[setup]`, `[sandbox]`, and `[vars]`. These defaults are applied to every run unless the run config overrides them. - -For variables, defaults and run config are **merged** — the run config wins on key collisions: - -```toml -# ~/.fabro/settings.toml -[vars] -default_key = "from_server" -shared = "from_server" - -# run.toml -[vars] -shared = "from_run" # wins -task_key = "from_run" -``` - -The same merge behavior applies to Daytona labels. All other fields use simple "first non-empty wins" precedence. +When running locally, the machine defaults at `~/.fabro/settings.toml` can set run-scoped defaults too. Same merge rules apply. ## Validation Fabro validates the run config when it loads: -- **Version check** — Only `version = 1` is accepted. Other versions are rejected immediately. -- **Required fields** — `version` and `graph` are required. `goal` is optional (can be provided via `--goal` or Graphviz graph attribute). -- **Unknown fields** — Extra fields not listed above are silently ignored. -- **Variable check** — Any `$variable` in the Graphviz file without a matching `[vars]` entry produces an error. +- **`_version` check** — Only `_version = 1` (or missing, which defaults to `1`) is accepted. The legacy top-level `version` key is rejected with a rename hint. +- **Unknown keys** — Any top-level key not in `[project]`, `[workflow]`, `[run]`, `[cli]`, `[server]`, `[features]`, or `_version` is rejected with a targeted rename hint pointing at the v2 replacement path. +- **Variable check** — Any `$variable` in the Graphviz file without a matching `[run.inputs]` entry produces an error. Use `fabro preflight` to validate a run config without executing it: diff --git a/docs/human-tools/ssh-access.mdx b/docs/human-tools/ssh-access.mdx index 7683148e4..41fd8934f 100644 --- a/docs/human-tools/ssh-access.mdx +++ b/docs/human-tools/ssh-access.mdx @@ -37,11 +37,11 @@ Without `--preserve-sandbox`, the SSH session is terminated when the run ends an You can also set `auto_stop_interval` in your run config to control how long an idle sandbox stays alive: ```toml title="run.toml" -[sandbox] +[run.sandbox] provider = "daytona" preserve = true -[sandbox.daytona] +[run.sandbox.daytona] auto_stop_interval = 60 ``` diff --git a/docs/integrations/daytona.mdx b/docs/integrations/daytona.mdx index 01fa8e2b4..52a875b8a 100644 --- a/docs/integrations/daytona.mdx +++ b/docs/integrations/daytona.mdx @@ -29,30 +29,30 @@ fabro run workflow.fabro --sandbox daytona ``` ```toml title="run.toml" -[sandbox] +[run.sandbox] provider = "daytona" ``` A full configuration example with all Daytona-specific options: ```toml title="run.toml" -[sandbox] +[run.sandbox] provider = "daytona" preserve = false -[sandbox.daytona] +[run.sandbox.daytona] auto_stop_interval = 60 -[sandbox.daytona.labels] +[run.sandbox.daytona.labels] project = "fabro" env = "staging" team = "platform" -[sandbox.daytona.snapshot] +[run.sandbox.daytona.snapshot] name = "rust-dev" cpu = 4 -memory = 8 -disk = 20 +memory = "8GB" +disk = "20GB" dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update && apt-get install -y git ripgrep" ``` @@ -64,15 +64,15 @@ Control outbound network access with the `network` field. Three modes are availa ```toml title="run.toml" # Full access (default) -[sandbox.daytona] +[run.sandbox.daytona] network = "allow_all" # Block all egress -[sandbox.daytona] +[run.sandbox.daytona] network = "block" # CIDR-based allow list -[sandbox.daytona] +[run.sandbox.daytona] network = { allow_list = ["208.80.154.232/32", "10.0.0.0/8"] } ``` @@ -138,7 +138,7 @@ fabro run workflow.fabro --sandbox daytona --preserve-sandbox Or in the run config: ```toml title="run.toml" -[sandbox] +[run.sandbox] provider = "daytona" preserve = true ``` @@ -150,7 +150,7 @@ When preserved, Fabro prints the sandbox name so you can find it in the [Daytona The `auto_stop_interval` setting tells Daytona to stop the sandbox after a period of inactivity, saving costs for preserved or long-running sandboxes: ```toml title="run.toml" -[sandbox.daytona] +[run.sandbox.daytona] auto_stop_interval = 30 ``` diff --git a/docs/integrations/github.mdx b/docs/integrations/github.mdx index ee8034c12..da7fad4e8 100644 --- a/docs/integrations/github.mdx +++ b/docs/integrations/github.mdx @@ -12,9 +12,9 @@ Fabro uses a [GitHub App](https://docs.github.com/en/apps/overview) to authentic | **OAuth login** | Users sign in to the web UI with their GitHub account | | **Private repo cloning** | Daytona and Docker sandboxes clone private repositories using short-lived Installation Access Tokens | | **Checkpoint pushing** | After each workflow stage, Fabro pushes the run branch and metadata branch back to origin from inside the sandbox | -| **Auto-PR** | When `[pull_request] enabled = true` in the [run config](/execution/run-configuration#pull_request), Fabro opens a PR from the agent's working branch after a successful run | -| **Auto-merge** | When `[pull_request] auto_merge = true`, Fabro enables GitHub's auto-merge on created PRs so they merge automatically once required checks pass | -| **Sandbox GITHUB_TOKEN** | When `[github] permissions` are declared in the run config, Fabro mints a scoped Installation Access Token and injects it as `GITHUB_TOKEN` in the sandbox | +| **Auto-PR** | When `[run.pull_request] enabled = true` in the [run config](/execution/run-configuration#runpull_request), Fabro opens a PR from the agent's working branch after a successful run | +| **Auto-merge** | When `[run.pull_request] auto_merge = true`, Fabro enables GitHub's auto-merge on created PRs so they merge automatically once required checks pass | +| **Sandbox GITHUB_TOKEN** | When `[server.integrations.github.permissions]` are declared in the server config, Fabro mints a scoped Installation Access Token and injects it as `GITHUB_TOKEN` in the sandbox | ## Setup @@ -61,8 +61,8 @@ The GitHub App check verifies five fields: | Field | Source | |---|---| -| `git.app_id` | `~/.fabro/settings.toml` | -| `git.client_id` | `~/.fabro/settings.toml` | +| `server.integrations.github.app_id` | `~/.fabro/settings.toml` | +| `server.integrations.github.client_id` | `~/.fabro/settings.toml` | | `GITHUB_APP_CLIENT_SECRET` | Server secret store | | `GITHUB_APP_WEBHOOK_SECRET` | Server secret store | | `GITHUB_APP_PRIVATE_KEY` | Server secret store | @@ -76,8 +76,7 @@ The GitHub App configuration lives in two places: ### `~/.fabro/settings.toml` ```toml title="settings.toml" -[git] -provider = "github" +[server.integrations.github] app_id = "123456" client_id = "Iv1.abc123def" slug = "fabro-a3f2" @@ -85,7 +84,6 @@ slug = "fabro-a3f2" | Field | Description | |---|---| -| `provider` | Always `"github"` (the only supported provider) | | `app_id` | Numeric GitHub App ID | | `client_id` | OAuth Client ID for the app | | `slug` | App slug, used for linking to the GitHub App settings page | diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx index a85d5302e..b9bcb993d 100644 --- a/docs/reference/cli.mdx +++ b/docs/reference/cli.mdx @@ -24,22 +24,29 @@ Connection-target flags like `--storage-dir` and `--server` are command-specific CLI defaults can be set in `~/.fabro/settings.toml` so you don't have to pass common flags every time: ```toml title="settings.toml" -[exec] +_version = 1 + +[cli.exec.model] provider = "anthropic" -model = "claude-opus-4-6" +name = "claude-opus-4-6" + +[cli.exec.agent] permissions = "read-write" -output_format = "text" -[llm] -model = "claude-sonnet-4-5" +[cli.output] +format = "text" -[server] -target = "https://fabro.example.com:3000/api/v1" +[run.model] +name = "claude-sonnet-4-5" + +[cli.target] +type = "http" +url = "https://fabro.example.com:3000/api/v1" ``` -`[exec]` config applies to `fabro exec`. `[llm]` sets the default workflow model/provider for commands like `fabro run` and `fabro preflight`. `[server]` stores connection info for commands that can target a remote Fabro server. +`[cli.exec]` config applies to `fabro exec`. `[run.model]` sets the default workflow model/provider for commands like `fabro run` and `fabro preflight`. `[cli.target]` stores connection info for commands that can target a remote Fabro server. -`fabro model` uses `[server].target` by default when no explicit `--storage-dir` is passed. `fabro exec` remains a local session unless you pass `--server`, even if `[server].target` is configured. +`fabro model` uses `[cli.target]` by default when no explicit `--storage-dir` is passed. `fabro exec` remains a local session unless you pass `--server`, even if `[cli.target]` is configured. CLI flags always override `settings.toml` values, which override hardcoded defaults. diff --git a/docs/reference/user-configuration.mdx b/docs/reference/user-configuration.mdx index c15c44157..e12555b02 100644 --- a/docs/reference/user-configuration.mdx +++ b/docs/reference/user-configuration.mdx @@ -17,114 +17,167 @@ The default path is `~/.fabro/settings.toml`. Use `fabro server start --config /path/to/settings.toml` if the server should read a different file. +## Schema version + +Every Fabro config file must declare its schema version with a top-level `_version` key: + +```toml title="settings.toml" +_version = 1 +``` + +Files that omit `_version` are treated as version `1`. The legacy top-level `version` key is no longer accepted and raises a targeted rename hint. + ## Who reads what -`settings.toml` uses the same schema as `fabro.toml` and `workflow.toml`, but each process only reads the fields it understands. +`settings.toml` uses the same schema as `fabro.toml` and `workflow.toml`, but each process only reads the fields it understands. The top-level schema is strictly namespaced — the only allowed domains are `[project]`, `[workflow]`, `[run]`, `[cli]`, `[server]`, and `[features]`. | Scope | Examples | |---|---| -| CLI-only | `verbose`, `upgrade_check`, `[server]`, `[exec]`, `[mcp_servers]` | -| Shared defaults | `[llm]`, `[log]`, `[git]`, `[pull_request]`, plus run-default sections like `[setup]`, `[sandbox]`, `[checkpoint]`, and `[vars]` | -| Server-only | `storage_dir`, `max_concurrent_runs`, `[web]`, `[api]`, `[features]` | +| 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.hooks]`, `[run.agent.mcps]` | +| Server-only | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.storage]`, `[server.artifacts]`, `[server.slatedb]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` | + +`[cli.*]` and `[server.*]` stanzas are owner-specific: they are only consumed from `~/.fabro/settings.toml` (plus process-local flags and env overrides). The same stanzas in `fabro.toml` or `workflow.toml` remain schema-valid but runtime-inert. See [Server Configuration](/administration/server-configuration) for the server-owned sections. ## Precedence -For CLI commands running on the local machine, precedence is: +Shared layered domains (`[project]`, `[workflow]`, `[run]`, `[features]`) use this override order: 1. **CLI flags** — always win -2. **`workflow.toml` / `run.toml`** — per-run overrides -3. **`fabro.toml`** — project defaults -4. **`settings.toml`** — machine defaults -5. **Built-in defaults** +2. **Environment overrides** — Fabro-defined override channels +3. **`workflow.toml`** — per-workflow overrides +4. **`fabro.toml`** — project defaults +5. **`~/.fabro/settings.toml`** — machine defaults +6. **Built-in defaults** -On a same-machine setup, the server reads the same `settings.toml`. On a remote setup, the CLI machine and server machine each use their own local `settings.toml`. +Owner-specific domains (`[cli.*]`, `[server.*]`) use a narrower trust boundary — only CLI flags, env overrides, `~/.fabro/settings.toml`, and built-in defaults apply. ## Full example ```toml title="settings.toml" -verbose = true -upgrade_check = true +_version = 1 -[server] -target = "https://fabro.example.com:3000/api/v1" +[cli.target] +type = "http" +url = "https://fabro.example.com:3000/api/v1" -[server.tls] +[cli.target.tls] cert = "~/.fabro/tls/client.crt" key = "~/.fabro/tls/client.key" ca = "~/.fabro/tls/ca.crt" -[exec] +[cli.exec] +prevent_idle_sleep = true + +[cli.exec.model] provider = "anthropic" -model = "claude-opus-4-6" +name = "claude-opus-4-6" + +[cli.exec.agent] permissions = "read-write" -output_format = "text" -[llm] -model = "claude-sonnet-4-5" +[cli.output] +format = "text" +verbosity = "normal" -[log] +[cli.updates] +check = true + +[cli.logging] level = "info" -[git.author] +[run.model] +name = "claude-sonnet-4-5" + +[run.git.author] name = "fabro-bot" email = "fabro-bot@company.com" -[pull_request] +[run.pull_request] enabled = true -[mcp_servers.filesystem] +[run.agent.mcps.filesystem] type = "stdio" command = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/workspace"] -startup_timeout_secs = 15 -tool_timeout_secs = 90 +startup_timeout = "15s" +tool_timeout = "90s" -[mcp_servers.filesystem.env] +[run.agent.mcps.filesystem.env] NODE_ENV = "production" -[mcp_servers.sentry] +[run.agent.mcps.sentry] type = "http" url = "https://mcp.sentry.dev/mcp" -[mcp_servers.sentry.headers] +[run.agent.mcps.sentry.headers] Authorization = "Bearer sk-xxx" ``` -All fields are optional. Include only the sections and keys you want to override. On a same-machine install, this same file can also include server sections such as `[web]` and `[api]`. +All fields are optional. Include only the sections and keys you want to override. On a same-machine install, this same file can also include server sections such as `[server.web]` and `[server.api]`. -## `upgrade_check` +## `[cli.updates]` Controls whether Fabro runs a daily background check for new releases. The check runs during `run`, `exec`, `init`, and `install` commands and prints a notice to stderr when a newer version is available. -| Value | Description | -|---|---| -| `true` | Check for new releases (default) | -| `false` | Disable automatic upgrade checks | +```toml title="settings.toml" +[cli.updates] +check = true +``` + +| Key | Value | Description | +|---|---|---| +| `check` | `true` | Check for new releases (default) | +| `check` | `false` | Disable automatic upgrade checks | The `--no-upgrade-check` CLI flag overrides this for a single invocation. See [`fabro upgrade`](/reference/cli#fabro-upgrade) for manual upgrades. -## `verbose` +## `[cli.output]` -Enable verbose output by default for `fabro run start` and `fabro doctor`, without passing `-v` every time. +Generic CLI output defaults. -| Value | Description | -|---|---| -| `true` | Verbose output on by default | -| `false` | Normal output (default) | +```toml title="settings.toml" +[cli.output] +format = "text" +verbosity = "verbose" +``` + +| Key | Values | Default | +|---|---|---| +| `format` | `"text"`, `"json"` | `"text"` | +| `verbosity` | `"quiet"`, `"normal"`, `"verbose"` | `"normal"` | The `-v` / `--verbose` CLI flag always takes effect regardless of this setting. -## `[exec]` section +## `[cli.exec]` section Defaults for `fabro exec` sessions. +```toml title="settings.toml" +[cli.exec] +prevent_idle_sleep = true + +[cli.exec.model] +provider = "anthropic" +name = "claude-opus-4-6" + +[cli.exec.agent] +permissions = "read-write" +``` + +`[cli.exec.model]` selects the default LLM for exec: + +| Key | Description | Values | +|---|---|---| +| `provider` | LLM provider | `"anthropic"`, `"openai"`, `"gemini"`, etc. | +| `name` | Model name | Any model ID from `fabro model list` | + +`[cli.exec.agent]` controls agent behavior during exec: + | Key | Description | Values | Default | |---|---|---|---| -| `provider` | LLM provider | `"anthropic"`, `"openai"`, `"gemini"`, etc. | `"anthropic"` | -| `model` | Model name | Any model ID from `fabro model list` | Per provider | | `permissions` | Tool permission level | `"read-only"`, `"read-write"`, `"full"` | `"read-write"` | -| `output_format` | Output format | `"text"`, `"json"` | `"text"` | ### Permission levels @@ -134,62 +187,91 @@ Defaults for `fabro exec` sessions. Tools outside the permission level are interactively prompted (if a TTY is present) or denied (with `--auto-approve`). -### Output formats - -- **`text`** — human-readable terminal output -- **`json`** — NDJSON event stream - -## `[llm]` section +## `[run.model]` section Defaults for workflow model selection in commands like `fabro run` and `fabro preflight`. +```toml title="settings.toml" +[run.model] +provider = "anthropic" +name = "claude-sonnet-4-5" +fallbacks = ["openai", "gpt-5.4", "gemini/gemini-flash"] +``` + | Key | Description | Values | Default | |---|---|---|---| -| `model` | Model name | Any model ID from `fabro model list` | Per provider | +| `name` | Model name | Any model ID from `fabro model list` | Per provider | | `provider` | Provider name | `"anthropic"`, `"openai"`, `"gemini"`, etc. | Auto-inferred from model/catalog | +| `fallbacks` | Ordered list of fallback model references | bare provider, bare alias, or `provider/model` | `[]` | -Use `[exec]` to configure provider, permissions, and output format for `fabro exec`. Use `[llm]` for workflow-oriented defaults. +Use `[cli.exec.model]` to configure provider and model for `fabro exec`. Use `[run.model]` for workflow-oriented defaults. -## `[log]` section +## `[cli.logging]` section -Configure the default log level. Precedence: `FABRO_LOG` env var > `--debug` flag > `[log]` level > `"info"`. +Configure the default CLI log level. Precedence: `FABRO_LOG` env var > `--debug` flag > `[cli.logging].level` > `"info"`. -| Key | Description | Values | Default | -|---|---|---|---| -| `level` | Log level | `"error"`, `"warn"`, `"info"`, `"debug"`, `"trace"` | `"info"` | +```toml title="settings.toml" +[cli.logging] +level = "info" +``` -## `[git]` section +| Key | Values | Default | +|---|---|---| +| `level` | `"error"`, `"warn"`, `"info"`, `"debug"`, `"trace"` | `"info"` | -### `[git.author]` +Server-side logging is a separate namespace at `[server.logging]`. -Customize the git author identity used for checkpoint commits. On same-machine setups, the CLI and server read the same `[git.author]` value. On remote setups, each machine uses its own local `settings.toml`. +## `[run.git.author]` + +Customize the git author identity used for checkpoint commits. + +```toml title="settings.toml" +[run.git.author] +name = "fabro-bot" +email = "fabro-bot@company.com" +``` | Key | Description | Default | |---|---|---| | `name` | Git author name | `"fabro"` | | `email` | Git author email | `"fabro@local"` | -## `[server]` section +## `[cli.target]` section Connection info for commands that target a remote Fabro server. -| Key | Description | Default | -|---|---|---| -| `target` | Server target: `http(s)` URL or absolute Unix socket path | none | +```toml title="settings.toml" +[cli.target] +type = "http" +url = "https://fabro.example.com:3000/api/v1" +``` -`fabro model` uses `[server].target` by default when no explicit `--storage-dir` is passed. An explicit `--server` flag overrides `server.target`: +| Key | Description | +|---|---| +| `type` | `"http"` or `"unix"` — explicit transport selection | +| `url` | Required for `type = "http"` — the API base URL | +| `path` | Required for `type = "unix"` — the absolute Unix socket path | + +`fabro model` uses `[cli.target]` by default when no explicit `--storage-dir` is passed. An explicit `--server` flag overrides the configured target: ```bash fabro model list --server https://fabro.example.com:3000/api/v1 ``` -`fabro exec` does not automatically use `[server].target`. It only routes model traffic through a Fabro server when you pass `--server` for that invocation. +`fabro exec` does not automatically use `[cli.target]`. It only routes model traffic through a Fabro server when you pass `--server` for that invocation. -### `[server.tls]` section +### `[cli.target.tls]` section -Optional mTLS configuration for authenticating with the server. When present, the CLI presents a client certificate during the TLS handshake. +Optional mTLS configuration for authenticating with an HTTP target. When present, the CLI presents a client certificate during the TLS handshake. + +```toml title="settings.toml" +[cli.target.tls] +cert = "~/.fabro/tls/client.crt" +key = "~/.fabro/tls/client.key" +ca = "~/.fabro/tls/ca.crt" +``` | Key | Description | |---|---| @@ -197,46 +279,42 @@ Optional mTLS configuration for authenticating with the server. When present, th | `key` | Path to client private key PEM file | | `ca` | Path to CA certificate PEM file (to verify the server) | -Paths support `~/` expansion. Example: +Paths support `~/` expansion. + +## `[run.pull_request]` + +Enable auto-PR globally so workflows open a GitHub pull request on successful completion. ```toml title="settings.toml" -[server.tls] -cert = "~/.fabro/tls/client.crt" -key = "~/.fabro/tls/client.key" -ca = "~/.fabro/tls/ca.crt" -``` - -## `[pull_request]` - -Enable auto-PR globally so workflows open a GitHub pull request on successful completion, even when running with a `.fabro` file instead of a `run.toml`. - -```toml title="settings.toml" -[pull_request] +[run.pull_request] enabled = true ``` | Key | Description | Default | |---|---|---| | `enabled` | Automatically create a PR after successful runs | `false` | +| `draft` | Open the PR as a draft | `true` | +| `auto_merge` | Enable GitHub auto-merge on the created PR (implies `draft = false`) | `false` | +| `merge_strategy` | One of `"squash"`, `"merge"`, `"rebase"` | `"squash"` | -Precedence: `run.toml` > `fabro.toml` > `settings.toml` > built-in default (`false`). +Precedence: `workflow.toml` > `fabro.toml` > `~/.fabro/settings.toml` > built-in default (`false`). -## `[mcp_servers]` section +## `[run.agent.mcps]` section -Configure [MCP servers](/agents/mcp) to connect to during `fabro exec` sessions. Each server is a named TOML table under `[mcp_servers]`. MCP servers can also be configured per-workflow in [run config TOML](/execution/run-configuration#mcp_servers). +Configure [MCP servers](/agents/mcp) to connect to during agent-driven runs. Each server is a named TOML table under `[run.agent.mcps]`. For `fabro exec`-only MCPs, use `[cli.exec.agent.mcps.*]` with the same shape. ### Stdio transport Spawn a local process and communicate over stdin/stdout: ```toml title="settings.toml" -[mcp_servers.filesystem] +[run.agent.mcps.filesystem] type = "stdio" command = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/workspace"] -startup_timeout_secs = 15 -tool_timeout_secs = 90 +startup_timeout = "15s" +tool_timeout = "90s" -[mcp_servers.filesystem.env] +[run.agent.mcps.filesystem.env] NODE_ENV = "production" ``` @@ -245,19 +323,19 @@ NODE_ENV = "production" | `type` | Must be `"stdio"` | — | | `command` | Array: executable + arguments | — | | `env` | Additional environment variables for the child process | `{}` | -| `startup_timeout_secs` | Max seconds for the MCP handshake | `10` | -| `tool_timeout_secs` | Max seconds for a single tool call | `60` | +| `startup_timeout` | Max duration for the MCP handshake (e.g. `"10s"`, `"30s"`) | `"10s"` | +| `tool_timeout` | Max duration for a single tool call (e.g. `"60s"`, `"2m"`) | `"60s"` | ### HTTP transport Connect to a remote MCP server over Streamable HTTP: ```toml title="settings.toml" -[mcp_servers.sentry] +[run.agent.mcps.sentry] type = "http" url = "https://mcp.sentry.dev/mcp" -[mcp_servers.sentry.headers] +[run.agent.mcps.sentry.headers] Authorization = "Bearer sk-xxx" ``` @@ -266,20 +344,20 @@ Authorization = "Bearer sk-xxx" | `type` | Must be `"http"` | — | | `url` | The MCP server endpoint URL | — | | `headers` | Optional HTTP headers (for example, for authentication) | `{}` | -| `startup_timeout_secs` | Max seconds for the MCP handshake | `10` | -| `tool_timeout_secs` | Max seconds for a single tool call | `60` | +| `startup_timeout` | Max duration for the MCP handshake | `"10s"` | +| `tool_timeout` | Max duration for a single tool call | `"60s"` | ### Sandbox transport -Run an MCP server inside the workflow's sandbox and connect via preview URL. Only available with remote sandbox providers ([Daytona](/integrations/daytona)) that support port previews. Typically configure this in [run config TOML](/execution/run-configuration#mcp_servers) rather than `settings.toml`. +Run an MCP server inside the workflow's sandbox and connect via preview URL. Only available with remote sandbox providers ([Daytona](/integrations/daytona)) that support port previews. Typically configured in `workflow.toml` rather than `settings.toml`: -```toml title="run.toml" -[mcp_servers.playwright] +```toml title="workflow.toml" +[run.agent.mcps.playwright] type = "sandbox" command = ["npx", "@playwright/mcp@latest", "--port", "3100", "--headless"] port = 3100 -startup_timeout_secs = 60 -tool_timeout_secs = 120 +startup_timeout = "60s" +tool_timeout = "2m" ``` | Key | Description | Default | @@ -288,7 +366,7 @@ tool_timeout_secs = 120 | `command` | Array: the command to run inside the sandbox | — | | `port` | Port the server listens on inside the sandbox | — | | `env` | Additional environment variables for the server process | `{}` | -| `startup_timeout_secs` | Max seconds for startup + MCP handshake | `10` | -| `tool_timeout_secs` | Max seconds for a single tool call | `60` | +| `startup_timeout` | Max duration for startup + MCP handshake | `"10s"` | +| `tool_timeout` | Max duration for a single tool call | `"60s"` | See [MCP — Sandbox transport](/agents/mcp#sandbox) for how Fabro launches and connects to sandbox MCP servers. diff --git a/docs/workflows/variables.mdx b/docs/workflows/variables.mdx index 345375ec3..c0a0d72bb 100644 --- a/docs/workflows/variables.mdx +++ b/docs/workflows/variables.mdx @@ -5,22 +5,26 @@ description: "Using variables in workflows" Fabro supports `$variable` placeholders that let you parameterize workflows without editing the Graphviz file. -## Run config variables +## Run config inputs -Define variables in the `[vars]` section of a run config TOML file: +Define inputs in the `[run.inputs]` section of a run config TOML file: ```toml title="run.toml" -version = 1 -goal = "Run tests for $repo_name" +_version = 1 + +[workflow] graph = "check.fabro" -[vars] +[run] +goal = "Run tests for $repo_name" + +[run.inputs] repo_name = "fabro" repo_url = "https://github.com/fabro-sh/fabro" language = "rust" ``` -These variables are expanded into the Graphviz source **before** the graph is parsed. You can use `$variable` anywhere in the Graphviz file — goals, prompts, labels, scripts, or any other attribute: +These inputs are expanded into the Graphviz source **before** the graph is parsed. You can use `$variable` anywhere in the Graphviz file — goals, prompts, labels, scripts, or any other attribute: ```dot title="check.fabro" digraph Check { @@ -40,7 +44,7 @@ When launched with `fabro run run.toml`, Fabro replaces `$repo_name`, `$repo_url ### Undefined variables -If a `$variable` in the Graphviz file has no matching entry in `[vars]`, Fabro raises an error. This catches typos early — a misspelled `$langauge` fails immediately rather than passing a literal `$langauge` to the LLM. +If a `$variable` in the Graphviz file has no matching entry in `[run.inputs]`, Fabro raises an error. This catches typos early — a misspelled `$langauge` fails immediately rather than passing a literal `$langauge` to the LLM. ### Escaping `$` @@ -66,11 +70,15 @@ digraph Example { The plan node's prompt becomes `"Create a plan for: Implement the login feature"`. -## Variable merging +## Input merging -When using server-level run defaults alongside a run config TOML, variables are merged. Task config vars override default vars when keys collide: +`[run.inputs]` intentionally replaces the inherited map wholesale rather than merging by key. Whichever layer has the highest precedence and sets `[run.inputs]` wins its entire map — lower-precedence inputs do not show through. | Source | Priority | |---|---| -| Run config TOML `[vars]` | Highest — wins on collision | -| Server defaults `[vars]` | Lowest — provides fallback values | +| CLI flags (`-V key=value`, repeated) | Highest | +| `workflow.toml` `[run.inputs]` | | +| `fabro.toml` `[run.inputs]` | | +| `~/.fabro/settings.toml` `[run.inputs]` | Lowest | + +If you need per-key overrides on top of inherited defaults, set each input explicitly in the winning layer.