fabro/lib/crates/fabro-cli
Scott Werner 5b3b3b0a6b
Resolve {{ env.* }} in MCP transport config at the run and exec boundaries (#511)
First **enhancing** PR of the interpolation unification: now that the
reducing PRs have pinned interpolation to the workflow config language,
this adds real `{{ env.* }}` resolution for MCP server transports — at
**both** the `fabro run` and `fabro exec` boundaries.

Independent off `main` — **no dependency on #510** (zero file overlap;
#510 touches the control-plane server settings). Builds on the
already-merged InterpString foundation (#472).

## What users get

MCP server transport fields now interpolate `{{ env.* }}` tokens,
resolved **at the boundary where the server is actually launched**:

- **stdio / sandbox**: `command`, `args`, and per-server `env` values
- **http**: `url` and `headers`

A literal value passes through unchanged; a `{{ env.NAME }}` token is
resolved against the launching process's environment. **Missing env var
is a hard error** (D3) instead of the previous behavior where the raw
token leaked downstream as literal text. Reserved `secrets`/`inputs`
tokens (no resolver here yet) surface as a loud `Unavailable` error
rather than passing through.

Resolution happens at the run/exec boundary, not in the shared config
resolve layer, so `fabro validate` stays portable (env presence is a
runtime concern, not a validation one).

## Both consumers, one resolver

`fabro run` and `fabro exec` read the **same** MCP representation —
`run.agent.mcps` and `cli.exec.agent.mcps` both parse through
`McpEntryLayer` (InterpString) and collapse via the same
`resolve_mcp_entry`. Originally only the run boundary resolved env, so a
file-sourced `[cli.exec.agent.mcps.*.env] KEY = "{{ env.X }}"` (from
`~/.fabro/settings.toml`) resolved under `run` but **leaked the raw
token under `exec`** — a silent asymmetry that would generate confusing
bug reports.

This PR closes that by moving the resolution onto the type as
`McpServerSettings::resolve_transport_env` (in `fabro-types`, next to
the `vars` half `substitute_mcp_transport`), so both consumers share one
resolver with no drift:

- `runtime_mcp_server` (run worker) → resolves against the worker
process env
- `fabro exec` → resolves against the CLI process env

`runtime_mcp_server` becomes a thin wrapper that just adds the server
name to the error.

## Tests

- `fabro-types`: 5 `resolve_transport_env` unit tests — literal
passthrough, stdio command+env, http url+headers, sandbox env,
missing-env hard error, and the reserved-`secrets` loud-fail case.
- `fabro-workflow`: the 5 existing `runtime_mcp_server_*` tests are
unchanged and now exercise the shared resolver through the wrapper.

Files: `fabro-config/src/resolve/run.rs`,
`fabro-types/src/settings/run.rs`,
`fabro-workflow/src/operations/start.rs`,
`fabro-cli/src/commands/exec.rs`.

Verified: `cargo build`, nightly `clippy --all-targets -D warnings`,
nightly `fmt --check`, and `cargo nextest run -p fabro-types -p
fabro-workflow -p fabro-config -p fabro-cli` (2679 passed with ambient
provider keys stripped; the one failure otherwise is the pre-existing
ambient-`*_API_KEY` flake, unrelated to MCP).

> Note: the shared resolver takes `Resolved.value` and drops interp
`Provenance` (consistent with every other resolved path today —
`Provenance` currently has zero consumers, and resolved MCP transport
values never surface in logs/events/API). Whether MCP env should carry
provenance for precise redaction vs. relying on content-based
`fabro-redact` is tracked as an open decision under D4.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:14:04 -04:00
..
src Resolve {{ env.* }} in MCP transport config at the run and exec boundaries (#511) 2026-06-18 19:14:04 -04:00
tests Demote non-interpolating config fields to plain String (#492) 2026-06-16 13:16:31 -04:00
build.rs fix(build): refresh embedded git sha on branch commits 2026-05-09 14:10:04 -04:00
Cargo.toml feat(web): add server-managed Environments CRUD settings UI (#462) 2026-06-13 08:44:38 -04:00