fabro/docs/public/workflows/variables.mdx
fabro-sh-0530[bot] 7b7a2c9044
Add fabro variable CLI namespace for server-managed variables (#434)
## Summary

Exposes the existing variables API through a new `fabro variable` CLI
namespace (`list`, `get`, `set`, `rm`), following the same patterns as
`fabro secret`. Variables are intentionally readable — `list` and `get`
show stored values — while `fabro secret` remains write-only. This PR
also ships a significant set of accompanying changes: a refactored
sandbox lifecycle model in the web UI, removal of the
`fabro-devcontainer` crate, and a new `RunSandbox` OpenAPI schema that
models the full planned → initializing → ready/failed lifecycle.

## What Changed

### CLI (`fabro variable`)
- New `fabro variable` namespace with `list` (aliased `ls`), `get`,
`set`, and `rm` subcommands, dispatched through the same
`ServerTargetArgs` pattern as `fabro secret`.
- `fabro-client` gains five new wrapper methods (`list_variables`,
`get_variable`, `create_variable`, `update_variable`, `delete_variable`)
over the generated OpenAPI client.
- `set` is an upsert; `--value-stdin` accepts empty input after
newline-trimming (unlike the secrets equivalent).
- CLI reference docs (`docs/public/reference/cli.mdx`) regenerated;
`docs/public/workflows/variables.mdx` gains a short section explaining
`{{ vars.NAME }}` interpolation and the variables-vs-secrets security
boundary.

### Sandbox lifecycle model (web)
- New `RunSandbox` OpenAPI shape splits the old flat object into `kind`
(planned/initializing/ready/failed) + `plan` + optional `instance` +
optional `failure`.
- `apps/fabro-web/app/lib/run-sandbox-lifecycle.ts` centralises
lifecycle helpers (`sandboxLifecycleKind`, `sandboxInstance`,
`sandboxRuntime`, `sandboxIsReady`, `sandboxTabVisible`,
`SANDBOX_LIFECYCLE_DISPLAY`).
- Run summary panel and sandbox route now show lifecycle state
(Initializing / Failed with causes / Not created) before or instead of
the fully-loaded `SandboxDetails`.
- The sandbox details query is skipped entirely until `sandboxIsReady`
returns true, preventing unnecessary 404 fetches for planned/failed
sandboxes.
- `runHasSandbox` in `tabs-shell.tsx` delegates to `sandboxTabVisible`,
hiding the Sandbox tab for `planned` state and showing it for
`initializing`/`ready`/`failed`.
- Legacy flat sandbox shape (no `kind`) is handled via
backwards-compatible shims in the new helpers.

### `fabro-devcontainer` removal
- The `fabro-devcontainer` crate has been removed from `Cargo.lock` and
all dependent crates.
- References to devcontainer in internal plans, docs, changelog entries,
and event schemas have been cleaned up or reworded to reflect that the
feature is no longer present.

### Plan summary
- **Unit 1:** `fabro-client` variable wrappers
- **Unit 2:** CLI args, dispatch, and `commands/variable/mod.rs`
- **Unit 3:** `list`, `get`, `set`, `rm` behavior modules
- **Unit 4:** Test harness helpers and integration tests
- **Unit 5:** Regenerated CLI docs + `variables.mdx` update


### Fabro Details

<details>
<summary>Ran 8 stages in 58m 50s for $23.81</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 9s | – | 0 |
| preflight_lint | 2m 21s | – | 0 |
| implement | 31m 34s | $18.44 | 0 |
| simplify_opus | 8m 48s | $2.60 | 0 |
| simplify_gpt | 4m 1s | $2.77 | 0 |
| verify | 9m 17s | – | 0 |
| **Total** | **58m 50s** | **$23.81** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

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

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

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-05-27 13:57:25 -04:00

138 lines
5.1 KiB
Text

---
title: "Variables"
description: "Using templates in workflows"
---
Fabro uses `{{ ... }}` templates for workflow strings and prompts.
## Template context
Workflow and prompt templates can reference:
| Expression | Resolves to |
|---|---|
| `{{ goal }}` | The workflow goal |
| `{{ inputs.name }}` | A value from `[run.inputs]`, optionally overridden by CLI input flags |
Environment variables are **not** available in workflow or prompt templates. Use `{{ env.NAME }}` only in config strings and HTTP hook headers.
## Run config inputs
Define typed inputs in `[run.inputs]`:
```toml title="run.toml"
_version = 1
[workflow]
graph = "check.fabro"
[run]
goal = "Run repository checks"
[run.inputs]
repo_name = "fabro"
repo_url = "https://github.com/fabro-sh/fabro"
language = "rust"
```
These values are available in final string attributes such as `goal`, `prompt`, `script`, `label`, and edge labels:
```dot title="check.fabro"
digraph Check {
graph [goal="Run tests for {{ inputs.repo_name }}"]
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
clone [label="Clone", shape=parallelogram, script="git clone {{ inputs.repo_url }} repo"]
test [label="Test", prompt="Run the {{ inputs.language }} test suite in the repo/ directory."]
start -> clone -> test -> exit
}
```
Override individual inputs at run time with repeatable `-I` / `--input` flags:
```bash
fabro run .fabro/workflows/check/workflow.toml -I repo_name=fabro-2 --input language=rust
```
CLI input values use TOML scalar parsing when possible. Quoted strings, booleans, integers, and floats keep their typed values; unquoted bare text falls back to a string. Empty values such as `foo=` are accepted as empty strings. Arrays, inline tables, and datetimes are rejected.
## Server-managed run config variables
Use server-managed variables for non-sensitive values that should be shared across runs, such as deployment environments, default branches, regions, or image tags:
```bash
fabro variable set DEPLOY_ENV staging --description "Deployment target"
```
Run configuration strings can reference these values with `{{ vars.NAME }}`:
```toml title="workflow.toml"
_version = 1
[run]
goal = "Deploy {{ vars.DEPLOY_ENV }}"
```
Variables are intentionally readable: `fabro variable list` and `fabro variable get DEPLOY_ENV` show stored values. Do not store tokens, API keys, or credentials as variables; use `fabro secret set` for sensitive values.
## `goal`
Agent and prompt nodes also receive the workflow goal at runtime:
```dot title="example.fabro"
digraph Example {
graph [goal="Implement the login feature"]
plan [label="Plan", prompt="Create a plan for: {{ goal }}"]
}
```
That prompt becomes `Create a plan for: Implement the login feature`.
## Expansion timing
Fabro keeps workflow structure static and renders workflow templates once:
1. Fabro parses the root DOT and imported `.fabro` files without rendering them.
2. Literal `import`, `@file`, graph-goal file, and child-workflow references are resolved.
3. Final graph, node, and edge string attributes are rendered with the `{ goal, inputs }` context.
Templates are not supported in graph syntax, node IDs, edge structure, `import` paths, `@file` paths, child workflow paths, or other file references.
Fabro renders the graph `goal` first and stores the rendered value back onto the graph. Other attributes that use `{{ goal }}` receive that rendered value.
## Undefined variables
Fabro renders undefined workflow variables as empty text and records a `template_undefined_variable` diagnostic. `fabro validate` reports that diagnostic as a warning so you can validate workflow structure before all inputs are known. Run-style commands such as `fabro run`, `fabro create`, and preflight promote the same diagnostic to an error before proceeding.
## Template includes
Prompt and goal templates support static MiniJinja loader dependencies such as `{% include "partial.md" %}`. Includes are resolved relative to the template file being rendered and can be nested.
Fabro discovers those static dependencies while building the run manifest so sandbox providers receive every required prompt file. Dynamic loader expressions such as `{% include inputs.partial %}` are rejected; use a literal include path and choose content with normal template conditionals instead.
## Escaping
To emit literal template syntax, use MiniJinja escaping:
```dot
test [prompt="{% raw %}{{ goal }}{% endraw %}"]
```
You can also emit literal braces with expressions such as `{{ '{{' }}` when needed.
## Input merging
TOML `[run.inputs]` tables intentionally replace the inherited map wholesale rather than merging by key. Whichever TOML layer has the highest precedence and sets `[run.inputs]` wins its entire map.
CLI input flags are different: they are sparse per-key overrides applied after config resolution, so unrelated inherited inputs remain available. If a key is repeated on the CLI, the last value wins.
| Source | Priority |
|---|---|
| CLI flags (`-I key=value` / `--input key=value`, repeated; per-key merge) | Highest |
| `workflow.toml` `[run.inputs]` | |
| `.fabro/project.toml` `[run.inputs]` | |
| `~/.fabro/settings.toml` `[run.inputs]` | Lowest |