45 KiB
| title | type | status | date |
|---|---|---|---|
| refactor: Settings API entrypoints — owner-first context types | refactor | active | 2026-04-22 |
Settings API Entrypoints — Owner-First Context Types
Overview
Replace the free-function settings API with two owner-first context types that expose dense, resolved views of current config. The elegance rule:
SettingsLayeris the sparse transport/storage form — what TOML files parse into, what's persisted in run manifests, what merges with precedence.- Context types (
ServerSettings,UserSettings) are dense, owner-scoped, resolved views of a current process's config, computed from a layer at the moment a consumer needs them.
These roles do not overlap: stored artifacts are layers; current-config
reads are views. Stored-layer readers (code that reads specific
namespaces off a persisted run's SettingsLayer) keep using
per-namespace resolvers — the layer is the real artifact there.
After this refactor:
ServerSettings—server+featuresnamespaces. Derived from the server's effective runtime layer (the post-apply_runtime_settingsSettingsLayerthat folds in CLI overrides like--storage-dirand--bind) at startup and whenever hot-reload refreshes the layer. Held inAppStatealongside the shared layer itself.GET /api/v1/settingsserves the current in-memory view directly — no per-request disk read, no view toggle, no redaction.UserSettings—cli+featuresnamespaces. The CLI process builds one from its own~/.fabro/settings.toml.fabro run attachuses the liveUserSettingsat attach time.
Both context types expose from_layer(&SettingsLayer) (primitive) and
resolve() (convenience that loads the default file; used by tools
and tests, not the server startup path which composes from_layer
against the effective runtime layer).
The god type Settings goes away with no named replacement — its
former consumers either migrate to per-namespace resolvers (the
stored-layer readers) or are deleted wholesale (the view-toggle
machinery removed by Unit 7). EffectiveSettingsMode goes away;
fabro settings --local goes away; redaction machinery goes away.
Settings contain no secrets — InterpString templates preserve
{{ env.NAME }} unresolved on the wire. Actual secrets live in
ServerSecrets / Vault.
Problem Frame
The settings API grew around a layered-config design that's now visibly clunky:
- Every caller that wants a resolved value first takes a
SettingsLayer, then picks the rightresolve_*_from_filehelper. The layer plumbing is on the public surface even though most callers just want "give me the server config." EffectiveSettingsModehas three variants; production code always picksLocalDaemon. The enum is dead ceremony.fabro_types::settings::Settingsis a god type — server-side execution code needingrun.*also getscli.*in the same struct.- The CLI/server trust boundary is enforced by
fabro_cli::local_serverconvention plusbin/dev/check-boundary.sh. Type-level projection does this at compile time.
The settings schema itself is fine. This plan is the programmatic API over it.
Requirements Trace
- R1. Current-config callers (the server's in-memory settings, the
CLI process's settings) use the context types. Stored-layer readers —
code that reads specific namespaces off a persisted
SettingsLayer(runner.rs,operations/create.rs) — continue to use per-namespace resolvers. The layer is the real artifact there and must not be contorted to fit the context-type API. - R2. Each context type exposes namespaces owned by its consumer,
plus the cross-cutting
features.*namespace. - R3.
EffectiveSettingsModeis deleted. One merge path remains. - R4.
fabro settings --localand its filesystem-walking assembly are deleted. - R5.
GET /api/v1/settingsreturns the server's in-memoryServerSettingsas a typed JSON body. Noview=query param, noX-Fabro-Settings-Viewheader, no disk re-read, no redaction. - R6. Today's per-namespace resolved types (
ServerSettings,CliSettings,ProjectSettings,WorkflowSettings,RunSettings,FeaturesSettings) rename with a*Namespacesuffix, freeing the short names for context types. OnlyServerSettingsandUserSettingsare defined as context types in this PR. - R7. The
Settingsgod type (and the publicfabro_config::resolvefunction that returns it, and theload_and_resolvehelper that wraps them) are deleted. No named replacement type is introduced; consumers migrate to per-namespace resolvers or disappear with the view-toggle machinery Unit 7 removes. - R8. Layer-merge internals are
pub(crate)or private. Per-namespace resolver visibility matches reality (see Key Technical Decisions). - R9. The CLI/server trust boundary survives.
fabro_cli::local_serverremains the only sanctioned CLI-side gateway to[server.*];bin/dev/check-boundary.shupdates to cover the new symbols.
Scope Boundaries
- Not changing the TOML schema, namespace inventory, merge precedence, or owner-domain stripping rules.
- Not changing
PreparedManifest.settings— staysSettingsLayer. Wire format for persisted run settings unchanged. - Not migrating stored-layer readers.
runner.rs:507-508(resolve_run_from_file/resolve_server_from_fileonrecord.settings) andoperations/create.rs(metadata aggregation from stored layers) are legitimate consumers of the sparse layer, not candidates for context-type migration. - Not adding endpoints or CLI commands.
- Settings contain no secrets (invariant, with documented gaps).
Secret-bearing fields should be
InterpString. Known gaps not addressed here:McpTransport::Http.headers,McpTransport::Stdio.env,McpTransport::Sandbox.env,HookType::Http.headers,run.inputs,run.metadata. Pre-existing; deferred to a follow-up plan that retypes the maps toHashMap<String, InterpString>and adds submit-time validation. Any new field added by this refactor must satisfy the invariant.
Context & Research
Relevant Code and Patterns
lib/crates/fabro-config/src/effective_settings.rs— layer merge + owner-domain stripping.lib/crates/fabro-config/src/resolve/— per-namespace resolve helpers.lib/crates/fabro-config/src/user.rs— loads~/.fabro/settings.tomlinto aSettingsLayer.lib/crates/fabro-types/src/settings/resolved.rs— god typeSettings.lib/crates/fabro-types/src/settings/mod.rs— re-exports per-namespace types to be renamed.lib/crates/fabro-cli/src/local_server.rs— single sanctioned CLI gateway to[server.*].lib/crates/fabro-cli/src/commands/config/mod.rs—fabro settingscommand; contains--localbranch to delete.lib/crates/fabro-server/src/run_manifest.rs— sole production caller ofmaterialize_settings_layer.lib/crates/fabro-server/src/settings_view.rs— deleted by this refactor.lib/crates/fabro-server/src/server.rs—AppStatewill hold the server'sServerSettings; the settings endpoint handler serves it directly.docs/api-reference/fabro-api.yaml— OpenAPI spec.bin/dev/check-boundary.sh— CLI/server boundary regression guard.
Related Context
docs/brainstorms/2026-04-08-settings-toml-redesign-requirements.md— defined the six-namespace schema and owner-first trust boundaries. This plan operationalizes those boundaries in code.
Call-Site Inventory
resolve_server_from_fileoutsidefabro-config: ~20 call sites acrossfabro-cli,fabro-server,fabro-workflow,fabro-install. Re-rungrep -rn "resolve_server_from_file" lib/before Unit 4 to confirm the full set.resolve_cli_from_fileoutsidefabro-config:user_config.rs,attach.rs(deleted in Unit 5).materialize_settings_layeroutside tests:run_manifest.rsonly.- Per-namespace type name references to rename: ~57.
Institutional Learnings
- No prior
docs/solutions/entries cover this area.
Key Technical Decisions
-
Layer and view are distinct roles.
SettingsLayeris sparse transport/storage. Context types are dense, owner-scoped, resolved views computed from a layer (or combination) at read time. Persisted artifacts are layers; live reads are views. These don't overlap and don't convert in place. -
GET /api/v1/settingsservesAppStatein memory. The server builds itsServerSettingsat startup from the effective runtime layer (post-apply_runtime_settings, which folds in CLI overrides like--storage-dirand--bind) and stores it inAppState. Hot-reload keeps the derived view in sync viastate.replace_settings(...). The handler returns a clone of the current value. No per-request disk read. No view toggle. No redaction. No response header. -
No
WorkflowSettingsin this PR. An earlier draft introduced aWorkflowSettingscontext type as the dense resolved view for server-side execution, but the refactor has no production caller that needs it:operations/create.rsmigrates to per-namespace resolvers (legitimate stored-layer read),server.rs:1337is deleted wholesale as part of Unit 7's view-toggle removal, andsettings_view.rsdisappears entirely. IntroducingWorkflowSettings::resolve_for_runwith no caller is speculative abstraction. If future server code wants a dense multi-namespace view, it can add the type then with a real consumer.PreparedManifest.settingsstaysSettingsLayer(unchanged by this refactor); server execution code reads specific namespaces via per-namespace resolvers as it does today. -
Attach honors live CLI settings.
fabro run attachcallsUserSettings::resolve()on the attaching process's config. Submit-timecli.*on a stored run is inert — no code reads it back. -
Two constructors per context type.
ServerSettings::from_layer(&SettingsLayer)is the primitive;ServerSettings::resolve()loads the default~/.fabro/settings.tomland delegates.UserSettingshas the same pair. Noresolve_from(path):--confighandling is an existingserve.rsconcern that produces the on-disk settings layer beforeapply_runtime_settingsruns; the newServerSettings::from_layeris invoked on the resulting effective runtime layer, not on a fresh disk read. -
Delete
EffectiveSettingsMode. Production always picksLocalDaemon; the enum is ceremony.materialize_settings_layerbecomes a single straight-line function. -
Delete redaction. Settings contain no secrets.
settings_view.rs,redact_for_api,redact_resolved_value,SettingsApiView,SettingsQuery,X-Fabro-Settings-View— all deleted. Both settings endpoints serialize directly. If a future field should not be exposed, the fix is to type it asInterpString, not to reintroduce redaction. -
fabro settingscomposes local + server. The CLI rendersUserSettings::resolve()(local) plus the server'sServerSettings(fetched via the endpoint). Two sections. -
Typed OpenAPI schema via
Deserialize+with_replacement. Aligned with CLAUDE.md's API type ownership doctrine. TheServerSettingsOpenAPI schema describes the internal Rust type directly; progenitor reuses it viawith_replacement. Reachable namespace types gainDeserializederives; types with customSerializeget matching customDeserialize(notablyInterpString, which must preserve unresolved templates). -
features.*on both context types — the one carve-out to owner-first. Cross-cutting by design; server and CLI both gate behavior on feature flags, and server execution code consumes them via its per-namespaceresolve_features_from_filereads on stored layers. -
Context types live in
fabro-config. Inherentimplblocks must live with the type per Rust's orphan rules.fabro-typeskeeps only per-namespace shape types. -
Per-namespace resolver visibility, decided once: all six
resolve_*_from_filehelpers staypub, because each has at least one cross-crate consumer:resolve_server_from_file—runner.rs:508readsserver.*off storedrecord.settings.resolve_run_from_file—runner.rs:507,run_manifest.rs:369,operations/create.rsreadrun.*off stored layers.resolve_project_from_file,resolve_workflow_from_file—operations/create.rsreads their.metadatafor label aggregation.resolve_cli_from_file—fabro-cli/tests/it/cmd/create.rs:364integration test asserts the persistedcli.*wire shape.resolve_features_from_file—fabro-server/src/server.rs:1414readsfeatures.session_sandboxes; thefabro-config/tests/resolve_features.rsintegration test also depends on it.
-
Other
fabro-configvisibility:pub(crate):materialize_settings_layer— internal helper; the merge step stays insiderun_manifest.rs's call site, consumed only via the publicSettingsLayeroutput.pub:user::load_settings_config— external caller infabro-server/src/serve.rsloads the on-disk config layer beforeapply_runtime_settings.pub(crate):EffectiveSettingsLayers— no external consumer remains afterWorkflowSettingsis dropped (run_manifest.rsbuilds layers internally for its ownmaterialize_settings_layercall).
Open Questions
Resolved During Planning
- Introduce a
WorkflowSettingscontext type? No. The refactor has no production caller that needs a dense multi-namespace view —operations/create.rsmigrates to per-namespace resolvers, the view-toggle branches are deleted. Adding the type speculatively violates the no-speculative-cleanup directive. - Context types' crate?
fabro-config. GET /api/v1/settingsre-read from disk per request? No. ServesAppState.server_settings.PreparedManifest.settingsretype? No. StaysSettingsLayer.- Stored-layer readers migrate to context types? No. They stay on per-namespace resolvers.
- Redaction for the new endpoints? None. Deleted entirely.
Deferred to Implementation
fabro settingsoutput layout (two-section render — labels, ordering, field suppression). Snapshot tests drive.
High-Level Technical Design
Directional guidance, not implementation specification.
Type inventory:
// Per-namespace dense types (renamed from today's *Settings)
ServerNamespace, CliNamespace, ProjectNamespace,
WorkflowNamespace, RunNamespace, FeaturesNamespace
// Context types (new; all in fabro-config)
ServerSettings { server: ServerNamespace, features: FeaturesNamespace }
UserSettings { cli: CliNamespace, features: FeaturesNamespace }
Public constructors:
impl ServerSettings {
fn from_layer(&SettingsLayer) -> Result<Self>;
fn resolve() -> Result<Self>;
}
impl UserSettings {
fn from_layer(&SettingsLayer) -> Result<Self>;
fn resolve() -> Result<Self>;
}
Role separation:
SettingsLayer — sparse transport/storage. Unchanged.
ServerSettings — dense view. AppState holds one; API serves it.
UserSettings — dense view. CLI process builds one.
Call-site topology:
Before After
────── ─────
resolve_server_from_file(&layer) ─► ServerSettings::from_layer(&layer)
(for current-config reads) (stored-layer reads keep
resolve_*_from_file)
GET /api/v1/settings: load + resolve ─► GET /api/v1/settings: clone AppState.server_settings
+ redact + serialize per request
resolve_cli_from_file(&layer) ─► UserSettings::resolve() or ::from_layer
attach reads stored cli.* ─► attach calls UserSettings::resolve()
fabro_config::resolve(&layer) ─► per-namespace resolvers at each call site
(god-type god fn) in create.rs (project/workflow/run metadata reads)
materialize_settings_layer(...) ─► materialize_settings_layer(...)
(stays at run_manifest.rs merge site;
loses the `mode` parameter; output
still stored in PreparedManifest.settings
as SettingsLayer)
Settings god type ─► deleted; no named replacement
runner.rs / operations/create.rs: ─► unchanged (pub per-namespace resolvers)
per-namespace stored-layer reads
Unit dependency graph:
flowchart TB
U1[Unit 1: Delete --local]
U2[Unit 2: Delete mode enum]
U3[Unit 3: Rename to *Namespace]
U4[Unit 4: ServerSettings + AppState]
U5[Unit 5: UserSettings + live attach]
U6[Unit 6: Delete Settings god type]
U7[Unit 7: New /api/v1/settings shape]
U8[Unit 8: Privatize internals]
U1 --> U2
U2 --> U3
U3 --> U4
U3 --> U5
U3 --> U6
U4 --> U7
U6 --> U7
U4 --> U8
U5 --> U8
U6 --> U8
Implementation Units
- Unit 1: Delete
fabro settings --localand theLocalOnlypath
Goal: Remove --local, its helpers, and the LocalOnly variant.
Requirements: R4.
Dependencies: None.
Files:
- Modify:
lib/crates/fabro-cli/src/args.rs— removelocalandworkflowfields fromSettingsArgs. - Modify:
lib/crates/fabro-cli/src/commands/config/mod.rs— deletelocal_settings_value,workflow_and_project_layers,config_layers,strip_nulls,resolve_local_settings_value,render_resolve_errors, and theargs.local/args.workflowbranches. - Modify:
lib/crates/fabro-cli/src/main.rs— removeargs.localsnapshot-test assertions. - Modify:
lib/crates/fabro-config/src/effective_settings.rs— removeLocalOnlyvariant and its match arm. - Modify:
lib/crates/fabro-config/tests/resolve_root.rsand theeffective_settings.rstests module — dropLocalOnlycases. - Delete:
lib/crates/fabro-cli/tests/it/cmd/config.rstests that depend on--local(e.g.,settings_local_merges_cli_and_project_defaults,settings_local_workflow_name_applies_run_overlay_and_deep_merges). They assert filesystem-walked behavior that's being removed entirely.
Approach:
--workflow WORKFLOWwas only meaningful with--local. Both go.fabro settingscontinues to work via the server path (reshaped in Unit 7).LocalOnlyremoval is dead-code deletion once the CLI stops calling it.
Test scenarios:
- Contract:
fabro settingswith a running server returns the server's settings (shape reshaped in Unit 7). - Parser-level:
fabro settings --localfails to parse.fabro settings WORKFLOW_ARGfails to parse.
Verification:
cargo build --workspaceandcargo nextest run -p fabro-clipass.grep -rn "LocalOnly\|args\.local" lib/returns only deliberate doc references.
- Unit 2: Delete
EffectiveSettingsModeentirely
Goal: Kill the mode enum. materialize_settings_layer becomes a
single straight-line function.
Requirements: R3.
Dependencies: Unit 1.
Files:
- Modify:
lib/crates/fabro-config/src/effective_settings.rs— deleteEffectiveSettingsMode; removemodeparameter frommaterialize_settings_layer; flatten to a single path (strip owner domains, merge, apply server authority). Renameapply_local_daemon_overrides→enforce_server_authority. Deleteapply_server_defaults(no surviving caller). - Modify:
lib/crates/fabro-config/src/lib.rs— update internalload_and_resolveto match. - Modify:
lib/crates/fabro-server/src/run_manifest.rs— callmaterialize_settings_layer(layers, Some(server_settings))without a mode. - Modify:
lib/crates/fabro-server/src/server.rs— remove thelocal_daemon_modefield fromAppStateandAppStateConfig; update the three handler call sites that thread it (create_run~line 4104,run_preflight~line 4212,render_graph_from_manifest~line 4242); update helper signatures (~lines 2515, 2541, 2641, 2708); remove the test helper at ~line 2575. - Modify:
effective_settings.rstests module — delete theRemoteServertest case (cli_and_server_domains_from_fabro_toml_are_inert_under_remote_mode). Verify its unique assertions are covered by the survivingLocalDaemontest at ~line 437; if any coverage is unique, fold that assertion into the surviving test.
Approach:
apply_server_defaults(theRemoteServercode path) has no surviving caller; delete along with the enum.- Keep
strip_owner_domainsandenforce_server_authorityas private helpers.
Test scenarios:
- Contract:
materialize_settings_layerwith representative layers produces the same output as the pre-refactorLocalDaemoninvocation. - Edge case:
Some(empty_layer)for server settings preserves client values and doesn't panic.
Verification:
cargo build --workspaceandcargo nextest run -p fabro-configpass.grep -rn "EffectiveSettingsMode\|RemoteServer\|LocalDaemon\|local_daemon_mode" lib/returns zero hits.
- Unit 3: Rename per-namespace resolved types to
*Namespace
Goal: Free the short names for context types.
Requirements: R6.
Dependencies: Units 1, 2.
Files:
- Modify:
lib/crates/fabro-types/src/settings/{mod,server,cli,project,workflow,run,features}.rs— rename each per-namespace type (ServerSettings→ServerNamespace, etc.). - Modify:
lib/crates/fabro-types/src/settings/resolved.rs— update the god-type field types (the god type itself goes away in Unit 6). - Modify:
lib/crates/fabro-config/src/resolve/mod.rsand siblings — update return types. - Modify: ~57 other references across the workspace (mechanical).
Approach: Type-only rename. Function names, field names, and serde
wire format unchanged. Use cargo check --workspace between file batches.
Execution note: Mechanical; suitable for Execution target: external-delegate.
Test scenarios: All existing tests continue to pass. Compiler is the primary witness.
Verification:
cargo build --workspaceandcargo nextest run --workspacepass.grep -rn "fabro_types::settings::\(ServerSettings\|CliSettings\|ProjectSettings\|WorkflowSettings\|RunSettings\|FeaturesSettings\)\b" lib/returns zero hits.- Clippy clean.
- Unit 4: Introduce
ServerSettings;AppStateholds one
Goal: Add ServerSettings with from_layer + resolve. Build one at
server startup and store in AppState. Migrate current-config callers.
Requirements: R1, R2.
Dependencies: Unit 3.
Files:
- Create:
lib/crates/fabro-config/src/context.rs—ServerSettings { server: ServerNamespace, features: FeaturesNamespace }. Inherentfrom_layer(&SettingsLayer) -> Result<Self>andresolve() -> Result<Self>. - Modify:
lib/crates/fabro-config/src/lib.rs— re-exportServerSettings. - Modify:
lib/crates/fabro-server/src/serve.rs(~lines 478-482) — startup already computeseffective_settings = apply_runtime_settings(&disk_settings, &args, &data_dir)(the post-runtime-override layer that folds in--storage-dir,--bind, etc.). Addlet server_settings = ServerSettings::from_layer(&effective_settings)?;alongside the existingresolved_server_settings = resolve_server_settings(&effective_settings)?line, and threadserver_settingsintoAppState. Derive from the effective runtime layer, not from a fresh~/.fabro/settings.tomlread; a fresh read would drop the CLI overrides. - Modify:
lib/crates/fabro-server/src/serve.rs(~line 600, the hot-reload path) — whenapply_runtime_settings(...)is rerun andstate_for_poll.replace_settings(effective)is called, also refreshAppState.server_settingsfrom the new layer so the typed view stays in sync with theArc<RwLock<SettingsLayer>>. - Modify:
lib/crates/fabro-server/src/server.rs— addserver_settings: Arc<ServerSettings>(or equivalent interior mutability for hot-reload refresh) toAppState. Extendreplace_settings(...)to also update the derivedServerSettingsso both are consistent after reload. - Modify: every current-config caller of
resolve_server_from_file(~20 sites acrossfabro-cli,fabro-server,fabro-workflow,fabro-install) — switch toServerSettings::from_layer(&layer)where a layer is already in hand, orServerSettings::resolve()where defaults apply. Stored-layer readers (runner.rs:507-508) stay onresolve_server_from_file. - Modify:
bin/dev/check-boundary.sh— add grep patterns forServerSettings::resolveandServerSettings::from_layerso the regression guard covers the migration window. - Test:
lib/crates/fabro-config/tests/resolve_server.rs— add tests forServerSettings::from_layerandServerSettings::resolve.
Approach:
from_layerinvokes the per-namespace resolver and wraps the result. Single primitive.resolve()loads the default~/.fabro/settings.tomland delegates tofrom_layer. Useful for tools (fabro doctor-style), integration tests, and any process that wants "the defaults on this machine." Not used by the server startup path: the server's authoritative settings are the effective runtime layer produced byapply_runtime_settings, which a fresh disk read doesn't see.- Server startup: after
serve.rsbuildseffective_settings = apply_runtime_settings(...), it callsServerSettings::from_layer(&effective_settings)and stores the result inAppState. Hot-reload (state.replace_settings(...)) also refreshes that derived view. AppState.server_settingsis the server's canonical current-config value. Handlers read it, not disk. It's always in sync with the layer inAppState'sRwLock<SettingsLayer>.
Test scenarios:
- Contract:
from_layer(&layer)returns the same data as the old free function on the same layer. - Contract:
resolve()with$FABRO_HOMEset to a temp dir loads that directory'ssettings.toml. - Integration: Server startup derives
AppState.server_settingsfrom the effective runtime layer (post-apply_runtime_settings). A startup with--storage-dir /tmp/fooproducesAppState.server_settings.server.storagereflecting/tmp/foo, confirming CLI overrides flow through to the typed view. - Integration: Hot-reload (triggered via the existing
state.replace_settings(effective)pathway) refreshesAppState.server_settingsso the typed view reflects the updated layer.
Verification:
cargo build --workspaceandcargo nextest run --workspacepass.grep -rn "resolve_server_from_file" lib/returns onlyfabro-config/src/and stored-layer reader call sites.bin/dev/check-boundary.shstill passes; verify on a throwaway branch that it catches a deliberate unsanctionedServerSettings::resolve*import.
- Unit 5: Introduce
UserSettings; attach uses live config
Goal: Add UserSettings with from_layer + resolve.
fabro run attach reads the attaching process's live UserSettings.
Requirements: R1, R2.
Dependencies: Unit 3.
Files:
- Modify:
lib/crates/fabro-config/src/context.rs— addUserSettings { cli: CliNamespace, features: FeaturesNamespace }. Inherentfrom_layer(&SettingsLayer)andresolve(). - Modify:
lib/crates/fabro-config/src/lib.rs— re-exportUserSettings. - Modify:
lib/crates/fabro-cli/src/user_config.rs— replaceresolve_cli_from_fileusage withUserSettings::from_layerorUserSettings::resolve(). - Modify:
lib/crates/fabro-cli/src/commands/run/attach.rs— delete the stored-layerresolve_cli_from_file(&record.settings)read at ~line 90. Attach reads the attaching process'sUserSettings(already threaded through the command context) and honors its verbosity. - Delete: any test that asserted submit-time verbosity preservation on attach. Add a test that attach honors the attaching CLI's live verbosity.
- Test:
lib/crates/fabro-config/tests/resolve_cli.rs— addUserSettings::from_layerandUserSettings::resolvecases.
Approach:
- Mirror
ServerSettings's shape. - Attach-verbosity behavior changes: attach honors live settings. Stored
cli.*on a run is inert; wire format unchanged.
Test scenarios:
- Contract:
from_layerandresolvereturn the expected namespaces. - Edge case: Missing
~/.fabro/settings.tomlreturns defaults without erroring. - Behavior:
fabro run attach --verboseagainst a non-verbose submitted run prints verbose output; reversed case prints non-verbose output.
Verification:
cargo build --workspaceandcargo nextest run --workspacepass.grep -rn "resolve_cli_from_file" lib/returns onlyfabro-config/src/internal references plusfabro-cli/tests/it/cmd/create.rs:364(the integration test that asserts the persistedcli.*wire shape — per the KTD visibility policy, this test intentionally keepsresolve_cli_from_filepublic). The--localcall site atcommands/config/mod.rs:121was deleted in Unit 1;user_config.rsmigrates toUserSettings::from_layerin this unit; theattach.rsread is deleted in this unit.
- Unit 6: Delete the
Settingsgod type and its ecosystem
Goal: Remove fabro_types::settings::Settings, the
god-type-returning fabro_config::resolve function, the
fabro_config::load_and_resolve helper, and all their dependent code
— tests and production — without introducing a named replacement
type.
Requirements: R7.
Dependencies: Units 3 (renames) and 4 (context types exist for migrated callers).
Files:
Delete the type and wrapper functions:
- Delete:
lib/crates/fabro-types/src/settings/resolved.rs(theSettingsstruct itself). - Modify:
lib/crates/fabro-types/src/settings/mod.rs— remove theSettingsre-export. - Modify:
lib/crates/fabro-config/src/lib.rs— delete theload_and_resolvepublic helper (it returnsSettings); delete theuse fabro_types::settings::{Settings, SettingsLayer}import (keep theSettingsLayerimport via its ownuse). - Modify:
lib/crates/fabro-config/src/resolve/mod.rs— delete the publicfn resolve(&SettingsLayer) -> Result<Settings>function (the god-type-returning one); remove it frompub use resolve::{...}inlib.rs. Per-namespace resolvers are unaffected.
Migrate the one production caller of fabro_config::resolve:
- Modify:
lib/crates/fabro-workflow/src/operations/create.rs— usesSettingsat ~line 19 (import) and ~lines 289-298 (resolve_settings_tree/combined_labels). The caller at ~line 107 reads bothresolved_settings.server.storage.root(to computestorage_rootfor run persistence) andcombined_labels(&resolved_settings)(project/workflow/run metadata). Replaceresolve_settings_treeso it returns a small struct (or 4-tuple) containingServerNamespace+ProjectNamespace+WorkflowNamespace+RunNamespace, built fromfabro_config::resolve_server_from_file,resolve_project_from_file,resolve_workflow_from_file, andresolve_run_from_fileon the sameSettingsLayer. Update the call site at ~line 107 to read.server.storage.rootoff the returnedServerNamespace, andcombined_labelsto read.metadataoff each ofProjectNamespace/WorkflowNamespace/RunNamespace. All four per-namespace resolvers staypubper Unit 8.
Migrate or delete dependent tests:
- Modify:
lib/crates/fabro-config/tests/resolve_root.rs— tests (including ~line 12 imports and ~line 43 call) currently exercisefabro_config::resolve. Rewrite each assertion against the surviving per-namespace resolvers: e.g.,resolve_root.rs::resolves_root_settings_require_explicit_server_auth_methodsbecomes a test onresolve_server_from_file(&SettingsLayer::default()). Tests that check multi-namespace behavior split into per-namespace assertions. - Modify:
lib/crates/fabro-config/tests/defaults.rs— ~line 94 callsresolve(&SettingsLayer::default()). Rewrite to target the specific per-namespace resolver whose default the test is asserting (likelyresolve_server_from_file, from theserver.auth.methodscheck shown by grep). - Modify:
lib/crates/fabro-cli/tests/it/cmd/config.rs— ~line 160resolved_server_settings_fixturecallsfabro_config::resolve(...)to produce a Settings-shaped fixture for the settings command tests. Replace with a fixture built viaServerSettings::from_layer(&server_settings_layer_fixture())(produces onlyserver+features, matching the newGET /api/v1/settingsresponse shape).
Server.rs branch removed by Unit 7:
lib/crates/fabro-server/src/server.rs:1337(thefabro_config::resolve(&settings)call inside the?view=resolvedbranch ofretrieveServerSettings) is already deleted by Unit 7's handler simplification. No separate action needed here.
Settings_view deletion by Unit 7:
lib/crates/fabro-server/src/settings_view.rs(theredact_resolved_value(&Settings)function plus test at ~line 257) is deleted entirely by Unit 7. No separate action needed here.
Run_manifest.rs: mode parameter only:
- Modify:
lib/crates/fabro-server/src/run_manifest.rs— the existingmaterialize_settings_layer(layers, Some(server_settings), mode)call stays; this unit only removes themodeargument (Unit 2 deleted the enum).materialize_settings_layerstill produces the mergedSettingsLayerstored inPreparedManifest.settings. NoWorkflowSettingsor other context type is constructed here.
Approach:
- The god type has exactly one production consumer outside the
view-toggle machinery (
operations/create.rs); migrate it. Every otherfabro_config::resolvecall site is either a test (migrate or delete) or inside code already being deleted by Unit 7. - No replacement context type: stored-layer readers keep using
per-namespace resolvers per R1; current-config reads are covered by
ServerSettings/UserSettings(Units 4-5). PreparedManifest.settings: SettingsLayeris unchanged; the run_manifest merge pipeline stays intact.
Test scenarios:
- Contract:
operations/create.rslabel aggregation produces the same result as before — per-namespace metadata combined by the replacementresolve_settings_tree. - Migration: rewritten
resolve_root.rsanddefaults.rstests cover the same assertions at the per-namespace resolver level. - Migration: the
fabro settingscommand test intests/it/cmd/config.rsstill passes with theServerSettings-shaped fixture.
Verification:
cargo build --workspaceandcargo nextest run --workspacepass.grep -rn "fabro_types::settings::Settings\b" lib/returns zero hits.grep -rn "fabro_config::resolve\b" lib/returns zero hits (the god-type-returning function is gone; per-namespacefabro_config::resolve_*_from_fileand the new context-type constructors remain).grep -rn "load_and_resolve" lib/returns zero hits.
- Unit 7: Typed
GET /api/v1/settingsserved fromAppState; delete redaction
Goal: The handler returns a typed ServerSettings from AppState in
memory. Delete the redaction machinery. Typed OpenAPI schema via
Deserialize + with_replacement.
Requirements: R5.
Dependencies: Units 4, 6.
Files:
- Modify:
docs/api-reference/fabro-api.yaml:- Remove the
viewquery parameter andX-Fabro-Settings-Viewheader from theretrieveServerSettingsoperation. - Replace
ServerSettings'sadditionalProperties: truewith a typed schema (two fields:server,features) matching the RustServerSettings. - Rename the
RunSettingsschema to a sparse run-settings wire name and update its description to reflect that the endpoint returns the persistedSettingsLayeras-is. - Remove all
redact,redaction,secret subtreeslanguage across the YAML.
- Remove the
- Modify:
lib/crates/fabro-api/build.rs— addwith_replacement(...)entries mapping the OpenAPIServerSettingsschema (and nested types) to the internal Rust types. - Modify:
lib/crates/fabro-types/src/settings/server.rs,features.rs, and reachable child types — addDeserializederives. For types with customSerialize(serialize_socket_addr,InterpString, etc.), implement matching customDeserialize.InterpString::Deserializemust preserve unresolved{{ env.NAME }}templates. - Add:
lib/crates/fabro-api/tests/type-identity + JSON-parity test (per CLAUDE.md'swith_replacementrequirement). - Regenerate:
cargo build -p fabro-api(progenitor runs in build.rs). - Regenerate:
cd lib/packages/fabro-api-client && bun run generate. - Modify:
lib/crates/fabro-server/src/server.rs— theretrieveServerSettingshandler returns a clone ofAppState.server_settings. No view toggle, no response header, no redaction. The/runs/:id/settingshandler serializesrun_spec.settings(theSettingsLayer) directly. - Modify:
lib/crates/fabro-client/src/client.rs(~lines 493-513) — simplifyretrieve_resolved_server_settingsto drop?view=resolvedand the header check. - Delete:
lib/crates/fabro-server/src/settings_view.rs(module + tests). This removesSettingsApiView,SettingsQuery,RESOLVED_VIEW_HEADER_NAME,RESOLVED_VIEW_HEADER_VALUE,redact_for_api,redact_resolved_value. - Modify:
lib/crates/fabro-server/src/demo/mod.rs(~lines 602-621 and ~line 242) — the parallel demo settings handler importssettings_view::{SettingsQuery, SettingsApiView, RESOLVED_VIEW_HEADER_NAME}. Replace with direct serialization of the demo fixture in the new shape. - Delete or rewrite every test still wired to the old view-toggle /
X-Fabro-Settings-Viewcontract:lib/crates/fabro-server/tests/it/api/settings.rs:63and:143(the entireretrieve_server_settings_resolved_view_returns_dense_settings_and_markertest goes away; layer-view test reshapes to the new single-shape response).lib/crates/fabro-server/tests/it/api/runs.rs:116(redaction assertion on/runs/:id/settings— the endpoint now returns the layer unredacted).lib/crates/fabro-server/src/server.rs:7491(inline unit test that issuesGET /settings?view=resolved— rewrite to hit/settingswithout the query parameter, or delete if it was only covering the resolved view branch).lib/crates/fabro-cli/tests/it/cmd/config.rs:923, :1008, :1011(CLI config-command integration tests mock/api/v1/settings?view=resolvedwith anX-Fabro-Settings-View: resolvedresponse header; migrate the mocks to the new single-shape contract — noviewquery param, no custom header, body = typedServerSettingsfixture built viaServerSettings::from_layer(&server_settings_layer_fixture())).
- Modify:
lib/crates/fabro-cli/src/commands/config/mod.rs—rendered_configfetches the newServerSettingsfrom the endpoint and merges withUserSettings::resolve()for a two-section display. - Modify:
apps/fabro-web/app/routes/settings.tsx— consume the newly typedServerSettingsshape explicitly. - Run:
scripts/refresh-fabro-spa.sh.
Approach:
- The endpoint returns the in-memory
AppState.server_settings. No per-request disk read. - OpenAPI conformance (
openapi_conformance.rs) verifies the new single-shape endpoint against the spec.
Test scenarios (canonical only):
- Contract:
GET /api/v1/settingsreturns JSON with exactly two top-level keys,serverandfeatures. The typed shape matches the OpenAPIServerSettingsschema. - Contract: Generated TypeScript client returns a typed object with
serverandfeaturesfields. - Contract:
GET /api/v1/runs/:id/settingsreturns the persistedSettingsLayer(renamed to the sparse run-settings wire name in the spec) directly. - Behavior:
server.listenis present and visible in the main settings response. - Integration: OpenAPI conformance passes.
- Integration:
fabro settingsrenders two sections (user / server) without errors.
Verification:
cargo build -p fabro-apisucceeds (progenitor codegen clean).cargo nextest run -p fabro-serverpasses, including conformance.cd lib/packages/fabro-api-client && bun run generate && bun run typechecksucceeds.- CI's SPA-drift check passes.
- Unit 8: Privatize merge internals
Goal: Minimize the public surface of fabro-config. Context types
and their constructors are the primary API; internal helpers go
pub(crate).
Requirements: R1, R8.
Dependencies: Units 4, 5, 6.
Files:
- Modify:
lib/crates/fabro-config/src/effective_settings.rs—materialize_settings_layer→pub(crate). lib/crates/fabro-config/src/resolve/mod.rs— all sixresolve_*_from_filefunctions (project,workflow,run,cli,server,features) keep their existingpubvisibility. Each has at least one cross-crate consumer; see the KTD visibility decision for the specific sites.- Keep
pub:lib/crates/fabro-config/src/user.rs::load_settings_config—fabro-server/src/serve.rsloads the on-disk settings layer via it. No visibility change. - Modify:
lib/crates/fabro-config/src/lib.rs— settings-entrypoint re-exports only. Narrow scope:- Add:
ServerSettings,UserSettings(the new context types). - Remove: the
Settingstype and the god-type-returningresolvefunction from the public re-export list (both deleted in Unit 6). - Leave untouched: existing re-exports of
Error,Result,Home,expand_tilde,apply_builtin_defaults,defaults_layer,load_settings_*,parse_settings_layer, thestoragemodule, and any other non-settings-entrypoint APIs. These serve cross-workspace consumers (e.g.,fabro-workflow/src/run_lookup.rs,fabro-workflow/src/operations/create.rs) whose API surface is out of scope for this refactor. - Update the settings-entrypoint paragraph in the crate-level doc comment to describe the two context types and their constructors as the primary resolution API. Do not rewrite the full crate doc comment.
- Add:
Approach: Cleanup pass. Flip visibility; compile.
Test scenarios:
- Integration: Downstream crates compile after visibility narrowing.
Verification:
cargo build --workspaceandcargo nextest run --workspacepass.grep -rn "pub fn resolve_\(project\|workflow\|run\|cli\|server\|features\)_from_file" lib/crates/fabro-config/returns six hits (all six per-namespace resolvers remainpubbecause each has cross-crate consumers).bin/dev/check-boundary.shpasses.
System-Wide Impact
- Interaction graph: ~20 current-config call sites migrate from
resolve_server_from_file(&layer)toServerSettings::from_layer(&layer)(or::resolve()). Stored-layer reads unchanged. - Error propagation: Resolver errors flow back unchanged; constructors wrap today's error types.
- API surface:
GET /api/v1/settingsshape changes (single denseServerSettingsserved fromAppState).GET /api/v1/runs/:id/settingsshape unchanged (still the persistedSettingsLayer; OpenAPI schema renamed to the sparse run-settings wire name). - Integration coverage: OpenAPI conformance guards spec/router
alignment. Progenitor regen +
bun run generate+ SPA refresh is the known hygiene. - Unchanged invariants: TOML file format, namespace inventory,
layering precedence, server authority over server-owned fields,
PreparedManifest.settingswire format,fabro_cli::local_serveras the sanctioned CLI gateway to[server.*].
Risks & Dependencies
| Risk | Mitigation |
|---|---|
| The ~57-reference rename (Unit 3) lands incomplete, breaking the build mid-merge | Atomic commit for Unit 3; cargo check --workspace between file batches |
| TypeScript regen drift fails CI's SPA-bundle check | Run scripts/refresh-fabro-spa.sh after bun run generate |
fabro settings snapshot tests drift |
cargo insta pending-snapshots + review per-snapshot before accept |
bin/dev/check-boundary.sh silently passes after the rename |
Script updated in Unit 4 (not Unit 8) so coverage never drops; verified on a throwaway branch |
Documentation / Operational Notes
- OpenAPI description prose for
/api/v1/settingsupdated to reflect the single-shape response. CLAUDE.md's "API workflow" section already describes the OpenAPI → Rust → TypeScript pipeline; no changes needed.- Release notes:
fabro settings --localremoved;GET /api/v1/settingsresponse shape changed; redaction removed.
Sources & References
- Related code:
lib/crates/fabro-config/src/effective_settings.rs,lib/crates/fabro-types/src/settings/resolved.rs,lib/crates/fabro-cli/src/local_server.rs,lib/crates/fabro-server/src/run_manifest.rs,docs/api-reference/fabro-api.yaml. - Adjacent brainstorm:
docs/brainstorms/2026-04-08-settings-toml-redesign-requirements.md(R16 on owner-first namespace boundaries). - Boundary-enforcement commit:
5b1c40764+bin/dev/check-boundary.sh.