Commit graph

22 commits

Author SHA1 Message Date
Bryan Helmkamp
ef9606e6ec
Reuse PermissionLevel and fix stale run spec snapshots
AgentPermissions duplicated fabro_types::PermissionLevel: same variants,
same kebab-case wire form, same crate. PermissionLevel is strictly richer
(Hash, strum, clap::ValueEnum) and is already the with_replacement target
for the OpenAPI PermissionLevel schema, whose values are identical to the
AgentPermissions schema this branch deletes.

Delete AgentPermissions and type the [cli.exec.agent] permissions setting
as PermissionLevel. This drops the adapter match in `fabro exec` and the
`as AgentPermissionLevel` alias that existed only to tell the two names
apart. The TOML wire form is unchanged.

Removing run.agent.permissions also changed the serialized run spec, but
two fabro-cli inline snapshots still carried "permissions": null. They
failed on this branch and passed on main. Accept the updated snapshots.

Also tighten the removed-setting test to assert the exact unknown-field
message, rename its module to run_agent now that it covers more than
fabro_tools, and drop three doc references to the removed setting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 10:09:44 -04:00
Bryan Helmkamp
6226c8c517
fix: address env interpolation review findings
Restore the documented SDK env credential facade without reintroducing run fallback behavior. Fail closed on GitHub permission resolution, require worker storage at the CLI boundary, and align interpolation names and generated docs.
2026-07-28 17:30:50 -04:00
Bryan Helmkamp
f0a7423b51
refactor(config): stop resolving {{ env.* }} in interpolated config
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>
2026-07-27 21:09:35 -04:00
Bryan Helmkamp
5980627bc8
refactor(glob): unify workspace path matching 2026-07-25 10:30:19 -04:00
Bryan Helmkamp
1ca9fe977d
fix(sandbox): simplify Bash contract implementation 2026-07-24 21:55:36 -04:00
Bryan Helmkamp
3606ba6a0f
feat(sandbox): standardize command execution on non-login Bash
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>
2026-07-24 21:26:05 -04:00
Bryan Helmkamp
d0d1dac3ac
docs: sync product documentation 2026-07-24 11:39:16 -04:00
Bryan Helmkamp
ec3933d5de Move MCP servers to SQLite storage 2026-07-22 13:10:01 -04:00
Bryan Helmkamp
ec1b3f2084
feat(sandbox): secure daytona snapshot names (#429)
## 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`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
2026-05-27 11:52:35 -04:00
Bryan Helmkamp
2c851d5a41
Merge remote-tracking branch 'origin/main' 2026-05-25 15:50:43 -04:00
fabro-sh-0530[bot]
b15d8b8476
feat: Add approve/deny run controls to MCP and CLI (#400)
## Summary

Exposes the existing `POST /api/v1/runs/{id}/approve` and `POST
/api/v1/runs/{id}/deny` REST endpoints through the `fabro_run_interact`
MCP tool and two new top-level CLI commands (`fabro approve`, `fabro
deny`). Workflow agents are explicitly blocked from using these actions
— approval remains a human/user operation.

## What changed

**Client & tool backend** (`fabro-client`, `fabro-tool`): Added
`approve_run` and `deny_run` to `Client` and the `FabroToolBackend`
trait, implemented in `ClientBackend`. `deny_run` passes a
`DenyRunRequest` body; absent, blank, or whitespace-only reasons are
normalised to `None`.

**`fabro_run_interact` MCP tool**: Added `Approve` and `Deny` variants
to `RunInteractAction` / `ValidatedInteractAction`, and an optional
`reason` parameter (only valid for `deny`; validated and trimmed on
input). Both actions return `{ "summary": … }` using the existing shape.
The tool description is updated to list the new actions.

**Workflow-agent guard** (`fabro-workflow`): Before dispatching
`fabro_run_interact`, the handler checks
`validated.action.requires_user()`. If the action is `approve` or
`deny`, it returns an immediate `ToolError` without ever reaching the
backend, keeping the guard explicit and independent of server auth.

**CLI** (`fabro-cli`): Extracted the archive/unarchive batch loop into a
shared `run_resolved_run_batch` helper in `commands/runs/mod.rs`, then
implemented `approval.rs` using the same helper. Both commands follow
the same batch contract as archive: attempt all runs, collect per-run
errors, exit non-zero if any fail, and emit `{ "approved"/"denied": […],
"errors": […] }` in JSON mode.

**Server auth regression** (`fabro-server`): Extended
`run_tools_worker_cannot_call_user_only_non_mcp_routes` to cover `POST
/runs/{id}/deny` alongside the existing `approve` and `timeline` checks.

**Docs** (`mcp.mdx`, `cli.mdx`): Updated the `fabro_run_interact` table
entry and added approve/deny examples, plus reference sections for the
two new CLI commands.

### Plan Summary

- Add `approve_run` / `deny_run` to `Client` and `FabroToolBackend`
- Extend `fabro_run_interact` with `approve`, `deny`, and optional
`reason`
- Block workflow-agent self-approval with an early `ToolError`
- Refactor archive batch loop into shared `run_resolved_run_batch`
helper
- Add `fabro approve` and `fabro deny` CLI commands reusing that helper
- Add integration tests for CLI commands, MCP tool, and server auth
guard


### Fabro Details

<details>
<summary>Ran 9 stages in 63m 57s for $42.33</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 17s | – | 0 |
| implement | 28m 16s | $32.57 | 0 |
| simplify_opus | 10m 59s | $4.24 | 0 |
| simplify_gpt | 6m 13s | $3.96 | 0 |
| verify | 10m 51s | – | 0 |
| fixup | 2m 29s | $1.55 | 0 |
| **Total** | **63m 57s** | **$42.33** | **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-25 15:49:57 -04:00
Bryan Helmkamp
879969cf54
docs: add child runs guide
Document child-run orchestration as a first-class execution concept and link the related MCP, UI, and API surfaces back to it.
2026-05-25 15:43:53 -04:00
fabro-sh-0530[bot]
883a11ce4d
feat: MCP tool parity for fabro_tools workflow agents (#387)
## Summary

Workflow agents that opt in with `[run.agent] fabro_tools = true` now
see the full seven-tool Fabro run-management catalog — including
`fabro_run_pair` — matching what human MCP clients receive. Auth
extractors have been renamed from the ad-hoc "run tools" vocabulary to
product-level names, and all pair routes now accept run-management
actors instead of requiring a user principal.

### Plan Summary

- **Shared catalog parity** — `FABRO_RUN_PAIR_TOOL_NAME` added to
`TOOL_DEFINITIONS` in `fabro-tool`, making `register_fabro_run_tools()`
register all seven tools.
- **Workflow agent executor** — new `FABRO_RUN_PAIR_TOOL_NAME` match arm
in `execute_fabro_run_tool` parses `FabroRunPairParams`, calls
`fabro_tool::pair_run`, and renders the standard summary.
- **Auth extractor rename** — `RequiredRunToolActor` →
`RequiredRunManagementActor`; `RequireRunScopedOrRunTools` →
`RequireRunManagementTarget`. Semantics are unchanged; names now
describe the product policy.
- **Pair route migration** — all six pair handlers (`get_pair_status`,
`start_pair`, `get_pair`, `end_pair`, `send_pair_message`,
`get_transcript`) switch from `RequiredUser` to
`RequireRunManagementTarget`, removing the `Principal::User(auth.0)`
construction and the now-redundant `parse_run_id_path` calls.
- **Test coverage** — unit tests for the renamed extractors, integration
tests proving run-tools workers can read pair status/transcript
cross-run, that auth is accepted before domain logic
(worker-control-unavailable), that cross-run base workers remain
forbidden, and that run-tools workers still cannot call user-only routes
(approve, timeline).

### Key design decisions

**Forced-child behavior is preserved.** `fabro_run_create` from a
workflow agent still calls `ensure_current_run_parent`; the plan
specifically excludes relaxing this.

**Principal provenance is unchanged.** Workers keep `Principal::Worker {
run_id: … }` when acting through `fabro_tools`; no user principal is
forged.

**Pair handler run-id extraction simplified.** Because
`RequireRunManagementTarget` already extracts and validates the run ID
from the path, the pair handlers no longer repeat that parse — the
second `Path` component for pair-specific routes is bound to `_id` and
discarded.

**Twin-OpenAI gains `instructions_text` logging.** The integration test
for project-skill discovery needed to inspect the system prompt sent to
OpenAI; the twin now captures and exposes `instructions_text` in request
logs. This is a supporting change, not part of the auth model.


### Fabro Details

<details>
<summary>Ran 8 stages in 44m 10s for $39.32</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 17s | – | 0 |
| implement | 23m 9s | $33.23 | 0 |
| simplify_opus | 4m 6s | $1.06 | 0 |
| simplify_gpt | 4m 1s | $5.03 | 0 |
| verify | 7m 56s | – | 0 |
| **Total** | **44m 10s** | **$39.32** | **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>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-24 15:52:19 -04:00
Fabian Kukuck
990324710c
Add legacy SSE MCP transport support (#386)
## Summary

Adds support for MCP servers that use the SSE-based HTTP transport,
including Playwright MCP.

Fabro already supports stdio and Streamable HTTP MCP servers. Some MCP
servers still expose the SSE transport shape where the client opens an
SSE stream, receives an `endpoint` event, and sends JSON-RPC requests
back to that endpoint. This PR adds an explicit `protocol = "sse"`
option while keeping Streamable HTTP as the default.

## What Changed

- Added `McpHttpProtocol` with `streamable_http` as the default and
`sse` as an opt-in protocol.
- Added an SSE MCP client transport implementation.
- Wired HTTP MCP setup to choose Streamable HTTP or SSE based on config.
- Added `protocol = "sse"` support for both `http` and `sandbox` MCP
entries.
- Updated sandbox MCP resolution so SSE sandbox servers connect through
the preview `/sse` path.
- Documented `protocol = "sse"` for Playwright MCP.
- Added an integration test covering SSE initialize, tool listing, and
tool calls.

## Example

```toml
[run.agent.mcps.playwright]
type = "sandbox"
protocol = "sse"
command = ["npx", "@playwright/mcp@latest", "--port", "3100", "--headless", "--browser", "chromium"]
port = 3100
startup_timeout = "60s"
tool_timeout = "2m"
```

## Compatibility

Existing MCP configs are unchanged because `protocol` defaults to
`streamable_http`.

## Validation

- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo nextest run -p fabro-mcp`
- `cargo check -p fabro-agent -p fabro-workflow -p fabro-config`

---------

Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:29:01 -04:00
fabro-sh-0530[bot]
7f84ac5e3f
Replace run-scoped sandbox config with named environments (#360)
## Summary

Replaces the `[run.sandbox]` configuration surface with a named,
provider-explicit environment catalog. Runs now select an environment by
slug (`[run.environment] id = "..."`) rather than configuring a sandbox
inline. Fabro resolves the catalog through normal settings precedence,
applies sparse run-level overrides, and creates a concrete sandbox from
the resolved environment.

This is a clean break — no `[run.sandbox]` compatibility layer.

### Plan Summary

- **New config shape:** Top-level `[environments.<slug>]` catalog valid
in `settings.toml`, `.fabro/project.toml`, and `workflow.toml`. Runs
reference a slug via `[run.environment] id = "..."` with optional sparse
overrides under `[run.environment.*]`.
- **Unified environment fields:** `provider`, `image` (ref +
dockerfile), `resources` (cpu/memory/disk), `network` (mode + allow
CIDRs), `lifecycle` (preserve/stop_on_terminal/auto_stop), `labels`,
`volumes`, `env` — replacing the previous split between `[run.sandbox]`,
`[run.sandbox.docker]`, `[run.sandbox.daytona]`, and
`[run.sandbox.daytona.snapshot]`.
- **OpenAPI schema update:** `RunSandboxSettings`, `DockerSettings`,
`DaytonaSettings`, and `DaytonaNetworkLayer` replaced with
`RunEnvironmentSettings`, `EnvironmentSettings`, `EnvironmentProvider`,
`EnvironmentImageSettings`, `EnvironmentResourcesSettings`,
`EnvironmentNetworkSettings`, `EnvironmentLifecycleSettings`, and
`EnvironmentVolumeSettings`.
- **CLI flag rename:** `--sandbox <provider>` → `--environment <slug>`
on `run`, `create`, `preflight`, and `server start/restart`.
- **Provider capability model:** Hard errors for security properties a
provider cannot enforce (local with blocked/CIDR networking; docker with
CIDR allow-lists). Warnings for unsupported resource limits, volumes,
labels, auto-stop, and Docker Dockerfiles.
- **Docs and internal code updated** throughout: `.fabro/project.toml`,
workflow configs, all public docs, CLI args, manifest builders, and the
runner's GitHub credentials check.

### Provider mapping

| Environment field | Local | Docker | Daytona |
|---|---|---|---|
| `image.ref` | Ignored | Docker image | Snapshot name |
| `image.dockerfile` | Ignored | Warning; ignored | Snapshot Dockerfile
(requires `image.ref`) |
| `resources.cpu/memory/disk` | Warning; ignored | cpu_quota / memory
limit / warning | Snapshot sizing |
| `network.mode = block` | **Error** | `network_mode = none` | Daytona
block |
| `network.mode = cidr_allow_list` | **Error** | **Error** | Daytona
CIDR allow-list |
| `labels` | Warning; ignored | Warning; ignored | Daytona labels |
| `volumes` | Warning; ignored | Warning; ignored | Daytona volume
mounts |
| `lifecycle.auto_stop` | Warning; ignored | Warning; ignored | Daytona
auto-stop interval |
| `env` | Process env overlay | Container env | Sandbox env |


### Fabro Details

<details>
<summary>Ran 11 stages in 217m 39s for $129.86</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 4m 7s | – | 0 |
| preflight_lint | 4m 9s | – | 0 |
| fix_lints | 3m 46s | $1.06 | 0 |
| implement | 76m 6s | $57.39 | 0 |
| simplify_opus | 71m 50s | $38.17 | 0 |
| simplify_gpt | 8m 27s | $2.24 | 0 |
| verify | 6m 10s | – | 0 |
| fixup | 42m 1s | $31.00 | 0 |
| fmt | 3s | – | 0 |
| **Total** | **217m 39s** | **$129.86** | **0** |

</details>

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

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]

    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 -> fmt   [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Bryan Helmkamp <bhelmkamp@users.noreply.github.com>
2026-05-23 13:03:21 -04:00
fabro-sh-0530[bot]
0754f1ca4a
Add fabro_run_get read-only run inspection tool (#358)
## Summary

Adds a new `fabro_run_get` MCP tool that returns a run's summary,
resolved ID, projection, and pending questions without any mutation
capability. This separates read-only inspection from operational
control, allowing Ask Fabro sessions to inspect runs safely without
access to write operations.

## What changed and why

**New tool (`fabro-tool/src/get.rs`):** `FabroRunGetParams` /
`ValidatedRunGet` / `RunGetResult` follow the same
validation-and-dispatch pattern as other run tools. The implementation
resolves a selector, then fans out to three read-only API calls
(retrieve run, get state, list questions) and assembles them into a
single structured result.

**Tool registry and dispatch:** `FABRO_RUN_GET_TOOL_NAME` is exported
from `common.rs` and `lib.rs`, added to `TOOL_DEFINITIONS`, wired into
the MCP stdio server (`fabro-mcp-server/src/server.rs`), and dispatched
in the LLM agent executor (`fabro-workflow/src/handler/llm/api.rs`).

**Ask Fabro access policy (`fabro-server/.../sessions.rs`):** The
session now registers and allows only `fabro_run_events` +
`fabro_run_get` via the new `ASK_FABRO_RUN_TOOL_NAMES` constant.
`fabro_run_interact` is explicitly moved to the denied set, closing off
mutation from that session type. The policy match arm is refactored from
a hardcoded `|`-chain to a slice `contains` check so the constant is the
single source of truth.

**Docs:** `mcp.mdx` now lists `fabro_run_get` as the inspection tool and
redescribes `fabro_run_interact` as control-oriented.

**Backward compatibility:** `fabro_run_interact` (including `get` and
`get_questions` actions) is unchanged and still fully operational for
contexts that allow it.

### Plan Summary

- New `get.rs` module in `fabro-tool` with validation, async fetch, and
unit tests
- Constants + schema registration in `common.rs` / `lib.rs`
- MCP server and LLM dispatch branches added for
`FABRO_RUN_GET_TOOL_NAME`
- Ask Fabro session swaps `FABRO_RUN_INTERACT_TOOL_NAME` →
`FABRO_RUN_GET_TOOL_NAME` in registry and policy
- MCP integration tests: tool count constant, schema assertions, two new
end-to-end tests
(`mcp_get_resolves_selector_and_returns_summary_projection_and_questions`,
`mcp_get_rejects_blank_run_id_before_auth_or_network`)
- Public MCP docs updated


### Fabro Details

<details>
<summary>Ran 9 stages in 43m 30s for $13.42</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 2s | – | 0 |
| preflight_lint | 2m 14s | – | 0 |
| implement | 19m 43s | $8.85 | 0 |
| simplify_opus | 5m 52s | $1.45 | 0 |
| simplify_gpt | 10m 16s | $3.12 | 0 |
| verify | 2m 53s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **43m 30s** | **$13.42** | **0** |

</details>

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

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-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."]
    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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]

    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 -> fmt   [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-05-22 19:03:56 -04:00
Bryan Helmkamp
203ed5d029
docs: document agent pair and todo tools 2026-05-22 15:15:30 -04:00
Bryan Helmkamp
511371bd9a
feat(mcp): support goal files in run create
Allow fabro_run_create object specs to pass goal_file, reject goal and goal_file together, and preserve file-sourced goal semantics when building run manifests.
2026-05-22 14:27:42 -04:00
Bryan Helmkamp
c6356cbd77
feat: improve run board, thread, and MCP create flows (#347)
## Summary

This branch improves several run-management surfaces that agents and
users rely on: archived runs now stay visible and ordered correctly in
the board view, pair-session messages appear in the stage Thread tab,
and the `fabro_run_create` MCP tool accepts the workflow-string
shorthand it advertises.

## Changes

- Updates the web board cache invalidation and archived-column handling
so archive/unarchive actions refresh both active and archived board
queries and keep archived runs in a predictable column position.
- Adds pair user/system message events to stage activity parsing, Thread
rendering, search, details, and DNA timeline items.
- Aligns `fabro_run_create` MCP runtime deserialization and `tools/list`
schema so each run entry may be either a workflow string or a full
create spec object.

## Test Plan

- `cargo nextest run -p fabro-tool -p fabro-mcp-server`
- `cargo nextest run -p fabro-cli
stdio_server_initializes_and_lists_run_tools
mcp_create_string_shorthand_deserializes_before_auth
mcp_create_validation_errors_happen_before_auth_or_network
mcp_create_and_search_manage_real_runs_with_cli_auth`
- `cargo +nightly-2026-04-14 fmt --check --all`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:39:20 -04:00
Bryan Helmkamp
73ebb7d28b
feat(mcp): support run parent relationships (#295)
## Summary

- Add parent metadata (`parent_id`, `children_count`) to Fabro MCP run
summaries, search summaries, and created-run results.
- Allow MCP clients to create child runs, search direct children, and
link or unlink an existing run's parent through the existing run tools.
- Update MCP docs and tool descriptions for the parent-aware
create/search/interact behavior.

## Test Plan

- [x] `cargo +nightly-2026-04-14 fmt --check --all`
- [x] `cargo nextest run -p fabro-mcp-server`
- [x] `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:26:18 -04:00
Bryan Helmkamp
5e839ef4ea
docs(mcp): document Fabro MCP server 2026-05-12 08:29:02 -04:00
Bryan Helmkamp
283eab181f
refactor(docs): split docs/ into public/ and internal/
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/agents/mcp.mdx (Browse further)