mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
# Interpolation foundation (InterpString v2)
First step of unifying config-string interpolation across Fabro. This PR
is the
**behavior-neutral foundation** only — it introduces the type machinery
and a
clippy gate, but changes no field's interpolation behavior. The actual
field
work follows as separate stacked PRs, sequenced **reduce-first**:
narrowing
changes (demote fields that shouldn't interpolate, de-template DOT
attrs) land
before capability additions (resolve env in MCP / prepare / hooks).
## Why
Config strings interpolate `{{ ... }}` inconsistently today — some
fields
resolve `{{ env.X }}`, others are typed as if they do but silently pass
the
literal template text downstream. We're converging on three field types
(`String`, `InterpString`, and later an importable template for
prompts/goals)
with four namespaces (`env`, `vars`, `secrets`, `inputs`). This PR lays
the
`InterpString` foundation; it does not migrate any field.
## What's in it
- Segments generalize to `Token { namespace, name }` with a `Namespace`
enum
(`env`/`vars`/`secrets`/`inputs`). `secrets`/`inputs` are **reserved** —
parsed as tokens ahead of their resolvers.
- `ResolveCtx` with per-namespace lookups. `resolve_with()` fails loudly
(`Unavailable`) for a token whose namespace isn't provided in context;
`substitute_with()` substitutes provided namespaces and preserves the
rest.
`resolve()` / `substitute_variables()` are thin wrappers over one core
path.
- `ResolveEnvError` → `ResolveError { namespace, name, kind: Missing |
Unavailable }`
(message text unchanged for env/vars; the kind no longer bakes the
namespace
in, so it scales to four namespaces without an enum explosion).
- `Provenance` tracks secret-sourced names alongside env-sourced, for
uniform
redaction later.
- **`as_source()` is clippy-gated** (`disallowed-methods`). It keeps its
name;
every call site carries an `#[expect(..., reason)]` classifying it
(serialization, error display, known-leak-pending-fix, demotion-pending,
test). The lint turns the leak surface into a greppable, reasoned
work-list
and the method stays for its permanent uses (serde round-trip of the
unresolved template + diagnostics).
- fabro-server: five duplicate `process_env_var` facades and two
duplicate
`resolve_interp` helpers consolidated into one `crate::interp` module.
## Behavior changes (honest list)
- **`{{ secrets.* }}` / `{{ inputs.* }}` are now reserved.** On main
they
weren't recognized as tokens → silent literal passthrough. Now, at
`resolve()` consumers they **fail loud** (`Unavailable`) instead of
passing
the literal string through (nobody wants the literal characters as a
value —
strictly better, but technically a change). At `as_source` sites they
round-trip unchanged. Actual resolution lands in later enhancing PRs.
- Some fabro-server resolution errors gain a `"failed to resolve
<source>"`
context line.
Otherwise behavior-neutral: every field resolves exactly as it did on
main.
## What's deferred to follow-up PRs (reduce-first order)
- **Reducing / cleanup (next):** demote leak fields to `String`
(`run.model.*`, `cli.exec.model.*`, `run.git.author.*`,
`run.scm.owner/repository`); de-template `condition`/`label`/`model`/
`provider`/`speed` and `output_schema`.
- **Enhancing (after):** resolve `{{ env.* }}` in MCP transports,
prepare
steps, and hooks; wire `secrets`/`inputs`.
## Verification
- `cargo build --workspace`
- `cargo nextest run --workspace` → 6449 passed, 181 skipped
- `cargo +nightly fmt --check --all`
- `cargo +nightly clippy --workspace --all-targets -- -D warnings` →
clean
## Reviewer notes
- The reserved-namespace `Unavailable` error for `secrets`/`inputs` is
**intentional**, not a missing case — they're parsed ahead of their
resolvers so misuse fails loud instead of leaking.
- `as_source` is clippy-gated but keeps its name deliberately — the gate
is
the enforcement; renaming was avoided as unnecessary churn.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| approve.rs | ||
| archive.rs | ||
| artifact_cp.rs | ||
| artifact_list.rs | ||
| attach.rs | ||
| auth.rs | ||
| cli_reference.rs | ||
| config.rs | ||
| create.rs | ||
| deny.rs | ||
| diff.rs | ||
| discord.rs | ||
| docs.rs | ||
| doctor.rs | ||
| dump.rs | ||
| events.rs | ||
| exec.rs | ||
| fabro.rs | ||
| fork.rs | ||
| graph.rs | ||
| inspect.rs | ||
| install.rs | ||
| json_global.rs | ||
| logs.rs | ||
| mcp.rs | ||
| mod.rs | ||
| model.rs | ||
| model_list.rs | ||
| model_test.rs | ||
| parent.rs | ||
| parse.rs | ||
| pr.rs | ||
| pr_close.rs | ||
| pr_create.rs | ||
| pr_link.rs | ||
| pr_merge.rs | ||
| pr_unlink.rs | ||
| pr_view.rs | ||
| preflight.rs | ||
| provider.rs | ||
| provider_login.rs | ||
| ps.rs | ||
| render_graph.rs | ||
| repo.rs | ||
| repo_deinit.rs | ||
| repo_init.rs | ||
| resume.rs | ||
| rewind.rs | ||
| rm.rs | ||
| run.rs | ||
| runner.rs | ||
| sandbox_cp.rs | ||
| sandbox_preview.rs | ||
| sandbox_ssh.rs | ||
| secret.rs | ||
| secret_list.rs | ||
| secret_rm.rs | ||
| secret_set.rs | ||
| send_analytics.rs | ||
| send_panic.rs | ||
| server_start.rs | ||
| server_status.rs | ||
| server_stop.rs | ||
| start.rs | ||
| support.rs | ||
| system.rs | ||
| system_df.rs | ||
| system_events.rs | ||
| system_info.rs | ||
| system_prune.rs | ||
| system_repair.rs | ||
| test_panic.rs | ||
| top_level.rs | ||
| unarchive.rs | ||
| uninstall.rs | ||
| upgrade.rs | ||
| validate.rs | ||
| variable.rs | ||
| variable_get.rs | ||
| variable_list.rs | ||
| variable_rm.rs | ||
| variable_set.rs | ||
| version.rs | ||
| wait.rs | ||
| worker_auth.rs | ||
| workflow.rs | ||
| workflow_create.rs | ||
| workflow_list.rs | ||