fabro/lib
Scott Werner accd91a0a6
Some checks are pending
Rust / Test (Linux) (push) Waiting to run
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
Demote non-interpolating config fields to plain String (#492)
# Demote non-category leak fields to plain `String`

Second slice of the interpolation unification — the first **reducing**
PR
stacked on the foundation (#472), per the reduce-first sequencing:
narrowing
changes land before capability additions. (The other reducing slice, the
DOT
de-templating, already landed independently as #474.)

## Why

The target model gives `InterpString` to fields in five categories —
`command` / `script` / `headers` / `env` / `url` — wherever they appear.
A
handful of fields were typed `InterpString` but are *identifiers or
commit
content*, not in any category:

- `run.model.provider` / `run.model.name`
- `cli.exec.model.provider` / `cli.exec.model.name`
- `run.git.author.name` / `run.git.author.email`
- `run.scm.owner` / `run.scm.repository`

Their consumers never resolved them — they leaked raw source text via
`as_source()`. This PR demotes them to plain `String` (layer and
resolved
structs) with **no interpolation**.

## The principle: only `InterpString` fields access variables

These fields are dropped from the variable substitute pass entirely, so
both
`{{ vars.* }}` and `{{ env.* }}` are now literal text. This **removes an
incidental behavior**: run-scoped plain-`String` fields used to get
`{{ vars.* }}` substituted via the String pass (a lucky accident), while
`env` always leaked literally. Variable access becomes deliberate and
typed
rather than accidental; if any of these fields should support variables
later, that's a controlled promotion back to `InterpString`.

## Behavior changes (honest list)

- **The incidental run-scoped `{{ vars.* }}` substitution on these eight
fields stops working.** To keep the removal visible rather than silent,
a
`tracing::warn!` fires at resolve time when a demoted field still
contains
claimed template tokens (`warn_if_demoted_template`). Unclaimed `{{ ...
}}`
  text (jq programs, Go templates) never interpolated and does not warn.
- `{{ env.* }}` / `{{ secrets.* }}` / `{{ inputs.* }}` never resolved on
  these fields, so nothing else changes.

## Added in review: D11 demotions (separate commit, revertable)

The rule got refined during review: a field is `InterpString` iff it is
in one
of the five categories **and resolved at the run boundary** (the only
point
where `vars`/`secrets`/`inputs` exist — they're server state, so
connect-time
and startup-time fields can't reach them even in principle). A separate
commit
applies the clean subset so it can be cherry-picked out if we change
course:

- `cli.target.http.url` / `cli.target.unix.path` — consumed at CLI
connect
time; consumers only ever leaked raw source, so nothing working is
removed.
- `run.working_dir` — **the outlier; see the PR comment.** Its `{{
vars.* }}`
  substitution worked; demoted on the category test alone.

## What's deliberately NOT here

- The **control-plane fields** (`server.storage.root` /
`listen.unix.path` /
  S3 fields / `github.app_id/client_id/slug` / `server.api.url` /
  `server.web.url`) — untouched here, demoted in a follow-up PR.
**Resolved during review** (see the resolution comment): `InterpString`
was
  conflating the user-facing workflow language with the internal control
plane. Control-plane fields never interpolate; the few deployment knobs
that need late binding (e.g. `FABRO_WEB_URL`, whose only real usage is
the
  split-web PoC ferrying a compose env var across a file mount) become
explicit native `EnvVars` reads, and `fabro-server/src/interp.rs`
shrinks
to deletion. `slack.default_channel` stays `InterpString` (consumed with
  run context).

## Implementation notes

- Consumers move from `as_source()` to direct `String` access; the
  foundation's `#[expect(disallowed_methods, ... demotion pending ...)]`
  annotations for these fields are removed (no longer `InterpString`).
- `fabro-checkpoint`'s author plumbing and `fabro-manifest`'s scm fields
  simplify accordingly.

## Verification

- `cargo build --workspace`
- `cargo nextest run --workspace` → 6684 passed, 181 skipped
- `cargo +nightly fmt --check --all`
- `cargo +nightly clippy --workspace --all-targets -- -D warnings` →
clean

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:16:31 -04:00
..
crates Demote non-interpolating config fields to plain String (#492) 2026-06-16 13:16:31 -04:00
packages/fabro-api-client feat(workflow): support overriding cwd for local sandbox provider (#467) 2026-06-14 12:32:32 -04:00