Remove stale env-interpolation promises from docs

Config {{ env.NAME }} interpolation was removed workspace-wide (tokens
still parse only to fail with a migration message), but several doc
comments and the server-secrets strategy doc still presented it as a
live mechanism, including run goal file paths where the new
workflow-version validation now makes the contradiction user-visible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Scott Werner 2026-08-17 17:01:04 -04:00
parent 4e48d2887e
commit 14cc56b25f
5 changed files with 17 additions and 21 deletions

View file

@ -13,7 +13,7 @@ when does it resolve** — see [Which process resolves what](#which-process-reso
- Resolution is snapshot-based: env and file are read once at construction, then treated as immutable for the life of the process.
- `process env` wins over `server.env` on conflicts.
- Optional integration secrets are vault-only in the **server process**. Do not add optional server integrations to `ServerSecrets`, and do not add bespoke env fallback paths to it.
- Not every credential is a `ServerSecrets` or vault lookup. A third mechanism exists: **settings-declared credentials** in `InterpString` fields, resolved at consumption time from `{{ env.NAME }}` or `{{ secrets.NAME }}`. See [Settings-declared credentials](#settings-declared-credentials).
- Not every credential is a `ServerSecrets` or vault lookup. A third mechanism exists: **settings-declared credentials** in `InterpString` fields, resolved at consumption time from `{{ secrets.NAME }}`. See [Settings-declared credentials](#settings-declared-credentials).
- `fabro server start` never generates secrets. Missing required secrets are a startup error.
- `std::env::set_var` and `std::env::remove_var` are banned workspace-wide. Tests are not exempt. Enforced by clippy via `disallowed_methods` in `clippy.toml`; intentional exceptions must be annotated with a scoped `#[expect(clippy::disallowed_methods, reason = "...")]` at the call site.
@ -70,7 +70,6 @@ than saying "server runtime", which is ambiguous.
| Bootstrap server secret | Server process, via `ServerSecrets` | Once at construction, then immutable |
| Optional integration secret | Server process or worker, via the vault | At use |
| `{{ vars.NAME }}` | Server process | When the run is created, from that run's variable snapshot |
| `{{ env.NAME }}` | The process that owns the value (usually the worker) | At consumption time |
| `{{ secrets.NAME }}` | The process that owns the value, against the server vault | At consumption time |
`docs/public/agents/mcp.mdx` documents the same split for MCP server configuration and is a good
@ -80,18 +79,17 @@ worked example of the shape.
Some credentials are declared in settings rather than looked up by name. Those fields are
`InterpString` (`lib/foundation/fabro-types/src/settings/interp.rs`), which supports narrow
`{{ namespace.NAME }}` tokens with no template logic. Three namespaces resolve: `env` (process
environment, consumption time), `secrets` (vault, consumption time), and `vars` (non-sensitive run
variables, substituted early at run creation). A token whose namespace is unavailable in the
resolution context fails loudly.
`{{ namespace.NAME }}` tokens with no template logic. Two namespaces resolve: `secrets` (vault,
consumption time) and `vars` (non-sensitive run variables, substituted early at run creation).
`{{ env.NAME }}` tokens still parse but never resolve; they fail loudly with a migration message. A
token whose namespace is unavailable in the resolution context also fails loudly.
The reference implementation is LLM provider `extra_headers`, resolved against env plus vault at
The reference implementation is LLM provider `extra_headers`, resolved against the vault at
`lib/foundation/fabro-auth/src/resolve.rs:376-378`:
```toml
[llm.providers.example.extra_headers]
authorization = "Bearer {{ secrets.EXAMPLE_TOKEN }}"
x-tenant = "{{ env.EXAMPLE_TENANT }}"
```
Use this mechanism when the credential belongs to an operator-configured integration declared in
@ -149,7 +147,7 @@ First pick the mechanism. These are the only three:
|---|---|---|
| Bootstrap server secret | Platform env or install-written `server.env` | `state.server_secret(...)` |
| Optional integration secret | Vault (`fabro secret set`, `fabro install`) | `state.vault_secret(...)` |
| Settings-declared credential | `{{ secrets.* }}` or `{{ env.* }}` in an `InterpString` settings field | Resolved at consumption time by the owning process |
| Settings-declared credential | `{{ secrets.* }}` in an `InterpString` settings field | Resolved at consumption time by the owning process |
Then:

View file

@ -109,8 +109,8 @@ pub(crate) fn resolve_workflow(request: ResolveWorkflowInput) -> anyhow::Result<
/// Resolve the `run.goal` override for a direct (non-manifest) workflow
/// run. Reads the file from disk if the goal layer is the `file` variant.
/// Relative paths that survived config load (e.g. env-interpolated ones)
/// are anchored at `working_directory`.
/// Relative paths that survived config load are anchored at
/// `working_directory`.
fn resolve_goal_override(
settings: &WorkflowSettings,
working_directory: &Path,

View file

@ -78,8 +78,8 @@ pub struct LifecycleOptions {
}
/// A single setup (prepare) command and the per-step environment it runs with.
/// Both the command string and the env values are already fully resolved (their
/// `{{ env.* }}` tokens replaced at the run boundary) by the time they reach
/// Both the command string and the env values are already fully resolved
/// (interpolation tokens replaced at the run boundary) by the time they reach
/// the sandbox.
pub struct SetupCommand {
pub command: String,

View file

@ -116,10 +116,8 @@ impl Combine for RunIntegrationsGithubLayer {
///
/// Relative paths inside the `file` variant are resolved against the
/// directory of the config file that declared them at load time (see
/// `fabro_config::resolve_goal_file_paths`). `{{ env.NAME }}` interpolation is
/// supported inside the `file` path; env-tokenized relative paths stay
/// unresolved until consume time and are then resolved against the run's
/// effective working directory.
/// `fabro_config::resolve_goal_file_paths`). Interpolation tokens are not
/// supported inside the `file` path; a tokenized path fails to resolve.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
pub enum RunGoalLayer {

View file

@ -61,8 +61,8 @@ pub struct ProviderCatalogSettings {
pub api_key_url: Option<String>,
#[serde(default)]
pub base_url: Option<String>,
/// Unresolved interpolation source strings (literal text, `{{ env.NAME }}`,
/// or `{{ secrets.NAME }}` tokens), resolved at the credential boundary in
/// Unresolved interpolation source strings (literal text or
/// `{{ secrets.NAME }}` tokens), resolved at the credential boundary in
/// `fabro-auth`.
#[serde(default)]
pub extra_headers: Option<HashMap<String, String>>,
@ -438,8 +438,8 @@ pub struct CatalogProvider {
pub billing_policy: BillingPolicy,
pub api_key_url: Option<String>,
pub base_url: Option<String>,
/// Unresolved interpolation source strings (literal text, `{{ env.NAME }}`,
/// or `{{ secrets.NAME }}` tokens), resolved at the credential boundary in
/// Unresolved interpolation source strings (literal text or
/// `{{ secrets.NAME }}` tokens), resolved at the credential boundary in
/// `fabro-auth`.
pub extra_headers: HashMap<String, String>,
pub priority: i32,