fabro/lib/crates/fabro-cli/tests/it/cmd
Scott Werner 0244736b05
Resolve run.prepare.steps env and interpolation at the run boundary (#530)
## What

Per-step environment in `run.prepare.steps[].env` was parsed and then
**dropped** before it reached the resolved run settings, so prepare
steps could never see their declared env. This PR carries that env all
the way through to the executor, resolves prepare-step interpolation at
the run boundary, and fixes an argv-quoting bug.

Three things:

1. **Per-step env is carried through.** `RunPrepareSettings` now holds
`steps: Vec<PreparedStep>` (command plus per-step `env`) instead of a
flat `commands: Vec<String>`. The per-step env reaches `exec_command`,
which already accepts per-command env vars, and is merged on top of the
base sandbox environment.
2. **Interpolation resolves at the run boundary.** Prepare-step
`script`/`command` and per-step `env` values are carried in source form
out of the portable config resolve layer (so `fabro validate` stays
portable and never requires env to be set). Their `{{ env.* }}` tokens
resolve in the process that actually runs the steps, via
`RunPrepareSettings::resolve_step_env` — mirroring the existing MCP
transport env resolution. A missing env var is a **hard error**
(fail-closed); there is no fallback to the unresolved literal.
3. **Argv is shell-quoted.** Argv-style prepare steps were assembled
with `join(" ")`, so an argument containing spaces or quotes was
re-split by the shell. They are now shell-quoted per element with the
shared `shell_quote()` helper. `script` steps stay verbatim because they
are raw shell snippets.

## How

- `RunPrepareSettings.commands: Vec<String>` becomes
`RunPrepareSettings.steps: Vec<PreparedStep>` where `PreparedStep {
command, env }`. The server-side `{{ vars.* }}` substitution pass now
walks each step's command and env.
- New `RunPrepareSettings::resolve_step_env(env_lookup)` resolves `{{
env.* }}` in each step's command and env values, returning a hard error
on a missing var (and a loud `Unavailable` error for reserved
`secrets`/`inputs` tokens).
- The run boundary (`fabro_workflow::operations::start`) gains
`runtime_setup_commands`, the prepare-step counterpart to
`runtime_mcp_server`. `LifecycleOptions` now carries `Vec<SetupCommand>`
(command + env), and the initialize phase passes each step's env to
`exec_command`.
- `resolve_prepare` shell-quotes each argv element and carries per-step
env in source form. The stale lint suppression on the resolved fields is
rewritten to describe the deliberate source preservation that now
resolves at the run boundary.
- The shell-quoting helper moves to a shared `fabro_util::shell` module
(backed by `shlex`); `fabro_sandbox::shell_quote` delegates to it so the
config resolve layer and sandbox code share one audited implementation.
- The OpenAPI `RunPrepareSettings` schema and the generated TypeScript
client are updated to the new `steps`/`PreparedStep` shape.

## Testing

- `cargo build --workspace`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo nextest run` for `fabro-util`, `fabro-types`, `fabro-config`,
`fabro-sandbox`, `fabro-api`, `fabro-workflow`, `fabro-server`,
`fabro-cli` (provider keys stripped) — all green.
- `cd lib/packages/fabro-api-client && bun run typecheck` — clean.

New tests cover: per-step env carried through resolution; script/command
+ env resolved at the run boundary; a missing env var is a hard error
(in both the command and a per-step env value); reserved `secrets`
tokens surface as `Unavailable`; argv elements are shell-quoted (an arg
with spaces/quotes is correctly quoted) while a `script` stays verbatim;
and an end-to-end check that per-step env reaches the executed setup
command (with a negative control proving the success is attributable to
the per-step env).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:31:26 -04:00
..
approve.rs feat: Add approve/deny run controls to MCP and CLI (#400) 2026-05-25 15:49:57 -04:00
archive.rs feat: Add approve/deny run controls to MCP and CLI (#400) 2026-05-25 15:49:57 -04:00
artifact_cp.rs test(cli): trim slow integration fixture setup 2026-04-28 19:14:56 -07:00
artifact_list.rs test(cli): trim slow integration fixture setup 2026-04-28 19:14:56 -07:00
attach.rs Resolve run.prepare.steps env and interpolation at the run boundary (#530) 2026-07-01 10:31:26 -04:00
auth.rs test(cli): tighten env-dev-token-ignore tests 2026-05-01 16:25:13 -04:00
cli_reference.rs refactor(dev): decouple CLI reference generation 2026-05-06 12:31:02 -04:00
config.rs Resolve run.prepare.steps env and interpolation at the run boundary (#530) 2026-07-01 10:31:26 -04:00
create.rs Demote non-interpolating config fields to plain String (#492) 2026-06-16 13:16:31 -04:00
deny.rs feat: Add approve/deny run controls to MCP and CLI (#400) 2026-05-25 15:49:57 -04:00
diff.rs test(cli): prune slow integration outliers 2026-04-28 19:26:27 -07:00
discord.rs refactor(cli): deglobalize server and storage target flags 2026-04-05 16:06:42 -04:00
docs.rs refactor(cli): deglobalize server and storage target flags 2026-04-05 16:06:42 -04:00
doctor.rs fabro doctor: check Docker daemon when Docker sandbox is enabled (#525) 2026-06-25 21:09:31 -04:00
dump.rs feat: add server-owned environment store (Task 1 & 2 foundation) (#446) 2026-05-28 17:10:59 -04:00
events.rs fix: stabilize attach JSON timing snapshot (#385) 2026-05-24 13:09:09 -04:00
exec.rs feat(llm): Amazon Bedrock provider — Converse codec, SigV4 + API-key auth (#459) 2026-06-16 11:46:49 -04:00
fabro.rs Add fabro variable CLI namespace for server-managed variables (#434) 2026-05-27 13:57:25 -04:00
fork.rs refactor(runs): simplify run projection shape 2026-05-09 23:31:43 -04:00
graph.rs feat(cli): allow rendering invalid graphs 2026-05-25 10:19:32 -04:00
inspect.rs Resolve run.prepare.steps env and interpolation at the run boundary (#530) 2026-07-01 10:31:26 -04:00
install.rs refactor: rationalize server secret scopes (vault-only for optional int… (#401) 2026-05-25 17:26:01 -04:00
json_global.rs Add fabro variable CLI namespace for server-managed variables (#434) 2026-05-27 13:57:25 -04:00
logs.rs fix(cli): preserve API error details 2026-05-06 14:03:12 -04:00
mcp.rs feat: Add approve/deny run controls to MCP and CLI (#400) 2026-05-25 15:49:57 -04:00
mod.rs Add fabro variable CLI namespace for server-managed variables (#434) 2026-05-27 13:57:25 -04:00
model.rs test(cli): stabilize model list snapshots 2026-05-25 10:20:47 -04:00
model_list.rs refactor(cli): separate local socket and storage defaults 2026-04-06 11:57:14 -04:00
model_test.rs refactor(llm): split provider identity from adapters (#280) 2026-05-16 13:13:41 -04:00
parent.rs feat(cli): wire run parent commands (#288) 2026-05-16 15:15:28 -04:00
parse.rs feat(cli): render fatal errors with miette 2026-04-24 15:35:57 -04:00
pr.rs feat(pr): support GitHub pull request associations (#270) 2026-05-16 12:47:27 -04:00
pr_close.rs refactor(pr): simplify server-side PR plumbing 2026-04-24 11:17:01 -04:00
pr_create.rs feat(pr): support GitHub pull request associations (#270) 2026-05-16 12:47:27 -04:00
pr_link.rs feat(pr): support GitHub pull request associations (#270) 2026-05-16 12:47:27 -04:00
pr_merge.rs refactor(pr): simplify server-side PR plumbing 2026-04-24 11:17:01 -04:00
pr_unlink.rs feat(pr): support GitHub pull request associations (#270) 2026-05-16 12:47:27 -04:00
pr_view.rs feat(pr): support GitHub pull request associations (#270) 2026-05-16 12:47:27 -04:00
preflight.rs Replace run-scoped sandbox config with named environments (#360) 2026-05-23 13:03:21 -04:00
provider.rs refactor(cli): deglobalize server and storage target flags 2026-04-05 16:06:42 -04:00
provider_login.rs fix(cli): use server catalog for provider login (#529) 2026-06-26 08:46:38 -04:00
ps.rs fix(server): persist manifest metadata names (#302) 2026-05-18 08:31:23 -04:00
render_graph.rs fix(graph): support dotted Fabro graph attributes (#324) 2026-05-20 09:31:08 -04:00
repo.rs feat(cli): render fatal errors with miette 2026-04-24 15:35:57 -04:00
repo_deinit.rs refactor(config): move project state under .fabro 2026-04-11 12:55:46 -04:00
repo_init.rs Replace run-scoped sandbox config with named environments (#360) 2026-05-23 13:03:21 -04:00
resume.rs test(cli): prune slow integration outliers 2026-04-28 19:26:27 -07:00
rewind.rs feat(api): unify public run shape 2026-05-10 20:48:55 -04:00
rm.rs fix(cli): print full run id on rm (#315) 2026-05-20 08:24:31 -04:00
run.rs feat: add server-owned environment store (Task 1 & 2 foundation) (#446) 2026-05-28 17:10:59 -04:00
runner.rs Replace stdin JSONL control pipe with WebSocket worker control bus (#440) 2026-05-27 20:24:25 -04:00
sandbox_cp.rs Model run sandbox lifecycle explicitly (#431) 2026-05-27 12:48:56 -04:00
sandbox_preview.rs refactor(types): remove legacy run summary shape 2026-05-10 23:29:41 -04:00
sandbox_ssh.rs fix(cli): repair verification failures 2026-05-09 18:07:24 -04:00
secret.rs refactor(auth): split credential sources and vault schemas (#306) 2026-05-18 11:07:42 -04:00
secret_list.rs refactor(auth): split credential sources and vault schemas (#306) 2026-05-18 11:07:42 -04:00
secret_rm.rs refactor(cli): deglobalize server and storage target flags 2026-04-05 16:06:42 -04:00
secret_set.rs refactor(auth): split credential sources and vault schemas (#306) 2026-05-18 11:07:42 -04:00
send_analytics.rs refactor(cli): deglobalize server and storage target flags 2026-04-05 16:06:42 -04:00
send_panic.rs refactor(cli): deglobalize server and storage target flags 2026-04-05 16:06:42 -04:00
server_start.rs refactor: rationalize server secret scopes (vault-only for optional int… (#401) 2026-05-25 17:26:01 -04:00
server_status.rs refactor(server): unify daemon runtime metadata 2026-04-22 16:07:52 -04:00
server_stop.rs refactor(test): promote shared server-lifecycle test helpers into fabro-test 2026-04-19 16:43:26 -04:00
start.rs Replace run-scoped sandbox config with named environments (#360) 2026-05-23 13:03:21 -04:00
support.rs feat(web): add server-managed Environments CRUD settings UI (#462) 2026-06-13 08:44:38 -04:00
system.rs fix(system): expose unreadable run repair flow 2026-05-06 07:15:18 -04:00
system_df.rs fix(server): count whole storage tree in Fabro-managed bytes 2026-05-21 11:28:39 -04:00
system_events.rs refactor(cli): finish command context cleanup 2026-04-23 07:14:32 -04:00
system_info.rs refactor(cli): finish command context cleanup 2026-04-23 07:14:32 -04:00
system_prune.rs test(cli): trim slow integration fixture setup 2026-04-28 19:14:56 -07:00
system_repair.rs fix(runs): repair deletion of unreadable runs 2026-05-09 17:45:55 -04:00
test_panic.rs test: speed up slow default-profile tests and tighten nextest thresholds 2026-04-05 13:15:59 -04:00
top_level.rs feat(cli): show curated landing output for bare fabro 2026-04-17 08:59:03 -04:00
unarchive.rs feat: Add approve/deny run controls to MCP and CLI (#400) 2026-05-25 15:49:57 -04:00
uninstall.rs refactor(server): unify daemon runtime metadata 2026-04-22 16:07:52 -04:00
upgrade.rs feat(cli): render fatal errors with miette 2026-04-24 15:35:57 -04:00
validate.rs feat(template): resolve template error locations (#333) 2026-05-20 20:15:04 -04:00
variable.rs Add fabro variable CLI namespace for server-managed variables (#434) 2026-05-27 13:57:25 -04:00
variable_get.rs Add fabro variable CLI namespace for server-managed variables (#434) 2026-05-27 13:57:25 -04:00
variable_list.rs Add fabro variable CLI namespace for server-managed variables (#434) 2026-05-27 13:57:25 -04:00
variable_rm.rs Add fabro variable CLI namespace for server-managed variables (#434) 2026-05-27 13:57:25 -04:00
variable_set.rs Add fabro variable CLI namespace for server-managed variables (#434) 2026-05-27 13:57:25 -04:00
version.rs feat(cli): add fabro version command 2026-04-14 16:30:53 -04:00
wait.rs fix: stabilize attach JSON timing snapshot (#385) 2026-05-24 13:09:09 -04:00
worker_auth.rs refactor: rationalize server secret scopes (vault-only for optional int… (#401) 2026-05-25 17:26:01 -04:00
workflow.rs fix(workflow): ignore deprecated project directory 2026-05-09 10:55:56 -04:00
workflow_create.rs fix(workflow): ignore deprecated project directory 2026-05-09 10:55:56 -04:00
workflow_list.rs feat(cli): render fatal errors with miette 2026-04-24 15:35:57 -04:00