From 10a9038dcce1990c9854c2cc41b05415bdc33a21 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Wed, 22 Apr 2026 17:20:35 -0400 Subject: [PATCH 01/13] plan --- ...-refactor-settings-api-entrypoints-plan.md | 1485 ++++++++--------- 1 file changed, 733 insertions(+), 752 deletions(-) diff --git a/docs/plans/2026-04-22-001-refactor-settings-api-entrypoints-plan.md b/docs/plans/2026-04-22-001-refactor-settings-api-entrypoints-plan.md index 85c26ade8..c50b78300 100644 --- a/docs/plans/2026-04-22-001-refactor-settings-api-entrypoints-plan.md +++ b/docs/plans/2026-04-22-001-refactor-settings-api-entrypoints-plan.md @@ -9,338 +9,362 @@ date: 2026-04-22 ## Overview -Replace the current free-function / layer-passing settings API with three -owner-first context types that expose `::resolve*()` constructors. Callers ask -for exactly the settings their context owns, and the merge/layer machinery -becomes an internal concern of `fabro-config`. +Replace the free-function settings API with two owner-first context types +that expose dense, resolved views of current config. The elegance rule: -Today, reading resolved settings means composing free functions -(`fabro_config::resolve_server_from_file`, `resolve_cli_from_file`, etc.) over -a sparse `SettingsLayer`, driven by a three-variant `EffectiveSettingsMode` -enum (`LocalOnly`, `RemoteServer`, `LocalDaemon`). Two of those variants are -effectively dead: `RemoteServer` is only reached by test helpers, and -`LocalOnly` is only reached by `fabro settings --local`. A single god type -`Settings` carries all six namespaces, and callers freely reach into namespaces -they don't own. +- **`SettingsLayer`** is 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` + `features` namespaces. Resolved by the - server at startup; returned by `GET /api/v1/settings`. -- **`UserSettings`** — `cli` + `features` namespaces. Resolved by the CLI - process from its local `~/.fabro/settings.toml`. -- **`WorkflowSettings`** — all six namespaces (`server`, `project`, `workflow`, - `run`, `cli`, `features`). Resolved server-side when a submitted run is - materialized. Replaces today's `Settings` god type. +- **`ServerSettings`** — `server` + `features` namespaces. Derived from + the server's **effective runtime layer** (the post-`apply_runtime_settings` + `SettingsLayer` that folds in CLI overrides like `--storage-dir` and + `--bind`) at startup and whenever hot-reload refreshes the layer. + Held in `AppState` alongside the shared layer itself. + `GET /api/v1/settings` serves the current in-memory view directly — + no per-request disk read, no view toggle, no redaction. +- **`UserSettings`** — `cli` + `features` namespaces. The CLI process + builds one from its own `~/.fabro/settings.toml`. `fabro run attach` + uses the *live* `UserSettings` at attach time. -Each type gets an `::resolve*()` constructor; each hides the `SettingsLayer` -plumbing. The mode enum disappears, the `--local` diagnostic path is deleted, -and `materialize_settings_layer` becomes a single straight-line function -behind `WorkflowSettings::resolve_for_run`. +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 (`SettingsLayer` + -free-function resolvers + mode enum) that is now visibly clunky. Symptoms: +The settings API grew around a layered-config design that's now visibly +clunky: -- Every caller that wants a resolved value first needs a `SettingsLayer`, then - picks the right `resolve_*_from_file` helper. The layer is exposed in the - public API even though few callers should care. -- The `EffectiveSettingsMode` enum has three variants encoding - client/server trust rules, but today's production code path always picks - `LocalDaemon`. `RemoteServer` is test-only (a vestige of the removed - "CLI executes workflows directly" flow), and `LocalOnly` exists only to - back `fabro settings --local`. -- The god type `fabro_types::settings::Settings` carries all six namespaces - indiscriminately. A server-side path that needs `run.*` also gets `cli.*` - in the same struct, obscuring ownership. -- A recent CLI/server boundary cleanup (commit `5b1c40764`) moved all - `[server.*]` reads into `fabro_cli::local_server` so general CLI commands - couldn't accidentally reach them. That boundary is preserved today by - convention + a shell script (`bin/dev/check-boundary.sh`). Type-level - projection — where a CLI command that holds a `UserSettings` *cannot* - read `server.*` at all — is a stronger form of the same invariant. +- Every caller that wants a resolved value first takes a `SettingsLayer`, + then picks the right `resolve_*_from_file` helper. The layer plumbing is + on the public surface even though most callers just want "give me the + server config." +- `EffectiveSettingsMode` has three variants; production code always picks + `LocalDaemon`. The enum is dead ceremony. +- `fabro_types::settings::Settings` is a god type — server-side execution + code needing `run.*` also gets `cli.*` in the same struct. +- The CLI/server trust boundary is enforced by `fabro_cli::local_server` + convention plus `bin/dev/check-boundary.sh`. Type-level projection does + this at compile time. -The settings schema itself is in good shape (see origin-adjacent context -below). This plan is the programmatic API layer over that schema. +The settings schema itself is fine. This plan is the programmatic API over +it. ## Requirements Trace -- **R1.** Outside code calls `ServerSettings::resolve()`, - `UserSettings::resolve()`, or `WorkflowSettings::resolve_for_run(...)` — no - caller outside `fabro-config` constructs a `SettingsLayer` or calls - `resolve_*_from_file` for normal resolution. -- **R2.** Each context type exposes only the namespaces it owns (strict - projection): `ServerSettings` has `server` + `features`; `UserSettings` - has `cli` + `features`; `WorkflowSettings` has all six. -- **R3.** The `EffectiveSettingsMode` enum is deleted. The merge code path - that survives is the current `LocalDaemon` behavior (strip owner domains - from project/workflow; server is authoritative for server-owned fields). -- **R4.** The `fabro settings --local` flag and all supporting CLI-side - filesystem-walking layer assembly is removed. `fabro settings` always - talks to the server. -- **R5.** `GET /api/v1/settings` returns a dense `ServerSettings` payload - (no `view=layer|resolved` toggle, no `X-Fabro-Settings-View` header). - The OpenAPI schema describes the dense shape properly, not - `additionalProperties: true`. +- **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.** `EffectiveSettingsMode` is deleted. One merge path remains. +- **R4.** `fabro settings --local` and its filesystem-walking assembly + are deleted. +- **R5.** `GET /api/v1/settings` returns the server's in-memory + `ServerSettings` as a typed JSON body. No `view=` query param, no + `X-Fabro-Settings-View` header, no disk re-read, no redaction. - **R6.** Today's per-namespace resolved types (`ServerSettings`, `CliSettings`, `ProjectSettings`, `WorkflowSettings`, `RunSettings`, - `FeaturesSettings`) are renamed with a `*Namespace` suffix, freeing the - shorter names for the new context types. -- **R7.** Today's god type `fabro_types::settings::Settings` is deleted; - callers migrate to `WorkflowSettings`, which has the same field set. -- **R8.** `materialize_settings_layer`, `EffectiveSettingsLayers`'s - construction path, and `user::load_settings_config` are no longer part of - the public API of `fabro-config`. (`EffectiveSettingsLayers` the struct - may remain `pub` as the input to `WorkflowSettings::resolve_for_run`, - since only `fabro-server/run_manifest.rs` builds it.) -- **R9.** The CLI/server boundary enforced today by - `fabro_cli::local_server` + `bin/dev/check-boundary.sh` stays intact or - strengthens. In particular, CLI commands that don't need - `server.*` never hold a `ServerSettings` — they hold `UserSettings` - instead. + `FeaturesSettings`) rename with a `*Namespace` suffix, freeing the + short names for context types. Only `ServerSettings` and `UserSettings` + are defined as context types in this PR. +- **R7.** The `Settings` god type (and the public `fabro_config::resolve` + function that returns it, and the `load_and_resolve` helper 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_server` + remains the only sanctioned CLI-side gateway to `[server.*]`; + `bin/dev/check-boundary.sh` updates to cover the new symbols. ## Scope Boundaries - **Not changing** the TOML schema, namespace inventory, merge precedence, - or strip-owner-domains trust rules. The *rules* survive; only the API that - exposes them changes. -- **Not changing** the wire format of persisted run settings (still a - `SettingsLayer` serialized into the run spec). -- **Not adding** new endpoints or new CLI commands. Only the response shape - of `GET /api/v1/settings` changes. -- **Not reworking** the CLI/server boundary enforcement script beyond the - mechanical updates that follow from renames. -- **Not relocating** the `fabro_cli::local_server` module. Its three - boundary helpers (`storage_dir`, `bind_request`, `auth_methods`) continue - to be the sanctioned entry points from CLI lifecycle commands into - `[server.*]`. -- **Out of scope:** any UX redesign of `fabro settings` output beyond the - minimal change required by the narrower server response (see Unit 7). + or owner-domain stripping rules. +- **Not changing** `PreparedManifest.settings` — stays `SettingsLayer`. + Wire format for persisted run settings unchanged. +- **Not migrating** stored-layer readers. `runner.rs:507-508` + (`resolve_run_from_file` / `resolve_server_from_file` on `record.settings`) + and `operations/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 to `HashMap` 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, mode - enum, owner-domain stripping, server-authoritative overrides. -- `lib/crates/fabro-config/src/resolve/` — per-namespace resolve helpers - (`resolve_server_from_file` and siblings). -- `lib/crates/fabro-config/src/user.rs` — loads `~/.fabro/settings.toml` - into a `SettingsLayer`; exposes `default_settings_path()` via - `fabro_util::home::Home::from_env()`. -- `lib/crates/fabro-types/src/settings/resolved.rs` — the god type - `Settings` (six-field struct). -- `lib/crates/fabro-types/src/settings/mod.rs` — re-exports the - per-namespace resolved types that will be renamed. -- `lib/crates/fabro-cli/src/local_server.rs` — the single sanctioned CLI - entry point into `[server.*]`. Today's example of the owner-first - boundary pattern we're lifting to the type system. -- `lib/crates/fabro-cli/src/commands/config/mod.rs` — current - `fabro settings` command; contains the `--local` branch and the - filesystem-walking layer assembly that will be deleted. -- `lib/crates/fabro-server/src/run_manifest.rs` — the sole production - caller of `materialize_settings_layer`; always picks `LocalDaemon`. -- `lib/crates/fabro-server/src/settings_view.rs` — redacts `Settings` for - API responses. Signature retypes as part of the god-type removal. -- `docs/api-reference/fabro-api.yaml` — OpenAPI spec. `GET /api/v1/settings` - at line 1947; `ServerSettings` schema at line 4897 (currently - `additionalProperties: true`, with `view=layer|resolved` query param). - Rust types regenerate via `lib/crates/fabro-api/build.rs`; TypeScript - client regenerates via `cd lib/packages/fabro-api-client && bun run - generate`. -- `lib/crates/fabro-server/tests/it/openapi_conformance.rs` — conformance - test that catches spec/router drift; the refactor must keep this green. -- `bin/dev/check-boundary.sh` — regression guard for the - `fabro_cli::local_server` boundary; needs a pass after the renames to - confirm the grep patterns still work. +- `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.toml` into a `SettingsLayer`. +- `lib/crates/fabro-types/src/settings/resolved.rs` — god type `Settings`. +- `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 settings` + command; contains `--local` branch to delete. +- `lib/crates/fabro-server/src/run_manifest.rs` — sole production caller + of `materialize_settings_layer`. +- `lib/crates/fabro-server/src/settings_view.rs` — deleted by this + refactor. +- `lib/crates/fabro-server/src/server.rs` — `AppState` will hold the + server's `ServerSettings`; 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 (R3), owner-first trust boundaries (R16), - and the paste-anywhere / strict-namespacing rules this refactor now - operationalizes in code. Not a strict origin for this plan — it defined - the file schema; this plan defines the programmatic API over it — but - the motivation is directly downstream of R16. -- Recent cleanup commit `7cb6c65d5 Remove dev-token minting from server - start` and `77fc77872 Gate dev-token handling on explicit auth methods` - are the most recent touches in this area; they didn't change the settings - API shape. +- `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 -To size the migration: - - `resolve_server_from_file` outside `fabro-config`: ~20 call sites across - `fabro-cli` (`local_server.rs`, `commands/exec.rs`, `commands/install.rs`, - `commands/pr/mod.rs`, `commands/run/attach.rs`, `commands/run/runner.rs`), - `fabro-server` (`serve.rs`, `run_manifest.rs`, `install.rs`, `server.rs`, - `jwt_auth.rs`), and `fabro-install/src/lib.rs`. -- `resolve_cli_from_file` outside `fabro-config`: 3 call sites — - `fabro-cli/src/user_config.rs`, `fabro-cli/src/commands/run/attach.rs`, - `fabro-cli/src/commands/config/mod.rs` (`--local` path, will be deleted). -- `materialize_settings_layer`: 3 call sites — - `fabro-config/src/lib.rs` (internal `load_and_resolve`), - `fabro-cli/src/commands/config/mod.rs` (`--local`, will be deleted), - `fabro-server/src/run_manifest.rs` (becomes - `WorkflowSettings::resolve_for_run`). -- References to per-namespace resolved type names that will be renamed to - `*Namespace`: ~57 across the workspace. + `fabro-cli`, `fabro-server`, `fabro-workflow`, `fabro-install`. Re-run + `grep -rn "resolve_server_from_file" lib/` before Unit 4 to confirm the + full set. +- `resolve_cli_from_file` outside `fabro-config`: `user_config.rs`, + `attach.rs` (deleted in Unit 5). +- `materialize_settings_layer` outside tests: `run_manifest.rs` only. +- Per-namespace type name references to rename: ~57. ### Institutional Learnings -- No prior `docs/solutions/` entries cover this area; the refactor starts - from current code and the adjacent brainstorm. +- No prior `docs/solutions/` entries cover this area. ## Key Technical Decisions -- **Four types collapse to three.** An earlier sketch carried a - `LocalWorkflowSettings` type to serve `fabro settings --local`. Deleting - the `--local` flag eliminates the only caller and lets - `LocalWorkflowSettings` go away entirely. **Rationale:** the - diagnostic value of `--local` is narrow (first-run sanity checks, - debugging a server that can't start), mostly covered by reading - `~/.fabro/settings.toml` directly or by a future purpose-built - `fabro doctor`-style command. The simplification — one fewer type, no - mode enum, one merge path — dominates. -- **`UserSettings`, not `CliSettings`, for the CLI-process context type.** - Reflects both the *source* (`~/.fabro/settings.toml` is the user's file) - and the *scope* (personal/machine preferences). Also avoids the collision - with the now-renamed `cli.*` namespace type (`CliNamespace`) that lives - inside it. -- **`WorkflowSettings` includes `cli.*`.** The server does not read - `cli.*` for any decision (audited: zero reads in `fabro-server`, - `fabro-workflow`, `fabro-agent`). But `cli.*` is meaningfully *stored - per-run* so that `fabro run attach` can reproduce submit-time - `output.verbosity`. Treating `cli.*` inside `WorkflowSettings` as an - opaque snapshot of client state at submit time, not as a server input, - matches this usage and lets the CLI round-trip the info it needs. -- **`features.*` appears on all three context types.** Feature flags are - cross-cutting; both the CLI process and the server consult them, and a - run carries its own snapshot. Small cost for a uniform rule. -- **Delete `EffectiveSettingsMode` entirely, not just `RemoteServer`.** - After `LocalOnly` (Unit 1) and `RemoteServer` (Unit 2) are both removed, - the only remaining variant is `LocalDaemon`. A one-variant enum has no - value; the function loses its `mode` parameter and becomes a single - straight-line implementation. -- **`GET /api/v1/settings` collapses to one dense shape.** The current - `view=layer|resolved` toggle and `X-Fabro-Settings-View` header go away. - The endpoint returns a typed `ServerSettings` (schema properly described, - not `additionalProperties: true`). **Rationale:** the layer shape is an - internal representation, not a client contract; once internal code doesn't - pass layers around, the API shouldn't either. -- **`fabro settings` composes local + remote views.** Without `--local` - and with a narrower server response, the CLI constructs its display from - both sides: local `UserSettings::resolve()` (for `cli` + local `features`) - and the server's `ServerSettings` (for `server` + server's `features`). - This is more truthful than today's behavior, where the server synthesizes - a cross-namespace view that conflates its `cli.*` with the client's. -- **`EffectiveSettingsLayers` stays `pub`** (in `fabro-config`), since - `fabro-server/run_manifest.rs` is the one external caller that needs to - build one to pass to `WorkflowSettings::resolve_for_run`. Its - construction is simple enough that a builder isn't warranted. -- **Placement of context types.** New context types live in `fabro-types` - alongside existing namespace types (keeping the type definitions in one - crate and the resolution logic in `fabro-config`). `impl` blocks for - `::resolve*()` live in `fabro-config` via extension traits or inherent - impls in the resolution module — whichever is mechanically simplest - given orphan rules; this is an implementation-time call. +- **Layer and view are distinct roles.** `SettingsLayer` is 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/settings` serves `AppState` in memory.** The server + builds its `ServerSettings` at startup from the *effective runtime + layer* (post-`apply_runtime_settings`, which folds in CLI overrides + like `--storage-dir` and `--bind`) and stores it in `AppState`. + Hot-reload keeps the derived view in sync via + `state.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 `WorkflowSettings` in this PR.** An earlier draft introduced a + `WorkflowSettings` context type as the dense resolved view for + server-side execution, but the refactor has no production caller that + needs it: `operations/create.rs` migrates to per-namespace resolvers + (legitimate stored-layer read), `server.rs:1337` is deleted wholesale + as part of Unit 7's view-toggle removal, and `settings_view.rs` + disappears entirely. Introducing `WorkflowSettings::resolve_for_run` + with 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.settings` stays `SettingsLayer` + (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 attach` calls + `UserSettings::resolve()` on the attaching process's config. + Submit-time `cli.*` 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.toml` and delegates. `UserSettings` has the same + pair. No `resolve_from(path)`: `--config` handling is an existing + `serve.rs` concern that produces the on-disk settings layer before + `apply_runtime_settings` runs; the new `ServerSettings::from_layer` + is invoked on the resulting *effective runtime layer*, not on a + fresh disk read. + +- **Delete `EffectiveSettingsMode`.** Production always picks + `LocalDaemon`; the enum is ceremony. `materialize_settings_layer` + becomes 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 as `InterpString`, not to reintroduce redaction. + +- **`fabro settings` composes local + server.** The CLI renders + `UserSettings::resolve()` (local) plus the server's `ServerSettings` + (fetched via the endpoint). Two sections. + +- **Typed OpenAPI schema via `Deserialize` + `with_replacement`.** + Aligned with CLAUDE.md's API type ownership doctrine. The `ServerSettings` + OpenAPI schema describes the internal Rust type directly; progenitor + reuses it via `with_replacement`. Reachable namespace types gain + `Deserialize` derives; types with custom `Serialize` get matching custom + `Deserialize` (notably `InterpString`, 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-namespace `resolve_features_from_file` reads on stored + layers. + +- **Context types live in `fabro-config`.** Inherent `impl` blocks must + live with the type per Rust's orphan rules. `fabro-types` keeps only + per-namespace shape types. + +- **Per-namespace resolver visibility, decided once:** all six + `resolve_*_from_file` helpers stay `pub`, because each has at least + one cross-crate consumer: + - `resolve_server_from_file` — `runner.rs:508` reads `server.*` off + stored `record.settings`. + - `resolve_run_from_file` — `runner.rs:507`, `run_manifest.rs:369`, + `operations/create.rs` read `run.*` off stored layers. + - `resolve_project_from_file`, `resolve_workflow_from_file` — + `operations/create.rs` reads their `.metadata` for label + aggregation. + - `resolve_cli_from_file` — `fabro-cli/tests/it/cmd/create.rs:364` + integration test asserts the persisted `cli.*` wire shape. + - `resolve_features_from_file` — `fabro-server/src/server.rs:1414` + reads `features.session_sandboxes`; the + `fabro-config/tests/resolve_features.rs` integration test also + depends on it. +- **Other `fabro-config` visibility:** + - `pub(crate)`: `materialize_settings_layer` — internal helper; the + merge step stays inside `run_manifest.rs`'s call site, consumed only + via the public `SettingsLayer` output. + - `pub`: `user::load_settings_config` — external caller in + `fabro-server/src/serve.rs` loads the on-disk config layer before + `apply_runtime_settings`. + - `pub(crate)`: `EffectiveSettingsLayers` — no external consumer + remains after `WorkflowSettings` is dropped (`run_manifest.rs` + builds layers internally for its own `materialize_settings_layer` + call). ## Open Questions ### Resolved During Planning -- *Should `cli.*` be on `WorkflowSettings`?* Yes — see "Key Technical - Decisions." Snapshot semantics for attach, not a server input. -- *Should `features.*` duplicate across all three types?* Yes. Uniform - rule; low cost; all three contexts consult feature flags. -- *Should we keep a `fabro settings --local` equivalent for diagnostics?* - No. Deleted outright; re-introduce as a targeted `fabro doctor`-style - command later if needed. -- *Does the OpenAPI response narrow?* Yes. `GET /api/v1/settings` returns - only `server` + `features` namespaces. The CLI composes the rest locally. +- *Introduce a `WorkflowSettings` context type?* No. The refactor has + no production caller that needs a dense multi-namespace view — + `operations/create.rs` migrates 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/settings` re-read from disk per request?* No. Serves + `AppState.server_settings`. +- *`PreparedManifest.settings` retype?* No. Stays `SettingsLayer`. +- *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 -- **Exact placement of `impl ServerSettings { fn resolve() ... }` etc.** - Likely in `fabro-config` to keep resolution logic colocated with the - merge code, but orphan rules may push them back into `fabro-types`. - Mechanical call at implementation time. -- **`fabro settings` output layout.** The command currently dumps a flat - JSON/YAML tree with all six namespaces. Post-refactor it renders two - sections (user / server). The exact shape is a small UX call; snapshot - tests drive the answer during implementation. -- **Whether `resolve_run_from_file` and siblings survive.** Callers like - `fabro-cli/src/commands/run/attach.rs` and - `fabro-cli/src/commands/run/runner.rs` currently pluck individual - namespaces out of a stored `SettingsLayer`. They may migrate to - `WorkflowSettings::from_stored_layer(layer, server)` or keep using - per-namespace resolvers (now `pub(crate)`-visible through an adapter). - Resolved during Unit 6 based on which reads cleanly; either is fine. -- **Whether `EffectiveSettingsLayers::new` should gain a fluent builder.** - Single caller today; keep the existing positional constructor unless - Unit 6 surfaces a reason. +- `fabro settings` output layout (two-section render — labels, ordering, + field suppression). Snapshot tests drive. ## High-Level Technical Design -> *This illustrates the intended approach and is directional guidance for -> review, not implementation specification. The implementing agent should -> treat it as context, not code to reproduce.* +> *Directional guidance, not implementation specification.* -**Type inventory after refactor:** +**Type inventory:** ``` -// Per-namespace resolved types (renamed) -ServerNamespace // was ServerSettings -CliNamespace // was CliSettings -ProjectNamespace // was ProjectSettings -WorkflowNamespace // was WorkflowSettings -RunNamespace // was RunSettings -FeaturesNamespace // was FeaturesSettings +// Per-namespace dense types (renamed from today's *Settings) +ServerNamespace, CliNamespace, ProjectNamespace, +WorkflowNamespace, RunNamespace, FeaturesNamespace -// Context types (new) -ServerSettings { server: ServerNamespace, features: FeaturesNamespace } -UserSettings { cli: CliNamespace, features: FeaturesNamespace } -WorkflowSettings { server, project, workflow, run, cli, features } - // same field set as today's `Settings` god type +// Context types (new; all in fabro-config) +ServerSettings { server: ServerNamespace, features: FeaturesNamespace } +UserSettings { cli: CliNamespace, features: FeaturesNamespace } ``` -**Public entry points (sketch — directional):** +**Public constructors:** ``` impl ServerSettings { - fn resolve() -> Result; // reads ~/.fabro/settings.toml - fn resolve_from(path: &Path) -> Result; // honors --config override + fn from_layer(&SettingsLayer) -> Result; + fn resolve() -> Result; } impl UserSettings { - fn resolve() -> Result; - fn resolve_from(path: &Path) -> Result; -} - -impl WorkflowSettings { - fn resolve_for_run( - layers: EffectiveSettingsLayers, - server: &ServerSettings, - ) -> Result; + fn from_layer(&SettingsLayer) -> Result; + fn resolve() -> Result; } ``` -**Call-site topology, before and after:** +**Role separation:** ``` -Before After -────── ───── -user::load_settings_config(path) ─► ServerSettings::resolve_from(path) - → resolve_server_from_file(&layer) +SettingsLayer — sparse transport/storage. Unchanged. +ServerSettings — dense view. AppState holds one; API serves it. +UserSettings — dense view. CLI process builds one. +``` -user::load_settings_config(None) ─► UserSettings::resolve() - → resolve_cli_from_file(&layer) +**Call-site topology:** -EffectiveSettingsLayers + layers ─► WorkflowSettings::resolve_for_run( - materialize_settings_layer( layers, &server_settings) - layers, Some(server), LocalDaemon) +``` +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:** @@ -348,13 +372,13 @@ EffectiveSettingsLayers + layers ─► WorkflowSettings::resolve_for_run( ```mermaid flowchart TB U1[Unit 1: Delete --local] - U2[Unit 2: Delete RemoteServer + mode enum] - U3[Unit 3: Rename per-namespace types] - U4[Unit 4: ServerSettings::resolve] - U5[Unit 5: UserSettings::resolve] - U6[Unit 6: WorkflowSettings::resolve_for_run] - U7[Unit 7: Narrow GET /api/v1/settings] - U8[Unit 8: Privatize merge internals] + 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 @@ -362,23 +386,17 @@ flowchart TB U3 --> U5 U3 --> U6 U4 --> U7 + U6 --> U7 U4 --> U8 U5 --> U8 U6 --> U8 - U6 --> U7 ``` -`U4`, `U5`, and `U6` can progress in parallel once `U3` lands. `U7` -depends on `U4` and `U6` (the server handler for `GET /api/v1/settings` -needs `ServerSettings::resolve`, and the CLI's `fabro settings` command -consumes both the remote `ServerSettings` and local `UserSettings`). - ## Implementation Units - [ ] **Unit 1: Delete `fabro settings --local` and the `LocalOnly` path** -**Goal:** Remove `--local` flag, its helpers, and the `LocalOnly` variant -of `EffectiveSettingsMode`. `fabro settings` always talks to the server. +**Goal:** Remove `--local`, its helpers, and the `LocalOnly` variant. **Requirements:** R4. @@ -390,618 +408,585 @@ of `EffectiveSettingsMode`. `fabro settings` always talks to the server. - Modify: `lib/crates/fabro-cli/src/commands/config/mod.rs` — delete `local_settings_value`, `workflow_and_project_layers`, `config_layers`, `strip_nulls`, `resolve_local_settings_value`, `render_resolve_errors`, - and the `args.local` / `args.workflow` branches in `rendered_config`. -- Modify: `lib/crates/fabro-cli/src/main.rs` — remove the `args.local` - snapshot-test assertions (lines ~1072, ~1097). + and the `args.local` / `args.workflow` branches. +- Modify: `lib/crates/fabro-cli/src/main.rs` — remove `args.local` + snapshot-test assertions. - Modify: `lib/crates/fabro-config/src/effective_settings.rs` — remove `LocalOnly` variant and its match arm. -- Modify: `lib/crates/fabro-config/tests/resolve_root.rs` — drop the - `LocalOnly` test case (or migrate to `LocalDaemon` if still useful). -- Modify: `lib/crates/fabro-config/src/effective_settings.rs` (tests - module) — drop the `LocalOnly` test at line ~218/~307. -- Modify: CLI snapshot fixtures for `fabro settings --local` if any exist - under `lib/crates/fabro-cli/tests/`. -- Test: `lib/crates/fabro-cli/tests/it/cmd/config.rs` (or wherever the - settings-command tests live) — replace `--local` cases with - through-server cases. +- Modify: `lib/crates/fabro-config/tests/resolve_root.rs` and the + `effective_settings.rs` tests module — drop `LocalOnly` cases. +- Delete: `lib/crates/fabro-cli/tests/it/cmd/config.rs` tests 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 WORKFLOW` was only meaningful together with `--local` (the - current code bails if it's passed alone). Both flags go. -- Keep the `fabro settings` command working via the existing - `ctx.server().retrieve_resolved_server_settings()` path. That path - changes shape in Unit 7, not here. -- `LocalOnly` removal is purely dead-code deletion once the CLI stops - calling it; safe to do in the same unit. - -**Patterns to follow:** -- Existing CLI argument deletion pattern (check recent commits for - precedent on removed flags). +- `--workflow WORKFLOW` was only meaningful with `--local`. Both go. +- `fabro settings` continues to work via the server path (reshaped in + Unit 7). +- `LocalOnly` removal is dead-code deletion once the CLI stops calling it. **Test scenarios:** -- *Happy path:* `fabro settings` (no args) with a running server returns - the server's resolved settings. Unchanged from pre-refactor behavior - modulo Unit 7's narrowing. -- *Error path:* `fabro settings --local` no longer parses; - the CLI's usage error mentions the flag is gone (or the help output - omits it, which is enough). -- *Error path:* `fabro settings WORKFLOW_ARG` no longer parses — the - positional was only meaningful with `--local`. +- *Contract:* `fabro settings` with a running server returns the + server's settings (shape reshaped in Unit 7). +- *Parser-level:* `fabro settings --local` fails to parse. `fabro settings + WORKFLOW_ARG` fails to parse. **Verification:** -- `cargo build --workspace` succeeds. -- `cargo nextest run -p fabro-cli` passes; snapshot tests for - `fabro settings` no longer reference `--local`. -- `grep -rn "LocalOnly\|args\.local\|SettingsArgs.*local" lib/` returns - only deliberate references (e.g., doc comments), not code paths. +- `cargo build --workspace` and `cargo nextest run -p fabro-cli` pass. +- `grep -rn "LocalOnly\|args\.local" lib/` returns only deliberate doc + references. --- -- [ ] **Unit 2: Delete `EffectiveSettingsMode::RemoteServer` and collapse the enum** +- [ ] **Unit 2: Delete `EffectiveSettingsMode` entirely** -**Goal:** Migrate test-only `RemoteServer` calls to `LocalDaemon`, then -delete the enum entirely. `materialize_settings_layer` loses its `mode` -parameter. +**Goal:** Kill the mode enum. `materialize_settings_layer` becomes a +single straight-line function. **Requirements:** R3. -**Dependencies:** Unit 1 (removes `LocalOnly`; after both are gone, only -`LocalDaemon` remains). +**Dependencies:** Unit 1. **Files:** - Modify: `lib/crates/fabro-config/src/effective_settings.rs` — delete - `EffectiveSettingsMode` enum, remove `mode` parameter from - `materialize_settings_layer`, flatten the match into a single code path - (strip owner domains, merge, apply `apply_local_daemon_overrides`). + `EffectiveSettingsMode`; remove `mode` parameter from + `materialize_settings_layer`; flatten to a single path (strip owner + domains, merge, apply server authority). Rename + `apply_local_daemon_overrides` → `enforce_server_authority`. Delete + `apply_server_defaults` (no surviving caller). - Modify: `lib/crates/fabro-config/src/lib.rs` — update internal - `load_and_resolve` to match new signature. + `load_and_resolve` to match. - Modify: `lib/crates/fabro-server/src/run_manifest.rs` — call `materialize_settings_layer(layers, Some(server_settings))` without a - mode; remove the `local_daemon_mode` plumbing if it was solely driving - the enum choice. If `local_daemon_mode` is used elsewhere in AppState - for non-settings purposes, leave that alone. + mode. - Modify: `lib/crates/fabro-server/src/server.rs` — remove the - `local_daemon_mode` field from `AppState` (~line 581) and - `AppStateConfig` (~line 598); update the three handler call sites - that pass it into `run_manifest::prepare_manifest_with_mode` - (`create_run` ~line 4104, `run_preflight` ~line 4212, - `render_graph_from_manifest` ~line 4242); update helper function - signatures (~lines 2515, 2541, 2641, 2708); remove the test helper - at ~line 2575. Can be phrased as "remove the `local_daemon_mode` - field and all its threading." -- Modify: `lib/crates/fabro-config/src/effective_settings.rs` (tests - module) — replace `RemoteServer` test at line ~378 with a - `LocalDaemon`-style assertion, or delete if redundant. + `local_daemon_mode` field from `AppState` and `AppStateConfig`; 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.rs` tests module — delete the + `RemoteServer` test case (`cli_and_server_domains_from_fabro_toml_are_inert_under_remote_mode`). + Verify its unique assertions are covered by the surviving `LocalDaemon` + test at ~line 437; if any coverage is unique, fold that assertion into + the surviving test. **Approach:** -- Verify `local_daemon_mode` in `fabro-server` is only used for settings - mode selection before deleting it. Preliminary inspection says yes - (`serve.rs:523` sets it true, tests set it false, `run_manifest.rs` - consumes it only to pick between `RemoteServer` and `LocalDaemon`). -- `apply_server_defaults` (the RemoteServer-specific code path) should be - deletable along with the enum — no surviving caller. -- Keep `apply_local_daemon_overrides` (now the only override strategy); - consider renaming it to `apply_server_overrides` since "LocalDaemon" is - about to stop being a named concept. - -**Patterns to follow:** -- `strip_owner_domains` and `apply_local_daemon_overrides` stay as - private helpers inside `effective_settings`. +- `apply_server_defaults` (the `RemoteServer` code path) has no surviving + caller; delete along with the enum. +- Keep `strip_owner_domains` and `enforce_server_authority` as private + helpers. **Test scenarios:** -- *Happy path:* `materialize_settings_layer` with representative layers - produces the same output as the pre-refactor `LocalDaemon` invocation - (server owns storage/scheduler/artifacts/web; project/workflow - `[cli]/[server]` stripped). -- *Edge case:* When `server_settings` is `Some(empty_layer)`, the output - preserves client values and doesn't panic. +- *Contract:* `materialize_settings_layer` with representative layers + produces the same output as the pre-refactor `LocalDaemon` invocation. +- *Edge case:* `Some(empty_layer)` for server settings preserves client + values and doesn't panic. **Verification:** -- `cargo build --workspace` succeeds. -- `cargo nextest run -p fabro-config` passes. -- `grep -rn "EffectiveSettingsMode\|RemoteServer\|LocalDaemon" lib/` - returns no hits. +- `cargo build --workspace` and `cargo nextest run -p fabro-config` pass. +- `grep -rn "EffectiveSettingsMode\|RemoteServer\|LocalDaemon\|local_daemon_mode" lib/` + returns zero hits. --- -- [ ] **Unit 3: Rename per-namespace resolved types to `*Namespace` suffix** +- [ ] **Unit 3: Rename per-namespace resolved types to `*Namespace`** -**Goal:** Free the short names (`ServerSettings`, `CliSettings`, etc.) for -the new context types by renaming today's per-namespace types. +**Goal:** Free the short names for context types. **Requirements:** R6. -**Dependencies:** Units 1 and 2 (so the file churn touches stable code). +**Dependencies:** Units 1, 2. **Files:** -- Modify: `lib/crates/fabro-types/src/settings/mod.rs` — rename the - re-exports. -- Modify: `lib/crates/fabro-types/src/settings/server.rs` — rename - `ServerSettings` to `ServerNamespace`; adjust references. -- Modify: `lib/crates/fabro-types/src/settings/cli.rs` — rename - `CliSettings` to `CliNamespace`; propagate. -- Modify: `lib/crates/fabro-types/src/settings/project.rs`, - `workflow.rs`, `run.rs`, `features.rs` — same pattern. +- 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 - `Settings` god-type field types. + god-type field types (the god type itself goes away in Unit 6). - Modify: `lib/crates/fabro-config/src/resolve/mod.rs` and siblings — - update return types of `resolve_server_from_file` (now `ServerNamespace`) - and similar; function names unchanged in this unit. -- Modify: all ~57 other references across the workspace (mechanical). + update return types. +- Modify: ~57 other references across the workspace (mechanical). -**Approach:** -- Type-only rename. Function names, field names, and serde wire format - are unchanged. -- Use `cargo check --workspace` after each rename to catch missed sites. -- `replace_all` is safe for most grep hits; verify nothing outside the - settings domain shares these names (prior grep confirmed no collisions). +**Approach:** Type-only rename. Function names, field names, and serde +wire format unchanged. Use `cargo check --workspace` between file batches. -**Execution note:** Mechanical rename, suitable for `Execution target: -external-delegate` if desired. +**Execution note:** Mechanical; suitable for `Execution target: +external-delegate`. -**Patterns to follow:** -- Recent refactors in this repo tend to land renames in one commit per - type-family when the blast radius is contained. Single unit per this - plan is fine. - -**Test scenarios:** -- *Happy path:* All existing tests continue to pass — this unit changes - no behavior. The compiler is the primary witness. +**Test scenarios:** All existing tests continue to pass. Compiler is the +primary witness. **Verification:** - `cargo build --workspace` and `cargo nextest run --workspace` pass. - `grep -rn "fabro_types::settings::\(ServerSettings\|CliSettings\|ProjectSettings\|WorkflowSettings\|RunSettings\|FeaturesSettings\)\b" lib/` - returns no hits (all references now use `*Namespace`). -- `cargo +nightly-2026-04-14 clippy --workspace --all-targets - -- -D warnings` clean. + returns zero hits. +- Clippy clean. --- -- [ ] **Unit 4: Introduce `ServerSettings` context type and `::resolve*()` constructors** +- [ ] **Unit 4: Introduce `ServerSettings`; `AppState` holds one** -**Goal:** Add the new `ServerSettings { server, features }` type with -constructors, and migrate all existing `resolve_server_from_file` callers. +**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 / Modify: `lib/crates/fabro-types/src/settings/context.rs` - (new module) — define `ServerSettings` struct. -- Modify: `lib/crates/fabro-types/src/settings/mod.rs` — re-export - `ServerSettings` at the crate root. -- Modify: `lib/crates/fabro-config/src/resolve/mod.rs` or a new - `context.rs` — add `impl ServerSettings { fn resolve(); fn resolve_from(path); }`. -- Modify: every caller of `resolve_server_from_file` (~20 sites across - `fabro-cli`, `fabro-server`, `fabro-install`). -- Modify: `lib/crates/fabro-workflow/src/operations/start.rs` — the - `resolve_server_from_file` call at ~line 384 migrates along with the - other sites. (Earlier call-site inventory missed `fabro-workflow`; - re-run `grep -rn "resolve_server_from_file" lib/` before Unit 4 to - confirm the full set, including test harnesses under - `fabro-server/tests/it/` and `fabro-cli/tests/it/support/`.) -- Modify: `lib/crates/fabro-config/src/user.rs` — make - `load_settings_config` `pub(crate)` (it becomes an implementation detail - of `ServerSettings::resolve_from`). -- Modify: `bin/dev/check-boundary.sh` — add grep patterns for the new - `ServerSettings::resolve` / `ServerSettings::resolve_from` symbols so - the regression guard covers the migration window, not just the - pre-rename state. (Moved here from Unit 8 so the script never goes - blind to the new symbol between Unit 4 and Unit 8.) -- Test: `lib/crates/fabro-config/tests/resolve_server.rs` — add tests - for `ServerSettings::resolve_from(path)` that parallel existing - per-function tests. +- Create: `lib/crates/fabro-config/src/context.rs` — `ServerSettings { + server: ServerNamespace, features: FeaturesNamespace }`. Inherent + `from_layer(&SettingsLayer) -> Result` and `resolve() -> + Result`. +- Modify: `lib/crates/fabro-config/src/lib.rs` — re-export `ServerSettings`. +- Modify: `lib/crates/fabro-server/src/serve.rs` (~lines 478-482) — + startup already computes + `effective_settings = apply_runtime_settings(&disk_settings, &args, &data_dir)` + (the post-runtime-override layer that folds in `--storage-dir`, + `--bind`, etc.). Add + `let server_settings = ServerSettings::from_layer(&effective_settings)?;` + alongside the existing `resolved_server_settings = + resolve_server_settings(&effective_settings)?` line, and thread + `server_settings` into `AppState`. Derive from the effective runtime + layer, not from a fresh `~/.fabro/settings.toml` read; a fresh read + would drop the CLI overrides. +- Modify: `lib/crates/fabro-server/src/serve.rs` (~line 600, the + hot-reload path) — when `apply_runtime_settings(...)` is rerun and + `state_for_poll.replace_settings(effective)` is called, also refresh + `AppState.server_settings` from the new layer so the typed view + stays in sync with the `Arc>`. +- Modify: `lib/crates/fabro-server/src/server.rs` — add + `server_settings: Arc` (or equivalent interior + mutability for hot-reload refresh) to `AppState`. Extend + `replace_settings(...)` to also update the derived `ServerSettings` + so both are consistent after reload. +- Modify: every current-config caller of `resolve_server_from_file` (~20 + sites across `fabro-cli`, `fabro-server`, `fabro-workflow`, + `fabro-install`) — switch to `ServerSettings::from_layer(&layer)` where + a layer is already in hand, or `ServerSettings::resolve()` where + defaults apply. Stored-layer readers (`runner.rs:507-508`) stay on + `resolve_server_from_file`. +- Modify: `bin/dev/check-boundary.sh` — add grep patterns for + `ServerSettings::resolve` and `ServerSettings::from_layer` so the + regression guard covers the migration window. +- Test: `lib/crates/fabro-config/tests/resolve_server.rs` — add tests for + `ServerSettings::from_layer` and `ServerSettings::resolve`. **Approach:** -- `ServerSettings::resolve()` reads `Home::from_env().user_config()`. -- `ServerSettings::resolve_from(path)` honors an explicit override (the - `--config` flag case). -- Internally both call the same `user::load_settings_config` → - `resolve_server_from_file(&layer)` pipeline; the free function - `resolve_server_from_file` stays available as `pub(crate)` for - Unit 6 / Unit 8 to handle. -- Mechanical migration of ~20 call sites. Most are a one-line - substitution (`fabro_config::resolve_server_from_file(&layer)` → - `ServerSettings::resolve_from(path)` or similar). - -**Patterns to follow:** -- Existing inherent-impl pattern in `fabro-types` for dense types. -- `fabro_cli::local_server` continues to be the only module importing - `ServerSettings` in the CLI crate (with a narrow API surface); confirm - `bin/dev/check-boundary.sh` still passes. +- `from_layer` invokes the per-namespace resolver and wraps the result. + Single primitive. +- `resolve()` loads the default `~/.fabro/settings.toml` and delegates + to `from_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 by + `apply_runtime_settings`, which a fresh disk read doesn't see. +- Server startup: after `serve.rs` builds `effective_settings = + apply_runtime_settings(...)`, it calls + `ServerSettings::from_layer(&effective_settings)` and stores the + result in `AppState`. Hot-reload (`state.replace_settings(...)`) also + refreshes that derived view. +- `AppState.server_settings` is the server's canonical current-config + value. Handlers read it, not disk. It's always in sync with the + layer in `AppState`'s `RwLock`. **Test scenarios:** -- *Happy path:* `ServerSettings::resolve_from(valid_path)` returns the - same `ServerNamespace` + `FeaturesNamespace` values that - `resolve_server_from_file(&parsed_layer)` would have returned. -- *Happy path:* `ServerSettings::resolve()` with `$FABRO_HOME` set points - to a temp dir successfully loads that directory's `settings.toml`. -- *Error path:* `ServerSettings::resolve_from(nonexistent_path)` returns - the file-not-found error (not a panic, matches current behavior). -- *Error path:* `ServerSettings::resolve_from(path_with_invalid_toml)` - returns a parse error that preserves today's error formatting. -- *Integration:* the server's startup path (`serve.rs:353`) boots - successfully using `ServerSettings::resolve_from`. +- *Contract:* `from_layer(&layer)` returns the same data as the old + free function on the same layer. +- *Contract:* `resolve()` with `$FABRO_HOME` set to a temp dir loads + that directory's `settings.toml`. +- *Integration:* Server startup derives `AppState.server_settings` + from the effective runtime layer (post-`apply_runtime_settings`). + A startup with `--storage-dir /tmp/foo` produces + `AppState.server_settings.server.storage` reflecting `/tmp/foo`, + confirming CLI overrides flow through to the typed view. +- *Integration:* Hot-reload (triggered via the existing + `state.replace_settings(effective)` pathway) refreshes + `AppState.server_settings` so the typed view reflects the updated + layer. **Verification:** - `cargo build --workspace` and `cargo nextest run --workspace` pass. - `grep -rn "resolve_server_from_file" lib/` returns only - `fabro-config/src/` internal references. -- `bin/dev/check-boundary.sh` still passes. -- `bin/dev/check-boundary.sh` catches a deliberate unsanctioned - `ServerSettings::resolve*` import outside the allowlist (confirm by - temporarily introducing one in a throwaway branch before merging). + `fabro-config/src/` and stored-layer reader call sites. +- `bin/dev/check-boundary.sh` still passes; verify on a throwaway branch + that it catches a deliberate unsanctioned `ServerSettings::resolve*` + import. --- -- [ ] **Unit 5: Introduce `UserSettings` context type and `::resolve*()` constructors** +- [ ] **Unit 5: Introduce `UserSettings`; attach uses live config** -**Goal:** Add `UserSettings { cli, features }` with constructors; migrate -`resolve_cli_from_file` callers outside the `--local` path (already -deleted in Unit 1). +**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-types/src/settings/context.rs` — add - `UserSettings` next to `ServerSettings`. -- Modify: `lib/crates/fabro-config/src/resolve/mod.rs` (or the - context module) — add `impl UserSettings { fn resolve(); - fn resolve_from(path); }`. -- Modify: `lib/crates/fabro-cli/src/user_config.rs` — replace the - `resolve_cli_from_file` usage with `UserSettings::resolve_from`. -- Modify: `lib/crates/fabro-cli/src/commands/run/attach.rs` — the - call at line ~90 reads `cli.output.verbosity` from a *stored* run - layer (not the user's live config). Decide in Unit 6 whether this - migrates to a `WorkflowSettings`-shaped access or keeps a - `pub(crate)` per-namespace resolver. Leave this call site alone in - Unit 5. +- Modify: `lib/crates/fabro-config/src/context.rs` — add `UserSettings { + cli: CliNamespace, features: FeaturesNamespace }`. Inherent + `from_layer(&SettingsLayer)` and `resolve()`. +- Modify: `lib/crates/fabro-config/src/lib.rs` — re-export `UserSettings`. +- Modify: `lib/crates/fabro-cli/src/user_config.rs` — replace + `resolve_cli_from_file` usage with `UserSettings::from_layer` or + `UserSettings::resolve()`. +- Modify: `lib/crates/fabro-cli/src/commands/run/attach.rs` — delete the + stored-layer `resolve_cli_from_file(&record.settings)` read at ~line 90. + Attach reads the attaching process's `UserSettings` (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` — add - `UserSettings::resolve_from` cases. + `UserSettings::from_layer` and `UserSettings::resolve` cases. **Approach:** -- `UserSettings::resolve()` is the counterpart to - `ServerSettings::resolve()` for CLI-owned namespaces. Same loader, - different resolver. -- Today's `user_config.rs` wraps `resolve_cli_from_file` with error - formatting; shape that error formatting into `UserSettings::resolve_from` - or wrap at the call site. - -**Patterns to follow:** -- Same as Unit 4. +- Mirror `ServerSettings`'s shape. +- Attach-verbosity behavior changes: attach honors live settings. Stored + `cli.*` on a run is inert; wire format unchanged. **Test scenarios:** -- *Happy path:* `UserSettings::resolve_from(valid_path)` returns - `cli` + `features` namespaces, matching - `resolve_cli_from_file(&layer)` output. -- *Happy path:* `UserSettings::resolve()` picks up - `$FABRO_HOME/settings.toml`. +- *Contract:* `from_layer` and `resolve` return the expected namespaces. - *Edge case:* Missing `~/.fabro/settings.toml` returns defaults without - erroring (current behavior). -- *Error path:* Invalid TOML returns a parse error with the same shape as - today. + erroring. +- *Behavior:* `fabro run attach --verbose` against a non-verbose submitted + run prints verbose output; reversed case prints non-verbose output. **Verification:** - `cargo build --workspace` and `cargo nextest run --workspace` pass. -- `grep -rn "resolve_cli_from_file" lib/` returns only internal - `fabro-config/src/` references and (temporarily) the attach.rs site - to be addressed in Unit 6. +- `grep -rn "resolve_cli_from_file" lib/` returns only + `fabro-config/src/` internal references **plus** + `fabro-cli/tests/it/cmd/create.rs:364` (the integration test that + asserts the persisted `cli.*` wire shape — per the KTD visibility + policy, this test intentionally keeps `resolve_cli_from_file` + public). The `--local` call site at `commands/config/mod.rs:121` + was deleted in Unit 1; `user_config.rs` migrates to + `UserSettings::from_layer` in this unit; the `attach.rs` read is + deleted in this unit. --- -- [ ] **Unit 6: Introduce `WorkflowSettings` context type and `::resolve_for_run()`** +- [ ] **Unit 6: Delete the `Settings` god type and its ecosystem** -**Goal:** Add `WorkflowSettings` with all six namespaces, replace today's -`Settings` god type, and migrate the one `materialize_settings_layer` -call site in `fabro-server`. +**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:** R1, R2, R7. +**Requirements:** R7. -**Dependencies:** Unit 3, Unit 4 (needs `ServerSettings` as the second -parameter to `resolve_for_run`). +**Dependencies:** Units 3 (renames) and 4 (context types exist for +migrated callers). **Files:** -- Modify: `lib/crates/fabro-types/src/settings/context.rs` — add - `WorkflowSettings { server, project, workflow, run, cli, features }`. -- Modify: `lib/crates/fabro-types/src/settings/resolved.rs` — delete the - `Settings` god type (now shadowed by `WorkflowSettings`). + +*Delete the type and wrapper functions:* +- Delete: `lib/crates/fabro-types/src/settings/resolved.rs` (the + `Settings` struct itself). - Modify: `lib/crates/fabro-types/src/settings/mod.rs` — remove the `Settings` re-export. -- Modify: `lib/crates/fabro-config/src/resolve/mod.rs` (or context - module) — add `impl WorkflowSettings { fn resolve_for_run( - layers: EffectiveSettingsLayers, server: &ServerSettings) -> Result }`. -- Modify: `lib/crates/fabro-server/src/run_manifest.rs` — replace the - `materialize_settings_layer` call at line ~88 with - `WorkflowSettings::resolve_for_run(layers, &server_settings)`; - retype `PreparedManifest.settings` from `SettingsLayer` to - `WorkflowSettings`. -- Modify: `lib/crates/fabro-server/src/settings_view.rs:66` — update - `redact_resolved_value(&Settings)` to take `&WorkflowSettings`. -- Modify: downstream server code that reads `prepared.settings` — its - type narrowed from `SettingsLayer` to `WorkflowSettings`, which should - be a tightening rather than a loss (dense resolved values instead of - sparse layer). -- Modify: `lib/crates/fabro-cli/src/commands/run/attach.rs` and - `lib/crates/fabro-cli/src/commands/run/runner.rs` — migrate the - per-namespace resolver calls (`resolve_run_from_file`, - `resolve_server_from_file`, `resolve_cli_from_file` applied to a - stored `SettingsLayer`) to a single `WorkflowSettings::from_stored_layer` - constructor *or* keep the per-namespace access via a `pub(crate)` - adapter. Choose based on which reads cleanly at the call sites. -- Modify: `lib/crates/fabro-workflow/src/operations/create.rs` — imports - and uses the `Settings` god type (~line 19 import; ~line 289 read via - `resolve_settings_tree` / `combined_labels`). Retype alongside the - god-type removal. -- Test: add tests in `lib/crates/fabro-config/tests/` for - `WorkflowSettings::resolve_for_run` covering owner-domain stripping - and server-authoritative overrides. +- Modify: `lib/crates/fabro-config/src/lib.rs` — delete the + `load_and_resolve` public helper (it returns `Settings`); delete the + `use fabro_types::settings::{Settings, SettingsLayer}` import (keep + the `SettingsLayer` import via its own `use`). +- Modify: `lib/crates/fabro-config/src/resolve/mod.rs` — delete the + public `fn resolve(&SettingsLayer) -> Result` function + (the god-type-returning one); remove it from `pub use + resolve::{...}` in `lib.rs`. Per-namespace resolvers are unaffected. + +*Migrate the one production caller of `fabro_config::resolve`:* +- Modify: `lib/crates/fabro-workflow/src/operations/create.rs` — uses + `Settings` at ~line 19 (import) and ~lines 289-298 + (`resolve_settings_tree` / `combined_labels`). The caller at ~line + 107 reads **both** `resolved_settings.server.storage.root` (to + compute `storage_root` for run persistence) and + `combined_labels(&resolved_settings)` (project/workflow/run + metadata). Replace `resolve_settings_tree` so it returns a small + struct (or 4-tuple) containing `ServerNamespace` + + `ProjectNamespace` + `WorkflowNamespace` + `RunNamespace`, built + from `fabro_config::resolve_server_from_file`, + `resolve_project_from_file`, `resolve_workflow_from_file`, and + `resolve_run_from_file` on the same `SettingsLayer`. Update the + call site at ~line 107 to read `.server.storage.root` off the + returned `ServerNamespace`, and `combined_labels` to read + `.metadata` off each of `ProjectNamespace` / `WorkflowNamespace` / + `RunNamespace`. All four per-namespace resolvers stay `pub` per + 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 exercise + `fabro_config::resolve`. Rewrite each assertion against the + surviving per-namespace resolvers: e.g., + `resolve_root.rs::resolves_root_settings_require_explicit_server_auth_methods` + becomes a test on `resolve_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 calls + `resolve(&SettingsLayer::default())`. Rewrite to target the specific + per-namespace resolver whose default the test is asserting (likely + `resolve_server_from_file`, from the `server.auth.methods` check + shown by grep). +- Modify: `lib/crates/fabro-cli/tests/it/cmd/config.rs` — ~line 160 + `resolved_server_settings_fixture` calls `fabro_config::resolve(...)` + to produce a Settings-shaped fixture for the settings command tests. + Replace with a fixture built via + `ServerSettings::from_layer(&server_settings_layer_fixture())` + (produces only `server` + `features`, matching the new + `GET /api/v1/settings` response shape). + +*Server.rs branch removed by Unit 7:* +- `lib/crates/fabro-server/src/server.rs:1337` (the + `fabro_config::resolve(&settings)` call inside the `?view=resolved` + branch of `retrieveServerSettings`) 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` (the + `redact_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 existing + `materialize_settings_layer(layers, Some(server_settings), mode)` + call **stays**; this unit only removes the `mode` argument (Unit 2 + deleted the enum). `materialize_settings_layer` still produces the + merged `SettingsLayer` stored in `PreparedManifest.settings`. No + `WorkflowSettings` or other context type is constructed here. **Approach:** -- `WorkflowSettings` has the identical field set as today's `Settings`. - This unit is as much a rename + method-attachment as a new type. Once - renamed, the conceptual purpose is sharper: it's the per-run resolved - view, not a god bucket. -- `resolve_for_run` internally calls the (now private) - `materialize_settings_layer` and then the per-namespace resolvers to - produce each dense namespace. The implementation is straight-line. -- For `attach.rs` / `runner.rs`: a stored run's `SettingsLayer` is a - post-merge artifact (see `run_manifest.rs` where it's captured). A - `WorkflowSettings::from_stored_layer(layer, server)` that re-resolves - may be cleaner than three per-namespace calls. Confirm at - implementation time. - -**Execution note:** Start with a passing test for -`WorkflowSettings::resolve_for_run` that reproduces the current -`run_manifest.rs` output for a representative layer set; then migrate -the call site; then delete `Settings`. - -**Patterns to follow:** -- The existing resolve pipeline in `fabro-config/src/resolve/` — keep the - per-namespace resolvers as building blocks; `WorkflowSettings::resolve_for_run` - composes them. +- The god type has exactly one production consumer outside the + view-toggle machinery (`operations/create.rs`); migrate it. Every + other `fabro_config::resolve` call 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: SettingsLayer` is unchanged; the + run_manifest merge pipeline stays intact. **Test scenarios:** -- *Happy path:* `WorkflowSettings::resolve_for_run` with layers - containing `[project]`, `[workflow]`, `[run]` sections plus a - `ServerSettings` carrying non-default `server.storage` produces a - result where `server.storage` came from `ServerSettings` and run-level - fields came from the layers. -- *Happy path:* `cli.*` stanzas in the `project` or `workflow` layers - are stripped (owner-domain rule); only the user layer's `cli.*` - reaches the result. -- *Edge case:* Empty layers + non-empty `ServerSettings` produce a - result with `ServerSettings` values for server/features and default - values elsewhere. -- *Error path:* `resolve_for_run` returns the same resolution errors - (missing required fields, invalid enum values) that - `resolve_run_from_file` produces today on equivalent input. -- *Integration:* The full `run_manifest.rs` pipeline produces a - `PreparedManifest` whose `settings` field is a `WorkflowSettings` that - downstream validation (`validate_prepared_manifest`) accepts. +- *Contract:* `operations/create.rs` label aggregation produces the + same result as before — per-namespace metadata combined by the + replacement `resolve_settings_tree`. +- *Migration:* rewritten `resolve_root.rs` and `defaults.rs` tests + cover the same assertions at the per-namespace resolver level. +- *Migration:* the `fabro settings` command test in + `tests/it/cmd/config.rs` still passes with the `ServerSettings`-shaped + fixture. **Verification:** - `cargo build --workspace` and `cargo nextest run --workspace` pass. -- `grep -rn "fabro_types::settings::Settings\b" lib/` returns no hits. -- `grep -rn "materialize_settings_layer" lib/` returns only - `fabro-config/src/` internal references. +- `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-namespace + `fabro_config::resolve_*_from_file` and the new context-type + constructors remain). +- `grep -rn "load_and_resolve" lib/` returns zero hits. --- -- [ ] **Unit 7: Narrow `GET /api/v1/settings` to dense `ServerSettings`; compose `fabro settings` output** +- [ ] **Unit 7: Typed `GET /api/v1/settings` served from `AppState`; delete redaction** -**Goal:** Change the server's settings endpoint to return a single dense -shape matching the new `ServerSettings` context type. Update the CLI's -`fabro settings` to compose local `UserSettings` + remote `ServerSettings`. +**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:** Unit 4 (needs `ServerSettings`), Unit 6 (needs -`settings_view` migrated off `Settings`). +**Dependencies:** Units 4, 6. **Files:** -- Modify: `docs/api-reference/fabro-api.yaml` — at line ~1947 remove the - `view` query parameter and the `X-Fabro-Settings-View` header; at - line ~4897 replace the `ServerSettings` schema's - `additionalProperties: true` with a proper typed definition - (two fields: `server`, `features`) whose child schemas match the - new `ServerSettings` Rust type. -- Modify: `lib/crates/fabro-api/build.rs` — if `with_replacement(...)` - adapters are needed to wire the new `ServerSettings` Rust type into - the generated client, add them. -- Regenerate: `cargo build -p fabro-api` (build.rs runs progenitor). +- Modify: `docs/api-reference/fabro-api.yaml`: + - Remove the `view` query parameter and `X-Fabro-Settings-View` header + from the `retrieveServerSettings` operation. + - Replace `ServerSettings`'s `additionalProperties: true` with a typed + schema (two fields: `server`, `features`) matching the Rust + `ServerSettings`. + - Rename the `RunSettings` schema to `RunSettingsLayer` and update its + description to reflect that the endpoint returns the persisted + `SettingsLayer` as-is. + - Remove all `redact`, `redaction`, `secret subtrees` language across + the YAML. +- Modify: `lib/crates/fabro-api/build.rs` — add `with_replacement(...)` + entries mapping the OpenAPI `ServerSettings` schema (and nested types) + to the internal Rust types. +- Modify: `lib/crates/fabro-types/src/settings/server.rs`, `features.rs`, + and reachable child types — add `Deserialize` derives. For types with + custom `Serialize` (`serialize_socket_addr`, `InterpString`, etc.), + implement matching custom `Deserialize`. `InterpString::Deserialize` + must preserve unresolved `{{ env.NAME }}` templates. +- Add: `lib/crates/fabro-api/tests/` type-identity + JSON-parity test + (per CLAUDE.md's `with_replacement` requirement). +- 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` — the - `retrieveServerSettings` handler (find by `operationId`) returns - `ServerSettings::resolve()`'s serialized form; remove the view-toggle - branching and the custom response header. -- Modify: `lib/crates/fabro-client/src/client.rs` — simplify - `retrieve_resolved_server_settings` (~lines 493-513) to drop the - `?view=resolved` query parameter and the `X-Fabro-Settings-View` - response-header check; update the error message and any CLI-side test - assertions that pin the old error text (e.g., - `lib/crates/fabro-cli/tests/it/cmd/config.rs`). -- Modify: `lib/crates/fabro-server/src/settings_view.rs` — reshape - redaction to work on a `ServerSettings` (dense) rather than a - `SettingsLayer` or `Settings` god type. `server.listen` still - redacts. -- Modify: `lib/crates/fabro-cli/src/commands/config/mod.rs` — update - `rendered_config` to fetch the new `ServerSettings` shape from the - server and merge with a local `UserSettings::resolve()` for display. -- Modify: `apps/fabro-web/` or any other TypeScript consumer of the - generated `retrieveServerSettings` — if any consumer exists, update - to the new shape. (Initial inspection suggests the web app does not - call this endpoint; confirm at implementation time.) -- Run: `scripts/refresh-fabro-spa.sh` if any `apps/fabro-web/` code - changed. -- Test: `lib/crates/fabro-server/tests/it/openapi_conformance.rs` — - conformance test should verify the new single-shape endpoint matches - the spec. -- Test: snapshot tests for `fabro settings` output (under - `lib/crates/fabro-cli/tests/`) — regenerate and review with - `cargo insta pending-snapshots` → `cargo insta accept --snapshot ...`. + `retrieveServerSettings` handler returns a clone of + `AppState.server_settings`. No view toggle, no response header, no + redaction. The `/runs/:id/settings` handler serializes + `run_spec.settings` (the `SettingsLayer`) directly. +- Modify: `lib/crates/fabro-client/src/client.rs` (~lines 493-513) — + simplify `retrieve_resolved_server_settings` to drop `?view=resolved` + and the header check. +- Delete: `lib/crates/fabro-server/src/settings_view.rs` (module + tests). + This removes `SettingsApiView`, `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 imports + `settings_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-View` contract: + - `lib/crates/fabro-server/tests/it/api/settings.rs:63` and `:143` + (the entire + `retrieve_server_settings_resolved_view_returns_dense_settings_and_marker` + test 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 issues `GET /settings?view=resolved` — rewrite to hit + `/settings` without 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=resolved` with an + `X-Fabro-Settings-View: resolved` response header; migrate the + mocks to the new single-shape contract — no `view` query param, + no custom header, body = typed `ServerSettings` fixture built via + `ServerSettings::from_layer(&server_settings_layer_fixture())`). +- Modify: `lib/crates/fabro-cli/src/commands/config/mod.rs` — + `rendered_config` fetches the new `ServerSettings` from the endpoint + and merges with `UserSettings::resolve()` for a two-section display. +- Modify: `apps/fabro-web/app/routes/settings.tsx` — consume the newly + typed `ServerSettings` shape explicitly. +- Run: `scripts/refresh-fabro-spa.sh`. **Approach:** -- The endpoint's Rust handler path is selected via `operationId: - retrieveServerSettings` in the spec. Find the corresponding function in - `server.rs` and simplify it to `ServerSettings::resolve()` serialized - to JSON (after redaction). -- The OpenAPI schema rewrite should describe the full nested shape of - `ServerSettings` so generated clients get real types, not - `additionalProperties: true`. -- `fabro settings` display: two sections — one labeled something like - "user" (from local `UserSettings::resolve()`) and one labeled - "server" (from the API). Exact label/format is a minor UX call; keep - snapshot tests as the source of truth. +- 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. -**Patterns to follow:** -- OpenAPI-first workflow per `CLAUDE.md`: edit the YAML, rebuild - fabro-api, run conformance tests. -- `bun run generate` workflow for the TypeScript client. -- `scripts/refresh-fabro-spa.sh` if the SPA bundle is affected. - -**Test scenarios:** -- *Happy path:* `GET /api/v1/settings` returns JSON with `server` and - `features` top-level keys; no other keys. -- *Happy path:* `server.listen` remains redacted in the response (same - policy as today). -- *Edge case:* The endpoint has no `view` query parameter; passing - `?view=layer` returns a 200 with the new dense shape (query param - silently ignored per OpenAPI's permissive defaults) *or* a 400 if the - router rejects unknown params — assert whichever matches the router's - current behavior for unknown params. -- *Edge case:* `fabro settings` output with a running server prints both - the user section and the server section; output is stable across runs. -- *Integration:* OpenAPI conformance test - (`openapi_conformance.rs`) passes — spec and router match. -- *Integration:* Generated TypeScript client's - `retrieveServerSettings` returns a typed object with `server` and - `features` fields. +**Test scenarios (canonical only):** +- *Contract:* `GET /api/v1/settings` returns JSON with exactly two + top-level keys, `server` and `features`. The typed shape matches the + OpenAPI `ServerSettings` schema. +- *Contract:* Generated TypeScript client returns a typed object with + `server` and `features` fields. +- *Contract:* `GET /api/v1/runs/:id/settings` returns the persisted + `SettingsLayer` (renamed `RunSettingsLayer` in the spec) directly. +- *Behavior:* `server.listen` is present and visible in the main settings + response. +- *Integration:* OpenAPI conformance passes. +- *Integration:* `fabro settings` renders two sections (user / server) + without errors. **Verification:** - `cargo build -p fabro-api` succeeds (progenitor codegen clean). - `cargo nextest run -p fabro-server` passes, including conformance. - `cd lib/packages/fabro-api-client && bun run generate && bun run typecheck` succeeds. -- `fabro settings` (manual smoke test) displays a two-section layout - with no `SettingsLayer`-style sparse fields. -- CI's SPA-drift check passes (no uncommitted `lib/crates/fabro-spa/ - assets/` drift). +- CI's SPA-drift check passes. --- -- [ ] **Unit 8: Privatize merge internals; delete dead helpers** +- [ ] **Unit 8: Privatize merge internals** -**Goal:** Reduce the public surface of `fabro-config` to the three -context types and their `::resolve*()` methods. Delete free functions -and helpers whose callers have all migrated. +**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` — make - `materialize_settings_layer` `pub(crate)`. -- Modify: `lib/crates/fabro-config/src/lib.rs` — remove or privatize - `load_and_resolve` (it became a thin wrapper when the mode enum went - away; decide whether it's still worth keeping for tests or can be - deleted). -- Modify: `lib/crates/fabro-config/src/user.rs` — make - `load_settings_config` `pub(crate)`. -- Modify: `lib/crates/fabro-config/src/resolve/mod.rs` — make - `resolve_server_from_file`, `resolve_cli_from_file`, - `resolve_project_from_file`, `resolve_workflow_from_file`, - `resolve_run_from_file`, `resolve_features_from_file` either - `pub(crate)` or fully private. Some may still need visibility for - tests or for the attach/runner code paths decided in Unit 6. -- Modify: `lib/crates/fabro-config/src/lib.rs` — update crate-level - re-exports to expose only `ServerSettings`, `UserSettings`, - `WorkflowSettings`, `EffectiveSettingsLayers` (as input type), and - error types. -- Note: `bin/dev/check-boundary.sh` grep-pattern updates already - landed in Unit 4 (moved earlier so the script never loses coverage - during the Unit 4 → 7 migration window). Unit 8 only verifies the - script still passes after the privatization pass. +- Modify: `lib/crates/fabro-config/src/effective_settings.rs` — + `materialize_settings_layer` → `pub(crate)`. +- `lib/crates/fabro-config/src/resolve/mod.rs` — all six + `resolve_*_from_file` functions (`project`, `workflow`, `run`, `cli`, + `server`, `features`) keep their existing `pub` visibility. 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.rs` loads 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 `Settings` type and the god-type-returning + `resolve` function 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`, the + `storage` module, 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. -**Approach:** -- This is a cleanup pass. After Units 4–6, the grep-verified callers - are all internal; flipping visibility should compile cleanly. -- Prefer `pub(crate)` over fully private when the tests module in the - same crate still needs access. -- Document in the `fabro-config` crate-level doc comment what the public - surface is (three context types + their constructors + error types). - -**Patterns to follow:** -- Minimum-visibility convention in this codebase: start private, widen - only when a real caller needs it. +**Approach:** Cleanup pass. Flip visibility; compile. **Test scenarios:** -- *Happy path:* Downstream crates compile after the visibility - narrowing; no caller had a dependency that got cut. -- *Integration:* `bin/dev/check-boundary.sh` passes with updated grep - patterns. +- *Integration:* Downstream crates compile after visibility narrowing. **Verification:** -- `cargo build --workspace` succeeds. -- `cargo nextest run --workspace` passes. -- `grep -rn "pub fn resolve_.*_from_file" lib/crates/fabro-config/` - returns zero hits (all are now `pub(crate)` or private). +- `cargo build --workspace` and `cargo nextest run --workspace` pass. +- `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 remain `pub` + because each has cross-crate consumers). - `bin/dev/check-boundary.sh` passes. ## System-Wide Impact -- **Interaction graph:** Every CLI command that reads user/server config - changes its import. ~20 call sites across `fabro-cli`, `fabro-server`, - and `fabro-install` migrate from free-function calls to context-type - constructors. Mechanical but broad. -- **Error propagation:** Today's resolver errors flow back unchanged from - `resolve_*_from_file`; the new constructors wrap the same error types - at crate boundaries. No new error categories. -- **State lifecycle risks:** None. The refactor preserves merge semantics - and the persisted `SettingsLayer` wire format for run manifests. -- **API surface parity:** `GET /api/v1/settings` changes shape (Unit 7). - The CLI is the only known in-tree consumer. External API consumers - that called `?view=resolved` will see a similar-shaped response - (now the only shape); external consumers that called `?view=layer` - see a different shape. The project is single-node and greenfield, so - this break is acceptable per prior decisions; surface it in release - notes regardless. -- **Integration coverage:** OpenAPI conformance test - (`lib/crates/fabro-server/tests/it/openapi_conformance.rs`) is the - primary cross-layer guardrail. Generated Rust types (via progenitor) - and TypeScript client (via openapi-generator) must both regenerate - cleanly. -- **Unchanged invariants:** TOML file format, namespace inventory (six - namespaces), namespace ownership rules (cli/server are owner-first), - layering precedence (user → project → workflow → args), server's - authority over server-owned fields, `fabro_cli::local_server` as the - single CLI-side gateway to `[server.*]` for lifecycle commands. +- **Interaction graph:** ~20 current-config call sites migrate from + `resolve_server_from_file(&layer)` to `ServerSettings::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/settings` shape changes (single dense + `ServerSettings` served from `AppState`). `GET /api/v1/runs/:id/settings` + shape unchanged (still the persisted `SettingsLayer`; OpenAPI schema + renamed `RunSettingsLayer`). +- **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.settings` wire format, `fabro_cli::local_server` as + the sanctioned CLI gateway to `[server.*]`. ## Risks & Dependencies | Risk | Mitigation | |------|------------| -| The 57-reference mechanical rename (Unit 3) lands incomplete, leaving compilation broken mid-merge | Use `cargo check --workspace` after each file batch; prefer a single atomic commit for Unit 3; if splitting, keep the rename consistent within each commit so the build stays green | -| OpenAPI response shape change breaks an external consumer we don't know about | Single-node greenfield app; release notes flag the break; spec change is captured in git history for discoverability | -| `fabro settings` snapshot tests drift in non-obvious ways after Unit 7's output restructuring | Use `cargo insta pending-snapshots` and review diffs before accepting; don't batch-accept across unrelated changes | -| TypeScript client regeneration (Unit 7) drifts from committed bundle, failing CI's `git diff --exit-code` check on `lib/crates/fabro-spa/assets/` | Run `scripts/refresh-fabro-spa.sh` after `bun run generate`; confirm the committed SPA matches source before pushing | -| `fabro run attach` verbosity replay breaks if the stored-layer access pattern is migrated awkwardly in Unit 6 | Keep the attach test that asserts "submit-time verbosity is preserved on attach" green throughout; if needed, keep `resolve_cli_from_file` as `pub(crate)` for this specific read path | -| `bin/dev/check-boundary.sh` grep patterns reference old free-function names and silently pass after the rename (false-negative boundary check) | Script update moved from Unit 8 into Unit 4 so the guard never loses coverage during the Unit 4 → 7 migration window. Unit 4 verification includes a throwaway-branch test confirming the script catches an unsanctioned `ServerSettings::resolve*` import | +| 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 -- Update `docs/api-reference/fabro-api.yaml` description prose for - `/api/v1/settings` to reflect the single dense response shape. -- Update any in-repo docs that reference `fabro settings --local` (if - any; grep `docs/` during Unit 1). -- `CLAUDE.md`'s "API workflow" section already describes the OpenAPI → - Rust → TypeScript pipeline; no changes needed there. -- Release notes (or changelog equivalent): call out the - `fabro settings --local` removal and the `GET /api/v1/settings` - response-shape change as deliberate breaking cleanups. +- OpenAPI description prose for `/api/v1/settings` updated 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 --local` removed; `GET /api/v1/settings` + response shape changed; redaction removed. ## Sources & References @@ -1009,12 +994,8 @@ and helpers whose callers have all migrated. `lib/crates/fabro-types/src/settings/resolved.rs`, `lib/crates/fabro-cli/src/local_server.rs`, `lib/crates/fabro-server/src/run_manifest.rs`, - `lib/crates/fabro-server/src/settings_view.rs`, `docs/api-reference/fabro-api.yaml`. -- Related brainstorm (adjacent, not strict origin): +- Adjacent brainstorm: `docs/brainstorms/2026-04-08-settings-toml-redesign-requirements.md` - (especially R16 on owner-first namespace boundaries). -- Recent commits establishing the current boundary that this refactor - lifts to the type system: `5b1c40764` (CLI server-settings reads - restricted to `fabro_cli::local_server`), along with - `bin/dev/check-boundary.sh`. + (R16 on owner-first namespace boundaries). +- Boundary-enforcement commit: `5b1c40764` + `bin/dev/check-boundary.sh`. From ebb8bf7add822524744d7e0d26d7a230a0af9a4d Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Wed, 22 Apr 2026 18:58:47 -0400 Subject: [PATCH 02/13] refactor settings API entrypoints --- Cargo.lock | 11 + apps/fabro-web/app/lib/workflow-api.ts | 10 +- apps/fabro-web/app/routes/run-settings.tsx | 4 +- apps/fabro-web/app/routes/settings.tsx | 8 +- apps/fabro-web/app/routes/workflow-detail.tsx | 14 +- bin/dev/check-boundary.sh | 3 +- docs/api-reference/fabro-api.yaml | 371 +++++++++-- lib/crates/fabro-api/Cargo.toml | 1 + lib/crates/fabro-api/build.rs | 126 ++++ lib/crates/fabro-api/src/lib.rs | 11 + .../tests/server_settings_round_trip.rs | 78 +++ lib/crates/fabro-cli/src/args.rs | 7 - lib/crates/fabro-cli/src/command_context.rs | 26 +- .../fabro-cli/src/commands/artifact/cp.rs | 4 +- .../fabro-cli/src/commands/artifact/list.rs | 4 +- .../fabro-cli/src/commands/artifact/mod.rs | 6 +- .../fabro-cli/src/commands/auth/login.rs | 4 +- .../fabro-cli/src/commands/auth/logout.rs | 4 +- lib/crates/fabro-cli/src/commands/auth/mod.rs | 4 +- .../fabro-cli/src/commands/auth/status.rs | 4 +- .../fabro-cli/src/commands/config/mod.rs | 136 +--- lib/crates/fabro-cli/src/commands/doctor.rs | 4 +- lib/crates/fabro-cli/src/commands/exec.rs | 4 +- lib/crates/fabro-cli/src/commands/graph.rs | 4 +- lib/crates/fabro-cli/src/commands/install.rs | 30 +- lib/crates/fabro-cli/src/commands/model.rs | 4 +- lib/crates/fabro-cli/src/commands/parse.rs | 4 +- lib/crates/fabro-cli/src/commands/pr/close.rs | 4 +- .../fabro-cli/src/commands/pr/create.rs | 4 +- lib/crates/fabro-cli/src/commands/pr/list.rs | 4 +- lib/crates/fabro-cli/src/commands/pr/merge.rs | 4 +- lib/crates/fabro-cli/src/commands/pr/mod.rs | 24 +- lib/crates/fabro-cli/src/commands/pr/view.rs | 4 +- .../fabro-cli/src/commands/preflight.rs | 4 +- .../fabro-cli/src/commands/provider/login.rs | 4 +- .../fabro-cli/src/commands/provider/mod.rs | 4 +- .../fabro-cli/src/commands/repo/deinit.rs | 4 +- .../fabro-cli/src/commands/repo/init.rs | 6 +- lib/crates/fabro-cli/src/commands/repo/mod.rs | 4 +- .../fabro-cli/src/commands/run/attach.rs | 13 +- .../fabro-cli/src/commands/run/command.rs | 5 +- lib/crates/fabro-cli/src/commands/run/cp.rs | 6 +- lib/crates/fabro-cli/src/commands/run/diff.rs | 4 +- lib/crates/fabro-cli/src/commands/run/fork.rs | 4 +- lib/crates/fabro-cli/src/commands/run/logs.rs | 4 +- lib/crates/fabro-cli/src/commands/run/mod.rs | 7 +- .../fabro-cli/src/commands/run/preview.rs | 4 +- .../fabro-cli/src/commands/run/resume.rs | 7 +- .../fabro-cli/src/commands/run/rewind.rs | 4 +- lib/crates/fabro-cli/src/commands/run/ssh.rs | 4 +- lib/crates/fabro-cli/src/commands/run/wait.rs | 4 +- .../fabro-cli/src/commands/runs/archive.rs | 8 +- .../fabro-cli/src/commands/runs/inspect.rs | 4 +- .../fabro-cli/src/commands/runs/list.rs | 4 +- lib/crates/fabro-cli/src/commands/runs/mod.rs | 4 +- lib/crates/fabro-cli/src/commands/runs/rm.rs | 6 +- .../fabro-cli/src/commands/sandbox/mod.rs | 4 +- .../fabro-cli/src/commands/secret/list.rs | 4 +- .../fabro-cli/src/commands/secret/mod.rs | 4 +- .../fabro-cli/src/commands/secret/rm.rs | 4 +- .../fabro-cli/src/commands/secret/set.rs | 4 +- .../fabro-cli/src/commands/store/dump.rs | 4 +- .../fabro-cli/src/commands/store/mod.rs | 4 +- .../fabro-cli/src/commands/system/df.rs | 4 +- .../fabro-cli/src/commands/system/events.rs | 4 +- .../fabro-cli/src/commands/system/info.rs | 4 +- .../fabro-cli/src/commands/system/mod.rs | 4 +- .../fabro-cli/src/commands/system/prune.rs | 4 +- .../fabro-cli/src/commands/uninstall.rs | 4 +- lib/crates/fabro-cli/src/commands/upgrade.rs | 16 +- lib/crates/fabro-cli/src/commands/validate.rs | 4 +- lib/crates/fabro-cli/src/commands/version.rs | 4 +- .../fabro-cli/src/commands/workflow/create.rs | 4 +- .../fabro-cli/src/commands/workflow/list.rs | 4 +- .../fabro-cli/src/commands/workflow/mod.rs | 4 +- lib/crates/fabro-cli/src/local_server.rs | 4 +- lib/crates/fabro-cli/src/main.rs | 26 +- lib/crates/fabro-cli/src/user_config.rs | 25 +- lib/crates/fabro-cli/tests/it/cmd/config.rs | 590 +----------------- lib/crates/fabro-cli/tests/it/cmd/create.rs | 2 +- lib/crates/fabro-client/src/client.rs | 20 +- lib/crates/fabro-config/Cargo.toml | 1 + lib/crates/fabro-config/src/context.rs | 60 ++ .../fabro-config/src/effective_settings.rs | 229 ++----- lib/crates/fabro-config/src/lib.rs | 24 +- lib/crates/fabro-config/src/resolve/cli.rs | 6 +- .../fabro-config/src/resolve/features.rs | 6 +- lib/crates/fabro-config/src/resolve/mod.rs | 51 +- .../fabro-config/src/resolve/project.rs | 6 +- lib/crates/fabro-config/src/resolve/run.rs | 10 +- lib/crates/fabro-config/src/resolve/server.rs | 6 +- .../fabro-config/src/resolve/workflow.rs | 6 +- lib/crates/fabro-config/tests/defaults.rs | 17 +- lib/crates/fabro-config/tests/resolve_cli.rs | 69 ++ lib/crates/fabro-config/tests/resolve_root.rs | 86 ++- .../fabro-config/tests/resolve_server.rs | 59 ++ lib/crates/fabro-server/src/auth/cli_flow.rs | 2 +- lib/crates/fabro-server/src/auth/translate.rs | 1 - .../fabro-server/src/canonical_origin.rs | 2 +- lib/crates/fabro-server/src/demo/mod.rs | 263 ++------ lib/crates/fabro-server/src/diagnostics.rs | 13 +- lib/crates/fabro-server/src/install.rs | 16 +- lib/crates/fabro-server/src/jwt_auth.rs | 4 +- lib/crates/fabro-server/src/lib.rs | 1 - lib/crates/fabro-server/src/run_manifest.rs | 44 +- lib/crates/fabro-server/src/serve.rs | 18 +- lib/crates/fabro-server/src/server.rs | 199 ++---- lib/crates/fabro-server/src/settings_view.rs | 275 -------- lib/crates/fabro-server/src/web_auth.rs | 10 +- lib/crates/fabro-server/tests/it/api/runs.rs | 23 +- .../fabro-server/tests/it/api/settings.rs | 117 +--- lib/crates/fabro-types/src/settings/cli.rs | 2 +- .../fabro-types/src/settings/features.rs | 4 +- lib/crates/fabro-types/src/settings/mod.rs | 16 +- .../fabro-types/src/settings/project.rs | 2 +- .../fabro-types/src/settings/resolved.rs | 179 ------ lib/crates/fabro-types/src/settings/run.rs | 2 +- lib/crates/fabro-types/src/settings/server.rs | 79 ++- .../fabro-types/src/settings/workflow.rs | 2 +- .../fabro-workflow/src/operations/create.rs | 32 +- .../fabro-workflow/src/operations/start.rs | 32 +- .../src/.openapi-generator/FILES | 32 + .../src/api/run-internals-api.ts | 8 +- .../fabro-api-client/src/api/settings-api.ts | 37 +- .../models/discord-integration-settings.ts | 20 + .../src/models/features-namespace.ts | 20 + .../src/models/git-hub-meta-hooks-entry.ts | 25 + .../src/models/github-integration-settings.ts | 34 + .../src/models/github-integration-strategy.ts | 26 + .../fabro-api-client/src/models/index.ts | 32 + .../models/integration-webhooks-settings.ts | 29 + .../src/models/ip-allow-entry.ts | 28 + .../src/models/literal-ip-allow-entry.ts | 20 + .../src/models/object-store-local-settings.ts | 28 + .../src/models/object-store-s3-settings.ts | 31 + .../src/models/object-store-settings.ts | 28 + .../src/models/server-api-settings.ts | 20 + .../src/models/server-artifacts-settings.ts | 24 + .../src/models/server-auth-github-settings.ts | 20 + .../src/models/server-auth-method.ts | 26 + .../src/models/server-auth-settings.ts | 27 + .../models/server-integrations-settings.ts | 35 ++ .../server-ip-allowlist-override-settings.ts | 24 + .../models/server-ip-allowlist-settings.ts | 24 + .../src/models/server-listen-settings.ts | 28 + .../src/models/server-listen-tcp-settings.ts | 28 + .../src/models/server-listen-unix-settings.ts | 28 + .../src/models/server-logging-settings.ts | 20 + .../src/models/server-namespace.ts | 63 ++ .../src/models/server-scheduler-settings.ts | 20 + .../src/models/server-settings.ts | 30 + .../src/models/server-slate-db-settings.ts | 26 + .../src/models/server-storage-settings.ts | 20 + .../src/models/server-web-settings.ts | 21 + .../src/models/slack-integration-settings.ts | 21 + .../src/models/teams-integration-settings.ts | 20 + .../src/models/webhook-strategy.ts | 26 + 157 files changed, 2318 insertions(+), 2408 deletions(-) create mode 100644 lib/crates/fabro-api/tests/server_settings_round_trip.rs create mode 100644 lib/crates/fabro-config/src/context.rs delete mode 100644 lib/crates/fabro-server/src/settings_view.rs delete mode 100644 lib/crates/fabro-types/src/settings/resolved.rs create mode 100644 lib/packages/fabro-api-client/src/models/discord-integration-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/features-namespace.ts create mode 100644 lib/packages/fabro-api-client/src/models/git-hub-meta-hooks-entry.ts create mode 100644 lib/packages/fabro-api-client/src/models/github-integration-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/github-integration-strategy.ts create mode 100644 lib/packages/fabro-api-client/src/models/integration-webhooks-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/ip-allow-entry.ts create mode 100644 lib/packages/fabro-api-client/src/models/literal-ip-allow-entry.ts create mode 100644 lib/packages/fabro-api-client/src/models/object-store-local-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/object-store-s3-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/object-store-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-api-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-artifacts-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-auth-github-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-auth-method.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-auth-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-integrations-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-ip-allowlist-override-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-ip-allowlist-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-listen-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-listen-tcp-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-listen-unix-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-logging-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-namespace.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-scheduler-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-slate-db-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-storage-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/server-web-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/slack-integration-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/teams-integration-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/webhook-strategy.ts diff --git a/Cargo.lock b/Cargo.lock index a4d73154b..846f1a106 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1544,6 +1544,7 @@ name = "fabro-api" version = "0.211.0-nightly.1" dependencies = [ "chrono", + "fabro-config", "fabro-types", "openapiv3", "prettyplease", @@ -1726,6 +1727,7 @@ dependencies = [ "serde", "serde_json", "strsim 0.11.1", + "temp-env", "tempfile", "thiserror 2.0.18", "toml 0.8.23", @@ -6419,6 +6421,15 @@ dependencies = [ "xattr", ] +[[package]] +name = "temp-env" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050" +dependencies = [ + "parking_lot", +] + [[package]] name = "tempfile" version = "3.26.0" diff --git a/apps/fabro-web/app/lib/workflow-api.ts b/apps/fabro-web/app/lib/workflow-api.ts index e73d79ed0..6ee4ecb97 100644 --- a/apps/fabro-web/app/lib/workflow-api.ts +++ b/apps/fabro-web/app/lib/workflow-api.ts @@ -1,12 +1,10 @@ import type { PaginationMeta } from "@qltysh/fabro-api-client"; /** - * Opaque settings payload returned by `/api/v1/runs/:id/settings`. Mirrors the - * v2 `SettingsFile` shape in `lib/crates/fabro-types/src/settings/tree.rs`, - * with secret-bearing subtrees dropped before serialization. Treated as a - * loose JSON object on the web side — consumers only render it. + * Opaque persisted `SettingsLayer` payload returned by `/api/v1/runs/:id/settings`. + * Treated as a loose JSON object on the web side — consumers only render it. */ -export type RunSettings = Record; +export type RunSettingsLayer = Record; export interface WorkflowScheduleSummary { expression: string; @@ -35,6 +33,6 @@ export interface WorkflowDetailResponse { slug: string; description: string; filename: string; - settings: RunSettings; + settings: RunSettingsLayer; graph: string; } diff --git a/apps/fabro-web/app/routes/run-settings.tsx b/apps/fabro-web/app/routes/run-settings.tsx index 9bfa09f99..e66f4bbeb 100644 --- a/apps/fabro-web/app/routes/run-settings.tsx +++ b/apps/fabro-web/app/routes/run-settings.tsx @@ -6,14 +6,14 @@ import { apiJson } from "../api"; import { isVisibleStage } from "../data/runs"; import { formatDurationSecs } from "../lib/format"; import type { PaginatedRunStageList } from "@qltysh/fabro-api-client"; -import type { RunSettings } from "../lib/workflow-api"; +import type { RunSettingsLayer } from "../lib/workflow-api"; export const handle = { wide: true }; export async function loader({ request, params }: any) { const [{ data: apiStages }, settings] = await Promise.all([ apiJson(`/runs/${params.id}/stages`, { request }), - apiJson(`/runs/${params.id}/settings`, { request }), + apiJson(`/runs/${params.id}/settings`, { request }), ]); const stages: Stage[] = apiStages.filter((s) => isVisibleStage(s.id)).map((s) => ({ id: s.id, diff --git a/apps/fabro-web/app/routes/settings.tsx b/apps/fabro-web/app/routes/settings.tsx index e557d622a..dc5cb53ce 100644 --- a/apps/fabro-web/app/routes/settings.tsx +++ b/apps/fabro-web/app/routes/settings.tsx @@ -1,13 +1,7 @@ +import type { ServerSettings } from "@qltysh/fabro-api-client"; import { apiJson } from "../api"; import { CollapsibleFile } from "../components/collapsible-file"; -/** - * Opaque server settings payload returned by `/api/v1/settings`. Mirrors the - * v2 `SettingsFile` shape with secret-bearing subtrees dropped before - * serialization. The UI only renders it as JSON. - */ -type ServerSettings = Record; - export function meta({}: any) { return [{ title: "Settings — Fabro" }]; } diff --git a/apps/fabro-web/app/routes/workflow-detail.tsx b/apps/fabro-web/app/routes/workflow-detail.tsx index f7951413e..0999ef984 100644 --- a/apps/fabro-web/app/routes/workflow-detail.tsx +++ b/apps/fabro-web/app/routes/workflow-detail.tsx @@ -1,22 +1,24 @@ import { ChevronRightIcon } from "@heroicons/react/20/solid"; import { Link, Outlet, useLocation, useParams } from "react-router"; import { apiJsonOrNull } from "../api"; -import type { RunSettings, WorkflowDetailResponse as ApiWorkflowDetail } from "../lib/workflow-api"; +import type { + RunSettingsLayer, + WorkflowDetailResponse as ApiWorkflowDetail, +} from "../lib/workflow-api"; export interface WorkflowEntry { name: string; slug: string; description: string; filename: string; - settings: RunSettings; + settings: RunSettingsLayer; graph: string; } // Static sample data used by the `workflow-definition` index route for the -// hardcoded showcase workflows. Shape mirrors the v2 `SettingsFile` JSON -// returned by `/api/v1/runs/:id/settings` (see the Rust -// `fabro_types::settings::SettingsFile` type). Fields are opaque to the -// `RunSettings` TypeScript type, which is a bare `Record`. +// hardcoded showcase workflows. Shape mirrors the persisted `SettingsLayer` +// JSON returned by `/api/v1/runs/:id/settings`. Fields are opaque to the +// `RunSettingsLayer` TypeScript type, which is a bare `Record`. export const workflowData: Record = { fix_build: { name: "Fix Build", diff --git a/bin/dev/check-boundary.sh b/bin/dev/check-boundary.sh index d1cfb3f50..7771d58ed 100755 --- a/bin/dev/check-boundary.sh +++ b/bin/dev/check-boundary.sh @@ -6,6 +6,7 @@ cd "$(dirname "$0")/../.." symbol_allowlist=( "lib/crates/fabro-cli/src/local_server.rs" "lib/crates/fabro-cli/src/commands/install.rs" + "lib/crates/fabro-cli/src/commands/uninstall.rs" "lib/crates/fabro-cli/src/commands/run/runner.rs" "lib/crates/fabro-cli/src/commands/pr/mod.rs" "lib/crates/fabro-cli/src/commands/pr/create.rs" @@ -53,7 +54,7 @@ while IFS= read -r path; do echo "boundary check failed: gated server symbol used outside allowlist: $path" >&2 fail=1 fi -done < <(find_matches 'fabro_config::resolve_server_from_file|fabro_config::resolve_server\b|Storage::new') +done < <(find_matches 'fabro_config::resolve_server_from_file|fabro_config::resolve_server\b|fabro_config::ServerSettings::from_layer\b|fabro_config::ServerSettings::resolve\b|ServerSettings::from_layer\b|ServerSettings::resolve\b|Storage::new') while IFS= read -r path; do [[ -z "$path" ]] && continue diff --git a/docs/api-reference/fabro-api.yaml b/docs/api-reference/fabro-api.yaml index 9eefde011..9439eb1e6 100644 --- a/docs/api-reference/fabro-api.yaml +++ b/docs/api-reference/fabro-api.yaml @@ -1314,7 +1314,7 @@ paths: operationId: retrieveRunSettings tags: [Run Internals] summary: Retrieve Run Settings - description: Returns the structured settings used to launch this run. + description: Returns the persisted `SettingsLayer` used to launch this run. parameters: - $ref: "#/components/parameters/RunId" responses: @@ -1323,7 +1323,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/RunSettings" + $ref: "#/components/schemas/RunSettingsLayer" "404": description: Run not found content: @@ -1950,22 +1950,11 @@ paths: tags: [Settings] summary: Retrieve Server Settings description: > - Returns the server settings view selected by the optional `view` query - parameter. `view=layer` (the default) returns the current sparse - redacted `SettingsLayer` payload. `view=resolved` returns the server's - dense resolved settings payload after applying the same redaction - policy. - parameters: - - $ref: "#/components/parameters/SettingsView" + Returns the server's current in-memory settings view as the typed + `ServerSettings` payload. responses: "200": description: Server settings - headers: - X-Fabro-Settings-View: - description: Present with value `resolved` when the response body is the dense resolved settings view. - schema: - type: string - enum: [resolved] content: application/json: schema: @@ -2007,16 +1996,6 @@ components: type: string example: nightly-build - SettingsView: - name: view - in: query - required: false - description: Selects the server settings representation to return. - schema: - type: string - enum: [layer, resolved] - default: layer - StageId: name: stageId in: path @@ -4895,32 +4874,328 @@ components: # ── Settings Schemas ───────────────────────────────────────────────── ServerSettings: - description: | - Redacted server settings payload. - - The `/api/v1/settings` endpoint supports two response shapes: - - - `view=layer` (default): the sparse redacted `SettingsLayer` shape - - `view=resolved`: the dense resolved `Settings` shape - - Both views drop the same exact operational path: - - - `server.listen` - - For non-redacted `InterpString` fields, the wire payload preserves the - unresolved source/template string rather than any environment-resolved - secret value. + description: Current in-memory server settings view. type: object - additionalProperties: true + required: [server, features] + properties: + server: + $ref: "#/components/schemas/ServerNamespace" + features: + $ref: "#/components/schemas/FeaturesNamespace" - RunSettings: + ServerNamespace: + type: object + required: + - listen + - api + - web + - auth + - ip_allowlist + - storage + - artifacts + - slatedb + - scheduler + - logging + - integrations + properties: + listen: + $ref: "#/components/schemas/ServerListenSettings" + api: + $ref: "#/components/schemas/ServerApiSettings" + web: + $ref: "#/components/schemas/ServerWebSettings" + auth: + $ref: "#/components/schemas/ServerAuthSettings" + ip_allowlist: + $ref: "#/components/schemas/ServerIpAllowlistSettings" + storage: + $ref: "#/components/schemas/ServerStorageSettings" + artifacts: + $ref: "#/components/schemas/ServerArtifactsSettings" + slatedb: + $ref: "#/components/schemas/ServerSlateDbSettings" + scheduler: + $ref: "#/components/schemas/ServerSchedulerSettings" + logging: + $ref: "#/components/schemas/ServerLoggingSettings" + integrations: + $ref: "#/components/schemas/ServerIntegrationsSettings" + + FeaturesNamespace: + type: object + required: [session_sandboxes] + properties: + session_sandboxes: + type: boolean + + ServerListenSettings: + oneOf: + - $ref: "#/components/schemas/ServerListenTcpSettings" + - $ref: "#/components/schemas/ServerListenUnixSettings" + + ServerListenTcpSettings: + type: object + required: [type, address] + properties: + type: + type: string + enum: [tcp] + address: + type: string + + ServerListenUnixSettings: + type: object + required: [type, path] + properties: + type: + type: string + enum: [unix] + path: + type: string + + ServerApiSettings: + type: object + required: [url] + properties: + url: + type: ["string", "null"] + + ServerWebSettings: + type: object + required: [enabled, url] + properties: + enabled: + type: boolean + url: + type: string + + ServerAuthSettings: + type: object + required: [methods, github] + properties: + methods: + type: array + items: + $ref: "#/components/schemas/ServerAuthMethod" + github: + $ref: "#/components/schemas/ServerAuthGithubSettings" + + ServerAuthMethod: + type: string + enum: [dev-token, github] + + ServerAuthGithubSettings: + type: object + required: [allowed_usernames] + properties: + allowed_usernames: + type: array + items: + type: string + + ServerIpAllowlistSettings: + type: object + required: [entries, trusted_proxy_count] + properties: + entries: + type: array + items: + $ref: "#/components/schemas/IpAllowEntry" + trusted_proxy_count: + type: integer + + ServerIpAllowlistOverrideSettings: + type: object + required: [entries, trusted_proxy_count] + properties: + entries: + type: ["array", "null"] + items: + $ref: "#/components/schemas/IpAllowEntry" + trusted_proxy_count: + type: ["integer", "null"] + + IpAllowEntry: + oneOf: + - $ref: "#/components/schemas/LiteralIpAllowEntry" + - $ref: "#/components/schemas/GitHubMetaHooksEntry" + + LiteralIpAllowEntry: + type: object + required: [Literal] + properties: + Literal: + type: string + + GitHubMetaHooksEntry: + type: string + enum: [GitHubMetaHooks] + + ServerStorageSettings: + type: object + required: [root] + properties: + root: + type: string + + ServerArtifactsSettings: + type: object + required: [prefix, store] + properties: + prefix: + type: string + store: + $ref: "#/components/schemas/ObjectStoreSettings" + + ServerSlateDbSettings: + type: object + required: [prefix, store, flush_interval, disk_cache] + properties: + prefix: + type: string + store: + $ref: "#/components/schemas/ObjectStoreSettings" + flush_interval: + type: string + disk_cache: + type: boolean + + ObjectStoreSettings: + oneOf: + - $ref: "#/components/schemas/ObjectStoreLocalSettings" + - $ref: "#/components/schemas/ObjectStoreS3Settings" + + ObjectStoreLocalSettings: + type: object + required: [type, root] + properties: + type: + type: string + enum: [local] + root: + type: string + + ObjectStoreS3Settings: + type: object + required: [type, bucket, region, endpoint, path_style] + properties: + type: + type: string + enum: [s3] + bucket: + type: string + region: + type: string + endpoint: + type: ["string", "null"] + path_style: + type: boolean + + ServerSchedulerSettings: + type: object + required: [max_concurrent_runs] + properties: + max_concurrent_runs: + type: integer + + ServerLoggingSettings: + type: object + required: [level] + properties: + level: + type: ["string", "null"] + + ServerIntegrationsSettings: + type: object + required: [github, slack, discord, teams] + properties: + github: + $ref: "#/components/schemas/GithubIntegrationSettings" + slack: + $ref: "#/components/schemas/SlackIntegrationSettings" + discord: + $ref: "#/components/schemas/DiscordIntegrationSettings" + teams: + $ref: "#/components/schemas/TeamsIntegrationSettings" + + GithubIntegrationSettings: + type: object + required: + - enabled + - strategy + - app_id + - client_id + - slug + - permissions + - webhooks + properties: + enabled: + type: boolean + strategy: + $ref: "#/components/schemas/GithubIntegrationStrategy" + app_id: + type: ["string", "null"] + client_id: + type: ["string", "null"] + slug: + type: ["string", "null"] + permissions: + type: object + additionalProperties: + type: string + webhooks: + oneOf: + - $ref: "#/components/schemas/IntegrationWebhooksSettings" + - type: "null" + + GithubIntegrationStrategy: + type: string + enum: [token, app] + + SlackIntegrationSettings: + type: object + required: [enabled, default_channel] + properties: + enabled: + type: boolean + default_channel: + type: ["string", "null"] + + DiscordIntegrationSettings: + type: object + required: [enabled] + properties: + enabled: + type: boolean + + TeamsIntegrationSettings: + type: object + required: [enabled] + properties: + enabled: + type: boolean + + IntegrationWebhooksSettings: + type: object + required: [strategy, ip_allowlist] + properties: + strategy: + oneOf: + - $ref: "#/components/schemas/WebhookStrategy" + - type: "null" + ip_allowlist: + oneOf: + - $ref: "#/components/schemas/ServerIpAllowlistOverrideSettings" + - type: "null" + + WebhookStrategy: + type: string + enum: [tailscale_funnel, server_url] + + RunSettingsLayer: description: | - The merged, persisted v2 `[run]` subtree for a specific run, serialized - as the wrapping `SettingsFile` shape (so `settings.run.*` holds the run - config). Matches `fabro_types::settings::SettingsFile` minus secret - subtrees, identical to ServerSettings' redaction rules. - - See `lib/crates/fabro-types/src/settings/run.rs` for the full type. + The persisted `SettingsLayer` used for a specific run, serialized as-is. + This matches the stored run manifest shape rather than a resolved view. type: object additionalProperties: true diff --git a/lib/crates/fabro-api/Cargo.toml b/lib/crates/fabro-api/Cargo.toml index cf3335ea6..fe476f2c2 100644 --- a/lib/crates/fabro-api/Cargo.toml +++ b/lib/crates/fabro-api/Cargo.toml @@ -15,6 +15,7 @@ wildcard_imports = "warn" [dependencies] chrono = { workspace = true, features = ["serde"] } +fabro-config = { path = "../fabro-config" } fabro-types = { path = "../fabro-types" } progenitor-client = "0.13" regress = "0.10" diff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs index 168fe8bb9..86a7fb474 100644 --- a/lib/crates/fabro-api/build.rs +++ b/lib/crates/fabro-api/build.rs @@ -177,6 +177,132 @@ fn main() { "fabro_types::status::RunStatusRecord", &[], ), + ("ServerSettings", "fabro_config::ServerSettings", &[]), + ( + "ServerNamespace", + "fabro_types::settings::ServerNamespace", + &[], + ), + ( + "FeaturesNamespace", + "fabro_types::settings::FeaturesNamespace", + &[], + ), + ( + "ServerListenSettings", + "fabro_types::settings::server::ServerListenSettings", + &[], + ), + ( + "ServerApiSettings", + "fabro_types::settings::server::ServerApiSettings", + &[], + ), + ( + "ServerWebSettings", + "fabro_types::settings::server::ServerWebSettings", + &[], + ), + ( + "ServerAuthSettings", + "fabro_types::settings::server::ServerAuthSettings", + &[], + ), + ( + "ServerAuthMethod", + "fabro_types::settings::server::ServerAuthMethod", + &[], + ), + ( + "ServerAuthGithubSettings", + "fabro_types::settings::server::ServerAuthGithubSettings", + &[], + ), + ( + "ServerIpAllowlistSettings", + "fabro_types::settings::server::ServerIpAllowlistSettings", + &[], + ), + ( + "ServerIpAllowlistOverrideSettings", + "fabro_types::settings::server::ServerIpAllowlistOverrideSettings", + &[], + ), + ( + "IpAllowEntry", + "fabro_types::settings::server::IpAllowEntry", + &[], + ), + ( + "ServerStorageSettings", + "fabro_types::settings::server::ServerStorageSettings", + &[], + ), + ( + "ServerArtifactsSettings", + "fabro_types::settings::server::ServerArtifactsSettings", + &[], + ), + ( + "ServerSlateDbSettings", + "fabro_types::settings::server::ServerSlateDbSettings", + &[], + ), + ( + "ObjectStoreSettings", + "fabro_types::settings::server::ObjectStoreSettings", + &[], + ), + ( + "ServerSchedulerSettings", + "fabro_types::settings::server::ServerSchedulerSettings", + &[], + ), + ( + "ServerLoggingSettings", + "fabro_types::settings::server::ServerLoggingSettings", + &[], + ), + ( + "ServerIntegrationsSettings", + "fabro_types::settings::server::ServerIntegrationsSettings", + &[], + ), + ( + "GithubIntegrationSettings", + "fabro_types::settings::server::GithubIntegrationSettings", + &[], + ), + ( + "GithubIntegrationStrategy", + "fabro_types::settings::server::GithubIntegrationStrategy", + &[], + ), + ( + "SlackIntegrationSettings", + "fabro_types::settings::server::SlackIntegrationSettings", + &[], + ), + ( + "DiscordIntegrationSettings", + "fabro_types::settings::server::DiscordIntegrationSettings", + &[], + ), + ( + "TeamsIntegrationSettings", + "fabro_types::settings::server::TeamsIntegrationSettings", + &[], + ), + ( + "IntegrationWebhooksSettings", + "fabro_types::settings::server::IntegrationWebhooksSettings", + &[], + ), + ( + "WebhookStrategy", + "fabro_types::settings::server::WebhookStrategy", + &[], + ), ]; for (name, path, impls) in replacements { settings.with_replacement(*name, *path, impls.iter().copied()); diff --git a/lib/crates/fabro-api/src/lib.rs b/lib/crates/fabro-api/src/lib.rs index b039d6c12..949d0e215 100644 --- a/lib/crates/fabro-api/src/lib.rs +++ b/lib/crates/fabro-api/src/lib.rs @@ -14,6 +14,17 @@ mod generated { include!(concat!(env!("OUT_DIR"), "/codegen.rs")); } pub mod types { + pub use fabro_config::ServerSettings; + pub use fabro_types::settings::server::{ + DiscordIntegrationSettings, GithubIntegrationSettings, GithubIntegrationStrategy, + IntegrationWebhooksSettings, IpAllowEntry, ObjectStoreSettings, ServerApiSettings, + ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, + ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings, + ServerListenSettings, ServerLoggingSettings, ServerSchedulerSettings, + ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, + TeamsIntegrationSettings, WebhookStrategy, + }; + pub use fabro_types::settings::{FeaturesNamespace, ServerNamespace}; pub use fabro_types::status::{ BlockedReason, RunControlAction, RunStatus, RunStatusRecord, StatusReason, }; diff --git a/lib/crates/fabro-api/tests/server_settings_round_trip.rs b/lib/crates/fabro-api/tests/server_settings_round_trip.rs new file mode 100644 index 000000000..487c37f77 --- /dev/null +++ b/lib/crates/fabro-api/tests/server_settings_round_trip.rs @@ -0,0 +1,78 @@ +use std::any::{TypeId, type_name}; + +use fabro_api::types::{ + FeaturesNamespace as ApiFeaturesNamespace, ObjectStoreSettings as ApiObjectStoreSettings, + ServerNamespace as ApiServerNamespace, ServerSettings as ApiServerSettings, +}; +use fabro_config::{ServerSettings, parse_settings_layer}; +use fabro_types::settings::server::ObjectStoreSettings; +use fabro_types::settings::{FeaturesNamespace, ServerNamespace}; + +#[test] +fn server_settings_family_reuses_domain_types() { + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); +} + +#[test] +fn server_settings_json_matches_openapi_shape() { + let layer = parse_settings_layer( + r#" +_version = 1 + +[server.listen] +type = "tcp" +address = "127.0.0.1:32276" + +[server.api] +url = "https://api.fabro.example.com" + +[server.web] +enabled = true +url = "https://fabro.example.com" + +[server.auth] +methods = ["dev-token", "github"] + +[server.auth.github] +allowed_usernames = ["alice"] + +[server.storage] +root = "/srv/fabro" + +[server.integrations.github] +enabled = true +strategy = "app" +app_id = "12345" +client_id = "Iv1.abcdef" +slug = "fabro-dev" + +[features] +session_sandboxes = true +"#, + ) + .expect("settings fixture should parse"); + let settings = ServerSettings::from_layer(&layer).expect("settings should resolve"); + + let json = serde_json::to_value(&settings).expect("server settings should serialize"); + assert_eq!(json["server"]["listen"]["type"], "tcp"); + assert_eq!(json["server"]["listen"]["address"], "127.0.0.1:32276"); + assert_eq!(json["server"]["storage"]["root"], "/srv/fabro"); + assert_eq!(json["features"]["session_sandboxes"], true); + + let round_trip: ApiServerSettings = + serde_json::from_value(json).expect("server settings should deserialize"); + assert_eq!(round_trip, settings); +} + +fn assert_same_type() { + assert_eq!( + TypeId::of::(), + TypeId::of::(), + "{} should be the same type as {}", + type_name::(), + type_name::() + ); +} diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs index b8c92fddb..44e4160e3 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -712,13 +712,6 @@ pub(crate) struct SystemEventsArgs { pub(crate) struct SettingsArgs { #[command(flatten)] pub(crate) target: ServerTargetArgs, - - /// Show only locally resolved settings and skip the server call - #[arg(long, conflicts_with = "server")] - pub(crate) local: bool, - - /// Optional workflow name, .fabro path, or .toml run config to overlay - pub(crate) workflow: Option, } #[derive(Args)] diff --git a/lib/crates/fabro-cli/src/command_context.rs b/lib/crates/fabro-cli/src/command_context.rs index c8f6635b8..ac508f5ed 100644 --- a/lib/crates/fabro-cli/src/command_context.rs +++ b/lib/crates/fabro-cli/src/command_context.rs @@ -2,9 +2,10 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{Context as _, Result, bail}; +use fabro_config::UserSettings; use fabro_config::merge::combine_files; use fabro_types::settings::cli::CliLayer; -use fabro_types::settings::{CliSettings, SettingsLayer}; +use fabro_types::settings::{CliNamespace, SettingsLayer}; use fabro_util::printer::Printer; use tokio::sync::OnceCell; @@ -33,7 +34,8 @@ pub(crate) struct CommandContext { cwd: PathBuf, base_config_path: PathBuf, machine_settings: SettingsLayer, - cli_settings: CliSettings, + user_settings: UserSettings, + cli_settings: CliNamespace, server_mode: ServerMode, server: OnceCell>, } @@ -41,7 +43,7 @@ pub(crate) struct CommandContext { impl CommandContext { pub(crate) fn base( printer: Printer, - cli_settings: CliSettings, + cli_settings: CliNamespace, cli_layer: &CliLayer, ) -> Result { Self::new(printer, ServerMode::None, cli_settings, cli_layer) @@ -50,7 +52,7 @@ impl CommandContext { pub(crate) fn for_target( args: &ServerTargetArgs, printer: Printer, - cli_settings: CliSettings, + cli_settings: CliNamespace, cli_layer: &CliLayer, ) -> Result { Self::new( @@ -66,7 +68,7 @@ impl CommandContext { pub(crate) fn for_connection( args: &ServerConnectionArgs, printer: Printer, - cli_settings: CliSettings, + cli_settings: CliNamespace, cli_layer: &CliLayer, ) -> Result { Self::new( @@ -83,7 +85,7 @@ impl CommandContext { fn new( printer: Printer, server_mode: ServerMode, - cli_settings: CliSettings, + cli_settings: CliNamespace, cli_layer: &CliLayer, ) -> Result { let cwd = std::env::current_dir().context("Failed to get current directory")?; @@ -99,12 +101,14 @@ impl CommandContext { cli: Some(cli_layer.clone()), ..SettingsLayer::default() }); + let user_settings = user_config::resolve_user_settings(&machine_settings)?; Ok(Self { printer, cwd, base_config_path, machine_settings, + user_settings, cli_settings, server_mode, server: OnceCell::new(), @@ -123,15 +127,15 @@ impl CommandContext { &self.cwd } - pub(crate) fn base_config_path(&self) -> &Path { - &self.base_config_path - } - pub(crate) fn machine_settings(&self) -> &SettingsLayer { &self.machine_settings } - pub(crate) fn cli_settings(&self) -> &CliSettings { + pub(crate) fn user_settings(&self) -> &UserSettings { + &self.user_settings + } + + pub(crate) fn cli_settings(&self) -> &CliNamespace { &self.cli_settings } diff --git a/lib/crates/fabro-cli/src/commands/artifact/cp.rs b/lib/crates/fabro-cli/src/commands/artifact/cp.rs index a465af4f7..9b87dd6a1 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/cp.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/cp.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; @@ -16,7 +16,7 @@ use crate::shared::{print_json_pretty, split_run_path}; pub(super) async fn cp_command( args: &ArtifactCpArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/artifact/list.rs b/lib/crates/fabro-cli/src/commands/artifact/list.rs index fefe8b61c..0637f7a5b 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/list.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/list.rs @@ -1,7 +1,7 @@ use anyhow::Result; use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Style, Table}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -10,7 +10,7 @@ use crate::args::ArtifactListArgs; pub(super) async fn list_command( args: &ArtifactListArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/artifact/mod.rs b/lib/crates/fabro-cli/src/commands/artifact/mod.rs index 56cfa6ad8..571f3cdb0 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/mod.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/mod.rs @@ -2,7 +2,7 @@ mod cp; mod list; use anyhow::{Context, Result}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_types::{RunId, StageId}; use fabro_util::printer::Printer; @@ -26,7 +26,7 @@ pub(super) async fn resolve_artifacts( run_selector: &str, node: Option<&str>, retry: Option, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<(RunId, Client, Vec)> { @@ -65,7 +65,7 @@ pub(super) async fn resolve_artifacts( pub(crate) async fn dispatch( ns: ArtifactNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/auth/login.rs b/lib/crates/fabro-cli/src/commands/auth/login.rs index 8ccf866ca..5d3d69a30 100644 --- a/lib/crates/fabro-cli/src/commands/auth/login.rs +++ b/lib/crates/fabro-cli/src/commands/auth/login.rs @@ -4,7 +4,7 @@ use anyhow::{Context as _, Result, bail}; use chrono::{DateTime, Utc}; use fabro_client::{AuthEntry, AuthStore, StoredSubject}; use fabro_http::header::CONTENT_TYPE; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::browser; use fabro_util::printer::Printer; @@ -36,7 +36,7 @@ struct CliTokenSubject { pub(super) async fn login_command( args: AuthLoginArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/auth/logout.rs b/lib/crates/fabro-cli/src/commands/auth/logout.rs index 4b72ef5c1..eda47e51b 100644 --- a/lib/crates/fabro-cli/src/commands/auth/logout.rs +++ b/lib/crates/fabro-cli/src/commands/auth/logout.rs @@ -1,7 +1,7 @@ use anyhow::{Result, bail}; use fabro_client::{AuthEntry, AuthStore}; use fabro_http::header::AUTHORIZATION; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; @@ -12,7 +12,7 @@ use crate::user_config::ServerTarget; pub(super) async fn logout_command( args: AuthLogoutArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/auth/mod.rs b/lib/crates/fabro-cli/src/commands/auth/mod.rs index 0cb79d6b4..1d5a4546e 100644 --- a/lib/crates/fabro-cli/src/commands/auth/mod.rs +++ b/lib/crates/fabro-cli/src/commands/auth/mod.rs @@ -3,7 +3,7 @@ mod logout; mod status; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; @@ -11,7 +11,7 @@ use crate::args::{AuthCommand, AuthNamespace}; pub(crate) async fn dispatch( ns: AuthNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/auth/status.rs b/lib/crates/fabro-cli/src/commands/auth/status.rs index de81e60c2..86962b52a 100644 --- a/lib/crates/fabro-cli/src/commands/auth/status.rs +++ b/lib/crates/fabro-cli/src/commands/auth/status.rs @@ -1,7 +1,7 @@ use anyhow::Result; use chrono::{DateTime, Utc}; use fabro_client::{AuthEntry, AuthStore}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::dev_token::{read_dev_token_file, validate_dev_token_format}; use fabro_util::printer::Printer; @@ -43,7 +43,7 @@ struct StatusOutput { pub(super) fn status_command( args: &AuthStatusArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/config/mod.rs b/lib/crates/fabro-cli/src/commands/config/mod.rs index b81434d66..1f329af84 100644 --- a/lib/crates/fabro-cli/src/commands/config/mod.rs +++ b/lib/crates/fabro-cli/src/commands/config/mod.rs @@ -8,151 +8,41 @@ )] use std::io::Write; -use std::path::Path; -use fabro_config::effective_settings::{ - EffectiveSettingsLayers, EffectiveSettingsMode, materialize_settings_layer, -}; -use fabro_config::{load_settings_project, project}; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_types::settings::{CliSettings, SettingsLayer}; use fabro_util::printer::Printer; -use serde_json::json; +use serde::Serialize; use crate::args::SettingsArgs; use crate::command_context::CommandContext; use crate::shared::print_json_pretty; -use crate::user_config; -fn config_layers( - ctx: &CommandContext, - workflow: Option<&Path>, -) -> anyhow::Result { - let cwd = ctx.cwd(); - let (workflow_layer, project_layer) = match workflow { - Some(path) => workflow_and_project_layers(path, cwd)?, - None => (SettingsLayer::default(), load_settings_project(cwd)?), - }; - let user_layer = - user_config::load_settings_with_config_and_storage_dir(Some(ctx.base_config_path()), None)?; - Ok(EffectiveSettingsLayers::new( - SettingsLayer::default(), - workflow_layer, - project_layer, - user_layer, - )) -} - -fn workflow_and_project_layers( - path: &Path, - cwd: &Path, -) -> anyhow::Result<(SettingsLayer, SettingsLayer)> { - let resolution = project::resolve_workflow_path(path, cwd)?; - if resolution.workflow_config.is_none() && !resolution.resolved_workflow_path.is_file() { - anyhow::bail!( - "Workflow not found: {}", - resolution.resolved_workflow_path.display() - ); - } - - let workflow_layer = resolution.workflow_config.unwrap_or_default(); - let project_layer = project::discover_project_config( - resolution - .resolved_workflow_path - .parent() - .unwrap_or_else(|| Path::new(".")), - )? - .map(|(_, config)| config) - .unwrap_or_default(); - - Ok((workflow_layer, project_layer)) -} - -fn strip_nulls(value: &mut serde_json::Value) { - match value { - serde_json::Value::Object(map) => { - for child in map.values_mut() { - strip_nulls(child); - } - map.retain(|_, child| !child.is_null()); - } - serde_json::Value::Array(values) => { - for child in values { - strip_nulls(child); - } - } - _ => {} - } -} - -fn local_settings_value( - args: &SettingsArgs, - cli: &CliSettings, - cli_layer: &CliLayer, - printer: Printer, -) -> anyhow::Result { - let base_ctx = CommandContext::base(printer, cli.clone(), cli_layer)?; - let layers = config_layers(&base_ctx, args.workflow.as_deref())?; - let local_settings = - materialize_settings_layer(layers, None, EffectiveSettingsMode::LocalOnly)?; - let mut value = resolve_local_settings_value(&local_settings)?; - strip_nulls(&mut value); - Ok(value) -} - -fn render_resolve_errors(errors: Vec) -> anyhow::Error { - anyhow::anyhow!( - "failed to resolve local settings:\n{}", - errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n") - ) -} - -fn resolve_local_settings_value(file: &SettingsLayer) -> anyhow::Result { - let file = fabro_config::apply_builtin_defaults(file.clone()); - - let project = fabro_config::resolve_project_from_file(&file).map_err(render_resolve_errors)?; - let workflow = - fabro_config::resolve_workflow_from_file(&file).map_err(render_resolve_errors)?; - let run = fabro_config::resolve_run_from_file(&file).map_err(render_resolve_errors)?; - let cli = fabro_config::resolve_cli_from_file(&file).map_err(render_resolve_errors)?; - let features = - fabro_config::resolve_features_from_file(&file).map_err(render_resolve_errors)?; - - Ok(json!({ - "project": project, - "workflow": workflow, - "run": run, - "cli": cli, - "features": features, - })) +#[derive(Serialize)] +struct RenderedConfig { + user: fabro_config::UserSettings, + server: fabro_api::types::ServerSettings, } async fn rendered_config( args: &SettingsArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result { - if args.local { - return local_settings_value(args, cli, cli_layer, printer); - } - if args.workflow.is_some() { - anyhow::bail!("WORKFLOW requires --local; use `fabro settings --local WORKFLOW`"); - } let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; - ctx.server() + let user = fabro_config::UserSettings::resolve()?; + let server = ctx + .server() .await? .retrieve_resolved_server_settings() - .await + .await?; + serde_json::to_value(RenderedConfig { user, server }).map_err(Into::into) } pub(crate) async fn execute( args: &SettingsArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/doctor.rs b/lib/crates/fabro-cli/src/commands/doctor.rs index cdf9931ac..fb25bb147 100644 --- a/lib/crates/fabro-cli/src/commands/doctor.rs +++ b/lib/crates/fabro-cli/src/commands/doctor.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use anyhow::Result; use fabro_api::types as api_types; use fabro_config::user::active_settings_path; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; pub(crate) use fabro_util::check_report::{ CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus, @@ -143,7 +143,7 @@ fn render_report(report: &CheckReport, styles: &Styles, verbose: bool, printer: pub(crate) async fn run_doctor( args: &DoctorArgs, verbose: bool, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result { diff --git a/lib/crates/fabro-cli/src/commands/exec.rs b/lib/crates/fabro-cli/src/commands/exec.rs index 9680e605b..4048915e5 100644 --- a/lib/crates/fabro-cli/src/commands/exec.rs +++ b/lib/crates/fabro-cli/src/commands/exec.rs @@ -15,7 +15,7 @@ use fabro_llm::types::{ use fabro_mcp::config::{McpServerSettings, McpTransport}; use fabro_types::settings::cli::OutputFormat as SettingsOutputFormat; use fabro_types::settings::run::McpEntryLayer; -use fabro_types::settings::{CliSettings, InterpString}; +use fabro_types::settings::{CliNamespace, InterpString}; use fabro_util::exit::{ErrorExt, ExitClass}; use fabro_util::printer::Printer; use futures::stream; @@ -358,7 +358,7 @@ impl ProviderAdapter for AuthenticatedFabroServerAdapter { pub(crate) async fn execute( mut args: ExecArgs, - cli: &CliSettings, + cli: &CliNamespace, _printer: Printer, ) -> AnyResult<()> { use fabro_agent::cli::PermissionLevel as AgentPermissionLevel; diff --git a/lib/crates/fabro-cli/src/commands/graph.rs b/lib/crates/fabro-cli/src/commands/graph.rs index 7f7b21e93..0df39a2e1 100644 --- a/lib/crates/fabro-cli/src/commands/graph.rs +++ b/lib/crates/fabro-cli/src/commands/graph.rs @@ -14,7 +14,7 @@ use fabro_api::types; use fabro_config::load::load_settings_user; use fabro_config::user::active_settings_path; use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_types::settings::{CliSettings, SettingsLayer}; +use fabro_types::settings::{CliNamespace, SettingsLayer}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use tracing::debug; @@ -28,7 +28,7 @@ use crate::shared::{absolute_or_current, print_diagnostics, print_json_pretty, r pub(crate) async fn run( args: &GraphArgs, styles: &Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/install.rs b/lib/crates/fabro-cli/src/commands/install.rs index d7f2742ed..886524266 100644 --- a/lib/crates/fabro-cli/src/commands/install.rs +++ b/lib/crates/fabro-cli/src/commands/install.rs @@ -24,7 +24,7 @@ use fabro_auth::{AuthCredential, AuthMethod, codex_oauth_config, credential_id_f use fabro_config::bind::Bind; use fabro_config::daemon::ServerDaemon; use fabro_config::user::{SETTINGS_CONFIG_FILENAME, default_storage_dir}; -use fabro_config::{ResolveError, Storage, envfile}; +use fabro_config::{Storage, envfile}; use fabro_install::{ InstallListenConfig, generate_jwt_keypair, merge_server_settings as merge_server_settings_impl, write_github_app_settings, write_token_settings, @@ -34,7 +34,7 @@ use fabro_server::serve; use fabro_store::ArtifactStore; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_types::settings::server::ServerAuthMethod; -use fabro_types::settings::{CliSettings, SettingsLayer}; +use fabro_types::settings::{CliNamespace, SettingsLayer}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_util::version::FABRO_VERSION; @@ -1273,24 +1273,13 @@ fn persist_github_install_changes( Ok(()) } -fn render_server_resolve_errors(errors: Vec) -> anyhow::Error { - anyhow::anyhow!( - "failed to resolve server settings:\n{}", - errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n") - ) -} - async fn write_artifact_store_metadata( settings: &SettingsLayer, fabro_version: &str, ) -> Result<()> { let resolved = - fabro_config::resolve_server_from_file(settings).map_err(render_server_resolve_errors)?; - let (object_store, prefix) = serve::build_artifact_object_store(&resolved)?; + fabro_config::ServerSettings::from_layer(settings).map_err(anyhow::Error::from)?; + let (object_store, prefix) = serve::build_artifact_object_store(&resolved.server)?; let artifact_store = ArtifactStore::new(object_store, prefix); artifact_store.write_metadata(fabro_version).await?; Ok(()) @@ -1427,7 +1416,7 @@ where pub(crate) async fn execute( args: &InstallArgs, command: Option, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, @@ -1443,7 +1432,7 @@ pub(crate) async fn execute( async fn run_install_github_command( args: &InstallArgs, github_args: &InstallGithubArgs, - cli: &CliSettings, + cli: &CliNamespace, process_local_json: bool, printer: Printer, ) -> Result<()> { @@ -1606,7 +1595,7 @@ async fn run_install_github_inner( pub(crate) async fn run_install( args: &InstallArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, @@ -1632,7 +1621,7 @@ pub(crate) async fn run_install( async fn run_install_inner( args: &InstallArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { @@ -1808,8 +1797,7 @@ async fn run_install_inner( .context("failed to parse generated settings.toml")?, args.storage_dir.as_deref(), ); - fabro_config::resolve_server_from_file(&install_settings) - .map_err(render_server_resolve_errors)?; + fabro_config::ServerSettings::from_layer(&install_settings).map_err(anyhow::Error::from)?; // Secrets and auth material { diff --git a/lib/crates/fabro-cli/src/commands/model.rs b/lib/crates/fabro-cli/src/commands/model.rs index 61b22b490..c96b3c349 100644 --- a/lib/crates/fabro-cli/src/commands/model.rs +++ b/lib/crates/fabro-cli/src/commands/model.rs @@ -3,7 +3,7 @@ use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; use fabro_api::types as api_types; use fabro_model::{Catalog, Model, Provider}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -42,7 +42,7 @@ struct ModelTestOutput { pub(crate) async fn execute( command: Option, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/parse.rs b/lib/crates/fabro-cli/src/commands/parse.rs index bc0a21306..e14d970ce 100644 --- a/lib/crates/fabro-cli/src/commands/parse.rs +++ b/lib/crates/fabro-cli/src/commands/parse.rs @@ -11,13 +11,13 @@ use std::io::Write; use fabro_config::project::resolve_workflow; use fabro_graphviz::parser::parse_ast; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_util::printer::Printer; use crate::args::ParseArgs; use crate::shared::read_workflow_file; -pub(crate) fn run(args: &ParseArgs, _cli: &CliSettings, _printer: Printer) -> anyhow::Result<()> { +pub(crate) fn run(args: &ParseArgs, _cli: &CliNamespace, _printer: Printer) -> anyhow::Result<()> { let stdout = std::io::stdout(); run_to(args, stdout.lock()) } diff --git a/lib/crates/fabro-cli/src/commands/pr/close.rs b/lib/crates/fabro-cli/src/commands/pr/close.rs index 1732c6556..0cc9a4a39 100644 --- a/lib/crates/fabro-cli/src/commands/pr/close.rs +++ b/lib/crates/fabro-cli/src/commands/pr/close.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::info; @@ -9,7 +9,7 @@ use crate::shared::print_json_pretty; pub(super) async fn close_command( args: PrCloseArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/pr/create.rs b/lib/crates/fabro-cli/src/commands/pr/create.rs index 3e269ba79..d26b8080b 100644 --- a/lib/crates/fabro-cli/src/commands/pr/create.rs +++ b/lib/crates/fabro-cli/src/commands/pr/create.rs @@ -5,7 +5,7 @@ use fabro_auth::configured_providers_from_process_env; use fabro_config::Storage; use fabro_model::Catalog; use fabro_sandbox::daytona::detect_repo_info; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_vault::Vault; @@ -27,7 +27,7 @@ use crate::user_config; )] pub(super) async fn create_command( args: PrCreateArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/pr/list.rs b/lib/crates/fabro-cli/src/commands/pr/list.rs index bcd497a12..2e0f51f1b 100644 --- a/lib/crates/fabro-cli/src/commands/pr/list.rs +++ b/lib/crates/fabro-cli/src/commands/pr/list.rs @@ -1,7 +1,7 @@ use anyhow::Result; use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -25,7 +25,7 @@ struct PrRow { pub(super) async fn list_command( args: PrListArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/pr/merge.rs b/lib/crates/fabro-cli/src/commands/pr/merge.rs index 7e68240c8..be7ce9f61 100644 --- a/lib/crates/fabro-cli/src/commands/pr/merge.rs +++ b/lib/crates/fabro-cli/src/commands/pr/merge.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::info; @@ -9,7 +9,7 @@ use crate::shared::print_json_pretty; pub(super) async fn merge_command( args: PrMergeArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/pr/mod.rs b/lib/crates/fabro-cli/src/commands/pr/mod.rs index bf7a3671b..8d1933f66 100644 --- a/lib/crates/fabro-cli/src/commands/pr/mod.rs +++ b/lib/crates/fabro-cli/src/commands/pr/mod.rs @@ -9,7 +9,7 @@ use fabro_config::Storage; use fabro_github::GitHubCredentials; use fabro_types::PullRequestRecord; use fabro_types::settings::cli::CliLayer; -use fabro_types::settings::{CliSettings, InterpString}; +use fabro_types::settings::{CliNamespace, InterpString}; use fabro_util::printer::Printer; use crate::args::{PrCommand, PrNamespace, ServerTargetArgs}; @@ -22,7 +22,7 @@ const GITHUB_CREDENTIALS_REQUIRED: &str = pub(crate) async fn dispatch( ns: PrNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { @@ -42,28 +42,20 @@ pub(crate) async fn dispatch( reason = "boundary-exempt(pr-api): remove with follow-up #1 when PR ops move server-side" )] fn load_github_credentials_required( - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result { let ctx = CommandContext::base(printer, cli.clone(), cli_layer)?; - let server_settings = - fabro_config::resolve_server_from_file(ctx.machine_settings()).map_err(|errors| { - anyhow!( - "failed to resolve server settings:\n{}", - errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n") - ) - })?; + let server_settings = fabro_config::ServerSettings::from_layer(ctx.machine_settings()) + .map_err(anyhow::Error::from)?; let vault = user_config::storage_dir(ctx.machine_settings()) .ok() .and_then(|dir| fabro_vault::Vault::load(Storage::new(&dir).secrets_path()).ok()); let creds = build_github_credentials( - server_settings.integrations.github.strategy, + server_settings.server.integrations.github.strategy, server_settings + .server .integrations .github .app_id @@ -79,7 +71,7 @@ fn load_github_credentials_required( pub(crate) async fn load_pr_record( server: &ServerTargetArgs, run_id: &str, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<(PullRequestRecord, fabro_types::RunId)> { diff --git a/lib/crates/fabro-cli/src/commands/pr/view.rs b/lib/crates/fabro-cli/src/commands/pr/view.rs index f093838dc..79eabadeb 100644 --- a/lib/crates/fabro-cli/src/commands/pr/view.rs +++ b/lib/crates/fabro-cli/src/commands/pr/view.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::info; @@ -9,7 +9,7 @@ use crate::shared::print_json_pretty; pub(super) async fn view_command( args: PrViewArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/preflight.rs b/lib/crates/fabro-cli/src/commands/preflight.rs index 6ebba6168..03dd4304a 100644 --- a/lib/crates/fabro-cli/src/commands/preflight.rs +++ b/lib/crates/fabro-cli/src/commands/preflight.rs @@ -1,7 +1,7 @@ use anyhow::bail; use fabro_config::load::load_settings_user; use fabro_config::user::active_settings_path; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -17,7 +17,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn execute( mut args: PreflightArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/provider/login.rs b/lib/crates/fabro-cli/src/commands/provider/login.rs index 45b33e9f9..1c613936d 100644 --- a/lib/crates/fabro-cli/src/commands/provider/login.rs +++ b/lib/crates/fabro-cli/src/commands/provider/login.rs @@ -1,7 +1,7 @@ use anyhow::Result; use fabro_api::types; use fabro_auth::credential_id_for; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -12,7 +12,7 @@ use crate::shared::provider_auth; pub(super) async fn login_command( args: ProviderLoginArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/provider/mod.rs b/lib/crates/fabro-cli/src/commands/provider/mod.rs index 10db120ae..85dd085a8 100644 --- a/lib/crates/fabro-cli/src/commands/provider/mod.rs +++ b/lib/crates/fabro-cli/src/commands/provider/mod.rs @@ -1,7 +1,7 @@ mod login; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; @@ -9,7 +9,7 @@ use crate::args::{ProviderCommand, ProviderNamespace}; pub(crate) async fn dispatch( ns: ProviderNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/repo/deinit.rs b/lib/crates/fabro-cli/src/commands/repo/deinit.rs index 6d7ccd294..8ed8ae707 100644 --- a/lib/crates/fabro-cli/src/commands/repo/deinit.rs +++ b/lib/crates/fabro-cli/src/commands/repo/deinit.rs @@ -1,9 +1,9 @@ use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; -pub(crate) fn run_deinit(cli: &CliSettings, printer: Printer) -> Result> { +pub(crate) fn run_deinit(cli: &CliNamespace, printer: Printer) -> Result> { let repo_root = super::init::git_repo_root()?; let mut removed = Vec::new(); diff --git a/lib/crates/fabro-cli/src/commands/repo/init.rs b/lib/crates/fabro-cli/src/commands/repo/init.rs index 136eb2bff..6b3d717f3 100644 --- a/lib/crates/fabro-cli/src/commands/repo/init.rs +++ b/lib/crates/fabro-cli/src/commands/repo/init.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tokio::process::Command as TokioCommand; @@ -36,7 +36,7 @@ pub(super) fn git_repo_root() -> Result { pub(crate) async fn run_init( args: &RepoInitArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result> { @@ -159,7 +159,7 @@ draft = true async fn check_github_app_installation( target: &ServerTargetArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) { diff --git a/lib/crates/fabro-cli/src/commands/repo/mod.rs b/lib/crates/fabro-cli/src/commands/repo/mod.rs index 30ce0fd41..5ddeaa95b 100644 --- a/lib/crates/fabro-cli/src/commands/repo/mod.rs +++ b/lib/crates/fabro-cli/src/commands/repo/mod.rs @@ -2,7 +2,7 @@ pub(crate) mod deinit; pub(crate) mod init; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; @@ -11,7 +11,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn dispatch( ns: RepoNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs index 0c634fbd9..bad2a27dd 100644 --- a/lib/crates/fabro-cli/src/commands/run/attach.rs +++ b/lib/crates/fabro-cli/src/commands/run/attach.rs @@ -19,7 +19,6 @@ use anyhow::Result; use fabro_api::types; use fabro_interview::{AnswerValue, ConsoleInterviewer, Question, QuestionOption, QuestionType}; use fabro_store::EventEnvelope; -use fabro_types::settings::cli::OutputVerbosity; use fabro_types::settings::run::ApprovalMode; use fabro_types::{EventBody, RunId}; use fabro_util::json::normalize_json_value; @@ -49,6 +48,7 @@ pub(crate) async fn attach_run( kill_on_detach: bool, styles: &'static Styles, json_output: bool, + live_verbose: bool, ) -> Result { let inferred_storage_dir = infer_storage_dir(run_dir); let inferred_run_id = infer_run_id(run_dir); @@ -63,6 +63,7 @@ pub(crate) async fn attach_run( kill_on_detach, styles, json_output, + live_verbose, Printer::Default, )) .await; @@ -79,6 +80,7 @@ pub(crate) async fn attach_run_with_client( kill_on_detach: bool, styles: &'static Styles, json_output: bool, + live_verbose: bool, printer: Printer, ) -> Result { let state = client.get_run_state(run_id).await?; @@ -86,10 +88,6 @@ pub(crate) async fn attach_run_with_client( fabro_config::resolve_run_from_file(&record.settings) .is_ok_and(|settings| settings.execution.approval == ApprovalMode::Auto) }); - let verbose = state.spec.as_ref().is_some_and(|record| { - fabro_config::resolve_cli_from_file(&record.settings) - .is_ok_and(|settings| settings.output.verbosity == OutputVerbosity::Verbose) - }); let events = client.list_run_events(run_id, None, None).await?; let replay_events = events.clone(); let next_seq = events.last().map_or(1, |event| event.seq.saturating_add(1)); @@ -98,7 +96,7 @@ pub(crate) async fn attach_run_with_client( if state_is_terminal(&state) || initial_exit_code.is_some() { return replay_run_with_client( - verbose, + live_verbose, events, initial_exit_code .or(state_exit_code) @@ -116,7 +114,7 @@ pub(crate) async fn attach_run_with_client( styles, AttachOptions { auto_approve, - verbose, + verbose: live_verbose, kill_on_detach, json_output, }, @@ -542,6 +540,7 @@ mod tests { false, no_color_styles(), false, + false, )) .await .unwrap_err(); diff --git a/lib/crates/fabro-cli/src/commands/run/command.rs b/lib/crates/fabro-cli/src/commands/run/command.rs index ffb83ce03..1f63dbef5 100644 --- a/lib/crates/fabro-cli/src/commands/run/command.rs +++ b/lib/crates/fabro-cli/src/commands/run/command.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -11,7 +11,7 @@ use crate::user_config::load_settings_with_storage_dir; pub(crate) async fn execute( mut args: RunArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { @@ -64,6 +64,7 @@ pub(crate) async fn execute( true, styles, json, + ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose, printer, )) .await?; diff --git a/lib/crates/fabro-cli/src/commands/run/cp.rs b/lib/crates/fabro-cli/src/commands/run/cp.rs index 732312219..a757ac81c 100644 --- a/lib/crates/fabro-cli/src/commands/run/cp.rs +++ b/lib/crates/fabro-cli/src/commands/run/cp.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tokio::fs; @@ -28,7 +28,7 @@ enum CopyDirection { pub(crate) async fn cp_command( args: CpArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { @@ -128,7 +128,7 @@ fn parse_direction(src: &str, dst: &str) -> Result { async fn resolve_client_and_run_id( server: &ServerTargetArgs, run_prefix: &str, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<(Client, fabro_types::RunId)> { diff --git a/lib/crates/fabro-cli/src/commands/run/diff.rs b/lib/crates/fabro-cli/src/commands/run/diff.rs index 32951fff1..138c54162 100644 --- a/lib/crates/fabro-cli/src/commands/run/diff.rs +++ b/lib/crates/fabro-cli/src/commands/run/diff.rs @@ -10,7 +10,7 @@ use std::io::{self, IsTerminal, Write}; use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::{debug, info}; @@ -22,7 +22,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn run( args: DiffArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/run/fork.rs b/lib/crates/fabro-cli/src/commands/run/fork.rs index b4f8c81e4..e08b934b0 100644 --- a/lib/crates/fabro-cli/src/commands/run/fork.rs +++ b/lib/crates/fabro-cli/src/commands/run/fork.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; use fabro_checkpoint::git::Store; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -16,7 +16,7 @@ use crate::shared::repo::ensure_matching_repo_origin; pub(crate) async fn run( args: &ForkArgs, styles: &Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/run/logs.rs b/lib/crates/fabro-cli/src/commands/run/logs.rs index a732bc023..fbaca77cd 100644 --- a/lib/crates/fabro-cli/src/commands/run/logs.rs +++ b/lib/crates/fabro-cli/src/commands/run/logs.rs @@ -13,7 +13,7 @@ use std::time::Duration; use anyhow::{Context, Result, bail}; use chrono::{DateTime, Utc}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::json::normalize_json_value; use fabro_util::printer::Printer; @@ -32,7 +32,7 @@ const FOLLOW_TERMINAL_GRACE: Duration = Duration::from_millis(500); pub(crate) async fn run( args: &LogsArgs, styles: &Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/run/mod.rs b/lib/crates/fabro-cli/src/commands/run/mod.rs index b9a8eaec0..ff0641f29 100644 --- a/lib/crates/fabro-cli/src/commands/run/mod.rs +++ b/lib/crates/fabro-cli/src/commands/run/mod.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; +use fabro_types::settings::CliNamespace; +use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -29,7 +29,7 @@ pub(crate) mod wait; pub(crate) async fn dispatch( cmd: RunCommands, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, _process_local_json: bool, printer: Printer, @@ -77,6 +77,7 @@ pub(crate) async fn dispatch( false, styles, cli.output.format == OutputFormat::Json, + ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose, printer, )) .await?; diff --git a/lib/crates/fabro-cli/src/commands/run/preview.rs b/lib/crates/fabro-cli/src/commands/run/preview.rs index 55a9f8c0d..1bcb5f6b2 100644 --- a/lib/crates/fabro-cli/src/commands/run/preview.rs +++ b/lib/crates/fabro-cli/src/commands/run/preview.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::info; @@ -10,7 +10,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn run( args: PreviewArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/run/resume.rs b/lib/crates/fabro-cli/src/commands/run/resume.rs index 85d801474..fb175cb51 100644 --- a/lib/crates/fabro-cli/src/commands/run/resume.rs +++ b/lib/crates/fabro-cli/src/commands/run/resume.rs @@ -1,5 +1,5 @@ -use fabro_types::settings::CliSettings; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; +use fabro_types::settings::CliNamespace; +use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -15,7 +15,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn resume_command( args: ResumeArgs, styles: &'static Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result<()> { @@ -39,6 +39,7 @@ pub(crate) async fn resume_command( true, styles, json, + ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose, printer, )) .await?; diff --git a/lib/crates/fabro-cli/src/commands/run/rewind.rs b/lib/crates/fabro-cli/src/commands/run/rewind.rs index 1645dacfa..6e65a5f98 100644 --- a/lib/crates/fabro-cli/src/commands/run/rewind.rs +++ b/lib/crates/fabro-cli/src/commands/run/rewind.rs @@ -3,7 +3,7 @@ use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; use fabro_checkpoint::git::Store; use fabro_types::run_event::{CheckpointCompletedProps, RunRewoundProps, RunSubmittedProps}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_types::{EventBody, RunEvent}; use fabro_util::printer::Printer; @@ -33,7 +33,7 @@ pub(crate) struct TimelineEntryJson { pub(crate) async fn run( args: &RewindArgs, styles: &Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/run/ssh.rs b/lib/crates/fabro-cli/src/commands/run/ssh.rs index 4e9268de1..f4465d499 100644 --- a/lib/crates/fabro-cli/src/commands/run/ssh.rs +++ b/lib/crates/fabro-cli/src/commands/run/ssh.rs @@ -1,5 +1,5 @@ use anyhow::{Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::info; @@ -10,7 +10,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn run( args: SshArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/crates/fabro-cli/src/commands/run/wait.rs index 49e8557a7..639960771 100644 --- a/lib/crates/fabro-cli/src/commands/run/wait.rs +++ b/lib/crates/fabro-cli/src/commands/run/wait.rs @@ -11,7 +11,7 @@ use std::io::Write; use anyhow::{Result, bail}; use fabro_types::RunId; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -27,7 +27,7 @@ use crate::shared::{format_duration_ms, format_usd_micros}; pub(crate) async fn run( args: &WaitArgs, styles: &Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/runs/archive.rs b/lib/crates/fabro-cli/src/commands/runs/archive.rs index 4a0627759..6052d9457 100644 --- a/lib/crates/fabro-cli/src/commands/runs/archive.rs +++ b/lib/crates/fabro-cli/src/commands/runs/archive.rs @@ -1,5 +1,5 @@ use anyhow::{Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; @@ -11,7 +11,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn archive_command( args: &RunsArchiveArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { @@ -28,7 +28,7 @@ pub(crate) async fn archive_command( pub(crate) async fn unarchive_command( args: &RunsUnarchiveArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { @@ -66,7 +66,7 @@ async fn run_bulk( action: Action, identifiers: &[String], client: &server_client::Client, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let json = cli.output.format == OutputFormat::Json; diff --git a/lib/crates/fabro-cli/src/commands/runs/inspect.rs b/lib/crates/fabro-cli/src/commands/runs/inspect.rs index 2c3b7911d..7858260f1 100644 --- a/lib/crates/fabro-cli/src/commands/runs/inspect.rs +++ b/lib/crates/fabro-cli/src/commands/runs/inspect.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; use fabro_workflow::run_status::RunStatus; @@ -23,7 +23,7 @@ pub(crate) struct InspectOutput { pub(crate) async fn run( args: &InspectArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/runs/list.rs b/lib/crates/fabro-cli/src/commands/runs/list.rs index 5864f4da0..8b81b5f7b 100644 --- a/lib/crates/fabro-cli/src/commands/runs/list.rs +++ b/lib/crates/fabro-cli/src/commands/runs/list.rs @@ -4,7 +4,7 @@ use anyhow::Result; use chrono::Utc; use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -20,7 +20,7 @@ use crate::shared::{color_if, format_duration_ms, tilde_path}; pub(crate) async fn list_command( args: &RunsListArgs, styles: &Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/runs/mod.rs b/lib/crates/fabro-cli/src/commands/runs/mod.rs index f3900a94e..fb62b7f2a 100644 --- a/lib/crates/fabro-cli/src/commands/runs/mod.rs +++ b/lib/crates/fabro-cli/src/commands/runs/mod.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -13,7 +13,7 @@ pub(crate) mod rm; pub(crate) async fn dispatch( cmd: RunsCommands, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/runs/rm.rs b/lib/crates/fabro-cli/src/commands/runs/rm.rs index becdc9969..eb1047332 100644 --- a/lib/crates/fabro-cli/src/commands/runs/rm.rs +++ b/lib/crates/fabro-cli/src/commands/runs/rm.rs @@ -1,5 +1,5 @@ use anyhow::{Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; @@ -11,7 +11,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn remove_command( args: &RunsRemoveArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { @@ -22,7 +22,7 @@ pub(crate) async fn remove_command( async fn remove_from( args: &RunsRemoveArgs, client: &server_client::Client, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let json = cli.output.format == OutputFormat::Json; diff --git a/lib/crates/fabro-cli/src/commands/sandbox/mod.rs b/lib/crates/fabro-cli/src/commands/sandbox/mod.rs index c584f3e81..3adc7bd13 100644 --- a/lib/crates/fabro-cli/src/commands/sandbox/mod.rs +++ b/lib/crates/fabro-cli/src/commands/sandbox/mod.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; @@ -7,7 +7,7 @@ use crate::args::SandboxCommand; pub(crate) async fn dispatch( command: SandboxCommand, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/secret/list.rs b/lib/crates/fabro-cli/src/commands/secret/list.rs index 033afdf17..25d7f7680 100644 --- a/lib/crates/fabro-cli/src/commands/secret/list.rs +++ b/lib/crates/fabro-cli/src/commands/secret/list.rs @@ -2,7 +2,7 @@ use anyhow::Result; use chrono::{DateTime, Utc}; use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Style, Table}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -25,7 +25,7 @@ fn format_age(dt: DateTime, now: DateTime) -> String { pub(super) async fn list_command( client: &Client, _args: &SecretListArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let secrets = client.list_secrets().await?; diff --git a/lib/crates/fabro-cli/src/commands/secret/mod.rs b/lib/crates/fabro-cli/src/commands/secret/mod.rs index d0148c60e..59146f0a8 100644 --- a/lib/crates/fabro-cli/src/commands/secret/mod.rs +++ b/lib/crates/fabro-cli/src/commands/secret/mod.rs @@ -3,7 +3,7 @@ mod rm; mod set; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; @@ -12,7 +12,7 @@ use crate::command_context::CommandContext; pub(crate) async fn dispatch( ns: SecretNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/secret/rm.rs b/lib/crates/fabro-cli/src/commands/secret/rm.rs index 43c466501..253ca09d0 100644 --- a/lib/crates/fabro-cli/src/commands/secret/rm.rs +++ b/lib/crates/fabro-cli/src/commands/secret/rm.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; @@ -10,7 +10,7 @@ use crate::shared::print_json_pretty; pub(super) async fn rm_command( client: &Client, args: &SecretRmArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { client.delete_secret_by_name(&args.key).await?; diff --git a/lib/crates/fabro-cli/src/commands/secret/set.rs b/lib/crates/fabro-cli/src/commands/secret/set.rs index a8c88debd..1ec66418e 100644 --- a/lib/crates/fabro-cli/src/commands/secret/set.rs +++ b/lib/crates/fabro-cli/src/commands/secret/set.rs @@ -11,7 +11,7 @@ use std::io::{IsTerminal, Read as _}; use anyhow::{Context as _, Result, bail}; use fabro_api::types; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use tokio::task::spawn_blocking; @@ -60,7 +60,7 @@ async fn resolve_value(args: &SecretSetArgs) -> Result { pub(super) async fn set_command( client: &Client, args: &SecretSetArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let value = resolve_value(args).await?; diff --git a/lib/crates/fabro-cli/src/commands/store/dump.rs b/lib/crates/fabro-cli/src/commands/store/dump.rs index 700e94783..1713ce6b2 100644 --- a/lib/crates/fabro-cli/src/commands/store/dump.rs +++ b/lib/crates/fabro-cli/src/commands/store/dump.rs @@ -11,7 +11,7 @@ use bytes::Bytes; #[cfg(test)] use fabro_store::{ArtifactStore, RunDatabase}; use fabro_store::{EventEnvelope, RunProjection, StageId}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_types::{RunBlobId, RunId}; use fabro_util::printer::Printer; @@ -28,7 +28,7 @@ use crate::shared::{absolute_or_current, print_json_pretty}; pub(crate) async fn dump_command( args: &StoreDumpArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/store/mod.rs b/lib/crates/fabro-cli/src/commands/store/mod.rs index f4cab294d..2c885231b 100644 --- a/lib/crates/fabro-cli/src/commands/store/mod.rs +++ b/lib/crates/fabro-cli/src/commands/store/mod.rs @@ -3,7 +3,7 @@ pub(crate) mod rebuild; mod run_export; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; @@ -11,7 +11,7 @@ use crate::args::{StoreCommand, StoreNamespace}; pub(crate) async fn dispatch( ns: StoreNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/system/df.rs b/lib/crates/fabro-cli/src/commands/system/df.rs index ae6e30343..f5cebe0b4 100644 --- a/lib/crates/fabro-cli/src/commands/system/df.rs +++ b/lib/crates/fabro-cli/src/commands/system/df.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Style, Table}; use fabro_api::types; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; @@ -13,7 +13,7 @@ use crate::shared::{format_size, print_json_pretty}; pub(super) async fn df_command( args: &DfArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/system/events.rs b/lib/crates/fabro-cli/src/commands/system/events.rs index da5bdad57..7e94cfa1d 100644 --- a/lib/crates/fabro-cli/src/commands/system/events.rs +++ b/lib/crates/fabro-cli/src/commands/system/events.rs @@ -1,6 +1,6 @@ use anyhow::Result; use fabro_client::sse; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use futures::StreamExt; @@ -10,7 +10,7 @@ use crate::command_context::CommandContext; pub(super) async fn events_command( args: &SystemEventsArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/system/info.rs b/lib/crates/fabro-cli/src/commands/system/info.rs index 17668bfe5..a1c584453 100644 --- a/lib/crates/fabro-cli/src/commands/system/info.rs +++ b/lib/crates/fabro-cli/src/commands/system/info.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; @@ -9,7 +9,7 @@ use crate::shared::print_json_pretty; pub(super) async fn info_command( args: &SystemInfoArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/system/mod.rs b/lib/crates/fabro-cli/src/commands/system/mod.rs index 86d35e367..45f35dee6 100644 --- a/lib/crates/fabro-cli/src/commands/system/mod.rs +++ b/lib/crates/fabro-cli/src/commands/system/mod.rs @@ -4,7 +4,7 @@ mod info; mod prune; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; pub(crate) use prune::parse_duration; @@ -13,7 +13,7 @@ use crate::args::{SystemCommand, SystemNamespace}; pub(crate) async fn dispatch( ns: SystemNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/system/prune.rs b/lib/crates/fabro-cli/src/commands/system/prune.rs index 1ed6d684d..f7242b023 100644 --- a/lib/crates/fabro-cli/src/commands/system/prune.rs +++ b/lib/crates/fabro-cli/src/commands/system/prune.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use anyhow::{Context, Result, bail}; use fabro_api::types; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::{debug, info}; @@ -13,7 +13,7 @@ use crate::shared::{format_size, print_json_pretty}; pub(super) async fn prune_command( args: &RunsPruneArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/uninstall.rs b/lib/crates/fabro-cli/src/commands/uninstall.rs index ed358496d..5d19aa459 100644 --- a/lib/crates/fabro-cli/src/commands/uninstall.rs +++ b/lib/crates/fabro-cli/src/commands/uninstall.rs @@ -15,7 +15,7 @@ use std::time::Duration; use anyhow::{Context, Result}; use fabro_config::Storage; use fabro_config::daemon::ServerDaemon; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::Home; use fabro_util::printer::Printer; @@ -45,7 +45,7 @@ struct Inventory { )] pub(crate) async fn run_uninstall( args: &UninstallArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let json = cli.output.format == OutputFormat::Json; diff --git a/lib/crates/fabro-cli/src/commands/upgrade.rs b/lib/crates/fabro-cli/src/commands/upgrade.rs index ddfaaf659..53390cfd5 100644 --- a/lib/crates/fabro-cli/src/commands/upgrade.rs +++ b/lib/crates/fabro-cli/src/commands/upgrade.rs @@ -13,7 +13,7 @@ use std::io::{IsTerminal, Write}; use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use semver::Version; @@ -436,7 +436,7 @@ impl UpgradeCheckState { pub(crate) async fn run_upgrade( args: UpgradeArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let current_exe = std::env::current_exe() @@ -599,7 +599,7 @@ pub(crate) async fn run_upgrade( fn run_upgrade_brew( args: &UpgradeArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, channel: BrewChannel, ) -> Result<()> { @@ -1153,7 +1153,7 @@ mod tests { #[test] fn run_upgrade_brew_refuses_by_default() { - let cli = CliSettings::default(); + let cli = CliNamespace::default(); let err = run_upgrade_brew( &brew_args(None, false, false, false), &cli, @@ -1169,7 +1169,7 @@ mod tests { #[test] fn run_upgrade_brew_dry_run_returns_ok() { - let cli = CliSettings::default(); + let cli = CliNamespace::default(); let result = run_upgrade_brew( &brew_args(None, false, false, true), &cli, @@ -1181,7 +1181,7 @@ mod tests { #[test] fn run_upgrade_brew_rejects_version_flag() { - let cli = CliSettings::default(); + let cli = CliNamespace::default(); let err = run_upgrade_brew( &brew_args(Some("0.1.0"), false, false, false), &cli, @@ -1194,7 +1194,7 @@ mod tests { #[test] fn run_upgrade_brew_rejects_prerelease_flag() { - let cli = CliSettings::default(); + let cli = CliNamespace::default(); let err = run_upgrade_brew( &brew_args(None, true, false, false), &cli, @@ -1207,7 +1207,7 @@ mod tests { #[test] fn run_upgrade_brew_rejects_force_flag() { - let cli = CliSettings::default(); + let cli = CliNamespace::default(); let err = run_upgrade_brew( &brew_args(None, false, true, false), &cli, diff --git a/lib/crates/fabro-cli/src/commands/validate.rs b/lib/crates/fabro-cli/src/commands/validate.rs index a530e5470..95691b74e 100644 --- a/lib/crates/fabro-cli/src/commands/validate.rs +++ b/lib/crates/fabro-cli/src/commands/validate.rs @@ -2,7 +2,7 @@ use anyhow::bail; use fabro_config::load::load_settings_user; use fabro_config::user::active_settings_path; use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_types::settings::{CliSettings, SettingsLayer}; +use fabro_types::settings::{CliNamespace, SettingsLayer}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -15,7 +15,7 @@ use crate::shared::{print_diagnostics, print_json_pretty, relative_path}; pub(crate) async fn run( args: &ValidateArgs, styles: &Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/version.rs b/lib/crates/fabro-cli/src/commands/version.rs index a58e0541f..826ec108b 100644 --- a/lib/crates/fabro-cli/src/commands/version.rs +++ b/lib/crates/fabro-cli/src/commands/version.rs @@ -6,7 +6,7 @@ use std::io::IsTerminal; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use serde_json::{Map, Value, json}; @@ -18,7 +18,7 @@ use crate::user_config::{self, ServerTarget}; pub(crate) async fn version_command( args: &VersionArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/workflow/create.rs b/lib/crates/fabro-cli/src/commands/workflow/create.rs index 845508a2a..6a4f7b5f8 100644 --- a/lib/crates/fabro-cli/src/commands/workflow/create.rs +++ b/lib/crates/fabro-cli/src/commands/workflow/create.rs @@ -7,7 +7,7 @@ use std::path::Path; use anyhow::{Context, Result, bail}; use fabro_config::project::{discover_project_config, resolve_fabro_root}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; @@ -16,7 +16,7 @@ use crate::shared::{print_json_pretty, relative_path}; pub(super) fn create_command( args: &WorkflowCreateArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let cwd = std::env::current_dir()?; diff --git a/lib/crates/fabro-cli/src/commands/workflow/list.rs b/lib/crates/fabro-cli/src/commands/workflow/list.rs index 44eee6f46..40da91203 100644 --- a/lib/crates/fabro-cli/src/commands/workflow/list.rs +++ b/lib/crates/fabro-cli/src/commands/workflow/list.rs @@ -5,7 +5,7 @@ use fabro_config::project::{ WorkflowInfo, WorkflowSource, discover_project_config, list_workflows_detailed, resolve_fabro_root, }; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -17,7 +17,7 @@ const GOAL_MAX_LEN: usize = 60; pub(super) fn list_command( _args: &WorkflowListArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let styles = Styles::detect_stderr(); diff --git a/lib/crates/fabro-cli/src/commands/workflow/mod.rs b/lib/crates/fabro-cli/src/commands/workflow/mod.rs index 46478dd2c..44de6cf4e 100644 --- a/lib/crates/fabro-cli/src/commands/workflow/mod.rs +++ b/lib/crates/fabro-cli/src/commands/workflow/mod.rs @@ -2,12 +2,12 @@ mod create; mod list; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_util::printer::Printer; use crate::args::{WorkflowCommand, WorkflowNamespace}; -pub(crate) fn dispatch(ns: WorkflowNamespace, cli: &CliSettings, printer: Printer) -> Result<()> { +pub(crate) fn dispatch(ns: WorkflowNamespace, cli: &CliNamespace, printer: Printer) -> Result<()> { match ns.command { WorkflowCommand::List(args) => list::list_command(&args, cli, printer), WorkflowCommand::Create(args) => create::create_command(&args, cli, printer), diff --git a/lib/crates/fabro-cli/src/local_server.rs b/lib/crates/fabro-cli/src/local_server.rs index 6ba1a01aa..cdba3b556 100644 --- a/lib/crates/fabro-cli/src/local_server.rs +++ b/lib/crates/fabro-cli/src/local_server.rs @@ -27,8 +27,8 @@ pub(crate) fn bind_request( } pub(crate) fn auth_methods(settings: &SettingsLayer) -> Vec { - fabro_config::resolve_server_from_file(settings) - .map(|resolved| resolved.auth.methods) + fabro_config::ServerSettings::from_layer(settings) + .map(|resolved| resolved.server.auth.methods) .unwrap_or_default() } diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 5a5ffd7fb..bd2cd9e06 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -1077,36 +1077,22 @@ level = "warn" assert_eq!(cli.command.as_ref().unwrap().name(), "settings"); match *cli.command.unwrap() { Commands::Settings(args) => { - assert!(!args.local); assert!(args.target.server.is_none()); - assert!(args.workflow.is_none()); } _ => panic!("unexpected command variant"), } } #[test] - fn parse_settings_with_workflow() { - let cli = Cli::try_parse_from(["fabro", "settings", "demo"]).expect("should parse"); - match *cli.command.unwrap() { - Commands::Settings(args) => { - assert_eq!(args.workflow, Some(std::path::PathBuf::from("demo"))); - } - _ => panic!("unexpected command variant"), - } + fn parse_settings_rejects_workflow_argument() { + let result = Cli::try_parse_from(["fabro", "settings", "demo"]); + assert!(result.is_err(), "should reject settings workflow argument"); } #[test] - fn parse_settings_local_mode() { - let cli = - Cli::try_parse_from(["fabro", "settings", "--local", "demo"]).expect("should parse"); - match *cli.command.unwrap() { - Commands::Settings(args) => { - assert!(args.local); - assert_eq!(args.workflow, Some(std::path::PathBuf::from("demo"))); - } - _ => panic!("unexpected command variant"), - } + fn parse_settings_rejects_local_flag() { + let result = Cli::try_parse_from(["fabro", "settings", "--local"]); + assert!(result.is_err(), "should reject settings --local"); } #[test] diff --git a/lib/crates/fabro-cli/src/user_config.rs b/lib/crates/fabro-cli/src/user_config.rs index 76fbc9507..687831cc4 100644 --- a/lib/crates/fabro-cli/src/user_config.rs +++ b/lib/crates/fabro-cli/src/user_config.rs @@ -5,7 +5,7 @@ use anyhow::Result; pub(crate) use fabro_client::ServerTarget; pub(crate) use fabro_config::user::*; use fabro_types::settings::cli::CliTargetSettings; -use fabro_types::settings::{CliSettings, SettingsLayer}; +use fabro_types::settings::{CliNamespace, SettingsLayer}; use fabro_util::version::FABRO_VERSION; use tracing::debug; @@ -30,19 +30,14 @@ pub(crate) fn load_settings_with_config_and_storage_dir( Ok(apply_storage_dir_override(layer, storage_dir)) } -fn render_resolve_errors(errors: Vec) -> anyhow::Error { - anyhow::anyhow!( - "failed to resolve cli settings:\n{}", - errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n") - ) +pub(crate) fn resolve_user_settings( + file: &SettingsLayer, +) -> anyhow::Result { + fabro_config::UserSettings::from_layer(file).map_err(anyhow::Error::from) } -pub(crate) fn resolve_cli_settings(file: &SettingsLayer) -> anyhow::Result { - fabro_config::resolve_cli_from_file(file).map_err(render_resolve_errors) +pub(crate) fn resolve_cli_settings(file: &SettingsLayer) -> anyhow::Result { + resolve_user_settings(file).map(|settings| settings.cli) } pub(crate) fn apply_storage_dir_override( @@ -64,7 +59,7 @@ pub(crate) fn apply_storage_dir_override( /// Pull the resolved CLI target configuration out of `[cli.target]`. /// Returns either an http(s) URL or a unix socket path. -fn cli_target_from_settings(settings: &CliSettings) -> Option { +fn cli_target_from_settings(settings: &CliNamespace) -> Option { let target = settings.target.as_ref()?; match target { CliTargetSettings::Http { url } => Some(url.as_source()), @@ -73,8 +68,8 @@ fn cli_target_from_settings(settings: &CliSettings) -> Option { } fn configured_server_target(settings: &SettingsLayer) -> Result> { - let cli_settings = resolve_cli_settings(settings)?; - let Some(value) = cli_target_from_settings(&cli_settings) else { + let user_settings = resolve_user_settings(settings)?; + let Some(value) = cli_target_from_settings(&user_settings.cli) else { return Ok(None); }; parse_server_target(&value).map(Some) diff --git a/lib/crates/fabro-cli/tests/it/cmd/config.rs b/lib/crates/fabro-cli/tests/it/cmd/config.rs index ddf031666..28dc1db55 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/config.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/config.rs @@ -40,93 +40,10 @@ fn parse_settings(stdout: &[u8]) -> serde_json::Value { serde_yaml::from_slice(stdout).expect("stdout should be valid YAML settings") } -fn parse_settings_json(stdout: &[u8]) -> serde_json::Value { - serde_json::from_slice(stdout).expect("stdout should be valid JSON settings") -} - -fn run_goal_inline(settings: &serde_json::Value) -> Option<&str> { - let goal = settings.get("run")?.get("goal")?; - (goal.get("type")?.as_str() == Some("inline")) - .then(|| goal.get("value")?.as_str()) - .flatten() -} - -fn run_model_name(settings: &serde_json::Value) -> Option<&str> { - settings.get("run")?.get("model")?.get("name")?.as_str() -} - -fn run_model_provider(settings: &serde_json::Value) -> Option<&str> { - settings.get("run")?.get("model")?.get("provider")?.as_str() -} - -fn run_inputs(settings: &serde_json::Value) -> &serde_json::Map { - settings - .get("run") - .and_then(|run| run.get("inputs")) - .and_then(serde_json::Value::as_object) - .expect("run.inputs") -} - -fn run_sandbox(settings: &serde_json::Value) -> &serde_json::Value { - settings - .get("run") - .and_then(|run| run.get("sandbox")) - .expect("run.sandbox") -} - -fn run_checkpoint(settings: &serde_json::Value) -> &serde_json::Value { - settings - .get("run") - .and_then(|run| run.get("checkpoint")) - .expect("run.checkpoint") -} - -fn run_hooks(settings: &serde_json::Value) -> &[serde_json::Value] { - settings - .get("run") - .and_then(|run| run.get("hooks")) - .and_then(serde_json::Value::as_array) - .expect("run.hooks") -} - -fn run_agent_mcps(settings: &serde_json::Value) -> &serde_json::Map { - settings - .get("run") - .and_then(|run| run.get("agent")) - .and_then(|agent| agent.get("mcps")) - .and_then(serde_json::Value::as_object) - .expect("run.agent.mcps") -} - -fn auto_approve_enabled(settings: &serde_json::Value) -> bool { - settings - .get("run") - .and_then(|run| run.get("execution")) - .and_then(|execution| execution.get("approval")) - .and_then(serde_json::Value::as_str) - == Some("auto") -} - -fn run_prepare_commands(settings: &serde_json::Value) -> Vec { - settings - .get("run") - .and_then(|run| run.get("prepare")) - .and_then(|prepare| prepare.get("commands")) - .and_then(serde_json::Value::as_array) - .expect("run.prepare.commands") - .iter() - .map(|value| { - value - .as_str() - .expect("command should be a string") - .to_string() - }) - .collect() -} - fn server_storage_root(settings: &serde_json::Value) -> &str { settings .get("server") + .and_then(|server| server.get("server")) .and_then(|server| server.get("storage")) .and_then(|storage| storage.get("root")) .and_then(serde_json::Value::as_str) @@ -157,7 +74,7 @@ shared = "server" } fn resolved_server_settings_fixture() -> serde_json::Value { - let settings = fabro_config::resolve(&server_settings_layer_fixture()) + let settings = fabro_config::ServerSettings::from_layer(&server_settings_layer_fixture()) .expect("server settings fixture should resolve"); serde_json::to_value(settings).expect("resolved settings payload should serialize") } @@ -389,155 +306,6 @@ script = "workflow-setup" // Tests // --------------------------------------------------------------------------- -#[test] -fn settings_local_merges_cli_and_project_defaults() { - let context = test_context!(); - let project = setup_settings_fixture(&context); - - let output = context - .settings() - .arg("--local") - .current_dir(project.path()) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let cfg = parse_settings(&output); - assert!(cfg.get("_version").is_none()); - assert_eq!(cfg["project"]["directory"].as_str(), Some(".")); - assert_eq!(cfg["workflow"]["graph"].as_str(), Some("workflow.fabro")); - assert_eq!(cfg["run"]["execution"]["approval"].as_str(), Some("prompt")); - assert_eq!(cfg["run"]["sandbox"]["provider"].as_str(), Some("daytona")); - assert_eq!(run_model_name(&cfg), Some("project-model")); - assert_eq!(run_model_provider(&cfg), Some("openai")); - assert_eq!(run_goal_inline(&cfg), None); - - // v2 R22: run.inputs replaces the inherited map wholesale rather than - // merging by key, so the project layer wipes out the CLI layer's inputs. - let vars = run_inputs(&cfg); - assert_eq!( - vars.get("project_only").and_then(serde_json::Value::as_str), - Some("1") - ); - assert_eq!( - vars.get("shared").and_then(serde_json::Value::as_str), - Some("project") - ); - assert!( - !vars.contains_key("cli_only"), - "run.inputs should replace across layers, not merge by key" - ); - - // v2 R71: provider-native maps such as run.sandbox.daytona.labels remain - // sticky merge-by-key, so CLI labels persist under the project layer. - let sandbox = run_sandbox(&cfg); - let labels = &sandbox["daytona"]["labels"]; - assert_eq!(labels["cli_only"].as_str(), Some("1")); - assert_eq!(labels["shared"].as_str(), Some("cli")); -} - -#[test] -fn settings_local_workflow_name_applies_run_overlay_and_deep_merges() { - let context = test_context!(); - let project = setup_settings_fixture(&context); - - let output = context - .settings() - .current_dir(project.path()) - .args(["--local", "demo"]) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let cfg = parse_settings(&output); - assert_eq!(run_goal_inline(&cfg), Some("demo goal")); - assert_eq!(run_model_name(&cfg), Some("run-model")); - assert_eq!(run_model_provider(&cfg), Some("anthropic")); - - // v2 R22: run.inputs replaces wholesale, so the workflow layer wins - // over project and cli. - let vars = run_inputs(&cfg); - assert_eq!(vars.get("run_only").and_then(|v| v.as_str()), Some("1")); - assert_eq!(vars.get("shared").and_then(|v| v.as_str()), Some("run")); - - // checkpoint.exclude_globs is a security/policy list: replace by default. - let checkpoint = run_checkpoint(&cfg); - assert_eq!( - checkpoint["exclude_globs"], - serde_json::json!(["run-only", "shared"]) - ); - - // Hooks: id-based replacement. The "shared" hook appears in both cli and - // workflow layers and resolves to the workflow entry; project and run-only - // contribute the other two ids. - let hooks = run_hooks(&cfg); - assert!(hooks.len() >= 2); - let shared_hook = hooks - .iter() - .find(|hook| hook["name"].as_str() == Some("shared")) - .expect("shared hook"); - assert_eq!(shared_hook["command"].as_str(), Some("echo run")); - assert!( - hooks - .iter() - .any(|hook| hook["name"].as_str() == Some("run-only")) - ); - - let mcps = run_agent_mcps(&cfg); - let shared = mcps.get("shared").expect("shared mcp"); - assert_eq!(shared["transport"]["type"].as_str(), Some("stdio")); - assert_eq!( - shared["transport"]["command"], - serde_json::json!(["echo", "run"]) - ); - assert!(mcps.contains_key("run_only")); - - // run.sandbox.daytona.labels stays sticky merge-by-key per R71. - let sandbox = run_sandbox(&cfg); - let labels = &sandbox["daytona"]["labels"]; - assert_eq!(labels["run_only"].as_str(), Some("1")); - assert_eq!(labels["shared"].as_str(), Some("run")); - - // run.sandbox.env stays sticky merge-by-key per R71. - let env = &sandbox["env"]; - assert_eq!(env["CLI_ONLY"].as_str(), Some("1")); - assert_eq!(env["RUN_ONLY"].as_str(), Some("1")); - assert_eq!(env["SHARED"].as_str(), Some("run")); -} - -#[test] -fn settings_local_explicit_workflow_path_uses_workflow_project_layers() { - let mut context = test_context!(); - let (project, _storage_dir) = setup_external_workflow_fixture(&mut context); - let cwd = tempfile::tempdir().unwrap(); - let workflow = project.path().join("workflow.toml"); - - // Remove FABRO_STORAGE_DIR so the CLI uses storage_dir from settings.toml - let output = context - .settings() - .env_remove("FABRO_STORAGE_DIR") - .current_dir(cwd.path()) - .args(["--local", workflow.to_str().unwrap()]) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let cfg = parse_settings(&output); - assert!(auto_approve_enabled(&cfg)); - // v2 R30: run.prepare.steps replaces the whole ordered list across layers. - // The highest-precedence layer (workflow) wins. - assert_eq!(run_prepare_commands(&cfg), vec![ - "workflow-setup".to_string() - ]); - assert_eq!(run_sandbox(&cfg)["preserve"].as_bool(), Some(true)); -} - #[test] fn create_explicit_workflow_path_uses_project_config_relative_to_workflow() { let mut context = test_context!(); @@ -608,254 +376,6 @@ fn create_explicit_workflow_path_uses_project_config_relative_to_workflow() { ); } -#[test] -fn settings_fabro_path_matches_ambient_defaults() { - let context = test_context!(); - let project = setup_settings_fixture(&context); - - let ambient = context - .settings() - .arg("--local") - .current_dir(project.path()) - .assert() - .success() - .get_output() - .stdout - .clone(); - let graph = context - .settings() - .current_dir(project.path()) - .args(["--local", "standalone.fabro"]) - .assert() - .success() - .get_output() - .stdout - .clone(); - - assert_eq!(parse_settings(&graph), parse_settings(&ambient)); -} - -#[test] -fn settings_missing_run_config_errors() { - let context = test_context!(); - let project = setup_settings_fixture(&context); - - let mut cmd = context.settings(); - cmd.current_dir(project.path()); - cmd.args(["--local", "missing.toml"]); - let output = cmd.output().expect("command should execute"); - assert!(!output.status.success()); - assert!(String::from_utf8_lossy(&output.stdout).trim().is_empty()); - let stderr = String::from_utf8_lossy(&output.stderr); - assert!( - stderr.contains("workflow not found:"), - "stderr should report missing workflow path, got:\n{stderr}" - ); - assert!( - stderr.contains("missing.toml"), - "stderr should include missing workflow filename, got:\n{stderr}" - ); -} - -#[test] -fn settings_legacy_cli_config_is_silently_ignored() { - let context = test_context!(); - let project = tempfile::tempdir().unwrap(); - - context.write_home( - ".fabro/cli.toml", - r#" -_version = 1 - -[cli.output] -verbosity = "verbose" - -[run.model] -name = "legacy-model" -"#, - ); - - let assert = context - .settings() - .arg("--local") - .current_dir(project.path()) - .assert() - .success(); - - assert!( - assert.get_output().stderr.is_empty(), - "settings should not warn about legacy config files: {}", - String::from_utf8_lossy(&assert.get_output().stderr) - ); - - let cfg = parse_settings(&assert.get_output().stdout); - assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal")); - assert!( - cfg["run"]["model"].get("name").is_none(), - "resolved dense settings should omit an unset run.model.name" - ); -} - -#[test] -fn settings_legacy_user_config_is_silently_ignored() { - let context = test_context!(); - let project = tempfile::tempdir().unwrap(); - - context.write_home( - ".fabro/user.toml", - r#" -_version = 1 - -[cli.output] -verbosity = "verbose" - -[run.model] -name = "legacy-model" -"#, - ); - - let assert = context - .settings() - .arg("--local") - .current_dir(project.path()) - .assert() - .success(); - - assert!( - assert.get_output().stderr.is_empty(), - "settings should not warn about legacy config files: {}", - String::from_utf8_lossy(&assert.get_output().stderr) - ); - - let cfg = parse_settings(&assert.get_output().stdout); - assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal")); - assert!( - cfg["run"]["model"].get("name").is_none(), - "resolved dense settings should omit an unset run.model.name" - ); -} - -#[test] -fn settings_legacy_server_config_is_silently_ignored() { - let context = test_context!(); - let project = tempfile::tempdir().unwrap(); - - context.write_home( - ".fabro/server.toml", - r#" -_version = 1 - -[cli.output] -verbosity = "verbose" - -[run.model] -name = "legacy-model" -"#, - ); - - let assert = context - .settings() - .arg("--local") - .current_dir(project.path()) - .assert() - .success(); - - assert!( - assert.get_output().stderr.is_empty(), - "settings should not warn about legacy config files: {}", - String::from_utf8_lossy(&assert.get_output().stderr) - ); - - let cfg = parse_settings(&assert.get_output().stdout); - assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal")); - assert!( - cfg["run"]["model"].get("name").is_none(), - "resolved dense settings should omit an unset run.model.name" - ); -} - -#[test] -fn settings_user_config_wins_over_legacy_cli_config() { - let context = test_context!(); - let project = setup_settings_fixture(&context); - context.write_home( - ".fabro/cli.toml", - r#" -_version = 1 - -[run.model] -name = "legacy-model" - -[run.inputs] -shared = "legacy" -"#, - ); - - let assert = context - .settings() - .arg("--local") - .current_dir(project.path()) - .assert() - .success(); - - assert!( - assert.get_output().stderr.is_empty(), - "settings should not warn about legacy config files: {}", - String::from_utf8_lossy(&assert.get_output().stderr) - ); - - let cfg = parse_settings(&assert.get_output().stdout); - assert_eq!(run_model_name(&cfg), Some("project-model")); - let vars = run_inputs(&cfg); - assert_eq!( - vars.get("shared").and_then(serde_json::Value::as_str), - Some("project") - ); -} - -#[test] -fn settings_uses_fabro_home_for_home_config_resolution() { - let context = test_context!(); - let fabro_home = tempfile::tempdir().unwrap(); - - std::fs::write( - fabro_home.path().join("settings.toml"), - r#" -_version = 1 - -[cli.output] -verbosity = "verbose" - -[run.model] -name = "from-fabro-home" -"#, - ) - .unwrap(); - - let output = context - .settings() - .args(["--local", "--json"]) - .env("FABRO_HOME", fabro_home.path()) - .env_remove("FABRO_STORAGE_DIR") - .output() - .expect("command should execute"); - - assert!( - output.status.success(), - "settings command failed:\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - - let cfg = parse_settings_json(&output.stdout); - assert!(cfg.get("_version").is_none()); - assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("verbose")); - assert_eq!( - cfg["run"]["model"]["name"].as_str(), - Some("from-fabro-home") - ); -} - #[test] fn settings_rejects_server_url_flag() { let context = test_context!(); @@ -883,32 +403,27 @@ fn settings_rejects_storage_dir_flag() { } #[test] -fn settings_rejects_local_and_server_combination() { +fn settings_rejects_local_flag() { let context = test_context!(); context .settings() - .args(["--local", "--server", "https://cli.example.com"]) + .arg("--local") .assert() .failure() .stderr(predicate::str::contains( - "the argument '--local' cannot be used with '--server '", + "unexpected argument '--local' found", )); } #[test] -fn settings_rejects_workflow_without_local() { +fn settings_rejects_workflow_argument() { let context = test_context!(); - let project = setup_settings_fixture(&context); - context .settings() - .current_dir(project.path()) .arg("demo") .assert() .failure() - .stderr(predicate::str::contains( - "WORKFLOW requires --local; use `fabro settings --local WORKFLOW`", - )); + .stderr(predicate::str::contains("unexpected argument 'demo' found")); } #[test] @@ -918,12 +433,9 @@ fn settings_fetches_server_resolved_settings() { let server = MockServer::start(); let server_settings = resolved_server_settings_fixture(); let mock = server.mock(|when, then| { - when.method("GET") - .path("/api/v1/settings") - .query_param("view", "resolved"); + when.method("GET").path("/api/v1/settings"); then.status(200) .header("Content-Type", "application/json") - .header("X-Fabro-Settings-View", "resolved") .body(server_settings_body(&server_settings)); }); context.write_home( @@ -962,30 +474,25 @@ shared = "cli" mock.assert(); let cfg = parse_settings(&output); - assert!(cfg.get("_version").is_none()); - assert_eq!(cfg["project"]["directory"].as_str(), Some(".")); - assert_eq!(cfg["workflow"]["graph"].as_str(), Some("workflow.fabro")); - assert_eq!(cfg["run"]["execution"]["approval"].as_str(), Some("prompt")); - assert_eq!(run_model_name(&cfg), Some("server-model")); - assert_eq!(run_model_provider(&cfg), Some("openai")); + assert_eq!( + cfg["user"]["cli"]["output"]["verbosity"].as_str(), + Some("verbose") + ); + assert_eq!( + cfg["user"]["features"]["session_sandboxes"].as_bool(), + Some(false) + ); + assert_eq!( + cfg["server"]["server"]["auth"]["methods"][0].as_str(), + Some("dev-token") + ); assert_eq!(server_storage_root(&cfg), "/srv/fabro-server"); - assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal")); - - // Server-backed mode now returns the selected server's own dense resolved - // settings; local project/user overlays are not merged into the output. - let vars = run_inputs(&cfg); assert_eq!( - vars.get("server_only").and_then(serde_json::Value::as_str), - Some("1") - ); - assert_eq!( - vars.get("shared").and_then(serde_json::Value::as_str), - Some("server") - ); - assert!( - !vars.contains_key("project_only"), - "server-backed settings output must not include local workflow/project overlays" + cfg["server"]["server"]["artifacts"]["store"]["type"].as_str(), + Some("local") ); + assert!(cfg.get("run").is_none()); + assert!(cfg.get("project").is_none()); } #[test] @@ -994,21 +501,16 @@ fn settings_cli_server_target_overrides_configured_server_target() { let project = setup_settings_fixture(&context); let configured_server = MockServer::start(); let configured_mock = configured_server.mock(|when, then| { - when.method("GET") - .path("/api/v1/settings") - .query_param("view", "resolved"); + when.method("GET").path("/api/v1/settings"); then.status(500) .body("configured-server-should-not-be-used"); }); let cli_server = MockServer::start(); let cli_server_settings = resolved_server_settings_fixture(); let cli_mock = cli_server.mock(|when, then| { - when.method("GET") - .path("/api/v1/settings") - .query_param("view", "resolved"); + when.method("GET").path("/api/v1/settings"); then.status(200) .header("Content-Type", "application/json") - .header("X-Fabro-Settings-View", "resolved") .body(server_settings_body(&cli_server_settings)); }); context.write_home( @@ -1044,46 +546,6 @@ verbosity = "verbose" assert_eq!(server_storage_root(&cfg), "/srv/fabro-server"); } -#[test] -fn settings_errors_when_server_lacks_resolved_view_marker() { - let context = test_context!(); - let project = setup_settings_fixture(&context); - let server = MockServer::start(); - let server_settings = resolved_server_settings_fixture(); - let mock = server.mock(|when, then| { - when.method("GET") - .path("/api/v1/settings") - .query_param("view", "resolved"); - then.status(200) - .header("Content-Type", "application/json") - .body(server_settings_body(&server_settings)); - }); - context.write_home( - ".fabro/settings.toml", - format!( - r#" -_version = 1 - -[cli.target] -type = "http" -url = "{}/api/v1" -"#, - server.base_url() - ), - ); - - context - .settings() - .current_dir(project.path()) - .assert() - .failure() - .stderr(predicate::str::contains( - "server does not support resolved settings view; upgrade the server or use --local", - )); - - mock.assert(); -} - #[test] fn settings_unreachable_http_target_fails_clearly() { let context = test_context!(); diff --git a/lib/crates/fabro-cli/tests/it/cmd/create.rs b/lib/crates/fabro-cli/tests/it/cmd/create.rs index d09845eaa..1de7d5f97 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/create.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/create.rs @@ -8,7 +8,7 @@ use crate::support::{fabro_json_snapshot, unique_run_id}; fn resolved_run( settings: &fabro_types::settings::SettingsLayer, -) -> fabro_types::settings::RunSettings { +) -> fabro_types::settings::RunNamespace { fabro_config::resolve_run_from_file(settings).expect("run settings should resolve") } diff --git a/lib/crates/fabro-client/src/client.rs b/lib/crates/fabro-client/src/client.rs index 61cbace50..f674ee474 100644 --- a/lib/crates/fabro-client/src/client.rs +++ b/lib/crates/fabro-client/src/client.rs @@ -490,26 +490,18 @@ impl Client { } } - pub async fn retrieve_resolved_server_settings(&self) -> Result { - let url = format!("{}/api/v1/settings?view=resolved", self.base_url()); + pub async fn retrieve_resolved_server_settings( + &self, + ) -> Result { + let url = format!("{}/api/v1/settings", self.base_url()); let response = self .send_http(|http_client| async move { http_client.get(&url).send().await }) .await?; - let marker = response - .headers() - .get("x-fabro-settings-view") - .and_then(|value| value.to_str().ok()); - if marker != Some("resolved") { - bail!( - "server does not support resolved settings view; upgrade the server or use --local" - ); - } - response - .json::() + .json::() .await - .context("server returned invalid JSON for the resolved settings view") + .context("server returned invalid JSON for server settings") } pub async fn create_run_from_manifest(&self, manifest: types::RunManifest) -> Result { diff --git a/lib/crates/fabro-config/Cargo.toml b/lib/crates/fabro-config/Cargo.toml index c0a220b9f..a4ec98f7a 100644 --- a/lib/crates/fabro-config/Cargo.toml +++ b/lib/crates/fabro-config/Cargo.toml @@ -37,3 +37,4 @@ ulid.workspace = true [dev-dependencies] toml.workspace = true fabro-types = { path = "../fabro-types", features = ["test-support"] } +temp-env = "0.3" diff --git a/lib/crates/fabro-config/src/context.rs b/lib/crates/fabro-config/src/context.rs new file mode 100644 index 000000000..1197a4d99 --- /dev/null +++ b/lib/crates/fabro-config/src/context.rs @@ -0,0 +1,60 @@ +use fabro_types::settings::{CliNamespace, FeaturesNamespace, ServerNamespace, SettingsLayer}; +use serde::{Deserialize, Serialize}; + +use crate::resolve::{resolve_cli, resolve_features, resolve_server}; +use crate::user::load_settings_config; +use crate::{Error, Result, apply_builtin_defaults}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ServerSettings { + pub server: ServerNamespace, + pub features: FeaturesNamespace, +} + +impl ServerSettings { + pub fn from_layer(layer: &SettingsLayer) -> Result { + let layer = apply_builtin_defaults(layer.clone()); + let mut errors = Vec::new(); + let server_layer = layer.server.clone().unwrap_or_default(); + let features_layer = layer.features.clone().unwrap_or_default(); + let server = resolve_server(&server_layer, &mut errors); + let features = resolve_features(&features_layer, &mut errors); + if errors.is_empty() { + Ok(Self { server, features }) + } else { + Err(Error::resolve("failed to resolve server settings", errors)) + } + } + + pub fn resolve() -> Result { + let layer = load_settings_config(None)?; + Self::from_layer(&layer) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub struct UserSettings { + pub cli: CliNamespace, + pub features: FeaturesNamespace, +} + +impl UserSettings { + pub fn from_layer(layer: &SettingsLayer) -> Result { + let layer = apply_builtin_defaults(layer.clone()); + let mut errors = Vec::new(); + let cli_layer = layer.cli.clone().unwrap_or_default(); + let features_layer = layer.features.clone().unwrap_or_default(); + let cli = resolve_cli(&cli_layer, &mut errors); + let features = resolve_features(&features_layer, &mut errors); + if errors.is_empty() { + Ok(Self { cli, features }) + } else { + Err(Error::resolve("failed to resolve user settings", errors)) + } + } + + pub fn resolve() -> Result { + let layer = load_settings_config(None)?; + Self::from_layer(&layer) + } +} diff --git a/lib/crates/fabro-config/src/effective_settings.rs b/lib/crates/fabro-config/src/effective_settings.rs index 685bf9bae..bf69160ec 100644 --- a/lib/crates/fabro-config/src/effective_settings.rs +++ b/lib/crates/fabro-config/src/effective_settings.rs @@ -5,7 +5,7 @@ //! across all three config files (settings.toml, .fabro/project.toml, //! workflow.toml). //! Owner-specific domains (`cli`, `server`) are consumed only from the local -//! `~/.fabro/settings.toml` plus explicit process-local overrides — their +//! `~/.fabro/settings.toml` plus explicit process-local overrides. Their //! stanzas in `.fabro/project.toml` and `workflow.toml` remain schema-valid but //! inert. @@ -16,13 +16,6 @@ use fabro_types::settings::server::ServerLayer; use crate::merge::combine_files; use crate::{Error, Result, apply_builtin_defaults}; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum EffectiveSettingsMode { - LocalOnly, - RemoteServer, - LocalDaemon, -} - #[derive(Clone, Debug, Default)] pub struct EffectiveSettingsLayers { pub args: SettingsLayer, @@ -53,7 +46,6 @@ impl EffectiveSettingsLayers { pub fn materialize_settings_layer( layers: EffectiveSettingsLayers, server_settings: Option<&SettingsLayer>, - mode: EffectiveSettingsMode, ) -> Result { let EffectiveSettingsLayers { args, @@ -61,47 +53,28 @@ pub fn materialize_settings_layer( mut project, user, } = layers; + let server_settings = server_settings.ok_or(Error::MissingServerSettings)?; - let settings = match mode { - EffectiveSettingsMode::LocalOnly => { - combine_files(combine_files(combine_files(user, project), workflow), args) - } - EffectiveSettingsMode::RemoteServer | EffectiveSettingsMode::LocalDaemon => { - let server_settings = server_settings.ok_or(Error::MissingServerSettings)?; - // Owner-specific domains (cli, server) may only come from the - // local ~/.fabro/settings.toml, never from .fabro/project.toml or - // workflow.toml. The user layer keeps its cli/server fields. - strip_owner_domains(&mut workflow); - strip_owner_domains(&mut project); + // Owner-specific domains (cli, server) may only come from the local + // ~/.fabro/settings.toml, never from .fabro/project.toml or workflow.toml. + // The user layer keeps its cli/server fields. + strip_owner_domains(&mut workflow); + strip_owner_domains(&mut project); - let server_defaults = server_settings.clone(); + let combined = combine_files(combine_files(combine_files(user, project), workflow), args); + let mut settings = enforce_server_authority(combined, server_settings); - let combined = - combine_files(combine_files(combine_files(user, project), workflow), args); - - let mut settings = match mode { - EffectiveSettingsMode::RemoteServer => { - apply_server_defaults(combined, &server_defaults) - } - EffectiveSettingsMode::LocalDaemon => { - apply_local_daemon_overrides(combined, &server_defaults) - } - EffectiveSettingsMode::LocalOnly => unreachable!(), - }; - // Storage root always comes from the server's local - // ~/.fabro/settings.toml, never from the client. - if let Some(server_root) = server_settings - .server - .as_ref() - .and_then(|s| s.storage.as_ref()) - .cloned() - { - let server = settings.server.get_or_insert_with(ServerLayer::default); - server.storage = Some(server_root); - } - settings - } - }; + // Storage root always comes from the server's local ~/.fabro/settings.toml, + // never from the client. + if let Some(server_root) = server_settings + .server + .as_ref() + .and_then(|server| server.storage.as_ref()) + .cloned() + { + let server = settings.server.get_or_insert_with(ServerLayer::default); + server.storage = Some(server_root); + } Ok(apply_builtin_defaults(settings)) } @@ -111,30 +84,11 @@ fn strip_owner_domains(file: &mut SettingsLayer) { file.server = None; } -/// Apply server-side defaults to a client-layered [`SettingsLayer`]. +/// Enforce server-owned fields on a client-layered [`SettingsLayer`]. /// -/// Server-owned domains (`server`, `features`, and parts of `run`) flow from -/// the server's local `~/.fabro/settings.toml` when the corresponding client -/// value is absent. Run-shaped defaults (model, prepare, sandbox, checkpoint, -/// hooks, agent mcps, etc.) also flow from server to client so the persisted -/// run spec matches the server's local configuration. -fn apply_server_defaults(mut settings: SettingsLayer, server: &SettingsLayer) -> SettingsLayer { - // Server-owned domains: server-side always wins when client left blank. - // Use the v2 merge matrix with the server layer in lower precedence so - // that client-supplied values still dominate when present. - settings = combine_files(server.clone(), settings); - settings -} - -/// Apply server-side overrides in LocalDaemon mode. -/// -/// In LocalDaemon mode, a subset of server-owned fields unconditionally -/// override any client-side values. Client-controlled run-level fields are -/// left alone. -fn apply_local_daemon_overrides( - mut settings: SettingsLayer, - server: &SettingsLayer, -) -> SettingsLayer { +/// A subset of server-owned fields unconditionally override any client-side +/// values. Client-controlled run-level fields are left alone. +fn enforce_server_authority(mut settings: SettingsLayer, server: &SettingsLayer) -> SettingsLayer { if let Some(server_layer) = server.server.clone() { let client = settings.server.get_or_insert_with(ServerLayer::default); if let Some(storage) = server_layer.storage { @@ -173,7 +127,7 @@ mod tests { use fabro_types::settings::server::{ServerLayer, ServerSchedulerLayer, ServerStorageLayer}; use fabro_types::settings::{InterpString, SettingsLayer}; - use super::{EffectiveSettingsLayers, EffectiveSettingsMode, materialize_settings_layer}; + use super::{EffectiveSettingsLayers, materialize_settings_layer}; use crate::parse::parse_settings_layer; fn layer(source: &str) -> SettingsLayer { @@ -181,7 +135,7 @@ mod tests { } #[test] - fn local_only_merges_project_and_user_layers() { + fn materialize_settings_layer_merges_layers_and_applies_server_authority() { let settings = materialize_settings_layer( EffectiveSettingsLayers::new( SettingsLayer::default(), @@ -214,8 +168,17 @@ shared = "user" "#, ), ), - None, - EffectiveSettingsMode::LocalOnly, + Some(&layer( + r#" +_version = 1 + +[server.storage] +root = "/srv/fabro" + +[server.scheduler] +max_concurrent_runs = 7 +"#, + )), ) .unwrap(); @@ -229,7 +192,7 @@ shared = "user" .as_deref(), Some("project-model") ); - // Per R22, run.inputs replaces wholesale — the winning layer is the + // Per R22, run.inputs replaces wholesale. The winning layer is the // highest-precedence layer that sets `inputs` (project here, since it // wins over user). let inputs = settings @@ -239,13 +202,31 @@ shared = "user" .unwrap(); assert!(inputs.contains_key("project_only")); assert_eq!( - inputs.get("shared").and_then(|v| v.as_str()), + inputs.get("shared").and_then(|value| value.as_str()), Some("project") ); assert!( !inputs.contains_key("user_only"), "project.inputs should replace user.inputs wholesale" ); + assert_eq!( + settings + .server + .as_ref() + .and_then(|server| server.storage.as_ref()) + .and_then(|storage| storage.root.as_ref()) + .map(InterpString::as_source) + .as_deref(), + Some("/srv/fabro") + ); + assert_eq!( + settings + .server + .as_ref() + .and_then(|server| server.scheduler.as_ref()) + .and_then(|scheduler| scheduler.max_concurrent_runs), + Some(7) + ); assert_eq!( settings .project @@ -271,7 +252,7 @@ shared = "user" } #[test] - fn local_only_merges_workflow_project_user() { + fn materialize_settings_layer_preserves_client_values_with_empty_server_layer() { let settings = materialize_settings_layer( EffectiveSettingsLayers::new( SettingsLayer::default(), @@ -303,16 +284,13 @@ provider = "openai" "#, ), ), - None, - EffectiveSettingsMode::LocalOnly, + Some(&SettingsLayer::default()), ) .unwrap(); assert_eq!( match settings.run.as_ref().and_then(|run| run.goal.as_ref()) { - Some(RunGoalLayer::Inline(value)) => { - Some(value.as_source()) - } + Some(RunGoalLayer::Inline(value)) => Some(value.as_source()), _ => None, } .as_deref(), @@ -341,83 +319,7 @@ provider = "openai" } #[test] - fn cli_and_server_domains_from_fabro_toml_are_inert_under_remote_mode() { - let server_settings = SettingsLayer { - server: Some(ServerLayer { - storage: Some(ServerStorageLayer { - root: Some(InterpString::parse("/srv/fabro")), - }), - scheduler: Some(ServerSchedulerLayer { - max_concurrent_runs: Some(9), - }), - ..ServerLayer::default() - }), - ..SettingsLayer::default() - }; - - let project_with_server = layer( - r#" -_version = 1 - -[run] -goal = "project goal" - -[server.storage] -root = "/tmp/should-be-inert" -"#, - ); - - let settings = materialize_settings_layer( - EffectiveSettingsLayers::new( - SettingsLayer::default(), - SettingsLayer::default(), - project_with_server, - SettingsLayer::default(), - ), - Some(&server_settings), - EffectiveSettingsMode::RemoteServer, - ) - .unwrap(); - - assert_eq!( - settings - .server - .as_ref() - .and_then(|server| server.storage.as_ref()) - .and_then(|storage| storage.root.as_ref()) - .map(InterpString::as_source) - .as_deref(), - Some("/srv/fabro") - ); - assert_eq!( - match settings.run.as_ref().and_then(|run| run.goal.as_ref()) { - Some(RunGoalLayer::Inline(value)) => { - Some(value.as_source()) - } - _ => None, - } - .as_deref(), - Some("project goal") - ); - assert_eq!( - settings - .workflow - .as_ref() - .and_then(|workflow| workflow.graph.as_deref()), - Some("workflow.fabro") - ); - assert_eq!( - settings - .run - .as_ref() - .and_then(|run| run.sandbox.as_ref()) - .and_then(|sandbox| sandbox.provider.as_deref()), - Some("local") - ); - } - - #[test] - fn local_daemon_mode_only_applies_server_owned_overrides() { + fn materialize_settings_layer_applies_server_owned_overrides() { let server_settings = SettingsLayer { server: Some(ServerLayer { storage: Some(ServerStorageLayer { @@ -431,12 +333,9 @@ root = "/tmp/should-be-inert" ..SettingsLayer::default() }; - let settings = materialize_settings_layer( - EffectiveSettingsLayers::default(), - Some(&server_settings), - EffectiveSettingsMode::LocalDaemon, - ) - .unwrap(); + let settings = + materialize_settings_layer(EffectiveSettingsLayers::default(), Some(&server_settings)) + .unwrap(); assert_eq!( settings diff --git a/lib/crates/fabro-config/src/lib.rs b/lib/crates/fabro-config/src/lib.rs index a405fdac7..dc654cbeb 100644 --- a/lib/crates/fabro-config/src/lib.rs +++ b/lib/crates/fabro-config/src/lib.rs @@ -2,9 +2,14 @@ clippy::disallowed_methods, reason = "sync config loading utilities used at startup; not on a Tokio path" )] +//! Settings resolution entrypoints are owner-first context types: +//! [`ServerSettings`] for current server/runtime config and [`UserSettings`] +//! for current CLI/user config. Stored `SettingsLayer` artifacts still use the +//! per-namespace `resolve_*_from_file` helpers. extern crate self as fabro_config; +pub mod context; mod defaults; pub mod bind; @@ -24,9 +29,9 @@ pub mod user; use std::path::Path; +pub use context::{ServerSettings, UserSettings}; pub use defaults::{apply_builtin_defaults, defaults_layer}; pub use error::{Error, Result}; -use fabro_types::settings::{Settings, SettingsLayer}; pub use fabro_util::path::expand_tilde; pub use home::Home; pub use load::{ @@ -34,23 +39,14 @@ pub use load::{ }; pub use parse::{ParseError, parse_settings_layer}; pub use resolve::{ - ResolveError, dev_token_auth_enabled, resolve, resolve_cli, resolve_cli_from_file, - resolve_features, resolve_features_from_file, resolve_project, resolve_project_from_file, - resolve_run, resolve_run_from_file, resolve_server, resolve_server_from_file, - resolve_storage_root, resolve_workflow, resolve_workflow_from_file, + ResolveError, dev_token_auth_enabled, resolve_cli, resolve_cli_from_file, resolve_features, + resolve_features_from_file, resolve_project, resolve_project_from_file, resolve_run, + resolve_run_from_file, resolve_server, resolve_server_from_file, resolve_storage_root, + resolve_workflow, resolve_workflow_from_file, }; use serde::de::DeserializeOwned; pub use storage::{RunScratch, RuntimeDirectory, Storage}; -pub fn load_and_resolve( - layers: effective_settings::EffectiveSettingsLayers, - server_settings: Option<&SettingsLayer>, - mode: effective_settings::EffectiveSettingsMode, -) -> Result { - let layer = effective_settings::materialize_settings_layer(layers, server_settings, mode)?; - resolve(&layer).map_err(|errors| Error::resolve("failed to resolve settings", errors)) -} - /// Load a TOML config from an explicit path or `~/.fabro/{filename}`. /// /// Returns `T::default()` when no explicit path is given and the default file diff --git a/lib/crates/fabro-config/src/resolve/cli.rs b/lib/crates/fabro-config/src/resolve/cli.rs index 4905359d1..2c7e09345 100644 --- a/lib/crates/fabro-config/src/resolve/cli.rs +++ b/lib/crates/fabro-config/src/resolve/cli.rs @@ -1,13 +1,13 @@ use fabro_types::settings::cli::{ CliAuthSettings, CliExecAgentSettings, CliExecLayer, CliExecModelSettings, CliExecSettings, - CliLayer, CliLoggingSettings, CliOutputSettings, CliSettings, CliTargetLayer, + CliLayer, CliLoggingSettings, CliNamespace, CliOutputSettings, CliTargetLayer, CliTargetSettings, CliUpdatesSettings, }; use super::{ResolveError, require_interp}; -pub fn resolve_cli(layer: &CliLayer, errors: &mut Vec) -> CliSettings { - CliSettings { +pub fn resolve_cli(layer: &CliLayer, errors: &mut Vec) -> CliNamespace { + CliNamespace { target: resolve_target(layer.target.as_ref(), errors), auth: CliAuthSettings { strategy: layer.auth.as_ref().and_then(|auth| auth.strategy), diff --git a/lib/crates/fabro-config/src/resolve/features.rs b/lib/crates/fabro-config/src/resolve/features.rs index e6c8eeced..a9a2d1254 100644 --- a/lib/crates/fabro-config/src/resolve/features.rs +++ b/lib/crates/fabro-config/src/resolve/features.rs @@ -1,12 +1,12 @@ -use fabro_types::settings::features::{FeaturesLayer, FeaturesSettings}; +use fabro_types::settings::features::{FeaturesLayer, FeaturesNamespace}; use super::ResolveError; pub fn resolve_features( layer: &FeaturesLayer, _errors: &mut Vec, -) -> FeaturesSettings { - FeaturesSettings { +) -> FeaturesNamespace { + FeaturesNamespace { session_sandboxes: layer .session_sandboxes .expect("defaults.toml should provide features.session_sandboxes"), diff --git a/lib/crates/fabro-config/src/resolve/mod.rs b/lib/crates/fabro-config/src/resolve/mod.rs index 9032464a0..c72ca5930 100644 --- a/lib/crates/fabro-config/src/resolve/mod.rs +++ b/lib/crates/fabro-config/src/resolve/mod.rs @@ -9,8 +9,8 @@ mod workflow; pub use cli::resolve_cli; pub use error::ResolveError; use fabro_types::settings::{ - CliSettings, FeaturesSettings, InterpString, ProjectSettings, RunSettings, ServerSettings, - Settings, SettingsLayer, WorkflowSettings, + CliNamespace, FeaturesNamespace, InterpString, ProjectNamespace, RunNamespace, ServerNamespace, + SettingsLayer, WorkflowNamespace, }; pub use features::resolve_features; pub use project::resolve_project; @@ -20,33 +20,7 @@ pub use workflow::resolve_workflow; use crate::apply_builtin_defaults; -pub fn resolve(file: &SettingsLayer) -> Result> { - let file = apply_builtin_defaults(file.clone()); - let mut errors = Vec::new(); - let project_layer = file.project.clone().unwrap_or_default(); - let workflow_layer = file.workflow.clone().unwrap_or_default(); - let run_layer = file.run.clone().unwrap_or_default(); - let cli_layer = file.cli.clone().unwrap_or_default(); - let server_layer = file.server.clone().unwrap_or_default(); - let features_layer = file.features.clone().unwrap_or_default(); - - let settings = Settings { - project: resolve_project(&project_layer, &mut errors), - workflow: resolve_workflow(&workflow_layer, &mut errors), - run: resolve_run(&run_layer, &mut errors), - cli: resolve_cli(&cli_layer, &mut errors), - server: resolve_server(&server_layer, &mut errors), - features: resolve_features(&features_layer, &mut errors), - }; - - if errors.is_empty() { - Ok(settings) - } else { - Err(errors) - } -} - -pub fn resolve_cli_from_file(file: &SettingsLayer) -> Result> { +pub fn resolve_cli_from_file(file: &SettingsLayer) -> Result> { let file = apply_builtin_defaults(file.clone()); let mut errors = Vec::new(); let cli_layer = file.cli.clone().unwrap_or_default(); @@ -58,7 +32,9 @@ pub fn resolve_cli_from_file(file: &SettingsLayer) -> Result Result> { +pub fn resolve_server_from_file( + file: &SettingsLayer, +) -> Result> { let file = apply_builtin_defaults(file.clone()); let mut errors = Vec::new(); let server_layer = file.server.clone().unwrap_or_default(); @@ -72,7 +48,7 @@ pub fn resolve_server_from_file(file: &SettingsLayer) -> Result Result> { +) -> Result> { let file = apply_builtin_defaults(file.clone()); let mut errors = Vec::new(); let project_layer = file.project.clone().unwrap_or_default(); @@ -86,7 +62,7 @@ pub fn resolve_project_from_file( pub fn resolve_features_from_file( file: &SettingsLayer, -) -> Result> { +) -> Result> { let file = apply_builtin_defaults(file.clone()); let mut errors = Vec::new(); let features_layer = file.features.clone().unwrap_or_default(); @@ -98,7 +74,7 @@ pub fn resolve_features_from_file( } } -pub fn resolve_run_from_file(file: &SettingsLayer) -> Result> { +pub fn resolve_run_from_file(file: &SettingsLayer) -> Result> { let file = apply_builtin_defaults(file.clone()); let mut errors = Vec::new(); let run_layer = file.run.clone().unwrap_or_default(); @@ -112,7 +88,7 @@ pub fn resolve_run_from_file(file: &SettingsLayer) -> Result Result> { +) -> Result> { let file = apply_builtin_defaults(file.clone()); let mut errors = Vec::new(); let workflow_layer = file.workflow.clone().unwrap_or_default(); @@ -165,7 +141,7 @@ mod tests { use fabro_types::settings::run::{HookType, McpTransport, TlsMode}; - use super::resolve; + use super::resolve_run_from_file; use crate::parse_settings_layer; #[test] @@ -210,8 +186,8 @@ Authorization = "Bearer {{ env.HOOK_TOKEN }}" ) .expect("settings fixture should parse"); - let resolved = resolve(&settings).expect("settings should resolve"); - let mcps = &resolved.run.agent.mcps; + let resolved = resolve_run_from_file(&settings).expect("run settings should resolve"); + let mcps = &resolved.agent.mcps; assert_eq!( mcps.get("stdio").map(|mcp| &mcp.transport), @@ -246,7 +222,6 @@ Authorization = "Bearer {{ env.HOOK_TOKEN }}" ); let hook = resolved - .run .hooks .iter() .find(|hook| hook.name.as_deref() == Some("notify")) diff --git a/lib/crates/fabro-config/src/resolve/project.rs b/lib/crates/fabro-config/src/resolve/project.rs index dbed59297..1bd5ee0e7 100644 --- a/lib/crates/fabro-config/src/resolve/project.rs +++ b/lib/crates/fabro-config/src/resolve/project.rs @@ -1,9 +1,9 @@ -use fabro_types::settings::project::{ProjectLayer, ProjectSettings}; +use fabro_types::settings::project::{ProjectLayer, ProjectNamespace}; use super::ResolveError; -pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec) -> ProjectSettings { - ProjectSettings { +pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec) -> ProjectNamespace { + ProjectNamespace { name: layer.name.clone(), description: layer.description.clone(), directory: layer diff --git a/lib/crates/fabro-config/src/resolve/run.rs b/lib/crates/fabro-config/src/resolve/run.rs index 353715645..f2505fd23 100644 --- a/lib/crates/fabro-config/src/resolve/run.rs +++ b/lib/crates/fabro-config/src/resolve/run.rs @@ -8,15 +8,15 @@ use fabro_types::settings::run::{ NotificationRouteLayer, NotificationRouteSettings, PullRequestSettings, RunAgentLayer, RunAgentSettings, RunArtifactsLayer, RunCheckpointLayer, RunCheckpointSettings, RunExecutionLayer, RunExecutionSettings, RunGitLayer, RunGitSettings, RunGoal, RunGoalLayer, - RunInterviewsSettings, RunLayer, RunModelLayer, RunModelSettings, RunPrepareLayer, - RunPrepareSettings, RunPullRequestLayer, RunSandboxLayer, RunSandboxSettings, RunScmLayer, - RunScmSettings, RunSettings, ScmGitHubSettings, StringOrSplice, TlsMode, + RunInterviewsSettings, RunLayer, RunModelLayer, RunModelSettings, RunNamespace, + RunPrepareLayer, RunPrepareSettings, RunPullRequestLayer, RunSandboxLayer, RunSandboxSettings, + RunScmLayer, RunScmSettings, ScmGitHubSettings, StringOrSplice, TlsMode, }; use super::ResolveError; -pub fn resolve_run(layer: &RunLayer, errors: &mut Vec) -> RunSettings { - RunSettings { +pub fn resolve_run(layer: &RunLayer, errors: &mut Vec) -> RunNamespace { + RunNamespace { goal: resolve_goal(layer.goal.as_ref()), working_dir: layer.working_dir.clone(), metadata: layer.metadata.clone(), diff --git a/lib/crates/fabro-config/src/resolve/server.rs b/lib/crates/fabro-config/src/resolve/server.rs index 8f8307b25..1ceb10092 100644 --- a/lib/crates/fabro-config/src/resolve/server.rs +++ b/lib/crates/fabro-config/src/resolve/server.rs @@ -6,7 +6,7 @@ use fabro_types::settings::server::{ ServerAuthLayer, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsLayer, ServerIntegrationsSettings, ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings, ServerLayer, ServerListenLayer, - ServerListenSettings, ServerLoggingSettings, ServerSchedulerSettings, ServerSettings, + ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSchedulerSettings, ServerSlateDbLayer, ServerSlateDbSettings, ServerStorageLayer, ServerStorageSettings, ServerWebLayer, ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings, WebhookStrategy, @@ -35,7 +35,7 @@ pub fn dev_token_auth_enabled(layer: &SettingsLayer) -> bool { .is_some_and(|methods| methods.contains(&ServerAuthMethod::DevToken)) } -pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec) -> ServerSettings { +pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec) -> ServerNamespace { let storage = resolve_storage(layer.storage.as_ref()); let listen = resolve_listen(layer.listen.as_ref(), errors); let web = resolve_web(layer.api.as_ref(), layer.web.as_ref()); @@ -46,7 +46,7 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec) -> Se validate_github_webhook_ip_allowlist_for_listen(&listen, &ip_allowlist, &integrations, errors); validate_github_webhook_strategy(&integrations, layer.api.as_ref(), errors); - ServerSettings { + ServerNamespace { listen, api: ServerApiSettings { url: layer.api.as_ref().and_then(|api| api.url.clone()), diff --git a/lib/crates/fabro-config/src/resolve/workflow.rs b/lib/crates/fabro-config/src/resolve/workflow.rs index dee80d1ba..5bb5bc139 100644 --- a/lib/crates/fabro-config/src/resolve/workflow.rs +++ b/lib/crates/fabro-config/src/resolve/workflow.rs @@ -1,12 +1,12 @@ -use fabro_types::settings::workflow::{WorkflowLayer, WorkflowSettings}; +use fabro_types::settings::workflow::{WorkflowLayer, WorkflowNamespace}; use super::ResolveError; pub fn resolve_workflow( layer: &WorkflowLayer, _errors: &mut Vec, -) -> WorkflowSettings { - WorkflowSettings { +) -> WorkflowNamespace { + WorkflowNamespace { name: layer.name.clone(), description: layer.description.clone(), graph: layer diff --git a/lib/crates/fabro-config/tests/defaults.rs b/lib/crates/fabro-config/tests/defaults.rs index ff67dc914..dc5aa78c3 100644 --- a/lib/crates/fabro-config/tests/defaults.rs +++ b/lib/crates/fabro-config/tests/defaults.rs @@ -1,4 +1,7 @@ -use fabro_config::{apply_builtin_defaults, defaults_layer, parse_settings_layer, resolve}; +use fabro_config::{ + apply_builtin_defaults, defaults_layer, parse_settings_layer, resolve_run_from_file, + resolve_server_from_file, resolve_workflow_from_file, +}; use fabro_types::settings::SettingsLayer; use fabro_types::settings::cli::OutputFormat; use fabro_types::settings::run::{ApprovalMode, RunMode, WorktreeMode}; @@ -91,7 +94,8 @@ fn apply_builtin_defaults_materializes_expected_layer() { #[test] fn resolve_empty_settings_requires_explicit_server_auth_methods() { - let errors = resolve(&SettingsLayer::default()).expect_err("empty settings should fail"); + let errors = resolve_server_from_file(&SettingsLayer::default()) + .expect_err("empty server settings should fail"); assert!(errors.iter().any(|error| { matches!( @@ -115,9 +119,10 @@ mode = "dry_run" "#, ); - let settings = resolve(&layer).expect("settings should resolve"); + let workflow = resolve_workflow_from_file(&layer).expect("workflow settings should resolve"); + let run = resolve_run_from_file(&layer).expect("run settings should resolve"); - assert_eq!(settings.run.execution.mode, RunMode::DryRun); - assert_eq!(settings.run.execution.approval, ApprovalMode::Prompt); - assert_eq!(settings.workflow.graph, "workflow.fabro"); + assert_eq!(run.execution.mode, RunMode::DryRun); + assert_eq!(run.execution.approval, ApprovalMode::Prompt); + assert_eq!(workflow.graph, "workflow.fabro"); } diff --git a/lib/crates/fabro-config/tests/resolve_cli.rs b/lib/crates/fabro-config/tests/resolve_cli.rs index e23f0fce8..f6dc06005 100644 --- a/lib/crates/fabro-config/tests/resolve_cli.rs +++ b/lib/crates/fabro-config/tests/resolve_cli.rs @@ -2,6 +2,7 @@ use fabro_config::{parse_settings_layer, resolve_cli_from_file}; use fabro_types::settings::cli::{CliTargetSettings, OutputFormat, OutputVerbosity}; use fabro_types::settings::run::AgentPermissions; use fabro_types::settings::{InterpString, SettingsLayer}; +use temp_env::with_var; #[test] fn resolves_cli_defaults_from_empty_settings() { @@ -17,6 +18,74 @@ fn resolves_cli_defaults_from_empty_settings() { assert!(cli.logging.level.is_none()); } +#[test] +fn user_settings_from_layer_matches_namespace_resolvers() { + let settings: SettingsLayer = parse_settings_layer( + r#" +_version = 1 + +[cli.target] +type = "http" +url = "https://config.example.com" + +[features] +session_sandboxes = true +"#, + ) + .expect("fixture should parse"); + + let user_settings = + fabro_config::UserSettings::from_layer(&settings).expect("user settings should resolve"); + + assert_eq!( + user_settings.cli, + resolve_cli_from_file(&settings).expect("cli namespace should resolve") + ); + assert_eq!( + user_settings.features, + fabro_config::resolve_features_from_file(&settings) + .expect("features namespace should resolve") + ); +} + +#[test] +fn user_settings_resolve_reads_default_settings_from_fabro_home() { + let home = tempfile::tempdir().unwrap(); + std::fs::write( + home.path().join("settings.toml"), + r#" +_version = 1 + +[cli.output] +verbosity = "verbose" + +[features] +session_sandboxes = true +"#, + ) + .unwrap(); + + with_var("FABRO_HOME", Some(home.path()), || { + let user_settings = + fabro_config::UserSettings::resolve().expect("user settings should resolve"); + assert_eq!(user_settings.cli.output.verbosity, OutputVerbosity::Verbose); + assert!(user_settings.features.session_sandboxes); + }); +} + +#[test] +fn user_settings_resolve_returns_defaults_when_default_settings_file_is_missing() { + let home = tempfile::tempdir().unwrap(); + + with_var("FABRO_HOME", Some(home.path()), || { + let user_settings = + fabro_config::UserSettings::resolve().expect("user settings should resolve"); + assert_eq!(user_settings.cli.output.format, OutputFormat::Text); + assert_eq!(user_settings.cli.output.verbosity, OutputVerbosity::Normal); + assert!(!user_settings.features.session_sandboxes); + }); +} + #[test] fn resolves_cli_target_exec_and_output_settings() { let settings: SettingsLayer = parse_settings_layer( diff --git a/lib/crates/fabro-config/tests/resolve_root.rs b/lib/crates/fabro-config/tests/resolve_root.rs index b6176c6ad..d4620053e 100644 --- a/lib/crates/fabro-config/tests/resolve_root.rs +++ b/lib/crates/fabro-config/tests/resolve_root.rs @@ -1,4 +1,3 @@ -use fabro_config::effective_settings::{EffectiveSettingsLayers, EffectiveSettingsMode}; use fabro_config::parse_settings_layer; use fabro_types::settings::{InterpString, SettingsLayer}; @@ -8,8 +7,8 @@ fn parse(source: &str) -> SettingsLayer { #[test] fn resolves_root_settings_require_explicit_server_auth_methods() { - let errors = - fabro_config::resolve(&SettingsLayer::default()).expect_err("empty settings should fail"); + let errors = fabro_config::resolve_server_from_file(&SettingsLayer::default()) + .expect_err("empty server settings should fail"); assert!(errors.iter().any(|error| { matches!( @@ -40,12 +39,20 @@ provider = "not-a-provider" "#, ); - let errors = fabro_config::resolve(&settings).expect_err("invalid shape should fail"); - let rendered = errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n"); + let mut rendered = Vec::new(); + rendered.extend( + fabro_config::resolve_server_from_file(&settings) + .expect_err("invalid server settings should fail") + .into_iter() + .map(|error| error.to_string()), + ); + rendered.extend( + fabro_config::resolve_run_from_file(&settings) + .expect_err("invalid run settings should fail") + .into_iter() + .map(|error| error.to_string()), + ); + let rendered = rendered.join("\n"); assert!(rendered.contains("server.listen.address")); assert!(rendered.contains("server.auth.github.allowed_usernames")); @@ -53,66 +60,45 @@ provider = "not-a-provider" } #[test] -fn load_and_resolve_merges_layers_before_resolution() { - let settings = fabro_config::load_and_resolve( - EffectiveSettingsLayers::new( - SettingsLayer::default(), - parse( - r#" -_version = 1 - -[workflow] -graph = "graphs/workflow.dot" -"#, - ), - parse( - r#" +fn namespace_resolvers_cover_root_level_settings_shape() { + let settings = parse( + r#" _version = 1 [project] directory = ".fabro" -"#, - ), - parse( - r#" -_version = 1 + +[workflow] +graph = "graphs/workflow.dot" [server.storage] root = "/srv/fabro" [server.auth] methods = ["dev-token"] - [run.model] provider = "openai" name = "gpt-5" "#, - ), - ), - None, - EffectiveSettingsMode::LocalOnly, - ) - .expect("layers should load and resolve"); + ); - assert_eq!(settings.project.directory, ".fabro"); - assert_eq!(settings.workflow.graph, "graphs/workflow.dot"); - assert_eq!(settings.server.storage.root.as_source(), "/srv/fabro"); + let project = fabro_config::resolve_project_from_file(&settings) + .expect("project settings should resolve"); + let workflow = fabro_config::resolve_workflow_from_file(&settings) + .expect("workflow settings should resolve"); + let server = + fabro_config::resolve_server_from_file(&settings).expect("server settings should resolve"); + let run = fabro_config::resolve_run_from_file(&settings).expect("run settings should resolve"); + + assert_eq!(project.directory, ".fabro"); + assert_eq!(workflow.graph, "graphs/workflow.dot"); + assert_eq!(server.storage.root.as_source(), "/srv/fabro"); assert_eq!( - settings - .run - .model - .provider - .as_ref() - .map(InterpString::as_source), + run.model.provider.as_ref().map(InterpString::as_source), Some("openai".to_string()) ); assert_eq!( - settings - .run - .model - .name - .as_ref() - .map(InterpString::as_source), + run.model.name.as_ref().map(InterpString::as_source), Some("gpt-5".to_string()) ); } diff --git a/lib/crates/fabro-config/tests/resolve_server.rs b/lib/crates/fabro-config/tests/resolve_server.rs index 1da6f3ed1..11681904d 100644 --- a/lib/crates/fabro-config/tests/resolve_server.rs +++ b/lib/crates/fabro-config/tests/resolve_server.rs @@ -5,6 +5,7 @@ use fabro_types::settings::server::{ }; use fabro_types::settings::{InterpString, SettingsLayer}; use fabro_util::Home; +use temp_env::with_var; fn parse(source: &str) -> SettingsLayer { let mut layer = parse_settings_layer(source).expect("fixture should parse"); @@ -69,6 +70,64 @@ fn resolves_server_defaults_from_empty_settings() { assert!(!settings.slatedb.disk_cache); } +#[test] +fn server_settings_from_layer_matches_namespace_resolvers() { + let settings = parse( + r#" +_version = 1 + +[server.auth] +methods = ["dev-token"] + +[server.storage] +root = "/srv/fabro" + +[features] +session_sandboxes = true +"#, + ); + + let context = + fabro_config::ServerSettings::from_layer(&settings).expect("settings should resolve"); + + assert_eq!( + context.server, + fabro_config::resolve_server_from_file(&settings).expect("server namespace should resolve") + ); + assert_eq!( + context.features, + fabro_config::resolve_features_from_file(&settings) + .expect("features namespace should resolve") + ); +} + +#[test] +fn server_settings_resolve_reads_default_settings_from_fabro_home() { + let home = tempfile::tempdir().unwrap(); + std::fs::write( + home.path().join("settings.toml"), + r#" +_version = 1 + +[server.auth] +methods = ["dev-token"] + +[server.storage] +root = "/srv/from-home" + +[features] +session_sandboxes = true +"#, + ) + .unwrap(); + + with_var("FABRO_HOME", Some(home.path()), || { + let settings = fabro_config::ServerSettings::resolve().expect("settings should resolve"); + assert_eq!(settings.server.storage.root.as_source(), "/srv/from-home"); + assert!(settings.features.session_sandboxes); + }); +} + #[test] fn parsing_rejects_inbound_listener_tls_configuration() { let err = fabro_config::parse_settings_layer( diff --git a/lib/crates/fabro-server/src/auth/cli_flow.rs b/lib/crates/fabro-server/src/auth/cli_flow.rs index 36345cdc2..4b052a40d 100644 --- a/lib/crates/fabro-server/src/auth/cli_flow.rs +++ b/lib/crates/fabro-server/src/auth/cli_flow.rs @@ -1013,6 +1013,7 @@ fn pkce_challenge(verifier: &str) -> String { fn login_allowed(state: &AppState, login: &str) -> bool { state .server_settings() + .server .auth .github .allowed_usernames @@ -1372,7 +1373,6 @@ mod tests { let state = server::create_test_app_state_with_session_key( settings, Some("cli-flow-test-key-material-0123456789"), - false, ); let app = axum::Router::new() .nest("/auth", web_routes()) diff --git a/lib/crates/fabro-server/src/auth/translate.rs b/lib/crates/fabro-server/src/auth/translate.rs index 5c05edb37..2d124f2c5 100644 --- a/lib/crates/fabro-server/src/auth/translate.rs +++ b/lib/crates/fabro-server/src/auth/translate.rs @@ -225,7 +225,6 @@ mod tests { server::create_test_app_state_with_session_key( SettingsLayer::default(), Some(SESSION_SECRET), - false, ) } diff --git a/lib/crates/fabro-server/src/canonical_origin.rs b/lib/crates/fabro-server/src/canonical_origin.rs index fdbb5fec9..05bd7793c 100644 --- a/lib/crates/fabro-server/src/canonical_origin.rs +++ b/lib/crates/fabro-server/src/canonical_origin.rs @@ -1,4 +1,4 @@ -use fabro_types::settings::ServerSettings as ResolvedServerSettings; +use fabro_types::settings::ServerNamespace as ResolvedServerSettings; use url::Url; use crate::server::EnvLookup; diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs index 7ca48a246..d68c6f4b5 100644 --- a/lib/crates/fabro-server/src/demo/mod.rs +++ b/lib/crates/fabro-server/src/demo/mod.rs @@ -24,7 +24,6 @@ use crate::error::ApiError; use crate::jwt_auth::AuthenticatedService; use crate::run_selector::{ResolveRunError, resolve_run_by_selector}; use crate::server::{AppState, PaginationParams}; -use crate::settings_view; fn paginated_response( items: Vec, @@ -602,22 +601,8 @@ pub(crate) async fn list_query_history( pub(crate) async fn get_server_settings( _auth: AuthenticatedService, State(_state): State>, - Query(query): Query, ) -> Response { - match query.view { - settings_view::SettingsApiView::Layer => { - (StatusCode::OK, Json(settings::server_settings())).into_response() - } - settings_view::SettingsApiView::Resolved => { - let mut response = - (StatusCode::OK, Json(settings::resolved_server_settings())).into_response(); - response.headers_mut().insert( - settings_view::RESOLVED_VIEW_HEADER_NAME, - axum::http::HeaderValue::from_static(settings_view::RESOLVED_VIEW_HEADER_VALUE), - ); - response - } - } + (StatusCode::OK, Json(settings::server_settings())).into_response() } // ── System ──────────────────────────────────────────────────────────── @@ -1529,208 +1514,50 @@ mod insights { mod settings { pub(super) fn server_settings() -> serde_json::Value { - // v2 SettingsLayer shape — matches what /api/v1/settings returns in - // production, so the demo renders identically. - serde_json::json!({ - "_version": 1, - "server": { - "storage": { - "root": "/home/fabro/.fabro" - }, - "scheduler": { - "max_concurrent_runs": 10 - }, - "api": { - "url": "https://api.fabro.example.com" - }, - "web": { - "enabled": true, - "url": "https://fabro.example.com" - }, - "auth": { - "api": { - "jwt": { "enabled": true } - }, - "web": { - "allowed_usernames": ["brynary", "alice"], - "providers": { - "github": { - "enabled": true, - "client_id": "Iv1.abc123" - } - } - } - }, - "integrations": { - "github": { - "app_id": "12345", - "client_id": "Iv1.abc123", - "slug": "fabro-dev" - } - } - }, - "run": { - "model": { - "provider": "anthropic", - "name": "claude-sonnet" - }, - "sandbox": { - "provider": "daytona", - "daytona": { - "auto_stop_interval": 60, - "network": "block" - } - } - }, - "features": { - "session_sandboxes": false, - "retros": false - } - }) - } + let settings = fabro_config::parse_settings_layer( + r#" +_version = 1 - pub(super) fn resolved_server_settings() -> serde_json::Value { - serde_json::json!({ - "project": { - "directory": "." - }, - "workflow": { - "graph": "workflow.fabro" - }, - "run": { - "model": { - "provider": "anthropic", - "name": "claude-sonnet", - "fallbacks": [] - }, - "execution": { - "mode": "normal", - "approval": "prompt", - "retros": true - }, - "sandbox": { - "provider": "daytona", - "preserve": false, - "devcontainer": false, - "env": {}, - "local": { - "worktree_mode": "clean" - }, - "daytona": { - "auto_stop_interval": 60, - "labels": {}, - "network": "block", - "skip_clone": false - } - }, - "notifications": {}, - "interviews": {}, - "agent": { - "mcps": {} - }, - "hooks": [], - "scm": {}, - "artifacts": { - "include": [] - }, - "inputs": {}, - "metadata": {}, - "git": {}, - "prepare": { - "commands": [], - "timeout_ms": 300000 - }, - "checkpoint": { - "exclude_globs": [] - } - }, - "cli": { - "auth": {}, - "exec": { - "prevent_idle_sleep": false, - "model": {}, - "agent": { - "mcps": {} - } - }, - "output": { - "format": "text", - "verbosity": "normal" - }, - "updates": { - "check": true - }, - "logging": {} - }, - "server": { - "api": { - "url": "https://api.fabro.example.com" - }, - "web": { - "enabled": true, - "url": "https://fabro.example.com" - }, - "auth": { - "api": { - "jwt": { - "enabled": true - } - }, - "web": { - "allowed_usernames": ["brynary", "alice"], - "providers": { - "github": { - "enabled": true, - "client_id": "Iv1.abc123" - } - } - } - }, - "storage": { - "root": "/home/fabro/.fabro" - }, - "artifacts": { - "prefix": "", - "store": { - "type": "local", - "root": "" - } - }, - "slatedb": { - "prefix": "", - "store": { - "type": "local", - "root": "" - }, - "flush_interval": "0s" - }, - "scheduler": { - "max_concurrent_runs": 10 - }, - "logging": {}, - "integrations": { - "github": { - "enabled": false, - "strategy": "token", - "app_id": "12345", - "client_id": "Iv1.abc123", - "slug": "fabro-dev", - "permissions": {} - }, - "slack": { - "enabled": false - }, - "discord": { - "enabled": false - }, - "teams": { - "enabled": false - } - } - }, - "features": { - "session_sandboxes": false - } - }) +[server.listen] +type = "tcp" +address = "127.0.0.1:32276" + +[server.api] +url = "https://api.fabro.example.com" + +[server.web] +enabled = true +url = "https://fabro.example.com" + +[server.auth] +methods = ["github"] + +[server.auth.github] +allowed_usernames = ["brynary", "alice"] + +[server.storage] +root = "/home/fabro/.fabro" + +[server.scheduler] +max_concurrent_runs = 10 + +[server.integrations.github] +enabled = true +strategy = "app" +app_id = "12345" +client_id = "Iv1.abc123" +slug = "fabro-dev" + +[features] +session_sandboxes = false +"#, + ) + .expect("demo settings fixture should parse"); + + serde_json::to_value( + fabro_config::ServerSettings::from_layer(&settings) + .expect("demo settings fixture should resolve"), + ) + .expect("demo settings should serialize") } } diff --git a/lib/crates/fabro-server/src/diagnostics.rs b/lib/crates/fabro-server/src/diagnostics.rs index 582816a76..926f4c9b3 100644 --- a/lib/crates/fabro-server/src/diagnostics.rs +++ b/lib/crates/fabro-server/src/diagnostics.rs @@ -177,8 +177,8 @@ async fn probe_llm_provider(client: &LlmClient, provider: Provider) -> Result<() async fn check_github_app(state: &AppState) -> CheckResult { let settings = state.server_settings(); - if settings.integrations.github.strategy == GithubIntegrationStrategy::Token { - let token = match state.github_credentials(&settings.integrations.github) { + if settings.server.integrations.github.strategy == GithubIntegrationStrategy::Token { + let token = match state.github_credentials(&settings.server.integrations.github) { Ok(Some(fabro_github::GitHubCredentials::Token(token))) => token, Ok(Some(_)) => unreachable!("token strategy should not return app credentials"), Ok(None) => { @@ -263,19 +263,21 @@ async fn check_github_app(state: &AppState) -> CheckResult { } let app_id = settings + .server .integrations .github .app_id .as_ref() .map(InterpString::as_source); let slug = settings + .server .integrations .github .slug .as_ref() .map(InterpString::as_source); let private_key_raw = state.server_secret("GITHUB_APP_PRIVATE_KEY"); - let client_id = settings.integrations.github.client_id.is_some(); + let client_id = settings.server.integrations.github.client_id.is_some(); let client_secret = state.server_secret("GITHUB_APP_CLIENT_SECRET").is_some(); let webhook_secret = state.server_secret("GITHUB_APP_WEBHOOK_SECRET").is_some(); @@ -504,7 +506,7 @@ fn check_crypto(state: &AppState) -> CheckResult { let mut details = Vec::new(); let mut errors = Vec::new(); - if resolved_server_settings.web.enabled { + if resolved_server_settings.server.web.enabled { match state.server_secret("SESSION_SECRET") { Some(secret) => { if let Err(err) = validate_session_secret(&secret) { @@ -515,7 +517,7 @@ fn check_crypto(state: &AppState) -> CheckResult { } } - let methods = &resolved_server_settings.auth.methods; + let methods = &resolved_server_settings.server.auth.methods; if methods.contains(&ServerAuthMethod::DevToken) { match state.server_secret("FABRO_DEV_TOKEN") { Some(token) if validate_dev_token_format(&token) => {} @@ -525,6 +527,7 @@ fn check_crypto(state: &AppState) -> CheckResult { } if methods.contains(&ServerAuthMethod::Github) { if resolved_server_settings + .server .integrations .github .client_id diff --git a/lib/crates/fabro-server/src/install.rs b/lib/crates/fabro-server/src/install.rs index 13d3ba3ec..ba18f44ac 100644 --- a/lib/crates/fabro-server/src/install.rs +++ b/lib/crates/fabro-server/src/install.rs @@ -13,8 +13,8 @@ use axum::{Json, Router, middleware}; use base64::Engine as _; use base64::engine::general_purpose::{STANDARD as BASE64_STANDARD, URL_SAFE_NO_PAD}; use fabro_auth::{AuthCredential, AuthDetails, credential_id_for}; +use fabro_config::Storage; use fabro_config::bind::{Bind, BindRequest}; -use fabro_config::{Storage, resolve_server_from_file}; use fabro_install::{ InstallListenConfig, PendingSettingsWrite, VaultSecretWrite, generate_jwt_keypair, merge_server_settings, persist_install_outputs_direct, write_github_app_settings, @@ -1372,17 +1372,9 @@ async fn write_artifact_store_metadata( .get_or_insert_with(ServerStorageLayer::default); storage.root = Some(InterpString::parse(&storage_dir.display().to_string())); - let resolved = resolve_server_from_file(&settings).map_err(|errors| { - anyhow::anyhow!( - "failed to resolve server settings:\n{}", - errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n") - ) - })?; - let (object_store, prefix) = serve::build_artifact_object_store(&resolved)?; + let resolved = + fabro_config::ServerSettings::from_layer(&settings).map_err(anyhow::Error::from)?; + let (object_store, prefix) = serve::build_artifact_object_store(&resolved.server)?; let artifact_store = ArtifactStore::new(object_store, prefix); artifact_store.write_metadata(FABRO_VERSION).await?; Ok(()) diff --git a/lib/crates/fabro-server/src/jwt_auth.rs b/lib/crates/fabro-server/src/jwt_auth.rs index 8d3ca7f03..4b2b53d54 100644 --- a/lib/crates/fabro-server/src/jwt_auth.rs +++ b/lib/crates/fabro-server/src/jwt_auth.rs @@ -2,7 +2,7 @@ use anyhow::{Result, anyhow}; use axum::extract::FromRequestParts; use axum::http::header; use axum::http::request::Parts; -use fabro_types::settings::{ServerAuthMethod, ServerSettings as ResolvedServerSettings}; +use fabro_types::settings::{ServerAuthMethod, ServerNamespace as ResolvedServerSettings}; use fabro_types::{IdpIdentity, RunAuthMethod}; use fabro_util::dev_token::validate_dev_token_format; use hmac::{Hmac, Mac}; @@ -563,7 +563,7 @@ methods = [] let errors = resolve_server_from_file(&file).expect_err("empty auth methods should fail"); assert!(errors.iter().any(|err| matches!( err, - fabro_config::resolve::ResolveError::Invalid { path, reason } + fabro_config::ResolveError::Invalid { path, reason } if path == "server.auth.methods" && reason.contains("must not be empty") ))); } diff --git a/lib/crates/fabro-server/src/lib.rs b/lib/crates/fabro-server/src/lib.rs index 92c3d936d..55b6e3db0 100644 --- a/lib/crates/fabro-server/src/lib.rs +++ b/lib/crates/fabro-server/src/lib.rs @@ -31,7 +31,6 @@ pub mod security_headers; pub mod serve; pub mod server; mod server_secrets; -mod settings_view; pub mod static_files; pub mod web_auth; diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs index ded7f88c1..96d5db9b6 100644 --- a/lib/crates/fabro-server/src/run_manifest.rs +++ b/lib/crates/fabro-server/src/run_manifest.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use anyhow::{Result, anyhow, bail}; use fabro_api::types; -use fabro_config::effective_settings::{EffectiveSettingsLayers, EffectiveSettingsMode}; +use fabro_config::effective_settings::EffectiveSettingsLayers; use fabro_config::merge::combine_files; use fabro_config::project::resolve_working_directory; use fabro_config::run::parse_run_config; @@ -23,10 +23,10 @@ use fabro_types::settings::cli::{CliLayer, CliOutputLayer, OutputVerbosity}; use fabro_types::settings::interp::InterpString; use fabro_types::settings::run::{ ApprovalMode, DaytonaDockerfileLayer, DaytonaNetworkLayer, DaytonaSettings, DockerfileSource, - RunExecutionLayer, RunGoalLayer, RunLayer, RunMode, RunModelLayer, RunSandboxLayer, - RunSettings, + RunExecutionLayer, RunGoalLayer, RunLayer, RunMode, RunModelLayer, RunNamespace, + RunSandboxLayer, }; -use fabro_types::settings::{ServerSettings, SettingsLayer}; +use fabro_types::settings::{ServerNamespace, SettingsLayer}; use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus}; use fabro_validate::Severity; use fabro_workflow::Error as WorkflowError; @@ -51,10 +51,9 @@ pub(crate) struct PreparedManifest { pub working_directory: PathBuf, } -pub(crate) fn prepare_manifest_with_mode( +pub(crate) fn prepare_manifest( server_settings: &SettingsLayer, manifest: &types::RunManifest, - local_daemon_mode: bool, ) -> Result { if manifest.version != 1 { bail!("unsupported manifest version {}", manifest.version); @@ -88,11 +87,6 @@ pub(crate) fn prepare_manifest_with_mode( let mut settings = effective_settings::materialize_settings_layer( EffectiveSettingsLayers::new(args_layer, workflow_layer, project_layer, user_layer), Some(server_settings), - if local_daemon_mode { - EffectiveSettingsMode::LocalDaemon - } else { - EffectiveSettingsMode::RemoteServer - }, )?; if let Some(goal) = manifest.goal.as_ref() { let run = settings.run.get_or_insert_with(RunLayer::default); @@ -471,7 +465,7 @@ fn base_preflight_checks(prepared: &PreparedManifest, graph: &Graph) -> Vec Result { +fn resolve_sandbox_provider(settings: &RunNamespace) -> Result { Ok(Some(str::parse::( settings.sandbox.provider.as_str(), )) @@ -480,7 +474,7 @@ fn resolve_sandbox_provider(settings: &RunSettings) -> Result { .unwrap_or_default()) } -fn resolve_daytona_config(settings: &RunSettings) -> Option { +fn resolve_daytona_config(settings: &RunNamespace) -> Option { settings .sandbox .daytona @@ -492,7 +486,7 @@ async fn run_sandbox_check( checks: &mut Vec, sandbox_provider: SandboxProvider, prepared: &PreparedManifest, - resolved_run: &RunSettings, + resolved_run: &RunNamespace, github_app: Option, daytona_api_key: Option, ) -> bool { @@ -567,7 +561,7 @@ async fn run_llm_check( state: &AppState, checks: &mut Vec, graph: &Graph, - settings: &RunSettings, + settings: &RunNamespace, configured_providers: &[Provider], ) -> bool { let (model, provider) = resolve_model_provider(settings, graph, configured_providers); @@ -679,7 +673,7 @@ async fn run_llm_check( } fn resolve_model_provider( - settings: &RunSettings, + settings: &RunNamespace, _graph: &Graph, configured_providers: &[Provider], ) -> (String, Option) { @@ -753,7 +747,7 @@ fn runtime_daytona_config(settings: &DaytonaSettings) -> DaytonaConfig { async fn run_github_token_check( checks: &mut Vec, prepared: &PreparedManifest, - settings: &ServerSettings, + settings: &ServerNamespace, github_app: Option, ) { if settings.integrations.github.permissions.is_empty() { @@ -990,7 +984,7 @@ root = "/srv/fabro" verbose: None, }); - let prepared = prepare_manifest_with_mode(&server_settings, &manifest, false).unwrap(); + let prepared = prepare_manifest(&server_settings, &manifest).unwrap(); assert_eq!( fabro_config::resolve_run_from_file(&prepared.settings) @@ -1002,7 +996,7 @@ root = "/srv/fabro" } #[test] - fn prepare_manifest_local_daemon_prefers_bundled_settings_without_duplication() { + fn prepare_manifest_prefers_bundled_settings_without_duplication() { let server_settings = server_settings_fixture( r#" _version = 1 @@ -1050,7 +1044,7 @@ app_id = "snapshotted-app-id" type_: types::ManifestConfigType::User, }); - let prepared = prepare_manifest_with_mode(&server_settings, &manifest, true).unwrap(); + let prepared = prepare_manifest(&server_settings, &manifest).unwrap(); let resolved_run = fabro_config::resolve_run_from_file(&prepared.settings).unwrap(); let resolved_server = fabro_config::resolve_server_from_file(&prepared.settings).unwrap(); @@ -1075,9 +1069,7 @@ app_id = "snapshotted-app-id" #[tokio::test] async fn invalid_preflight_returns_diagnostics_without_runtime_checks() { let state = crate::server::create_app_state(); - let prepared = - prepare_manifest_with_mode(&default_settings_fixture(), &invalid_manifest(), false) - .unwrap(); + let prepared = prepare_manifest(&default_settings_fixture(), &invalid_manifest()).unwrap(); let validated = validate_prepared_manifest(&prepared).unwrap(); assert!(validated.has_errors()); @@ -1112,8 +1104,7 @@ enabled = true type_: types::ManifestConfigType::Project, }); - let prepared = - prepare_manifest_with_mode(&default_settings_fixture(), &manifest, false).unwrap(); + let prepared = prepare_manifest(&default_settings_fixture(), &manifest).unwrap(); let validated = validate_prepared_manifest(&prepared).unwrap(); assert!(!validated.has_errors()); @@ -1150,8 +1141,7 @@ provider = "daytona" type_: types::ManifestConfigType::Project, }); - let prepared = - prepare_manifest_with_mode(&default_settings_fixture(), &manifest, false).unwrap(); + let prepared = prepare_manifest(&default_settings_fixture(), &manifest).unwrap(); let validated = validate_prepared_manifest(&prepared).unwrap(); let (response, _ok) = run_preflight(state.as_ref(), &prepared, &validated) diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs index 31bcecf5e..49ae4d959 100644 --- a/lib/crates/fabro-server/src/serve.rs +++ b/lib/crates/fabro-server/src/serve.rs @@ -8,14 +8,14 @@ use clap::Args; use fabro_config::bind::{self, Bind, BindRequest}; use fabro_config::merge::combine_files; use fabro_config::user::load_settings_config; -use fabro_config::{Storage, resolve_server_from_file}; +use fabro_config::{ServerSettings as CurrentServerSettings, Storage}; use fabro_sandbox::SandboxProvider; use fabro_types::settings::server::{ GithubIntegrationStrategy, ServerLayer, ServerListenLayer, WebhookStrategy, }; use fabro_types::settings::{ GithubIntegrationSettings, InterpString, ObjectStoreSettings, ServerListenSettings, - ServerSettings as ResolvedServerSettings, SettingsLayer, + ServerNamespace as ResolvedServerSettings, SettingsLayer, }; use fabro_util::terminal::Styles; use object_store::ObjectStore; @@ -350,16 +350,9 @@ fn build_object_store_from_settings( } fn resolve_server_settings(file: &SettingsLayer) -> anyhow::Result { - resolve_server_from_file(file).map_err(|errors| { - anyhow::anyhow!( - "failed to resolve server settings:\n{}", - errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n") - ) - }) + CurrentServerSettings::from_layer(file) + .map(|settings| settings.server) + .map_err(anyhow::Error::from) } pub fn resolve_bind_request_from_settings( @@ -520,7 +513,6 @@ where artifact_store, vault_path, server_env_path, - local_daemon_mode: true, env_lookup, http_client: None, })?; diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index 04896566c..fa97ac90e 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -12,7 +12,7 @@ use axum::body::Body; use axum::body::to_bytes; use axum::extract::{self as axum_extract, DefaultBodyLimit, Path, Query, State}; use axum::http::request::Parts; -use axum::http::{HeaderMap, HeaderValue, Method, StatusCode, header}; +use axum::http::{HeaderMap, Method, StatusCode, header}; use axum::middleware::{self}; use axum::response::sse::{Event, KeepAlive, Sse}; use axum::response::{IntoResponse, Response}; @@ -34,13 +34,13 @@ pub use fabro_api::types::{ RenderWorkflowGraphDirection, RenderWorkflowGraphRequest, RunArtifactEntry, RunArtifactListResponse, RunBilling, RunBillingStage, RunBillingTotals, RunError, RunManifest, RunStage, RunStatusResponse, SandboxFileEntry, SandboxFileListResponse, - SecretType as ApiSecretType, ServerSettings, SshAccessRequest, SshAccessResponse, + SecretType as ApiSecretType, SshAccessRequest, SshAccessResponse, StageStatus as ApiStageStatus, StartRunRequest, SubmitAnswerRequest, SystemFeatures, SystemInfoResponse, SystemRunCounts, WriteBlobResponse, }; use fabro_auth::parse_credential_secret; use fabro_config::daemon::ServerDaemon; -use fabro_config::{Storage, resolve_server_from_file}; +use fabro_config::{ServerSettings as CurrentServerSettings, Storage}; use fabro_interview::{ Answer, ControlInterviewer, Interviewer, Question, QuestionType, WorkerControlEnvelope, }; @@ -67,9 +67,7 @@ use fabro_types::settings::server::{ GithubIntegrationSettings, GithubIntegrationStrategy, ServerAuthLayer, ServerAuthMethod, ServerLayer, }; -use fabro_types::settings::{ - InterpString, ServerSettings as ResolvedServerSettings, SettingsLayer, -}; +use fabro_types::settings::{InterpString, SettingsLayer}; use fabro_types::{ ActorRef, BlockedReason, EventBody, InterviewQuestionRecord, InterviewQuestionType, RunBlobId, RunClientProvenance, RunControlAction, RunEvent, RunId, RunProvenance, RunServerProvenance, @@ -125,9 +123,7 @@ use crate::run_selector::{ResolveRunError, resolve_run_by_selector}; use crate::server_secrets::{ LlmClientResult, ProviderCredentials, ServerSecrets, auth_issue_message, }; -use crate::{ - demo, diagnostics, run_manifest, security_headers, settings_view, static_files, web_auth, -}; +use crate::{demo, diagnostics, run_manifest, security_headers, static_files, web_auth}; pub(crate) type EnvLookup = Arc Option + Send + Sync>; @@ -577,8 +573,7 @@ pub struct AppState { pub(crate) server_secrets: ServerSecrets, pub(crate) provider_credentials: ProviderCredentials, pub(crate) settings: Arc>, - pub(crate) server_settings: RwLock>, - pub(crate) local_daemon_mode: bool, + pub(crate) server_settings: RwLock>, pub(crate) env_lookup: EnvLookup, http_client: Option, shutting_down: AtomicBool, @@ -595,7 +590,6 @@ pub(crate) struct AppStateConfig { pub(crate) artifact_store: ArtifactStore, pub(crate) vault_path: PathBuf, pub(crate) server_env_path: PathBuf, - pub(crate) local_daemon_mode: bool, pub(crate) env_lookup: EnvLookup, pub(crate) http_client: Option, } @@ -644,7 +638,7 @@ fn accumulate_model_billing(entry: &mut ModelBillingTotals, usage: &BilledModelU } impl AppState { - pub(crate) fn server_settings(&self) -> Arc { + pub(crate) fn server_settings(&self) -> Arc { Arc::clone( &self .server_settings @@ -662,7 +656,7 @@ impl AppState { pub(crate) fn server_storage_dir(&self) -> PathBuf { PathBuf::from( - resolve_interp_string(&self.server_settings().storage.root) + resolve_interp_string(&self.server_settings().server.storage.root) .expect("server storage root should be resolved at startup"), ) } @@ -704,7 +698,7 @@ impl AppState { } pub(crate) fn canonical_origin(&self) -> Result { - resolve_canonical_origin(&self.server_settings(), &self.env_lookup) + resolve_canonical_origin(&self.server_settings().server, &self.env_lookup) } pub(crate) fn session_key(&self) -> Option { @@ -786,17 +780,8 @@ impl AppState { } pub(crate) fn replace_settings(&self, settings: SettingsLayer) -> anyhow::Result<()> { - let resolved = Arc::new(resolve_server_from_file(&settings).map_err(|errors| { - anyhow::anyhow!( - "failed to resolve server settings:\n{}", - errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n") - ) - })?); - resolve_canonical_origin(&resolved, &self.env_lookup).map_err(anyhow::Error::msg)?; + let resolved = Arc::new(CurrentServerSettings::from_layer(&settings)?); + resolve_canonical_origin(&resolved.server, &self.env_lookup).map_err(anyhow::Error::msg)?; *self.settings.write().expect("settings lock poisoned") = settings; *self @@ -1317,53 +1302,12 @@ async fn health() -> Response { async fn get_server_settings( _auth: AuthenticatedService, State(state): State>, - Query(query): Query, ) -> Response { - let settings = state.settings.read().unwrap().clone(); - match query.view { - settings_view::SettingsApiView::Layer => { - let redacted = settings_view::redact_for_api(&settings); - let mut value = match serde_json::to_value(&redacted) { - Ok(value) => value, - Err(err) => { - return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) - .into_response(); - } - }; - strip_nulls(&mut value); - (StatusCode::OK, Json(value)).into_response() - } - settings_view::SettingsApiView::Resolved => { - let resolved = match fabro_config::resolve(&settings) { - Ok(settings) => settings, - Err(err) => { - return ApiError::new( - StatusCode::INTERNAL_SERVER_ERROR, - format!("failed to resolve settings: {err:?}"), - ) - .into_response(); - } - }; - let mut value = match settings_view::redact_resolved_value(&resolved) { - Ok(value) => value, - Err(err) => { - return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) - .into_response(); - } - }; - strip_nulls(&mut value); - let mut response = (StatusCode::OK, Json(value)).into_response(); - response.headers_mut().insert( - settings_view::RESOLVED_VIEW_HEADER_NAME, - HeaderValue::from_static(settings_view::RESOLVED_VIEW_HEADER_VALUE), - ); - response - } - } -} - -fn strip_nulls(value: &mut serde_json::Value) { - settings_view::strip_nulls(value); + ( + StatusCode::OK, + Json(state.server_settings().as_ref().clone()), + ) + .into_response() } async fn get_system_info( @@ -1711,18 +1655,10 @@ fn system_sandbox_provider(settings: &SettingsLayer) -> String { ) } -fn render_resolve_errors(errors: &[fabro_config::ResolveError]) -> String { - errors - .iter() - .map(ToString::to_string) - .collect::>() - .join("; ") -} - fn resolved_storage_dir(settings: &SettingsLayer) -> Result { - let resolved = - resolve_server_from_file(settings).map_err(|errors| render_resolve_errors(&errors))?; + let resolved = CurrentServerSettings::from_layer(settings).map_err(|err| err.to_string())?; resolved + .server .storage .root .resolve(|name| std::env::var(name).ok()) @@ -1730,15 +1666,14 @@ fn resolved_storage_dir(settings: &SettingsLayer) -> Result { .map_err(|err| { format!( "failed to resolve {}: {err}", - resolved.storage.root.as_source() + resolved.server.storage.root.as_source() ) }) } fn resolved_github_settings(settings: &SettingsLayer) -> Result { - let resolved = - resolve_server_from_file(settings).map_err(|errors| render_resolve_errors(&errors))?; - Ok(resolved.integrations.github) + let resolved = CurrentServerSettings::from_layer(settings).map_err(|err| err.to_string())?; + Ok(resolved.server.integrations.github) } fn parse_system_duration(raw: &str) -> anyhow::Result { @@ -1958,7 +1893,7 @@ async fn get_github_repo( return response; } let settings = state.server_settings(); - let github_settings = &settings.integrations.github; + let github_settings = &settings.server.integrations.github; let base_url = fabro_github::github_api_base_url(); let mut client: Option = None; let token = match github_settings.strategy { @@ -2514,7 +2449,6 @@ pub fn create_app_state_with_env_lookup( pub(crate) fn create_test_app_state_with_session_key( settings: SettingsLayer, session_secret: Option<&str>, - local_daemon_mode: bool, ) -> Arc { let vault_path = test_secret_store_path(); let server_env_path = vault_path @@ -2540,7 +2474,6 @@ pub(crate) fn create_test_app_state_with_session_key( artifact_store, vault_path, server_env_path, - local_daemon_mode, env_lookup, http_client: Some(fabro_http::test_http_client().expect("test HTTP client should build")), }) @@ -2576,7 +2509,6 @@ fn default_test_app_state_config( artifact_store, vault_path, server_env_path, - local_daemon_mode: false, env_lookup, http_client: Some(fabro_http::test_http_client().expect("test HTTP client should build")), } @@ -2641,7 +2573,6 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result anyhow::Result>() - .join("\n") - ) - })?) + Arc::new(CurrentServerSettings::from_layer(&settings)?) }; let slack_service = { - resolved_server_settings + current_server_settings + .server .integrations .slack .default_channel @@ -2707,8 +2630,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result req, Err(err) => return ApiError::bad_request(err.to_string()).into_response(), }; - let prepared = match run_manifest::prepare_manifest_with_mode( - &state.settings.read().unwrap(), - &req, - state.local_daemon_mode, - ) { + let prepared = match run_manifest::prepare_manifest(&state.settings.read().unwrap(), &req) { Ok(prepared) => prepared, Err(err) => return ApiError::bad_request(err.to_string()).into_response(), }; @@ -4189,11 +4108,7 @@ async fn run_preflight( State(state): State>, Json(req): Json, ) -> Response { - let prepared = match run_manifest::prepare_manifest_with_mode( - &state.settings.read().unwrap(), - &req, - state.local_daemon_mode, - ) { + let prepared = match run_manifest::prepare_manifest(&state.settings.read().unwrap(), &req) { Ok(prepared) => prepared, Err(err) => return ApiError::bad_request(err.to_string()).into_response(), }; @@ -4219,14 +4134,11 @@ async fn render_graph_from_manifest( State(state): State>, Json(req): Json, ) -> Response { - let prepared = match run_manifest::prepare_manifest_with_mode( - &state.settings.read().unwrap(), - &req.manifest, - state.local_daemon_mode, - ) { - Ok(prepared) => prepared, - Err(err) => return ApiError::bad_request(err.to_string()).into_response(), - }; + let prepared = + match run_manifest::prepare_manifest(&state.settings.read().unwrap(), &req.manifest) { + Ok(prepared) => prepared, + Err(err) => return ApiError::bad_request(err.to_string()).into_response(), + }; let validated = match run_manifest::validate_prepared_manifest(&prepared) { Ok(validated) => validated, Err(err) => return ApiError::bad_request(err.to_string()).into_response(), @@ -5043,16 +4955,7 @@ async fn get_run_settings( let Some(run_spec) = run_state.spec else { return ApiError::not_found("Run not found.").into_response(); }; - let redacted = settings_view::redact_for_api(&run_spec.settings); - let mut value = match serde_json::to_value(&redacted) { - Ok(value) => value, - Err(err) => { - return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) - .into_response(); - } - }; - strip_nulls(&mut value); - (StatusCode::OK, Json(value)).into_response() + (StatusCode::OK, Json(run_spec.settings)).into_response() } async fn get_questions( @@ -7467,34 +7370,6 @@ url = "{url}" .expect("settings fixture should parse") } - #[tokio::test] - async fn resolved_settings_view_returns_internal_error_when_runtime_settings_stop_resolving() { - let state = create_app_state(); - *state.settings.write().unwrap() = fabro_config::parse_settings_layer( - r#" -_version = 1 - -[cli.target] -type = "http" -"#, - ) - .expect("settings fixture should parse"); - let app = build_router(state, AuthMode::Disabled); - - let response = app - .oneshot( - Request::builder() - .method("GET") - .uri(api("/settings?view=resolved")) - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_status!(response, StatusCode::INTERNAL_SERVER_ERROR).await; - } - #[test] fn replace_settings_rejects_invalid_canonical_origin_and_keeps_previous_settings() { for invalid in ["", "/relative/path", "ftp://fabro.example.com"] { @@ -8283,7 +8158,6 @@ slug = "fabro" create_test_app_state_with_session_key( settings, Some("github-redirect-test-key-0123456789"), - false, ), AuthMode::Enabled(ConfiguredAuth { methods: vec![ServerAuthMethod::Github], @@ -8795,7 +8669,6 @@ slug = "fabro" let state = create_test_app_state_with_session_key( SettingsLayer::default(), Some("server-test-session-key-0123456789"), - false, ); let app = build_router( Arc::clone(&state), diff --git a/lib/crates/fabro-server/src/settings_view.rs b/lib/crates/fabro-server/src/settings_view.rs deleted file mode 100644 index fe2640c53..000000000 --- a/lib/crates/fabro-server/src/settings_view.rs +++ /dev/null @@ -1,275 +0,0 @@ -//! Outward-facing view of [`SettingsLayer`] for API responses. -//! -//! `/api/v1/settings` and `/api/v1/runs/:id/settings` return the server's v2 -//! [`SettingsLayer`] directly as JSON so authenticated clients (the `fabro -//! settings` CLI, the web UI) can see the effective configuration. Before -//! serialization, this module drops the handful of fields that would leak -//! host-specific filesystem or network layout. -//! -//! ## What gets dropped -//! -//! Per the requirements doc, only the transport bind needs redaction now: -//! -//! - `server.listen` — the whole subtree. Bind addresses and socket paths -//! reveal network topology and host filesystem layout. -//! -//! ## Why that's all -//! -//! The rest of the v2 tree is either: -//! -//! - A literal non-secret value (storage root, scheduler limit, integration -//! slug, feature flag), OR -//! - An [`InterpString`] containing `{{ env.NAME }}` tokens. `InterpString`'s -//! default serialization preserves the *unresolved* template form, so the -//! wire payload surfaces `"Bearer {{ env.TOKEN }}"` instead of the resolved -//! secret value. No additional redaction pass is needed. -//! -//! Any future field that carries a raw secret in-band (without env -//! interpolation) must be added to the drop list below. - -use fabro_types::settings::{Settings, SettingsLayer}; -use serde::Deserialize; - -pub(crate) const RESOLVED_VIEW_HEADER_NAME: &str = "X-Fabro-Settings-View"; -pub(crate) const RESOLVED_VIEW_HEADER_VALUE: &str = "resolved"; - -const REDACTED_PATHS: &[&[&str]] = &[&["server", "listen"]]; - -#[derive(Debug, Clone, Copy, Default, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "lowercase")] -pub(crate) enum SettingsApiView { - #[default] - Layer, - Resolved, -} - -#[derive(Debug, Clone, Copy, Default, Deserialize)] -pub(crate) struct SettingsQuery { - #[serde(default)] - pub(crate) view: SettingsApiView, -} - -/// Build a redacted clone of `settings` safe to serialize outward. -/// -/// See the module docs for the drop-list rationale. -#[must_use] -pub(crate) fn redact_for_api(settings: &SettingsLayer) -> SettingsLayer { - let mut out = settings.clone(); - - if let Some(server) = out.server.as_mut() { - server.listen = None; - } - - out -} - -pub(crate) fn redact_resolved_value(settings: &Settings) -> serde_json::Result { - let mut value = serde_json::to_value(settings)?; - redact_value_paths(&mut value); - Ok(value) -} - -pub(crate) fn strip_nulls(value: &mut serde_json::Value) { - match value { - serde_json::Value::Object(map) => { - for child in map.values_mut() { - strip_nulls(child); - } - map.retain(|_, child| !child.is_null()); - } - serde_json::Value::Array(values) => { - for child in values { - strip_nulls(child); - } - } - _ => {} - } -} - -fn redact_value_paths(value: &mut serde_json::Value) { - for path in REDACTED_PATHS { - remove_path(value, path); - } -} - -fn remove_path(value: &mut serde_json::Value, path: &[&str]) { - let Some((head, tail)) = path.split_first() else { - return; - }; - - let Some(object) = value.as_object_mut() else { - return; - }; - - if tail.is_empty() { - object.remove(*head); - return; - } - - if let Some(child) = object.get_mut(*head) { - remove_path(child, tail); - } -} - -#[cfg(test)] -mod tests { - use fabro_config::parse_settings_layer; - - use super::*; - - fn parse(source: &str) -> SettingsLayer { - parse_settings_layer(source).expect("fixture should parse") - } - - #[test] - fn drops_server_listen_entirely() { - let settings = parse( - r#" -_version = 1 - -[server.listen] -type = "tcp" -address = "127.0.0.1:32276" -"#, - ); - let redacted = redact_for_api(&settings); - assert!(redacted.server.unwrap().listen.is_none()); - } - - #[test] - fn preserves_run_cli_project_and_features() { - let settings = parse( - r#" -_version = 1 - -[project] -name = "Fabro" - -[run] -goal = "ship it" - -[run.model] -provider = "anthropic" -name = "sonnet" - -[cli.output] -verbosity = "verbose" - -[features] -session_sandboxes = true - -[server.scheduler] -max_concurrent_runs = 9 - -[server.auth] -methods = ["dev-token", "github"] - -[server.auth.github] -allowed_usernames = ["alice"] - -[server.storage] -root = "/srv/fabro" - -[server.integrations.github] -app_id = "12345" -client_id = "Iv1.abcdef" -slug = "fabro-app" -"#, - ); - let redacted = redact_for_api(&settings); - assert!(redacted.project.is_some()); - let run = redacted.run.unwrap(); - assert!(run.goal.is_some()); - assert!(run.model.is_some()); - assert!(redacted.cli.is_some()); - assert!(redacted.features.is_some()); - let server = redacted.server.unwrap(); - assert_eq!( - server.scheduler.and_then(|s| s.max_concurrent_runs), - Some(9) - ); - assert!(server.storage.is_some()); - let github = server.integrations.unwrap().github.unwrap(); - assert!(github.app_id.is_some()); - assert!(github.client_id.is_some()); - assert!(github.slug.is_some()); - let auth = server.auth.unwrap(); - assert_eq!(auth.methods.unwrap().len(), 2); - assert_eq!(auth.github.unwrap().allowed_usernames, vec!["alice"]); - } - - #[test] - fn preserves_env_templates_for_non_redacted_fields() { - let settings = parse( - r#" -_version = 1 - -[server.storage] -root = "{{ env.FABRO_STORAGE_ROOT }}" - -[server.integrations.slack] -default_channel = "{{ env.SLACK_CHANNEL }}" -"#, - ); - - let redacted = redact_for_api(&settings); - let server = redacted - .server - .expect("server config should remain present"); - assert_eq!( - server - .storage - .and_then(|storage| storage.root) - .map(|value| value.as_source()), - Some("{{ env.FABRO_STORAGE_ROOT }}".to_string()) - ); - assert_eq!( - server - .integrations - .and_then(|integrations| integrations.slack) - .and_then(|slack| slack.default_channel) - .map(|value| value.as_source()), - Some("{{ env.SLACK_CHANNEL }}".to_string()) - ); - } - - #[test] - fn redacts_dense_resolved_settings_with_the_same_secret_paths() { - let settings = parse( - r#" -_version = 1 - -[server.listen] -type = "tcp" -address = "127.0.0.1:32276" - -[server.auth] -methods = ["github", "dev-token"] - -[server.auth.github] -allowed_usernames = ["alice"] - -[server.storage] -root = "{{ env.FABRO_STORAGE_ROOT }}" -"#, - ); - - let resolved = fabro_config::resolve(&settings).expect("settings should resolve"); - let mut redacted = - redact_resolved_value(&resolved).expect("resolved settings should serialize"); - - assert!(redacted["server"].get("listen").is_none()); - assert_eq!(redacted["server"]["auth"]["methods"][0], "github"); - assert_eq!( - redacted["server"]["auth"]["github"]["allowed_usernames"][0], - "alice" - ); - assert_eq!( - redacted["server"]["storage"]["root"], - "{{ env.FABRO_STORAGE_ROOT }}" - ); - - strip_nulls(&mut redacted); - assert!(redacted["server"].get("listen").is_none()); - } -} diff --git a/lib/crates/fabro-server/src/web_auth.rs b/lib/crates/fabro-server/src/web_auth.rs index d061915d8..410e29bd0 100644 --- a/lib/crates/fabro-server/src/web_auth.rs +++ b/lib/crates/fabro-server/src/web_auth.rs @@ -290,6 +290,7 @@ fn session_provider(auth_method: RunAuthMethod) -> &'static str { fn session_cookie_secure(state: &AppState) -> bool { state .server_settings() + .server .web .url .resolve(|name| std::env::var(name).ok()) @@ -374,7 +375,7 @@ async fn login_github( ); }; let settings = state.server_settings(); - let Some(client_id) = settings.integrations.github.client_id.as_ref() else { + let Some(client_id) = settings.server.integrations.github.client_id.as_ref() else { warn!("OAuth login failed: client_id not configured"); return json_response( StatusCode::CONFLICT, @@ -516,7 +517,7 @@ async fn callback_github( .as_deref() .expect("validated oauth callback state should exist"); - let Some(client_id) = settings.integrations.github.client_id.as_ref() else { + let Some(client_id) = settings.server.integrations.github.client_id.as_ref() else { error!("OAuth callback failed: client_id not configured"); return json_response( StatusCode::CONFLICT, @@ -686,7 +687,7 @@ async fn callback_github( _ => Vec::new(), }; - let allowed_usernames = settings.auth.github.allowed_usernames.clone(); + let allowed_usernames = settings.server.auth.github.allowed_usernames.clone(); if !allowed_usernames.iter().any(|user| user == &profile.login) { warn!(login = %profile.login, "OAuth callback denied: username not in allowlist"); return callback_error_redirect( @@ -909,7 +910,6 @@ mod tests { let state = server::create_test_app_state_with_session_key( settings, Some("web-auth-test-key-material-0123456789"), - false, ); let middleware_state = state.clone(); axum::Router::new() @@ -1105,7 +1105,6 @@ mod tests { let state = server::create_test_app_state_with_session_key( github_settings("https://fabro.example"), Some("web-auth-test-key-material-0123456789"), - false, ); let app = server::build_router_with_options( state, @@ -1198,7 +1197,6 @@ mod tests { let state = server::create_test_app_state_with_session_key( github_settings("https://fabro.example"), Some("web-auth-test-key-material-0123456789"), - false, ); let app = crate::server::build_router_with_options( state, diff --git a/lib/crates/fabro-server/tests/it/api/runs.rs b/lib/crates/fabro-server/tests/it/api/runs.rs index 061b87e80..758c69d07 100644 --- a/lib/crates/fabro-server/tests/it/api/runs.rs +++ b/lib/crates/fabro-server/tests/it/api/runs.rs @@ -3,7 +3,6 @@ use axum::http::{Request, StatusCode}; use fabro_config::parse_settings_layer; use fabro_server::jwt_auth::AuthMode; use fabro_server::server::build_router; -use serde_json::json; use tower::ServiceExt; use crate::helpers::{ @@ -11,7 +10,7 @@ use crate::helpers::{ }; #[tokio::test] -async fn retrieve_run_settings_preserves_templates_and_redacts_sensitive_fields() { +async fn retrieve_run_settings_returns_persisted_layer_without_redaction() { let storage_dir = tempfile::tempdir().unwrap(); let settings = parse_settings_layer(&format!( r#" @@ -97,21 +96,11 @@ session_sandboxes = true storage_dir.path().display().to_string() ); assert_eq!(body["server"]["scheduler"]["max_concurrent_runs"], 9); - assert_eq!( - body["server"]["integrations"]["github"]["app_id"], - "{{ env.GITHUB_APP_ID }}" - ); - assert_eq!( - body["server"]["integrations"]["github"]["client_id"], - "Iv1.github" - ); - assert_eq!( - body["server"]["auth"]["methods"], - json!(["dev-token", "github"]) - ); - assert_eq!( - body["server"]["auth"]["github"]["allowed_usernames"], - json!(["alice"]) + assert!(body.pointer("/server/integrations/github/app_id").is_none()); + assert!( + body.pointer("/server/integrations/github/client_id") + .is_none() ); + assert!(body.pointer("/server/auth").is_none()); assert!(body.pointer("/server/listen").is_none()); } diff --git a/lib/crates/fabro-server/tests/it/api/settings.rs b/lib/crates/fabro-server/tests/it/api/settings.rs index 5c892313f..1495b43da 100644 --- a/lib/crates/fabro-server/tests/it/api/settings.rs +++ b/lib/crates/fabro-server/tests/it/api/settings.rs @@ -4,13 +4,12 @@ use fabro_config::parse_settings_layer; use fabro_server::jwt_auth::AuthMode; use fabro_server::server::{build_router, create_app_state_with_options}; use fabro_types::settings::SettingsLayer; -use serde_json::json; use tower::ServiceExt; -use crate::helpers::{body_json, checked_response, response_json}; +use crate::helpers::response_json; #[tokio::test] -async fn retrieve_server_settings_default_view_returns_redacted_layer_settings() { +async fn retrieve_server_settings_returns_dense_server_settings_from_app_state() { let settings: SettingsLayer = parse_settings_layer( r#" _version = 1 @@ -25,9 +24,6 @@ root = "/srv/fabro" [server.scheduler] max_concurrent_runs = 9 -[cli.output] -verbosity = "verbose" - [server.auth] methods = ["dev-token", "github"] @@ -36,9 +32,6 @@ allowed_usernames = ["alice"] [server.integrations.github] client_id = "Iv1.abcdef" - -[run.inputs] -server_only = "1" "#, ) .expect("settings fixture should parse"); @@ -55,102 +48,28 @@ server_only = "1" let response = app.oneshot(request).await.unwrap(); let body = response_json(response, StatusCode::OK, "GET /api/v1/settings").await; - assert_eq!(body["_version"], 1); + let top_level = body + .as_object() + .expect("server settings response should be an object"); + assert_eq!(top_level.len(), 2); + assert!(top_level.contains_key("server")); + assert!(top_level.contains_key("features")); + + assert_eq!(body["server"]["listen"]["type"], "tcp"); + assert_eq!(body["server"]["listen"]["address"], "127.0.0.1:32276"); assert_eq!(body["server"]["storage"]["root"], "/srv/fabro"); assert_eq!(body["server"]["scheduler"]["max_concurrent_runs"], 9); - assert_eq!(body["cli"]["output"]["verbosity"], "verbose"); - assert_eq!(body["run"]["inputs"]["server_only"], "1"); - assert!(body["server"].get("listen").is_none()); + assert_eq!(body["server"]["auth"]["methods"][0], "dev-token"); + assert_eq!(body["server"]["auth"]["methods"][1], "github"); assert_eq!( - body["server"]["auth"]["methods"], - json!(["dev-token", "github"]) - ); - assert_eq!( - body["server"]["auth"]["github"]["allowed_usernames"], - json!(["alice"]) - ); - assert_eq!( - body["server"]["integrations"]["github"]["client_id"], - "Iv1.abcdef" - ); -} - -#[tokio::test] -async fn retrieve_server_settings_resolved_view_returns_dense_settings_and_marker() { - let settings: SettingsLayer = parse_settings_layer( - r#" -_version = 1 - -[server.listen] -type = "tcp" -address = "127.0.0.1:32276" - -[server.storage] -root = "/srv/fabro" - -[server.auth] -methods = ["dev-token", "github"] - -[server.auth.github] -allowed_usernames = ["alice"] - -[server.integrations.github] -client_id = "Iv1.abcdef" - -[run.model] -provider = "openai" -name = "server-model" - -[run.inputs] -server_only = "1" -"#, - ) - .expect("settings fixture should parse"); - let app = build_router( - create_app_state_with_options(settings, 5), - AuthMode::Disabled, - ); - - let request = Request::builder() - .method("GET") - .uri("/api/v1/settings?view=resolved") - .body(Body::empty()) - .unwrap(); - let response = app.oneshot(request).await.unwrap(); - - let response = checked_response( - response, - StatusCode::OK, - "GET /api/v1/settings?view=resolved", - ) - .await; - assert_eq!( - response - .headers() - .get("x-fabro-settings-view") - .and_then(|value| value.to_str().ok()), - Some("resolved") - ); - let body = body_json(response.into_body()).await; - assert!(body.get("_version").is_none()); - assert_eq!(body["project"]["directory"], "."); - assert_eq!(body["workflow"]["graph"], "workflow.fabro"); - assert_eq!(body["run"]["execution"]["approval"], "prompt"); - assert_eq!(body["run"]["model"]["provider"], "openai"); - assert_eq!(body["run"]["model"]["name"], "server-model"); - assert_eq!(body["run"]["inputs"]["server_only"], "1"); - assert_eq!(body["server"]["storage"]["root"], "/srv/fabro"); - assert!(body["server"].get("listen").is_none()); - assert_eq!( - body["server"]["auth"]["methods"], - json!(["dev-token", "github"]) - ); - assert_eq!( - body["server"]["auth"]["github"]["allowed_usernames"], - json!(["alice"]) + body["server"]["auth"]["github"]["allowed_usernames"][0], + "alice" ); assert_eq!( body["server"]["integrations"]["github"]["client_id"], "Iv1.abcdef" ); + assert_eq!(body["features"]["session_sandboxes"], false); + assert!(body.get("cli").is_none()); + assert!(body.get("run").is_none()); } diff --git a/lib/crates/fabro-types/src/settings/cli.rs b/lib/crates/fabro-types/src/settings/cli.rs index def190005..fb38d1bef 100644 --- a/lib/crates/fabro-types/src/settings/cli.rs +++ b/lib/crates/fabro-types/src/settings/cli.rs @@ -14,7 +14,7 @@ use super::run::{AgentPermissions, McpEntryLayer, McpServerSettings}; /// A structurally resolved `[cli]` view for consumers. #[derive(Debug, Clone, Default, PartialEq, Serialize)] -pub struct CliSettings { +pub struct CliNamespace { pub target: Option, pub auth: CliAuthSettings, pub exec: CliExecSettings, diff --git a/lib/crates/fabro-types/src/settings/features.rs b/lib/crates/fabro-types/src/settings/features.rs index 2ed227d00..79dfc9f59 100644 --- a/lib/crates/fabro-types/src/settings/features.rs +++ b/lib/crates/fabro-types/src/settings/features.rs @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; /// A structurally resolved `[features]` view for consumers. -#[derive(Debug, Clone, Default, PartialEq, Serialize)] -pub struct FeaturesSettings { +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct FeaturesNamespace { pub session_sandboxes: bool, } diff --git a/lib/crates/fabro-types/src/settings/mod.rs b/lib/crates/fabro-types/src/settings/mod.rs index e2851b37d..6b6638089 100644 --- a/lib/crates/fabro-types/src/settings/mod.rs +++ b/lib/crates/fabro-types/src/settings/mod.rs @@ -16,7 +16,6 @@ pub mod interp; pub mod layer; pub mod model_ref; pub mod project; -pub mod resolved; pub mod run; pub mod server; pub mod size; @@ -25,24 +24,23 @@ pub mod workflow; pub use cli::{ CliAuthSettings, CliExecAgentSettings, CliExecModelSettings, CliExecSettings, CliLayer, - CliLoggingSettings, CliOutputSettings, CliSettings, CliTargetSettings, CliUpdatesSettings, + CliLoggingSettings, CliNamespace, CliOutputSettings, CliTargetSettings, CliUpdatesSettings, }; pub use duration::{Duration, ParseDurationError}; -pub use features::{FeaturesLayer, FeaturesSettings}; +pub use features::{FeaturesLayer, FeaturesNamespace}; pub use interp::{InterpString, Provenance, ResolveEnvError, Resolved}; pub use layer::SettingsLayer; pub use model_ref::{ AmbiguousModelRef, ModelRef, ModelRegistry, ParseModelRefError, ResolvedModelRef, }; -pub use project::{ProjectLayer, ProjectSettings}; -pub use resolved::Settings; +pub use project::{ProjectLayer, ProjectNamespace}; pub use run::{ ArtifactsSettings, DaytonaSettings, DaytonaSnapshotSettings, DockerfileSource, GitAuthorSettings, HookDefinition, HookType, InterviewProviderSettings, McpServerSettings, McpTransport, NotificationProviderSettings, NotificationRouteSettings, PullRequestSettings, RunAgentSettings, RunCheckpointSettings, RunExecutionSettings, RunGitSettings, RunGoal, - RunInterviewsSettings, RunLayer, RunModelSettings, RunPrepareSettings, RunSandboxSettings, - RunScmSettings, RunSettings, ScmGitHubSettings, TlsMode, + RunInterviewsSettings, RunLayer, RunModelSettings, RunNamespace, RunPrepareSettings, + RunSandboxSettings, RunScmSettings, ScmGitHubSettings, TlsMode, }; pub use server::{ DiscordIntegrationSettings, GithubIntegrationSettings, IntegrationWebhooksSettings, @@ -50,9 +48,9 @@ pub use server::{ ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings, ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings, ServerLayer, ServerListenSettings, ServerLoggingSettings, - ServerSchedulerSettings, ServerSettings, ServerSlateDbSettings, ServerStorageSettings, + ServerNamespace, ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings, }; pub use size::{ParseSizeError, Size}; pub use splice_array::{SPLICE_MARKER, SpliceArray, SpliceArrayError}; -pub use workflow::{WorkflowLayer, WorkflowSettings}; +pub use workflow::{WorkflowLayer, WorkflowNamespace}; diff --git a/lib/crates/fabro-types/src/settings/project.rs b/lib/crates/fabro-types/src/settings/project.rs index dcdd529bb..e62757aed 100644 --- a/lib/crates/fabro-types/src/settings/project.rs +++ b/lib/crates/fabro-types/src/settings/project.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; /// A structurally resolved `[project]` view for consumers. #[derive(Debug, Clone, Default, PartialEq, Serialize)] -pub struct ProjectSettings { +pub struct ProjectNamespace { pub name: Option, pub description: Option, pub directory: String, diff --git a/lib/crates/fabro-types/src/settings/resolved.rs b/lib/crates/fabro-types/src/settings/resolved.rs deleted file mode 100644 index df7be72f9..000000000 --- a/lib/crates/fabro-types/src/settings/resolved.rs +++ /dev/null @@ -1,179 +0,0 @@ -use serde::Serialize; - -use super::{ - CliSettings, FeaturesSettings, ProjectSettings, RunSettings, ServerSettings, WorkflowSettings, -}; - -/// A fully resolved settings view across all namespaces. -/// -/// `Default` is intentionally not derived: a default `Settings` value would -/// contain empty `server.auth.methods`, which the resolver rejects. Construct -/// real values via `fabro_config::resolve` (production), or -/// `Settings::test_default()` behind the `test-support` feature (tests). -#[derive(Debug, Clone, PartialEq, Serialize)] -pub struct Settings { - pub project: ProjectSettings, - pub workflow: WorkflowSettings, - pub run: RunSettings, - pub cli: CliSettings, - pub server: ServerSettings, - pub features: FeaturesSettings, -} - -#[cfg(any(test, feature = "test-support"))] -impl Settings { - /// A trivial `Settings` value suitable for serialization or destructuring - /// tests. Server auth methods are empty (would not pass `resolve`); - /// use this only when the resolver is not in play. - #[must_use] - pub fn test_default() -> Self { - Self { - project: ProjectSettings::default(), - workflow: WorkflowSettings::default(), - run: RunSettings::default(), - cli: CliSettings::default(), - server: ServerSettings::test_default(), - features: FeaturesSettings::default(), - } - } -} - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - use std::time::Duration as StdDuration; - - use serde_json::json; - - use super::Settings; - use crate::settings::cli::CliTargetSettings; - use crate::settings::interp::InterpString; - use crate::settings::run::{ - DockerfileSource, McpServerSettings, McpTransport, RunAgentSettings, RunGoal, RunSettings, - }; - use crate::settings::server::{ - ObjectStoreSettings, ServerListenSettings, ServerSettings, ServerSlateDbSettings, - }; - - #[test] - fn settings_serializes_successfully() { - serde_json::to_value(Settings::test_default()).expect("resolved settings should serialize"); - } - - #[test] - fn resolved_enums_use_human_readable_tagged_shapes() { - assert_eq!( - serde_json::to_value(CliTargetSettings::Http { - url: InterpString::parse("https://api.example.com"), - }) - .unwrap(), - json!({ - "type": "http", - "url": "https://api.example.com", - }) - ); - - assert_eq!( - serde_json::to_value(RunGoal::Inline(InterpString::parse("ship it"))).unwrap(), - json!({ - "type": "inline", - "value": "ship it" - }) - ); - - assert_eq!( - serde_json::to_value(McpTransport::Sandbox { - command: vec!["fabro-mcp".to_string(), "--serve".to_string()], - port: 3333, - env: HashMap::from([("TOKEN".to_string(), "{{ env.MCP_TOKEN }}".to_string())]), - }) - .unwrap(), - json!({ - "type": "sandbox", - "command": ["fabro-mcp", "--serve"], - "port": 3333, - "env": { - "TOKEN": "{{ env.MCP_TOKEN }}" - } - }) - ); - - assert_eq!( - serde_json::to_value(DockerfileSource::Path { - path: "Dockerfile".to_string(), - }) - .unwrap(), - json!({ - "type": "path", - "path": "Dockerfile" - }) - ); - - assert_eq!( - serde_json::to_value(ObjectStoreSettings::S3 { - bucket: InterpString::parse("fabro-artifacts"), - region: InterpString::parse("us-east-1"), - endpoint: Some(InterpString::parse("https://s3.example.com")), - path_style: true, - }) - .unwrap(), - json!({ - "type": "s3", - "bucket": "fabro-artifacts", - "region": "us-east-1", - "endpoint": "https://s3.example.com", - "path_style": true - }) - ); - } - - #[test] - fn socket_addrs_and_std_durations_use_settings_strings() { - assert_eq!( - serde_json::to_value(ServerListenSettings::Tcp { - address: "127.0.0.1:8080".parse().unwrap(), - }) - .unwrap(), - json!({ - "type": "tcp", - "address": "127.0.0.1:8080" - }) - ); - - let settings = Settings { - server: ServerSettings { - slatedb: ServerSlateDbSettings { - prefix: InterpString::parse("slatedb/"), - store: ObjectStoreSettings::Local { - root: InterpString::parse("/srv/slatedb"), - }, - flush_interval: StdDuration::from_secs(30), - disk_cache: false, - }, - ..ServerSettings::test_default() - }, - run: RunSettings { - agent: RunAgentSettings { - mcps: HashMap::from([("sandboxed".to_string(), McpServerSettings { - name: "sandboxed".to_string(), - transport: McpTransport::Http { - url: "https://mcp.example.com".to_string(), - headers: HashMap::from([( - "Authorization".to_string(), - "Bearer {{ env.MCP_TOKEN }}".to_string(), - )]), - }, - startup_timeout_secs: 15, - tool_timeout_secs: 90, - })]), - ..RunAgentSettings::default() - }, - ..RunSettings::default() - }, - ..Settings::test_default() - }; - - let value = serde_json::to_value(settings).unwrap(); - assert_eq!(value["server"]["slatedb"]["flush_interval"], "30s"); - } -} diff --git a/lib/crates/fabro-types/src/settings/run.rs b/lib/crates/fabro-types/src/settings/run.rs index c5c45c13c..55c2cf0f7 100644 --- a/lib/crates/fabro-types/src/settings/run.rs +++ b/lib/crates/fabro-types/src/settings/run.rs @@ -18,7 +18,7 @@ use super::model_ref::ModelRef; /// A structurally resolved `[run]` view for consumers. #[derive(Debug, Clone, Default, PartialEq, Serialize)] -pub struct RunSettings { +pub struct RunNamespace { pub goal: Option, pub working_dir: Option, pub metadata: HashMap, diff --git a/lib/crates/fabro-types/src/settings/server.rs b/lib/crates/fabro-types/src/settings/server.rs index 659733dea..ef10b7b3b 100644 --- a/lib/crates/fabro-types/src/settings/server.rs +++ b/lib/crates/fabro-types/src/settings/server.rs @@ -10,20 +10,20 @@ use std::net::SocketAddr; use std::time::Duration as StdDuration; use ipnet::IpNet; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::duration::Duration as DurationLayer; use super::interp::InterpString; /// A structurally resolved `[server]` view for consumers. /// -/// `Default` is intentionally not derived: any "default" `ServerSettings` +/// `Default` is intentionally not derived: any "default" `ServerNamespace` /// would have empty `auth.methods`, which the resolver rejects. Construct /// real values via `fabro_config::resolve_server` (production), or -/// `ServerSettings::test_default()` behind the `test-support` feature +/// `ServerNamespace::test_default()` behind the `test-support` feature /// (tests). -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub struct ServerSettings { +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ServerNamespace { pub listen: ServerListenSettings, pub api: ServerApiSettings, pub web: ServerWebSettings, @@ -38,8 +38,8 @@ pub struct ServerSettings { } #[cfg(any(test, feature = "test-support"))] -impl ServerSettings { - /// A trivial `ServerSettings` value suitable for serialization or +impl ServerNamespace { + /// A trivial `ServerNamespace` value suitable for serialization or /// destructuring tests. Auth methods are empty (would not pass /// `resolve_server`); use this only when the resolver is not in play. #[must_use] @@ -60,11 +60,14 @@ impl ServerSettings { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] pub enum ServerListenSettings { Tcp { - #[serde(serialize_with = "serialize_socket_addr")] + #[serde( + serialize_with = "serialize_socket_addr", + deserialize_with = "deserialize_socket_addr" + )] address: SocketAddr, }, Unix { @@ -80,12 +83,12 @@ impl Default for ServerListenSettings { } } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerApiSettings { pub url: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerWebSettings { pub enabled: bool, pub url: InterpString, @@ -100,7 +103,7 @@ impl Default for ServerWebSettings { } } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerAuthSettings { pub methods: Vec, pub github: ServerAuthGithubSettings, @@ -113,24 +116,24 @@ pub enum ServerAuthMethod { Github, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerAuthGithubSettings { pub allowed_usernames: Vec, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerIpAllowlistSettings { pub entries: Vec, pub trusted_proxy_count: u32, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerIpAllowlistOverrideSettings { pub entries: Option>, pub trusted_proxy_count: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum IpAllowEntry { Literal(IpNet), GitHubMetaHooks, @@ -148,7 +151,7 @@ impl IpAllowEntry { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerStorageSettings { pub root: InterpString, } @@ -161,7 +164,7 @@ impl Default for ServerStorageSettings { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerArtifactsSettings { pub prefix: InterpString, pub store: ObjectStoreSettings, @@ -176,11 +179,14 @@ impl Default for ServerArtifactsSettings { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerSlateDbSettings { pub prefix: InterpString, pub store: ObjectStoreSettings, - #[serde(serialize_with = "serialize_std_duration")] + #[serde( + serialize_with = "serialize_std_duration", + deserialize_with = "deserialize_std_duration" + )] pub flush_interval: StdDuration, pub disk_cache: bool, } @@ -196,7 +202,7 @@ impl Default for ServerSlateDbSettings { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ObjectStoreSettings { Local { @@ -218,17 +224,17 @@ impl Default for ObjectStoreSettings { } } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerSchedulerSettings { pub max_concurrent_runs: usize, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerLoggingSettings { pub level: Option, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerIntegrationsSettings { pub github: GithubIntegrationSettings, pub slack: SlackIntegrationSettings, @@ -236,7 +242,7 @@ pub struct ServerIntegrationsSettings { pub teams: TeamsIntegrationSettings, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct GithubIntegrationSettings { pub enabled: bool, pub strategy: GithubIntegrationStrategy, @@ -247,23 +253,23 @@ pub struct GithubIntegrationSettings { pub webhooks: Option, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct SlackIntegrationSettings { pub enabled: bool, pub default_channel: Option, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct DiscordIntegrationSettings { pub enabled: bool, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct TeamsIntegrationSettings { pub enabled: bool, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct IntegrationWebhooksSettings { pub strategy: Option, pub ip_allowlist: Option, @@ -276,6 +282,14 @@ where serializer.serialize_str(&value.to_string()) } +fn deserialize_socket_addr<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let value = String::deserialize(deserializer)?; + value.parse().map_err(serde::de::Error::custom) +} + fn serialize_std_duration(value: &StdDuration, serializer: S) -> Result where S: Serializer, @@ -283,6 +297,13 @@ where serializer.serialize_str(&DurationLayer::from_std(*value).to_string()) } +fn deserialize_std_duration<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + Ok(DurationLayer::deserialize(deserializer)?.as_std()) +} + /// A sparse `[server]` layer as it appears in a single settings file. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] diff --git a/lib/crates/fabro-types/src/settings/workflow.rs b/lib/crates/fabro-types/src/settings/workflow.rs index d97c7e74e..1568cc5c2 100644 --- a/lib/crates/fabro-types/src/settings/workflow.rs +++ b/lib/crates/fabro-types/src/settings/workflow.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; /// A structurally resolved `[workflow]` view for consumers. #[derive(Debug, Clone, Default, PartialEq, Serialize)] -pub struct WorkflowSettings { +pub struct WorkflowNamespace { pub name: Option, pub description: Option, pub graph: String, diff --git a/lib/crates/fabro-workflow/src/operations/create.rs b/lib/crates/fabro-workflow/src/operations/create.rs index 36a995ed7..612e03cf2 100644 --- a/lib/crates/fabro-workflow/src/operations/create.rs +++ b/lib/crates/fabro-workflow/src/operations/create.rs @@ -16,7 +16,9 @@ use fabro_sandbox::daytona::detect_repo_info; use fabro_store::Database; use fabro_template::{TemplateContext, render as render_template}; use fabro_types::settings::run::RunMode; -use fabro_types::settings::{Settings, SettingsLayer}; +use fabro_types::settings::{ + InterpString, ProjectNamespace, RunNamespace, SettingsLayer, WorkflowNamespace, +}; use fabro_types::{RunId, RunProvenance}; use fabro_util::json::normalize_json_value; use tokio::task::spawn_blocking; @@ -58,6 +60,13 @@ pub struct CreatedRun { pub dot_path: Option, } +struct ResolvedSettingsTree { + server_storage_root: InterpString, + project: ProjectNamespace, + workflow: WorkflowNamespace, + run: RunNamespace, +} + struct PersistCreateOptions { settings: SettingsLayer, run_id: Option, @@ -107,14 +116,12 @@ pub async fn create(store: &Database, request: CreateRunInput) -> Result String { .join("; ") } -fn resolve_settings_tree(settings: &SettingsLayer) -> Result { - fabro_config::resolve(settings) - .map_err(|errors| Error::Precondition(render_resolve_errors(&errors))) +fn resolve_settings_tree(settings: &SettingsLayer) -> Result { + Ok(ResolvedSettingsTree { + server_storage_root: fabro_config::resolve_storage_root(settings), + project: fabro_config::resolve_project_from_file(settings) + .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?, + workflow: fabro_config::resolve_workflow_from_file(settings) + .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?, + run: fabro_config::resolve_run_from_file(settings) + .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?, + }) } -fn combined_labels(settings: &Settings) -> HashMap { +fn combined_labels(settings: &ResolvedSettingsTree) -> HashMap { let mut labels = settings.project.metadata.clone(); labels.extend(settings.workflow.metadata.clone()); labels.extend(settings.run.metadata.clone()); diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs index cae00d490..87c3b279f 100644 --- a/lib/crates/fabro-workflow/src/operations/start.rs +++ b/lib/crates/fabro-workflow/src/operations/start.rs @@ -24,7 +24,7 @@ use fabro_types::settings::run::{ HookEvent as ResolvedHookEvent, HookType as ResolvedHookType, McpServerSettings as ResolvedMcpServerSettings, McpTransport as ResolvedMcpTransport, PullRequestSettings, RunMode, RunModelSettings as ResolvedRunModelSettings, - RunSettings as ResolvedRunSettings, TlsMode as ResolvedTlsMode, + RunNamespace as ResolvedRunSettings, TlsMode as ResolvedTlsMode, }; use fabro_vault::Vault; use tokio::runtime::Handle; @@ -381,18 +381,24 @@ impl RunSession { .iter() .map(|(k, v)| (k.clone(), resolve_interp(v))) .collect(); - let resolved_server = fabro_config::resolve_server_from_file(settings) - .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?; - let github_permissions: Option> = - (!resolved_server.integrations.github.permissions.is_empty()).then(|| { - resolved_server - .integrations - .github - .permissions - .iter() - .map(|(k, v)| (k.clone(), resolve_interp(v))) - .collect() - }); + let resolved_server = fabro_config::ServerSettings::from_layer(settings) + .map_err(|err| Error::Precondition(err.to_string()))?; + let github_permissions: Option> = (!resolved_server + .server + .integrations + .github + .permissions + .is_empty()) + .then(|| { + resolved_server + .server + .integrations + .github + .permissions + .iter() + .map(|(k, v)| (k.clone(), resolve_interp(v))) + .collect() + }); let sandbox_env = SandboxEnvSpec { devcontainer_env: HashMap::new(), toml_env, diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 2f458c063..9ccdc9d3c 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -54,6 +54,7 @@ models/diagnostics-report.ts models/diagnostics-section.ts models/diff-file.ts models/diff-stats.ts +models/discord-integration-settings.ts models/disk-usage-response.ts models/disk-usage-run-row.ts models/disk-usage-summary-row.ts @@ -64,8 +65,12 @@ models/event-seq.ts models/execute-query-request.ts models/execute-query-response-rows-inner-inner.ts models/execute-query-response.ts +models/features-namespace.ts models/file-checkpoint.ts models/file-diff.ts +models/git-hub-meta-hooks-entry.ts +models/github-integration-settings.ts +models/github-integration-strategy.ts models/health-response.ts models/history-entry.ts models/index.ts @@ -86,7 +91,10 @@ models/install-llm-validation-response.ts models/install-prefill.ts models/install-server-config-input.ts models/install-session-response.ts +models/integration-webhooks-settings.ts models/internal-stage-status.ts +models/ip-allow-entry.ts +models/literal-ip-allow-entry.ts models/manifest-args.ts models/manifest-config.ts models/manifest-file-entry.ts @@ -105,6 +113,9 @@ models/model-test-result.ts models/model.ts models/node-state.ts models/node-status-record.ts +models/object-store-local-settings.ts +models/object-store-s3-settings.ts +models/object-store-settings.ts models/paginated-api-question-list.ts models/paginated-board-run-list.ts models/paginated-event-list.ts @@ -168,6 +179,25 @@ models/saved-query.ts models/secret-list-response.ts models/secret-metadata.ts models/secret-type.ts +models/server-api-settings.ts +models/server-artifacts-settings.ts +models/server-auth-github-settings.ts +models/server-auth-method.ts +models/server-auth-settings.ts +models/server-integrations-settings.ts +models/server-ip-allowlist-override-settings.ts +models/server-ip-allowlist-settings.ts +models/server-listen-settings.ts +models/server-listen-tcp-settings.ts +models/server-listen-unix-settings.ts +models/server-logging-settings.ts +models/server-namespace.ts +models/server-scheduler-settings.ts +models/server-settings.ts +models/server-slate-db-settings.ts +models/server-storage-settings.ts +models/server-web-settings.ts +models/slack-integration-settings.ts models/ssh-access-request.ts models/ssh-access-response.ts models/stage-status.ts @@ -180,9 +210,11 @@ models/system-features.ts models/system-info-response.ts models/system-run-counts.ts models/system-stage-turn.ts +models/teams-integration-settings.ts models/tool-stage-turn.ts models/tool-use.ts models/user-response.ts +models/webhook-strategy.ts models/workflow-diagnostic.ts models/workflow-reference.ts models/write-blob-response.ts diff --git a/lib/packages/fabro-api-client/src/api/run-internals-api.ts b/lib/packages/fabro-api-client/src/api/run-internals-api.ts index 03a7ceb0d..9b4c6eb3c 100644 --- a/lib/packages/fabro-api-client/src/api/run-internals-api.ts +++ b/lib/packages/fabro-api-client/src/api/run-internals-api.ts @@ -608,7 +608,7 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config }; }, /** - * Returns the structured settings used to launch this run. + * Returns the persisted `SettingsLayer` used to launch this run. * @summary Retrieve Run Settings * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -874,7 +874,7 @@ export const RunInternalsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Returns the structured settings used to launch this run. + * Returns the persisted `SettingsLayer` used to launch this run. * @summary Retrieve Run Settings * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1046,7 +1046,7 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b return localVarFp.retrieveRunCheckpoint(id, options).then((request) => request(axios, basePath)); }, /** - * Returns the structured settings used to launch this run. + * Returns the persisted `SettingsLayer` used to launch this run. * @summary Retrieve Run Settings * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1222,7 +1222,7 @@ export class RunInternalsApi extends BaseAPI { } /** - * Returns the structured settings used to launch this run. + * Returns the persisted `SettingsLayer` used to launch this run. * @summary Retrieve Run Settings * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. diff --git a/lib/packages/fabro-api-client/src/api/settings-api.ts b/lib/packages/fabro-api-client/src/api/settings-api.ts index 4310d6796..d5a78b60c 100644 --- a/lib/packages/fabro-api-client/src/api/settings-api.ts +++ b/lib/packages/fabro-api-client/src/api/settings-api.ts @@ -21,19 +21,20 @@ import globalAxios from 'axios'; import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction, replaceWithSerializableTypeIfNeeded } from '../common'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; +// @ts-ignore +import type { ServerSettings } from '../models'; /** * SettingsApi - axios parameter creator */ export const SettingsApiAxiosParamCreator = function (configuration?: Configuration) { return { /** - * Returns the server settings view selected by the optional `view` query parameter. `view=layer` (the default) returns the current sparse redacted `SettingsLayer` payload. `view=resolved` returns the server\'s dense resolved settings payload after applying the same redaction policy. + * Returns the server\'s current in-memory settings view as the typed `ServerSettings` payload. * @summary Retrieve Server Settings - * @param {RetrieveServerSettingsViewEnum} [view] Selects the server settings representation to return. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - retrieveServerSettings: async (view?: RetrieveServerSettingsViewEnum, options: RawAxiosRequestConfig = {}): Promise => { + retrieveServerSettings: async (options: RawAxiosRequestConfig = {}): Promise => { const localVarPath = `/api/v1/settings`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -52,10 +53,6 @@ export const SettingsApiAxiosParamCreator = function (configuration?: Configurat // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) - if (view !== undefined) { - localVarQueryParameter['view'] = view; - } - localVarHeaderParameter['Accept'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -77,14 +74,13 @@ export const SettingsApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = SettingsApiAxiosParamCreator(configuration) return { /** - * Returns the server settings view selected by the optional `view` query parameter. `view=layer` (the default) returns the current sparse redacted `SettingsLayer` payload. `view=resolved` returns the server\'s dense resolved settings payload after applying the same redaction policy. + * Returns the server\'s current in-memory settings view as the typed `ServerSettings` payload. * @summary Retrieve Server Settings - * @param {RetrieveServerSettingsViewEnum} [view] Selects the server settings representation to return. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async retrieveServerSettings(view?: RetrieveServerSettingsViewEnum, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<{ [key: string]: any; }>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveServerSettings(view, options); + async retrieveServerSettings(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveServerSettings(options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['SettingsApi.retrieveServerSettings']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -99,14 +95,13 @@ export const SettingsApiFactory = function (configuration?: Configuration, baseP const localVarFp = SettingsApiFp(configuration) return { /** - * Returns the server settings view selected by the optional `view` query parameter. `view=layer` (the default) returns the current sparse redacted `SettingsLayer` payload. `view=resolved` returns the server\'s dense resolved settings payload after applying the same redaction policy. + * Returns the server\'s current in-memory settings view as the typed `ServerSettings` payload. * @summary Retrieve Server Settings - * @param {RetrieveServerSettingsViewEnum} [view] Selects the server settings representation to return. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - retrieveServerSettings(view?: RetrieveServerSettingsViewEnum, options?: RawAxiosRequestConfig): AxiosPromise<{ [key: string]: any; }> { - return localVarFp.retrieveServerSettings(view, options).then((request) => request(axios, basePath)); + retrieveServerSettings(options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.retrieveServerSettings(options).then((request) => request(axios, basePath)); }, }; }; @@ -116,19 +111,13 @@ export const SettingsApiFactory = function (configuration?: Configuration, baseP */ export class SettingsApi extends BaseAPI { /** - * Returns the server settings view selected by the optional `view` query parameter. `view=layer` (the default) returns the current sparse redacted `SettingsLayer` payload. `view=resolved` returns the server\'s dense resolved settings payload after applying the same redaction policy. + * Returns the server\'s current in-memory settings view as the typed `ServerSettings` payload. * @summary Retrieve Server Settings - * @param {RetrieveServerSettingsViewEnum} [view] Selects the server settings representation to return. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - public retrieveServerSettings(view?: RetrieveServerSettingsViewEnum, options?: RawAxiosRequestConfig) { - return SettingsApiFp(this.configuration).retrieveServerSettings(view, options).then((request) => request(this.axios, this.basePath)); + public retrieveServerSettings(options?: RawAxiosRequestConfig) { + return SettingsApiFp(this.configuration).retrieveServerSettings(options).then((request) => request(this.axios, this.basePath)); } } -export const RetrieveServerSettingsViewEnum = { - LAYER: 'layer', - RESOLVED: 'resolved' -} as const; -export type RetrieveServerSettingsViewEnum = typeof RetrieveServerSettingsViewEnum[keyof typeof RetrieveServerSettingsViewEnum]; diff --git a/lib/packages/fabro-api-client/src/models/discord-integration-settings.ts b/lib/packages/fabro-api-client/src/models/discord-integration-settings.ts new file mode 100644 index 000000000..5ea86fd09 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/discord-integration-settings.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface DiscordIntegrationSettings { + 'enabled': boolean; +} + diff --git a/lib/packages/fabro-api-client/src/models/features-namespace.ts b/lib/packages/fabro-api-client/src/models/features-namespace.ts new file mode 100644 index 000000000..e8eb7dee1 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/features-namespace.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface FeaturesNamespace { + 'session_sandboxes': boolean; +} + diff --git a/lib/packages/fabro-api-client/src/models/git-hub-meta-hooks-entry.ts b/lib/packages/fabro-api-client/src/models/git-hub-meta-hooks-entry.ts new file mode 100644 index 000000000..ae429daef --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/git-hub-meta-hooks-entry.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + + +export const GitHubMetaHooksEntry = { + GIT_HUB_META_HOOKS: 'GitHubMetaHooks' +} as const; + +export type GitHubMetaHooksEntry = typeof GitHubMetaHooksEntry[keyof typeof GitHubMetaHooksEntry]; + + + diff --git a/lib/packages/fabro-api-client/src/models/github-integration-settings.ts b/lib/packages/fabro-api-client/src/models/github-integration-settings.ts new file mode 100644 index 000000000..487a5b511 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/github-integration-settings.ts @@ -0,0 +1,34 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { GithubIntegrationStrategy } from './github-integration-strategy'; +// May contain unused imports in some cases +// @ts-ignore +import type { IntegrationWebhooksSettings } from './integration-webhooks-settings'; + +export interface GithubIntegrationSettings { + 'enabled': boolean; + 'strategy': GithubIntegrationStrategy; + 'app_id': string | null; + 'client_id': string | null; + 'slug': string | null; + 'permissions': { [key: string]: string; }; + 'webhooks': IntegrationWebhooksSettings | null; +} + + + diff --git a/lib/packages/fabro-api-client/src/models/github-integration-strategy.ts b/lib/packages/fabro-api-client/src/models/github-integration-strategy.ts new file mode 100644 index 000000000..6e3d8710a --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/github-integration-strategy.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + + +export const GithubIntegrationStrategy = { + TOKEN: 'token', + APP: 'app' +} as const; + +export type GithubIntegrationStrategy = typeof GithubIntegrationStrategy[keyof typeof GithubIntegrationStrategy]; + + + diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index 2f416e7a4..19747bdcb 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -34,6 +34,7 @@ export * from './diagnostics-report'; export * from './diagnostics-section'; export * from './diff-file'; export * from './diff-stats'; +export * from './discord-integration-settings'; export * from './disk-usage-response'; export * from './disk-usage-run-row'; export * from './disk-usage-summary-row'; @@ -44,8 +45,12 @@ export * from './event-seq'; export * from './execute-query-request'; export * from './execute-query-response'; export * from './execute-query-response-rows-inner-inner'; +export * from './features-namespace'; export * from './file-checkpoint'; export * from './file-diff'; +export * from './git-hub-meta-hooks-entry'; +export * from './github-integration-settings'; +export * from './github-integration-strategy'; export * from './health-response'; export * from './history-entry'; export * from './install-finish-response'; @@ -65,7 +70,10 @@ export * from './install-llm-validation-response'; export * from './install-prefill'; export * from './install-server-config-input'; export * from './install-session-response'; +export * from './integration-webhooks-settings'; export * from './internal-stage-status'; +export * from './ip-allow-entry'; +export * from './literal-ip-allow-entry'; export * from './manifest-args'; export * from './manifest-config'; export * from './manifest-file-entry'; @@ -84,6 +92,9 @@ export * from './model-test-mode'; export * from './model-test-result'; export * from './node-state'; export * from './node-status-record'; +export * from './object-store-local-settings'; +export * from './object-store-s3-settings'; +export * from './object-store-settings'; export * from './paginated-api-question-list'; export * from './paginated-board-run-list'; export * from './paginated-event-list'; @@ -147,6 +158,25 @@ export * from './saved-query'; export * from './secret-list-response'; export * from './secret-metadata'; export * from './secret-type'; +export * from './server-api-settings'; +export * from './server-artifacts-settings'; +export * from './server-auth-github-settings'; +export * from './server-auth-method'; +export * from './server-auth-settings'; +export * from './server-integrations-settings'; +export * from './server-ip-allowlist-override-settings'; +export * from './server-ip-allowlist-settings'; +export * from './server-listen-settings'; +export * from './server-listen-tcp-settings'; +export * from './server-listen-unix-settings'; +export * from './server-logging-settings'; +export * from './server-namespace'; +export * from './server-scheduler-settings'; +export * from './server-settings'; +export * from './server-slate-db-settings'; +export * from './server-storage-settings'; +export * from './server-web-settings'; +export * from './slack-integration-settings'; export * from './ssh-access-request'; export * from './ssh-access-response'; export * from './stage-status'; @@ -159,9 +189,11 @@ export * from './system-features'; export * from './system-info-response'; export * from './system-run-counts'; export * from './system-stage-turn'; +export * from './teams-integration-settings'; export * from './tool-stage-turn'; export * from './tool-use'; export * from './user-response'; +export * from './webhook-strategy'; export * from './workflow-diagnostic'; export * from './workflow-reference'; export * from './write-blob-response'; diff --git a/lib/packages/fabro-api-client/src/models/integration-webhooks-settings.ts b/lib/packages/fabro-api-client/src/models/integration-webhooks-settings.ts new file mode 100644 index 000000000..ad78c9d04 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/integration-webhooks-settings.ts @@ -0,0 +1,29 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ServerIpAllowlistOverrideSettings } from './server-ip-allowlist-override-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { WebhookStrategy } from './webhook-strategy'; + +export interface IntegrationWebhooksSettings { + 'strategy': WebhookStrategy | null; + 'ip_allowlist': ServerIpAllowlistOverrideSettings | null; +} + + + diff --git a/lib/packages/fabro-api-client/src/models/ip-allow-entry.ts b/lib/packages/fabro-api-client/src/models/ip-allow-entry.ts new file mode 100644 index 000000000..9260b28f8 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/ip-allow-entry.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { GitHubMetaHooksEntry } from './git-hub-meta-hooks-entry'; +// May contain unused imports in some cases +// @ts-ignore +import type { LiteralIpAllowEntry } from './literal-ip-allow-entry'; + +/** + * @type IpAllowEntry + */ +export type IpAllowEntry = GitHubMetaHooksEntry | LiteralIpAllowEntry; + + diff --git a/lib/packages/fabro-api-client/src/models/literal-ip-allow-entry.ts b/lib/packages/fabro-api-client/src/models/literal-ip-allow-entry.ts new file mode 100644 index 000000000..95b991f54 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/literal-ip-allow-entry.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface LiteralIpAllowEntry { + 'Literal': string; +} + diff --git a/lib/packages/fabro-api-client/src/models/object-store-local-settings.ts b/lib/packages/fabro-api-client/src/models/object-store-local-settings.ts new file mode 100644 index 000000000..0c7d6379d --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/object-store-local-settings.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ObjectStoreLocalSettings { + 'type': ObjectStoreLocalSettingsTypeEnum; + 'root': string; +} + +export const ObjectStoreLocalSettingsTypeEnum = { + LOCAL: 'local' +} as const; + +export type ObjectStoreLocalSettingsTypeEnum = typeof ObjectStoreLocalSettingsTypeEnum[keyof typeof ObjectStoreLocalSettingsTypeEnum]; + + diff --git a/lib/packages/fabro-api-client/src/models/object-store-s3-settings.ts b/lib/packages/fabro-api-client/src/models/object-store-s3-settings.ts new file mode 100644 index 000000000..884247b13 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/object-store-s3-settings.ts @@ -0,0 +1,31 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ObjectStoreS3Settings { + 'type': ObjectStoreS3SettingsTypeEnum; + 'bucket': string; + 'region': string; + 'endpoint': string | null; + 'path_style': boolean; +} + +export const ObjectStoreS3SettingsTypeEnum = { + S3: 's3' +} as const; + +export type ObjectStoreS3SettingsTypeEnum = typeof ObjectStoreS3SettingsTypeEnum[keyof typeof ObjectStoreS3SettingsTypeEnum]; + + diff --git a/lib/packages/fabro-api-client/src/models/object-store-settings.ts b/lib/packages/fabro-api-client/src/models/object-store-settings.ts new file mode 100644 index 000000000..85320443e --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/object-store-settings.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ObjectStoreLocalSettings } from './object-store-local-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ObjectStoreS3Settings } from './object-store-s3-settings'; + +/** + * @type ObjectStoreSettings + */ +export type ObjectStoreSettings = ObjectStoreLocalSettings | ObjectStoreS3Settings; + + diff --git a/lib/packages/fabro-api-client/src/models/server-api-settings.ts b/lib/packages/fabro-api-client/src/models/server-api-settings.ts new file mode 100644 index 000000000..c05be8d81 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-api-settings.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerApiSettings { + 'url': string | null; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-artifacts-settings.ts b/lib/packages/fabro-api-client/src/models/server-artifacts-settings.ts new file mode 100644 index 000000000..0e5741c36 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-artifacts-settings.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ObjectStoreSettings } from './object-store-settings'; + +export interface ServerArtifactsSettings { + 'prefix': string; + 'store': ObjectStoreSettings; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-auth-github-settings.ts b/lib/packages/fabro-api-client/src/models/server-auth-github-settings.ts new file mode 100644 index 000000000..99fabfe83 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-auth-github-settings.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerAuthGithubSettings { + 'allowed_usernames': Array; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-auth-method.ts b/lib/packages/fabro-api-client/src/models/server-auth-method.ts new file mode 100644 index 000000000..4c2762743 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-auth-method.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + + +export const ServerAuthMethod = { + DEV_TOKEN: 'dev-token', + GITHUB: 'github' +} as const; + +export type ServerAuthMethod = typeof ServerAuthMethod[keyof typeof ServerAuthMethod]; + + + diff --git a/lib/packages/fabro-api-client/src/models/server-auth-settings.ts b/lib/packages/fabro-api-client/src/models/server-auth-settings.ts new file mode 100644 index 000000000..3849dd792 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-auth-settings.ts @@ -0,0 +1,27 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ServerAuthGithubSettings } from './server-auth-github-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerAuthMethod } from './server-auth-method'; + +export interface ServerAuthSettings { + 'methods': Array; + 'github': ServerAuthGithubSettings; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts b/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts new file mode 100644 index 000000000..bee2ea73c --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts @@ -0,0 +1,35 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { DiscordIntegrationSettings } from './discord-integration-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { GithubIntegrationSettings } from './github-integration-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { SlackIntegrationSettings } from './slack-integration-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { TeamsIntegrationSettings } from './teams-integration-settings'; + +export interface ServerIntegrationsSettings { + 'github': GithubIntegrationSettings; + 'slack': SlackIntegrationSettings; + 'discord': DiscordIntegrationSettings; + 'teams': TeamsIntegrationSettings; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-ip-allowlist-override-settings.ts b/lib/packages/fabro-api-client/src/models/server-ip-allowlist-override-settings.ts new file mode 100644 index 000000000..2e54e551d --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-ip-allowlist-override-settings.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { IpAllowEntry } from './ip-allow-entry'; + +export interface ServerIpAllowlistOverrideSettings { + 'entries': Array | null; + 'trusted_proxy_count': number | null; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-ip-allowlist-settings.ts b/lib/packages/fabro-api-client/src/models/server-ip-allowlist-settings.ts new file mode 100644 index 000000000..bb1a7dc86 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-ip-allowlist-settings.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { IpAllowEntry } from './ip-allow-entry'; + +export interface ServerIpAllowlistSettings { + 'entries': Array; + 'trusted_proxy_count': number; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-listen-settings.ts b/lib/packages/fabro-api-client/src/models/server-listen-settings.ts new file mode 100644 index 000000000..eaa674615 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-listen-settings.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ServerListenTcpSettings } from './server-listen-tcp-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerListenUnixSettings } from './server-listen-unix-settings'; + +/** + * @type ServerListenSettings + */ +export type ServerListenSettings = ServerListenTcpSettings | ServerListenUnixSettings; + + diff --git a/lib/packages/fabro-api-client/src/models/server-listen-tcp-settings.ts b/lib/packages/fabro-api-client/src/models/server-listen-tcp-settings.ts new file mode 100644 index 000000000..6850e1cd0 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-listen-tcp-settings.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerListenTcpSettings { + 'type': ServerListenTcpSettingsTypeEnum; + 'address': string; +} + +export const ServerListenTcpSettingsTypeEnum = { + TCP: 'tcp' +} as const; + +export type ServerListenTcpSettingsTypeEnum = typeof ServerListenTcpSettingsTypeEnum[keyof typeof ServerListenTcpSettingsTypeEnum]; + + diff --git a/lib/packages/fabro-api-client/src/models/server-listen-unix-settings.ts b/lib/packages/fabro-api-client/src/models/server-listen-unix-settings.ts new file mode 100644 index 000000000..85aaad72b --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-listen-unix-settings.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerListenUnixSettings { + 'type': ServerListenUnixSettingsTypeEnum; + 'path': string; +} + +export const ServerListenUnixSettingsTypeEnum = { + UNIX: 'unix' +} as const; + +export type ServerListenUnixSettingsTypeEnum = typeof ServerListenUnixSettingsTypeEnum[keyof typeof ServerListenUnixSettingsTypeEnum]; + + diff --git a/lib/packages/fabro-api-client/src/models/server-logging-settings.ts b/lib/packages/fabro-api-client/src/models/server-logging-settings.ts new file mode 100644 index 000000000..d9ab078f4 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-logging-settings.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerLoggingSettings { + 'level': string | null; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-namespace.ts b/lib/packages/fabro-api-client/src/models/server-namespace.ts new file mode 100644 index 000000000..0e6eda23c --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-namespace.ts @@ -0,0 +1,63 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ServerApiSettings } from './server-api-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerArtifactsSettings } from './server-artifacts-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerAuthSettings } from './server-auth-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerIntegrationsSettings } from './server-integrations-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerIpAllowlistSettings } from './server-ip-allowlist-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerListenSettings } from './server-listen-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerLoggingSettings } from './server-logging-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerSchedulerSettings } from './server-scheduler-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerSlateDbSettings } from './server-slate-db-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerStorageSettings } from './server-storage-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerWebSettings } from './server-web-settings'; + +export interface ServerNamespace { + 'listen': ServerListenSettings; + 'api': ServerApiSettings; + 'web': ServerWebSettings; + 'auth': ServerAuthSettings; + 'ip_allowlist': ServerIpAllowlistSettings; + 'storage': ServerStorageSettings; + 'artifacts': ServerArtifactsSettings; + 'slatedb': ServerSlateDbSettings; + 'scheduler': ServerSchedulerSettings; + 'logging': ServerLoggingSettings; + 'integrations': ServerIntegrationsSettings; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-scheduler-settings.ts b/lib/packages/fabro-api-client/src/models/server-scheduler-settings.ts new file mode 100644 index 000000000..e2b7a5a1c --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-scheduler-settings.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerSchedulerSettings { + 'max_concurrent_runs': number; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-settings.ts b/lib/packages/fabro-api-client/src/models/server-settings.ts new file mode 100644 index 000000000..8d4e07204 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-settings.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { FeaturesNamespace } from './features-namespace'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerNamespace } from './server-namespace'; + +/** + * Current in-memory server settings view. + */ +export interface ServerSettings { + 'server': ServerNamespace; + 'features': FeaturesNamespace; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-slate-db-settings.ts b/lib/packages/fabro-api-client/src/models/server-slate-db-settings.ts new file mode 100644 index 000000000..2790286c4 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-slate-db-settings.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ObjectStoreSettings } from './object-store-settings'; + +export interface ServerSlateDbSettings { + 'prefix': string; + 'store': ObjectStoreSettings; + 'flush_interval': string; + 'disk_cache': boolean; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-storage-settings.ts b/lib/packages/fabro-api-client/src/models/server-storage-settings.ts new file mode 100644 index 000000000..244fa2222 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-storage-settings.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerStorageSettings { + 'root': string; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-web-settings.ts b/lib/packages/fabro-api-client/src/models/server-web-settings.ts new file mode 100644 index 000000000..f0d47eaac --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-web-settings.ts @@ -0,0 +1,21 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerWebSettings { + 'enabled': boolean; + 'url': string; +} + diff --git a/lib/packages/fabro-api-client/src/models/slack-integration-settings.ts b/lib/packages/fabro-api-client/src/models/slack-integration-settings.ts new file mode 100644 index 000000000..60ece416a --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/slack-integration-settings.ts @@ -0,0 +1,21 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface SlackIntegrationSettings { + 'enabled': boolean; + 'default_channel': string | null; +} + diff --git a/lib/packages/fabro-api-client/src/models/teams-integration-settings.ts b/lib/packages/fabro-api-client/src/models/teams-integration-settings.ts new file mode 100644 index 000000000..be2a971cb --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/teams-integration-settings.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface TeamsIntegrationSettings { + 'enabled': boolean; +} + diff --git a/lib/packages/fabro-api-client/src/models/webhook-strategy.ts b/lib/packages/fabro-api-client/src/models/webhook-strategy.ts new file mode 100644 index 000000000..6cc1516e6 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/webhook-strategy.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + + +export const WebhookStrategy = { + TAILSCALE_FUNNEL: 'tailscale_funnel', + SERVER_URL: 'server_url' +} as const; + +export type WebhookStrategy = typeof WebhookStrategy[keyof typeof WebhookStrategy]; + + + From bb0d05be2b25fb71dc4c54447bcf30c1e7b1ed1f Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Wed, 22 Apr 2026 19:44:20 -0400 Subject: [PATCH 03/13] fix settings runtime refresh follow-ups --- bin/dev/check-boundary.sh | 20 ++++++- lib/crates/fabro-cli/src/commands/install.rs | 5 +- lib/crates/fabro-cli/src/local_server.rs | 6 +- .../fabro-cli/tests/it/scenario/smoke.rs | 6 +- lib/crates/fabro-server/src/serve.rs | 37 ++++++++++++ lib/crates/fabro-server/src/server.rs | 59 +++++++++++++++++++ 6 files changed, 121 insertions(+), 12 deletions(-) diff --git a/bin/dev/check-boundary.sh b/bin/dev/check-boundary.sh index 7771d58ed..974dc4442 100755 --- a/bin/dev/check-boundary.sh +++ b/bin/dev/check-boundary.sh @@ -3,8 +3,14 @@ set -euo pipefail cd "$(dirname "$0")/../.." -symbol_allowlist=( +server_symbol_allowlist=( "lib/crates/fabro-cli/src/local_server.rs" + "lib/crates/fabro-cli/src/commands/run/runner.rs" + "lib/crates/fabro-cli/src/commands/pr/mod.rs" + "lib/crates/fabro-cli/src/commands/pr/create.rs" +) + +storage_allowlist=( "lib/crates/fabro-cli/src/commands/install.rs" "lib/crates/fabro-cli/src/commands/uninstall.rs" "lib/crates/fabro-cli/src/commands/run/runner.rs" @@ -50,11 +56,19 @@ fail=0 while IFS= read -r path; do [[ -z "$path" ]] && continue - if ! in_array "$path" "${symbol_allowlist[@]}"; then + if ! in_array "$path" "${server_symbol_allowlist[@]}"; then echo "boundary check failed: gated server symbol used outside allowlist: $path" >&2 fail=1 fi -done < <(find_matches 'fabro_config::resolve_server_from_file|fabro_config::resolve_server\b|fabro_config::ServerSettings::from_layer\b|fabro_config::ServerSettings::resolve\b|ServerSettings::from_layer\b|ServerSettings::resolve\b|Storage::new') +done < <(find_matches 'fabro_config::resolve_server_from_file|fabro_config::resolve_server\b|fabro_config::ServerSettings::from_layer\b|fabro_config::ServerSettings::resolve\b|ServerSettings::from_layer\b|ServerSettings::resolve\b') + +while IFS= read -r path; do + [[ -z "$path" ]] && continue + if ! in_array "$path" "${storage_allowlist[@]}"; then + echo "boundary check failed: Storage::new used outside allowlist: $path" >&2 + fail=1 + fi +done < <(find_matches 'Storage::new') while IFS= read -r path; do [[ -z "$path" ]] && continue diff --git a/lib/crates/fabro-cli/src/commands/install.rs b/lib/crates/fabro-cli/src/commands/install.rs index 886524266..fa9d26f73 100644 --- a/lib/crates/fabro-cli/src/commands/install.rs +++ b/lib/crates/fabro-cli/src/commands/install.rs @@ -1277,8 +1277,7 @@ async fn write_artifact_store_metadata( settings: &SettingsLayer, fabro_version: &str, ) -> Result<()> { - let resolved = - fabro_config::ServerSettings::from_layer(settings).map_err(anyhow::Error::from)?; + let resolved = local_server::server_settings(settings)?; let (object_store, prefix) = serve::build_artifact_object_store(&resolved.server)?; let artifact_store = ArtifactStore::new(object_store, prefix); artifact_store.write_metadata(fabro_version).await?; @@ -1797,7 +1796,7 @@ async fn run_install_inner( .context("failed to parse generated settings.toml")?, args.storage_dir.as_deref(), ); - fabro_config::ServerSettings::from_layer(&install_settings).map_err(anyhow::Error::from)?; + local_server::server_settings(&install_settings)?; // Secrets and auth material { diff --git a/lib/crates/fabro-cli/src/local_server.rs b/lib/crates/fabro-cli/src/local_server.rs index cdba3b556..5128e70da 100644 --- a/lib/crates/fabro-cli/src/local_server.rs +++ b/lib/crates/fabro-cli/src/local_server.rs @@ -26,8 +26,12 @@ pub(crate) fn bind_request( resolve_bind_request_from_settings(settings, cli_override) } +pub(crate) fn server_settings(settings: &SettingsLayer) -> Result { + fabro_config::ServerSettings::from_layer(settings).map_err(anyhow::Error::from) +} + pub(crate) fn auth_methods(settings: &SettingsLayer) -> Vec { - fabro_config::ServerSettings::from_layer(settings) + server_settings(settings) .map(|resolved| resolved.server.auth.methods) .unwrap_or_default() } diff --git a/lib/crates/fabro-cli/tests/it/scenario/smoke.rs b/lib/crates/fabro-cli/tests/it/scenario/smoke.rs index 13160b955..cf1db65fb 100644 --- a/lib/crates/fabro-cli/tests/it/scenario/smoke.rs +++ b/lib/crates/fabro-cli/tests/it/scenario/smoke.rs @@ -134,16 +134,12 @@ fn help_smoke_covers_high_cost_commands() { ----- stdout ----- Inspect effective settings - Usage: fabro settings [OPTIONS] [WORKFLOW] - - Arguments: - [WORKFLOW] Optional workflow name, .fabro path, or .toml run config to overlay + Usage: fabro settings [OPTIONS] Options: --json Output as JSON [env: FABRO_JSON=] --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=] --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] - --local Show only locally resolved settings and skip the server call --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] --quiet Suppress non-essential output [env: FABRO_QUIET=] --verbose Enable verbose output [env: FABRO_VERBOSE=] diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs index 49ae4d959..0055eb108 100644 --- a/lib/crates/fabro-server/src/serve.rs +++ b/lib/crates/fabro-server/src/serve.rs @@ -901,6 +901,7 @@ mod tests { resolve_server_settings, resolve_startup_github_webhook_ip_allowlist, router_web_enabled, server_bind_title, server_title, }; + use crate::server::create_app_state_with_options; fn parse_settings(source: &str) -> SettingsLayer { let mut layer = parse_settings_layer(source).expect("v2 fixture should parse"); @@ -935,6 +936,42 @@ mod tests { assert_eq!(storage_root.as_deref(), Some("/srv/fabro-storage")); } + #[test] + fn app_state_server_settings_use_effective_runtime_layer_storage_override() { + let base = parse_settings( + r#" +_version = 1 + +[server.storage] +root = "/srv/from-disk" +"#, + ); + let args = ServeArgs { + bind: None, + model: None, + provider: None, + sandbox: None, + web: false, + no_web: false, + max_concurrent_runs: None, + config: None, + #[cfg(debug_assertions)] + watch_web: false, + }; + + let effective = apply_runtime_settings(&base, &args, &PathBuf::from("/srv/from-runtime")); + let state = create_app_state_with_options(effective, 5); + + assert_eq!( + state.server_settings().server.storage.root.as_source(), + "/srv/from-runtime" + ); + assert_eq!( + state.server_storage_dir(), + PathBuf::from("/srv/from-runtime") + ); + } + #[test] fn apply_runtime_settings_enables_web_from_cli_flag() { let base = parse_settings( diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index fa97ac90e..6d2526cb1 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -7397,6 +7397,65 @@ url = "{url}" } } + #[test] + fn replace_settings_updates_layer_and_typed_server_settings() { + let state = create_app_state_with_options( + fabro_config::parse_settings_layer( + r#" +_version = 1 + +[server.auth] +methods = ["dev-token"] + +[server.web] +url = "http://old.example.com" + +[server.storage] +root = "/srv/old" +"#, + ) + .expect("settings fixture should parse"), + 5, + ); + + let updated = fabro_config::parse_settings_layer( + r#" +_version = 1 + +[server.auth] +methods = ["dev-token"] + +[server.web] +url = "http://new.example.com" + +[server.storage] +root = "/srv/new" +"#, + ) + .expect("settings fixture should parse"); + + state + .replace_settings(updated) + .expect("valid settings should replace current state"); + + assert_eq!(state.canonical_origin().unwrap(), "http://new.example.com"); + assert_eq!( + state.server_settings().server.storage.root.as_source(), + "/srv/new" + ); + + let layer_root = state + .settings + .read() + .expect("settings lock poisoned") + .server + .as_ref() + .and_then(|server| server.storage.as_ref()) + .and_then(|storage| storage.root.as_ref()) + .map(InterpString::as_source); + assert_eq!(layer_root.as_deref(), Some("/srv/new")); + } + #[tokio::test] async fn create_secret_stores_file_secret_and_excludes_it_from_snapshot() { let state = create_app_state(); From 93b6577cd392551d5a80bbdbfbe4d5eb6425f819 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Wed, 22 Apr 2026 21:02:37 -0400 Subject: [PATCH 04/13] simplify: drop duplicate settings plumbing from cli/server refactor - Remove CommandContext::cli_settings and cascade through 11 functions whose only use of `cli: &CliNamespace` was constructing it; dispatchers now forward only cli_layer. - Drop `ServerSettings as CurrentServerSettings` / `ServerNamespace as ResolvedServerSettings` rename aliases; use the canonical type names in fabro-server. - Inline `local_server::server_settings` and `user_config::{resolve_user_settings, resolve_cli_settings}` wrappers; callers use `ServerSettings::from_layer` / `UserSettings::from_layer` directly (anyhow converts via `?`). - Trim narrative module doc in fabro-config/src/lib.rs. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/crates/fabro-cli/src/command_context.rs | 29 ++++--------------- .../fabro-cli/src/commands/artifact/cp.rs | 1 - .../fabro-cli/src/commands/artifact/list.rs | 1 - .../fabro-cli/src/commands/artifact/mod.rs | 3 +- .../fabro-cli/src/commands/auth/login.rs | 6 ++-- .../fabro-cli/src/commands/auth/logout.rs | 4 +-- lib/crates/fabro-cli/src/commands/auth/mod.rs | 8 ++--- .../fabro-cli/src/commands/auth/status.rs | 4 +-- .../fabro-cli/src/commands/config/mod.rs | 5 ++-- lib/crates/fabro-cli/src/commands/doctor.rs | 2 +- lib/crates/fabro-cli/src/commands/graph.rs | 2 +- lib/crates/fabro-cli/src/commands/install.rs | 4 +-- lib/crates/fabro-cli/src/commands/model.rs | 2 +- lib/crates/fabro-cli/src/commands/pr/close.rs | 4 +-- .../fabro-cli/src/commands/pr/create.rs | 4 +-- lib/crates/fabro-cli/src/commands/pr/list.rs | 4 +-- lib/crates/fabro-cli/src/commands/pr/merge.rs | 4 +-- lib/crates/fabro-cli/src/commands/pr/mod.rs | 6 ++-- lib/crates/fabro-cli/src/commands/pr/view.rs | 4 +-- .../fabro-cli/src/commands/preflight.rs | 2 +- .../fabro-cli/src/commands/provider/login.rs | 4 +-- .../fabro-cli/src/commands/provider/mod.rs | 4 +-- .../fabro-cli/src/commands/repo/init.rs | 5 ++-- .../fabro-cli/src/commands/run/command.rs | 4 +-- lib/crates/fabro-cli/src/commands/run/cp.rs | 9 ++---- lib/crates/fabro-cli/src/commands/run/diff.rs | 2 +- lib/crates/fabro-cli/src/commands/run/fork.rs | 2 +- lib/crates/fabro-cli/src/commands/run/logs.rs | 2 +- lib/crates/fabro-cli/src/commands/run/mod.rs | 11 ++++--- .../fabro-cli/src/commands/run/preview.rs | 2 +- .../fabro-cli/src/commands/run/resume.rs | 2 +- .../fabro-cli/src/commands/run/rewind.rs | 2 +- lib/crates/fabro-cli/src/commands/run/ssh.rs | 2 +- lib/crates/fabro-cli/src/commands/run/wait.rs | 2 +- .../fabro-cli/src/commands/runs/archive.rs | 4 +-- .../fabro-cli/src/commands/runs/inspect.rs | 10 ++----- .../fabro-cli/src/commands/runs/list.rs | 2 +- lib/crates/fabro-cli/src/commands/runs/mod.rs | 2 +- lib/crates/fabro-cli/src/commands/runs/rm.rs | 2 +- .../fabro-cli/src/commands/secret/mod.rs | 2 +- .../fabro-cli/src/commands/store/dump.rs | 2 +- .../fabro-cli/src/commands/system/df.rs | 2 +- .../fabro-cli/src/commands/system/events.rs | 2 +- .../fabro-cli/src/commands/system/info.rs | 2 +- .../fabro-cli/src/commands/system/prune.rs | 2 +- lib/crates/fabro-cli/src/commands/validate.rs | 2 +- lib/crates/fabro-cli/src/commands/version.rs | 2 +- lib/crates/fabro-cli/src/local_server.rs | 6 +--- lib/crates/fabro-cli/src/main.rs | 24 ++++----------- lib/crates/fabro-cli/src/user_config.rs | 12 +------- lib/crates/fabro-config/src/lib.rs | 6 ++-- .../fabro-server/src/canonical_origin.rs | 4 +-- lib/crates/fabro-server/src/jwt_auth.rs | 13 ++++----- lib/crates/fabro-server/src/serve.rs | 22 +++++++------- lib/crates/fabro-server/src/server.rs | 14 ++++----- 55 files changed, 104 insertions(+), 185 deletions(-) diff --git a/lib/crates/fabro-cli/src/command_context.rs b/lib/crates/fabro-cli/src/command_context.rs index ac508f5ed..b5351d640 100644 --- a/lib/crates/fabro-cli/src/command_context.rs +++ b/lib/crates/fabro-cli/src/command_context.rs @@ -4,8 +4,8 @@ use std::sync::Arc; use anyhow::{Context as _, Result, bail}; use fabro_config::UserSettings; use fabro_config::merge::combine_files; +use fabro_types::settings::SettingsLayer; use fabro_types::settings::cli::CliLayer; -use fabro_types::settings::{CliNamespace, SettingsLayer}; use fabro_util::printer::Printer; use tokio::sync::OnceCell; @@ -35,24 +35,18 @@ pub(crate) struct CommandContext { base_config_path: PathBuf, machine_settings: SettingsLayer, user_settings: UserSettings, - cli_settings: CliNamespace, server_mode: ServerMode, server: OnceCell>, } impl CommandContext { - pub(crate) fn base( - printer: Printer, - cli_settings: CliNamespace, - cli_layer: &CliLayer, - ) -> Result { - Self::new(printer, ServerMode::None, cli_settings, cli_layer) + pub(crate) fn base(printer: Printer, cli_layer: &CliLayer) -> Result { + Self::new(printer, ServerMode::None, cli_layer) } pub(crate) fn for_target( args: &ServerTargetArgs, printer: Printer, - cli_settings: CliNamespace, cli_layer: &CliLayer, ) -> Result { Self::new( @@ -60,7 +54,6 @@ impl CommandContext { ServerMode::ByTarget { target_override: args.server.clone(), }, - cli_settings, cli_layer, ) } @@ -68,7 +61,6 @@ impl CommandContext { pub(crate) fn for_connection( args: &ServerConnectionArgs, printer: Printer, - cli_settings: CliNamespace, cli_layer: &CliLayer, ) -> Result { Self::new( @@ -77,17 +69,11 @@ impl CommandContext { target_override: args.target.server.clone(), storage_dir_override: args.storage_dir.clone_path(), }, - cli_settings, cli_layer, ) } - fn new( - printer: Printer, - server_mode: ServerMode, - cli_settings: CliNamespace, - cli_layer: &CliLayer, - ) -> Result { + fn new(printer: Printer, server_mode: ServerMode, cli_layer: &CliLayer) -> Result { let cwd = std::env::current_dir().context("Failed to get current directory")?; let base_config_path = user_config::active_settings_path(None); let disk_settings = match &server_mode { @@ -101,7 +87,7 @@ impl CommandContext { cli: Some(cli_layer.clone()), ..SettingsLayer::default() }); - let user_settings = user_config::resolve_user_settings(&machine_settings)?; + let user_settings = fabro_config::UserSettings::from_layer(&machine_settings)?; Ok(Self { printer, @@ -109,7 +95,6 @@ impl CommandContext { base_config_path, machine_settings, user_settings, - cli_settings, server_mode, server: OnceCell::new(), }) @@ -135,10 +120,6 @@ impl CommandContext { &self.user_settings } - pub(crate) fn cli_settings(&self) -> &CliNamespace { - &self.cli_settings - } - pub(crate) async fn server(&self) -> Result> { let server_mode = self.server_mode.clone(); let base_config_path = self.base_config_path.clone(); diff --git a/lib/crates/fabro-cli/src/commands/artifact/cp.rs b/lib/crates/fabro-cli/src/commands/artifact/cp.rs index 9b87dd6a1..2756912c1 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/cp.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/cp.rs @@ -26,7 +26,6 @@ pub(super) async fn cp_command( run_id_selector, args.node.as_deref(), args.retry, - cli, cli_layer, printer, ) diff --git a/lib/crates/fabro-cli/src/commands/artifact/list.rs b/lib/crates/fabro-cli/src/commands/artifact/list.rs index 0637f7a5b..e7de1d4a4 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/list.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/list.rs @@ -19,7 +19,6 @@ pub(super) async fn list_command( &args.run_id, args.node.as_deref(), args.retry, - cli, cli_layer, printer, ) diff --git a/lib/crates/fabro-cli/src/commands/artifact/mod.rs b/lib/crates/fabro-cli/src/commands/artifact/mod.rs index 571f3cdb0..605d1beec 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/mod.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/mod.rs @@ -26,11 +26,10 @@ pub(super) async fn resolve_artifacts( run_selector: &str, node: Option<&str>, retry: Option, - cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<(RunId, Client, Vec)> { - let ctx = CommandContext::for_target(server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(server, printer, cli_layer)?; let client = ctx.server().await?; let run_id = client.resolve_run(run_selector).await?.run_id; let mut entries = Vec::new(); diff --git a/lib/crates/fabro-cli/src/commands/auth/login.rs b/lib/crates/fabro-cli/src/commands/auth/login.rs index 5d3d69a30..7968fb78e 100644 --- a/lib/crates/fabro-cli/src/commands/auth/login.rs +++ b/lib/crates/fabro-cli/src/commands/auth/login.rs @@ -4,7 +4,6 @@ use anyhow::{Context as _, Result, bail}; use chrono::{DateTime, Utc}; use fabro_client::{AuthEntry, AuthStore, StoredSubject}; use fabro_http::header::CONTENT_TYPE; -use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::browser; use fabro_util::printer::Printer; @@ -36,7 +35,6 @@ struct CliTokenSubject { pub(super) async fn login_command( args: AuthLoginArgs, - cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, @@ -45,7 +43,7 @@ pub(super) async fn login_command( #[cfg(not(unix))] { - let _ = (args, cli, cli_layer, printer); + let _ = (args, cli_layer, printer); bail!( "CLI OAuth login is not supported on Windows in this release. Use WSL, or use a dev-token server." ); @@ -53,7 +51,7 @@ pub(super) async fn login_command( #[cfg(unix)] { - let ctx = CommandContext::base(printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::base(printer, cli_layer)?; let target = user_config::resolve_server_target(&args.server, ctx.machine_settings())?; let web_url = browser_origin(&target)?; let pkce = fabro_oauth::generate_pkce(); diff --git a/lib/crates/fabro-cli/src/commands/auth/logout.rs b/lib/crates/fabro-cli/src/commands/auth/logout.rs index eda47e51b..6acf20275 100644 --- a/lib/crates/fabro-cli/src/commands/auth/logout.rs +++ b/lib/crates/fabro-cli/src/commands/auth/logout.rs @@ -1,7 +1,6 @@ use anyhow::{Result, bail}; use fabro_client::{AuthEntry, AuthStore}; use fabro_http::header::AUTHORIZATION; -use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; @@ -12,14 +11,13 @@ use crate::user_config::ServerTarget; pub(super) async fn logout_command( args: AuthLogoutArgs, - cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, ) -> Result<()> { require_no_json_override(process_local_json)?; - let ctx = CommandContext::base(printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::base(printer, cli_layer)?; let store = AuthStore::default(); if args.all { let entries = store.list()?; diff --git a/lib/crates/fabro-cli/src/commands/auth/mod.rs b/lib/crates/fabro-cli/src/commands/auth/mod.rs index 1d5a4546e..dd4ca8a83 100644 --- a/lib/crates/fabro-cli/src/commands/auth/mod.rs +++ b/lib/crates/fabro-cli/src/commands/auth/mod.rs @@ -3,7 +3,6 @@ mod logout; mod status; use anyhow::Result; -use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; @@ -11,20 +10,19 @@ use crate::args::{AuthCommand, AuthNamespace}; pub(crate) async fn dispatch( ns: AuthNamespace, - cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, ) -> Result<()> { match ns.command { AuthCommand::Login(args) => { - login::login_command(args, cli, cli_layer, process_local_json, printer).await + login::login_command(args, cli_layer, process_local_json, printer).await } AuthCommand::Logout(args) => { - logout::logout_command(args, cli, cli_layer, process_local_json, printer).await + logout::logout_command(args, cli_layer, process_local_json, printer).await } AuthCommand::Status(args) => { - status::status_command(&args, cli, cli_layer, process_local_json, printer) + status::status_command(&args, cli_layer, process_local_json, printer) } } } diff --git a/lib/crates/fabro-cli/src/commands/auth/status.rs b/lib/crates/fabro-cli/src/commands/auth/status.rs index 86962b52a..aa82b6f7e 100644 --- a/lib/crates/fabro-cli/src/commands/auth/status.rs +++ b/lib/crates/fabro-cli/src/commands/auth/status.rs @@ -1,7 +1,6 @@ use anyhow::Result; use chrono::{DateTime, Utc}; use fabro_client::{AuthEntry, AuthStore}; -use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::dev_token::{read_dev_token_file, validate_dev_token_format}; use fabro_util::printer::Printer; @@ -43,12 +42,11 @@ struct StatusOutput { pub(super) fn status_command( args: &AuthStatusArgs, - cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::base(printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::base(printer, cli_layer)?; let store = AuthStore::default(); let now = Utc::now(); let rows = if args.server.as_deref().is_some() { diff --git a/lib/crates/fabro-cli/src/commands/config/mod.rs b/lib/crates/fabro-cli/src/commands/config/mod.rs index 1f329af84..670cca826 100644 --- a/lib/crates/fabro-cli/src/commands/config/mod.rs +++ b/lib/crates/fabro-cli/src/commands/config/mod.rs @@ -26,11 +26,10 @@ struct RenderedConfig { async fn rendered_config( args: &SettingsArgs, - cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result { - let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?; let user = fabro_config::UserSettings::resolve()?; let server = ctx .server() @@ -46,7 +45,7 @@ pub(crate) async fn execute( cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result<()> { - let config = Box::pin(rendered_config(args, cli, cli_layer, printer)).await?; + let config = Box::pin(rendered_config(args, cli_layer, printer)).await?; if cli.output.format == OutputFormat::Json { print_json_pretty(&config)?; return Ok(()); diff --git a/lib/crates/fabro-cli/src/commands/doctor.rs b/lib/crates/fabro-cli/src/commands/doctor.rs index fb25bb147..85c8517c6 100644 --- a/lib/crates/fabro-cli/src/commands/doctor.rs +++ b/lib/crates/fabro-cli/src/commands/doctor.rs @@ -179,7 +179,7 @@ pub(crate) async fn run_doctor( }], }; - let ctx = match CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer) { + let ctx = match CommandContext::for_target(&args.target, printer, cli_layer) { Ok(ctx) => ctx, Err(err) => { report.sections.push(CheckSection { diff --git a/lib/crates/fabro-cli/src/commands/graph.rs b/lib/crates/fabro-cli/src/commands/graph.rs index 0df39a2e1..71ffa9f68 100644 --- a/lib/crates/fabro-cli/src/commands/graph.rs +++ b/lib/crates/fabro-cli/src/commands/graph.rs @@ -37,7 +37,7 @@ pub(crate) async fn run( require_no_json_override(process_local_json)?; } - let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?; let built = build_run_manifest(ManifestBuildInput { workflow: args.workflow.clone(), cwd: ctx.cwd().to_path_buf(), diff --git a/lib/crates/fabro-cli/src/commands/install.rs b/lib/crates/fabro-cli/src/commands/install.rs index fa9d26f73..3e0753af4 100644 --- a/lib/crates/fabro-cli/src/commands/install.rs +++ b/lib/crates/fabro-cli/src/commands/install.rs @@ -1277,7 +1277,7 @@ async fn write_artifact_store_metadata( settings: &SettingsLayer, fabro_version: &str, ) -> Result<()> { - let resolved = local_server::server_settings(settings)?; + let resolved = fabro_config::ServerSettings::from_layer(settings)?; let (object_store, prefix) = serve::build_artifact_object_store(&resolved.server)?; let artifact_store = ArtifactStore::new(object_store, prefix); artifact_store.write_metadata(fabro_version).await?; @@ -1796,7 +1796,7 @@ async fn run_install_inner( .context("failed to parse generated settings.toml")?, args.storage_dir.as_deref(), ); - local_server::server_settings(&install_settings)?; + fabro_config::ServerSettings::from_layer(&install_settings)?; // Secrets and auth material { diff --git a/lib/crates/fabro-cli/src/commands/model.rs b/lib/crates/fabro-cli/src/commands/model.rs index c96b3c349..d7c26c2a5 100644 --- a/lib/crates/fabro-cli/src/commands/model.rs +++ b/lib/crates/fabro-cli/src/commands/model.rs @@ -51,7 +51,7 @@ pub(crate) async fn execute( ModelsCommand::List(args) => &args.target, ModelsCommand::Test(args) => &args.target, }; - let ctx = CommandContext::for_target(target_args, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(target_args, printer, cli_layer)?; let server = ctx.server().await?; run_models(command, &server, cli.output.format == OutputFormat::Json).await diff --git a/lib/crates/fabro-cli/src/commands/pr/close.rs b/lib/crates/fabro-cli/src/commands/pr/close.rs index 0cc9a4a39..c48132ba0 100644 --- a/lib/crates/fabro-cli/src/commands/pr/close.rs +++ b/lib/crates/fabro-cli/src/commands/pr/close.rs @@ -14,9 +14,9 @@ pub(super) async fn close_command( printer: Printer, ) -> Result<()> { let (record, _run_id) = - super::load_pr_record(&args.server, &args.run_id, cli, cli_layer, printer).await?; + super::load_pr_record(&args.server, &args.run_id, cli_layer, printer).await?; - let creds = super::load_github_credentials_required(cli, cli_layer, printer)?; + let creds = super::load_github_credentials_required(cli_layer, printer)?; fabro_github::close_pull_request( &creds, diff --git a/lib/crates/fabro-cli/src/commands/pr/create.rs b/lib/crates/fabro-cli/src/commands/pr/create.rs index d26b8080b..e42c633e2 100644 --- a/lib/crates/fabro-cli/src/commands/pr/create.rs +++ b/lib/crates/fabro-cli/src/commands/pr/create.rs @@ -31,7 +31,7 @@ pub(super) async fn create_command( cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run_id).await?.run_id; let events = client.list_run_events(&run_id, None, None).await?; @@ -86,7 +86,7 @@ pub(super) async fn create_command( let (owner, repo) = fabro_github::parse_github_owner_repo(&https_url) .map_err(|err| anyhow::anyhow!("{err}"))?; - let creds = super::load_github_credentials_required(cli, cli_layer, printer)?; + let creds = super::load_github_credentials_required(cli_layer, printer)?; let branch_found = fabro_github::branch_exists( &creds, diff --git a/lib/crates/fabro-cli/src/commands/pr/list.rs b/lib/crates/fabro-cli/src/commands/pr/list.rs index 2e0f51f1b..bf86031eb 100644 --- a/lib/crates/fabro-cli/src/commands/pr/list.rs +++ b/lib/crates/fabro-cli/src/commands/pr/list.rs @@ -29,7 +29,7 @@ pub(super) async fn list_command( cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let mut entries = Vec::new(); @@ -50,7 +50,7 @@ pub(super) async fn list_command( return Ok(()); } - let creds = super::load_github_credentials_required(cli, cli_layer, printer)?; + let creds = super::load_github_credentials_required(cli_layer, printer)?; let futures: Vec<_> = entries .iter() diff --git a/lib/crates/fabro-cli/src/commands/pr/merge.rs b/lib/crates/fabro-cli/src/commands/pr/merge.rs index be7ce9f61..3ee3f3338 100644 --- a/lib/crates/fabro-cli/src/commands/pr/merge.rs +++ b/lib/crates/fabro-cli/src/commands/pr/merge.rs @@ -14,9 +14,9 @@ pub(super) async fn merge_command( printer: Printer, ) -> Result<()> { let (record, _run_id) = - super::load_pr_record(&args.server, &args.run_id, cli, cli_layer, printer).await?; + super::load_pr_record(&args.server, &args.run_id, cli_layer, printer).await?; - let creds = super::load_github_credentials_required(cli, cli_layer, printer)?; + let creds = super::load_github_credentials_required(cli_layer, printer)?; fabro_github::merge_pull_request( &creds, diff --git a/lib/crates/fabro-cli/src/commands/pr/mod.rs b/lib/crates/fabro-cli/src/commands/pr/mod.rs index 8d1933f66..dd70c83f7 100644 --- a/lib/crates/fabro-cli/src/commands/pr/mod.rs +++ b/lib/crates/fabro-cli/src/commands/pr/mod.rs @@ -42,11 +42,10 @@ pub(crate) async fn dispatch( reason = "boundary-exempt(pr-api): remove with follow-up #1 when PR ops move server-side" )] fn load_github_credentials_required( - cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result { - let ctx = CommandContext::base(printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::base(printer, cli_layer)?; let server_settings = fabro_config::ServerSettings::from_layer(ctx.machine_settings()) .map_err(anyhow::Error::from)?; let vault = user_config::storage_dir(ctx.machine_settings()) @@ -71,11 +70,10 @@ fn load_github_credentials_required( pub(crate) async fn load_pr_record( server: &ServerTargetArgs, run_id: &str, - cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<(PullRequestRecord, fabro_types::RunId)> { - let ctx = CommandContext::for_target(server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(server, printer, cli_layer)?; let client = ctx.server().await?; let run_id = client.resolve_run(run_id).await?.run_id; let state = client.get_run_state(&run_id).await?; diff --git a/lib/crates/fabro-cli/src/commands/pr/view.rs b/lib/crates/fabro-cli/src/commands/pr/view.rs index 79eabadeb..d766e7dd4 100644 --- a/lib/crates/fabro-cli/src/commands/pr/view.rs +++ b/lib/crates/fabro-cli/src/commands/pr/view.rs @@ -14,9 +14,9 @@ pub(super) async fn view_command( printer: Printer, ) -> Result<()> { let (record, _run_id) = - super::load_pr_record(&args.server, &args.run_id, cli, cli_layer, printer).await?; + super::load_pr_record(&args.server, &args.run_id, cli_layer, printer).await?; - let creds = super::load_github_credentials_required(cli, cli_layer, printer)?; + let creds = super::load_github_credentials_required(cli_layer, printer)?; let detail = fabro_github::get_pull_request( &creds, diff --git a/lib/crates/fabro-cli/src/commands/preflight.rs b/lib/crates/fabro-cli/src/commands/preflight.rs index 03dd4304a..1a49e8cb5 100644 --- a/lib/crates/fabro-cli/src/commands/preflight.rs +++ b/lib/crates/fabro-cli/src/commands/preflight.rs @@ -22,7 +22,7 @@ pub(crate) async fn execute( printer: Printer, ) -> anyhow::Result<()> { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?; args.verbose = args.verbose || cli.output.verbosity == OutputVerbosity::Verbose; let manifest = build_run_manifest(ManifestBuildInput { diff --git a/lib/crates/fabro-cli/src/commands/provider/login.rs b/lib/crates/fabro-cli/src/commands/provider/login.rs index 1c613936d..86a393a09 100644 --- a/lib/crates/fabro-cli/src/commands/provider/login.rs +++ b/lib/crates/fabro-cli/src/commands/provider/login.rs @@ -1,7 +1,6 @@ use anyhow::Result; use fabro_api::types; use fabro_auth::credential_id_for; -use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -12,14 +11,13 @@ use crate::shared::provider_auth; pub(super) async fn login_command( args: ProviderLoginArgs, - cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, ) -> Result<()> { require_no_json_override(process_local_json)?; let s = Styles::detect_stderr(); - let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?; let server = ctx.server().await?; let credential = if args.api_key_stdin { provider_auth::authenticate_provider_with_api_key_source( diff --git a/lib/crates/fabro-cli/src/commands/provider/mod.rs b/lib/crates/fabro-cli/src/commands/provider/mod.rs index 85dd085a8..07e3d9adf 100644 --- a/lib/crates/fabro-cli/src/commands/provider/mod.rs +++ b/lib/crates/fabro-cli/src/commands/provider/mod.rs @@ -1,7 +1,6 @@ mod login; use anyhow::Result; -use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; @@ -9,14 +8,13 @@ use crate::args::{ProviderCommand, ProviderNamespace}; pub(crate) async fn dispatch( ns: ProviderNamespace, - cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, ) -> Result<()> { match ns.command { ProviderCommand::Login(args) => { - login::login_command(args, cli, cli_layer, process_local_json, printer).await + login::login_command(args, cli_layer, process_local_json, printer).await } } } diff --git a/lib/crates/fabro-cli/src/commands/repo/init.rs b/lib/crates/fabro-cli/src/commands/repo/init.rs index 6b3d717f3..6a9ba0d28 100644 --- a/lib/crates/fabro-cli/src/commands/repo/init.rs +++ b/lib/crates/fabro-cli/src/commands/repo/init.rs @@ -151,7 +151,7 @@ draft = true } if cli.output.format != OutputFormat::Json { - check_github_app_installation(&args.target, cli, cli_layer, printer).await; + check_github_app_installation(&args.target, cli_layer, printer).await; } Ok(created) @@ -159,7 +159,6 @@ draft = true async fn check_github_app_installation( target: &ServerTargetArgs, - cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) { @@ -200,7 +199,7 @@ async fn check_github_app_installation( return; // Not a GitHub repo — skip silently }; - let ctx = match CommandContext::for_target(target, printer, cli.clone(), cli_layer) { + let ctx = match CommandContext::for_target(target, printer, cli_layer) { Ok(ctx) => ctx, Err(err) => { fabro_util::printerr!( diff --git a/lib/crates/fabro-cli/src/commands/run/command.rs b/lib/crates/fabro-cli/src/commands/run/command.rs index 1f63dbef5..20572cf2b 100644 --- a/lib/crates/fabro-cli/src/commands/run/command.rs +++ b/lib/crates/fabro-cli/src/commands/run/command.rs @@ -16,12 +16,12 @@ pub(crate) async fn execute( printer: Printer, ) -> Result<()> { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?; let cli_defaults = load_settings_with_storage_dir(None)?; args.verbose = args.verbose || cli.output.verbosity == OutputVerbosity::Verbose; let quiet = args.detach; - let prevent_idle_sleep = ctx.cli_settings().exec.prevent_idle_sleep; + let prevent_idle_sleep = ctx.user_settings().cli.exec.prevent_idle_sleep; let created_run = Box::pin(super::create::create_run( &ctx, &args, diff --git a/lib/crates/fabro-cli/src/commands/run/cp.rs b/lib/crates/fabro-cli/src/commands/run/cp.rs index a757ac81c..84c05258e 100644 --- a/lib/crates/fabro-cli/src/commands/run/cp.rs +++ b/lib/crates/fabro-cli/src/commands/run/cp.rs @@ -41,8 +41,7 @@ pub(crate) async fn cp_command( local_path, } => { let (client, run_id) = - resolve_client_and_run_id(&args.server, &run_prefix, cli, cli_layer, printer) - .await?; + resolve_client_and_run_id(&args.server, &run_prefix, cli_layer, printer).await?; let file_count = if args.recursive { Some(download_recursive(&client, &run_id, &remote_path, &local_path).await?) @@ -73,8 +72,7 @@ pub(crate) async fn cp_command( remote_path, } => { let (client, run_id) = - resolve_client_and_run_id(&args.server, &run_prefix, cli, cli_layer, printer) - .await?; + resolve_client_and_run_id(&args.server, &run_prefix, cli_layer, printer).await?; let file_count = if args.recursive { Some(upload_recursive(&client, &run_id, &local_path, &remote_path).await?) @@ -128,11 +126,10 @@ fn parse_direction(src: &str, dst: &str) -> Result { async fn resolve_client_and_run_id( server: &ServerTargetArgs, run_prefix: &str, - cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<(Client, fabro_types::RunId)> { - let ctx = CommandContext::for_target(server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(server, printer, cli_layer)?; let client = ctx.server().await?; let run_id = client.resolve_run(run_prefix).await?.run_id; Ok((client.clone_for_reuse(), run_id)) diff --git a/lib/crates/fabro-cli/src/commands/run/diff.rs b/lib/crates/fabro-cli/src/commands/run/diff.rs index 138c54162..51da824cc 100644 --- a/lib/crates/fabro-cli/src/commands/run/diff.rs +++ b/lib/crates/fabro-cli/src/commands/run/diff.rs @@ -27,7 +27,7 @@ pub(crate) async fn run( printer: Printer, ) -> Result<()> { info!(run_id = %args.run, "Showing diff"); - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run).await?.run_id; let state = client.get_run_state(&run_id).await?; diff --git a/lib/crates/fabro-cli/src/commands/run/fork.rs b/lib/crates/fabro-cli/src/commands/run/fork.rs index e08b934b0..70d5ebba6 100644 --- a/lib/crates/fabro-cli/src/commands/run/fork.rs +++ b/lib/crates/fabro-cli/src/commands/run/fork.rs @@ -21,7 +21,7 @@ pub(crate) async fn run( printer: Printer, ) -> Result<()> { let repo = Repository::discover(".").context("not in a git repository")?; - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run_id).await?.run_id; let state = client.get_run_state(&run_id).await?; diff --git a/lib/crates/fabro-cli/src/commands/run/logs.rs b/lib/crates/fabro-cli/src/commands/run/logs.rs index fbaca77cd..9098c2e85 100644 --- a/lib/crates/fabro-cli/src/commands/run/logs.rs +++ b/lib/crates/fabro-cli/src/commands/run/logs.rs @@ -36,7 +36,7 @@ pub(crate) async fn run( cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run).await?.run_id; info!(run_id = %run_id, "Showing logs"); diff --git a/lib/crates/fabro-cli/src/commands/run/mod.rs b/lib/crates/fabro-cli/src/commands/run/mod.rs index ff0641f29..7acd35e3e 100644 --- a/lib/crates/fabro-cli/src/commands/run/mod.rs +++ b/lib/crates/fabro-cli/src/commands/run/mod.rs @@ -39,7 +39,7 @@ pub(crate) async fn dispatch( RunCommands::Create(args) => { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); let cli_defaults = load_settings_with_storage_dir(None)?; - let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?; let created_run = Box::pin(create::create_run( &ctx, &args, @@ -57,7 +57,7 @@ pub(crate) async fn dispatch( Ok(()) } RunCommands::Start(StartArgs { server, run }) => { - let ctx = CommandContext::for_target(&server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&server, printer, cli_layer)?; let client = ctx.server().await?; let run_id = client.resolve_run(&run).await?.run_id; start::start_run_with_client(client.as_ref(), &run_id, false).await?; @@ -68,7 +68,7 @@ pub(crate) async fn dispatch( } RunCommands::Attach(AttachArgs { server, run }) => { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let ctx = CommandContext::for_target(&server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&server, printer, cli_layer)?; let client = ctx.server().await?; let run_id = client.resolve_run(&run).await?.run_id; let exit_code = Box::pin(attach::attach_run_with_client( @@ -113,9 +113,8 @@ pub(crate) async fn dispatch( let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); #[cfg(feature = "sleep_inhibitor")] let _sleep_guard = { - let ctx = - CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; - crate::sleep_inhibitor::guard(ctx.cli_settings().exec.prevent_idle_sleep) + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; + crate::sleep_inhibitor::guard(ctx.user_settings().cli.exec.prevent_idle_sleep) }; Box::pin(resume::resume_command( args, styles, cli, cli_layer, printer, diff --git a/lib/crates/fabro-cli/src/commands/run/preview.rs b/lib/crates/fabro-cli/src/commands/run/preview.rs index 1bcb5f6b2..f344ffd76 100644 --- a/lib/crates/fabro-cli/src/commands/run/preview.rs +++ b/lib/crates/fabro-cli/src/commands/run/preview.rs @@ -15,7 +15,7 @@ pub(crate) async fn run( process_local_json: bool, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run).await?.run_id; let expires_in_secs = diff --git a/lib/crates/fabro-cli/src/commands/run/resume.rs b/lib/crates/fabro-cli/src/commands/run/resume.rs index fb175cb51..9c892ea14 100644 --- a/lib/crates/fabro-cli/src/commands/run/resume.rs +++ b/lib/crates/fabro-cli/src/commands/run/resume.rs @@ -19,7 +19,7 @@ pub(crate) async fn resume_command( cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run).await?.run_id; diff --git a/lib/crates/fabro-cli/src/commands/run/rewind.rs b/lib/crates/fabro-cli/src/commands/run/rewind.rs index 6e65a5f98..1c541f2da 100644 --- a/lib/crates/fabro-cli/src/commands/run/rewind.rs +++ b/lib/crates/fabro-cli/src/commands/run/rewind.rs @@ -38,7 +38,7 @@ pub(crate) async fn run( printer: Printer, ) -> Result<()> { let repo = Repository::discover(".").context("not in a git repository")?; - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run_id).await?.run_id; let state = client.get_run_state(&run_id).await?; diff --git a/lib/crates/fabro-cli/src/commands/run/ssh.rs b/lib/crates/fabro-cli/src/commands/run/ssh.rs index f4465d499..d3ce4f9f8 100644 --- a/lib/crates/fabro-cli/src/commands/run/ssh.rs +++ b/lib/crates/fabro-cli/src/commands/run/ssh.rs @@ -19,7 +19,7 @@ pub(crate) async fn run( require_no_json_override(process_local_json)?; } - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run).await?.run_id; let ssh = client.create_run_ssh_access(&run_id, args.ttl).await?; diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/crates/fabro-cli/src/commands/run/wait.rs index 639960771..4642cddf1 100644 --- a/lib/crates/fabro-cli/src/commands/run/wait.rs +++ b/lib/crates/fabro-cli/src/commands/run/wait.rs @@ -31,7 +31,7 @@ pub(crate) async fn run( cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run).await?.run_id; info!(run_id = %run_id, "Waiting for run to complete"); diff --git a/lib/crates/fabro-cli/src/commands/runs/archive.rs b/lib/crates/fabro-cli/src/commands/runs/archive.rs index 6052d9457..dcc996fc9 100644 --- a/lib/crates/fabro-cli/src/commands/runs/archive.rs +++ b/lib/crates/fabro-cli/src/commands/runs/archive.rs @@ -15,7 +15,7 @@ pub(crate) async fn archive_command( cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; run_bulk( Action::Archive, &args.runs, @@ -32,7 +32,7 @@ pub(crate) async fn unarchive_command( cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; run_bulk( Action::Unarchive, &args.runs, diff --git a/lib/crates/fabro-cli/src/commands/runs/inspect.rs b/lib/crates/fabro-cli/src/commands/runs/inspect.rs index 7858260f1..0ac3f1c4e 100644 --- a/lib/crates/fabro-cli/src/commands/runs/inspect.rs +++ b/lib/crates/fabro-cli/src/commands/runs/inspect.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; use fabro_workflow::run_status::RunStatus; @@ -21,13 +20,8 @@ pub(crate) struct InspectOutput { pub sandbox: Option, } -pub(crate) async fn run( - args: &InspectArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; +pub(crate) async fn run(args: &InspectArgs, cli_layer: &CliLayer, printer: Printer) -> Result<()> { + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; let client = ctx.server().await?; let run = ServerRunSummaryInfo::from_summary(client.resolve_run(&args.run).await?); let run_id = run.run_id(); diff --git a/lib/crates/fabro-cli/src/commands/runs/list.rs b/lib/crates/fabro-cli/src/commands/runs/list.rs index 8b81b5f7b..2e48a3843 100644 --- a/lib/crates/fabro-cli/src/commands/runs/list.rs +++ b/lib/crates/fabro-cli/src/commands/runs/list.rs @@ -24,7 +24,7 @@ pub(crate) async fn list_command( cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let label_filters = parse_label_filters(&args.filter.label); let filtered = filter_server_runs( diff --git a/lib/crates/fabro-cli/src/commands/runs/mod.rs b/lib/crates/fabro-cli/src/commands/runs/mod.rs index fb62b7f2a..3804d4052 100644 --- a/lib/crates/fabro-cli/src/commands/runs/mod.rs +++ b/lib/crates/fabro-cli/src/commands/runs/mod.rs @@ -23,7 +23,7 @@ pub(crate) async fn dispatch( list::list_command(&args, &styles, cli, cli_layer, printer).await } RunsCommands::Rm(args) => rm::remove_command(&args, cli, cli_layer, printer).await, - RunsCommands::Inspect(args) => inspect::run(&args, cli, cli_layer, printer).await, + RunsCommands::Inspect(args) => inspect::run(&args, cli_layer, printer).await, RunsCommands::Archive(args) => { archive::archive_command(&args, cli, cli_layer, printer).await } diff --git a/lib/crates/fabro-cli/src/commands/runs/rm.rs b/lib/crates/fabro-cli/src/commands/runs/rm.rs index eb1047332..8990d4efc 100644 --- a/lib/crates/fabro-cli/src/commands/runs/rm.rs +++ b/lib/crates/fabro-cli/src/commands/runs/rm.rs @@ -15,7 +15,7 @@ pub(crate) async fn remove_command( cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; remove_from(args, ctx.server().await?.as_ref(), cli, printer).await } diff --git a/lib/crates/fabro-cli/src/commands/secret/mod.rs b/lib/crates/fabro-cli/src/commands/secret/mod.rs index 59146f0a8..19fdeef73 100644 --- a/lib/crates/fabro-cli/src/commands/secret/mod.rs +++ b/lib/crates/fabro-cli/src/commands/secret/mod.rs @@ -16,7 +16,7 @@ pub(crate) async fn dispatch( cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&ns.target, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&ns.target, printer, cli_layer)?; let server = ctx.server().await?; match ns.command { SecretCommand::List(args) => list::list_command(&server, &args, cli, printer).await, diff --git a/lib/crates/fabro-cli/src/commands/store/dump.rs b/lib/crates/fabro-cli/src/commands/store/dump.rs index 1713ce6b2..935ec6c0e 100644 --- a/lib/crates/fabro-cli/src/commands/store/dump.rs +++ b/lib/crates/fabro-cli/src/commands/store/dump.rs @@ -32,7 +32,7 @@ pub(crate) async fn dump_command( cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run).await?.run_id; let state = client.get_run_state(&run_id).await?; diff --git a/lib/crates/fabro-cli/src/commands/system/df.rs b/lib/crates/fabro-cli/src/commands/system/df.rs index f5cebe0b4..67a099a2e 100644 --- a/lib/crates/fabro-cli/src/commands/system/df.rs +++ b/lib/crates/fabro-cli/src/commands/system/df.rs @@ -17,7 +17,7 @@ pub(super) async fn df_command( cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?; let server = ctx.server().await?; let json = cli.output.format == OutputFormat::Json; diff --git a/lib/crates/fabro-cli/src/commands/system/events.rs b/lib/crates/fabro-cli/src/commands/system/events.rs index 7e94cfa1d..1e8d69346 100644 --- a/lib/crates/fabro-cli/src/commands/system/events.rs +++ b/lib/crates/fabro-cli/src/commands/system/events.rs @@ -14,7 +14,7 @@ pub(super) async fn events_command( cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?; let server = ctx.server().await?; let mut stream = server.attach_events(&args.run_ids).await?; let mut pending = Vec::new(); diff --git a/lib/crates/fabro-cli/src/commands/system/info.rs b/lib/crates/fabro-cli/src/commands/system/info.rs index a1c584453..314b3f347 100644 --- a/lib/crates/fabro-cli/src/commands/system/info.rs +++ b/lib/crates/fabro-cli/src/commands/system/info.rs @@ -13,7 +13,7 @@ pub(super) async fn info_command( cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?; let server = ctx.server().await?; let response = server.get_system_info().await?; diff --git a/lib/crates/fabro-cli/src/commands/system/prune.rs b/lib/crates/fabro-cli/src/commands/system/prune.rs index f7242b023..376ae2003 100644 --- a/lib/crates/fabro-cli/src/commands/system/prune.rs +++ b/lib/crates/fabro-cli/src/commands/system/prune.rs @@ -17,7 +17,7 @@ pub(super) async fn prune_command( cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?; let server = ctx.server().await?; let response = server .prune_runs(types::PruneRunsRequest { diff --git a/lib/crates/fabro-cli/src/commands/validate.rs b/lib/crates/fabro-cli/src/commands/validate.rs index 95691b74e..74bba2d11 100644 --- a/lib/crates/fabro-cli/src/commands/validate.rs +++ b/lib/crates/fabro-cli/src/commands/validate.rs @@ -19,7 +19,7 @@ pub(crate) async fn run( cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result<()> { - let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?; let built = build_run_manifest(ManifestBuildInput { workflow: args.workflow.clone(), cwd: ctx.cwd().to_path_buf(), diff --git a/lib/crates/fabro-cli/src/commands/version.rs b/lib/crates/fabro-cli/src/commands/version.rs index 826ec108b..993557834 100644 --- a/lib/crates/fabro-cli/src/commands/version.rs +++ b/lib/crates/fabro-cli/src/commands/version.rs @@ -23,7 +23,7 @@ pub(crate) async fn version_command( printer: Printer, ) -> Result<()> { let client = client_info(); - let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; + let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?; let server_target = user_config::resolve_server_target(&args.target, ctx.machine_settings())?; let server_address = format_server_target(&server_target); let server_info = match ctx.server().await { diff --git a/lib/crates/fabro-cli/src/local_server.rs b/lib/crates/fabro-cli/src/local_server.rs index 5128e70da..cdba3b556 100644 --- a/lib/crates/fabro-cli/src/local_server.rs +++ b/lib/crates/fabro-cli/src/local_server.rs @@ -26,12 +26,8 @@ pub(crate) fn bind_request( resolve_bind_request_from_settings(settings, cli_override) } -pub(crate) fn server_settings(settings: &SettingsLayer) -> Result { - fabro_config::ServerSettings::from_layer(settings).map_err(anyhow::Error::from) -} - pub(crate) fn auth_methods(settings: &SettingsLayer) -> Vec { - server_settings(settings) + fabro_config::ServerSettings::from_layer(settings) .map(|resolved| resolved.server.auth.methods) .unwrap_or_default() } diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index bd2cd9e06..6e54808ff 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -173,9 +173,9 @@ async fn main_inner() -> (String, Result<()>) { cli: Some(cli_layer.clone()), ..SettingsLayer::default() }); - let cli_settings = match user_config::resolve_cli_settings(&combined_settings) { - Ok(cli_settings) => cli_settings, - Err(err) => return (command_name, Err(err)), + let cli_settings = match fabro_config::UserSettings::from_layer(&combined_settings) { + Ok(settings) => settings.cli, + Err(err) => return (command_name, Err(err.into())), }; let printer = printer_from_verbosity(cli_settings.output.verbosity); @@ -318,14 +318,7 @@ async fn main_inner() -> (String, Result<()>) { commands::uninstall::run_uninstall(&args, &cli_settings, printer).await?; } Commands::Auth(ns) => { - commands::auth::dispatch( - ns, - &cli_settings, - &cli_layer, - process_local_json, - printer, - ) - .await?; + commands::auth::dispatch(ns, &cli_layer, process_local_json, printer).await?; } Commands::Pr(ns) => { Box::pin(commands::pr::dispatch( @@ -353,14 +346,7 @@ async fn main_inner() -> (String, Result<()>) { commands::upgrade::run_upgrade(args, &cli_settings, printer).await?; } Commands::Provider(ns) => { - commands::provider::dispatch( - ns, - &cli_settings, - &cli_layer, - process_local_json, - printer, - ) - .await?; + commands::provider::dispatch(ns, &cli_layer, process_local_json, printer).await?; } Commands::Sandbox { command } => { commands::sandbox::dispatch( diff --git a/lib/crates/fabro-cli/src/user_config.rs b/lib/crates/fabro-cli/src/user_config.rs index 687831cc4..03429dbb4 100644 --- a/lib/crates/fabro-cli/src/user_config.rs +++ b/lib/crates/fabro-cli/src/user_config.rs @@ -30,16 +30,6 @@ pub(crate) fn load_settings_with_config_and_storage_dir( Ok(apply_storage_dir_override(layer, storage_dir)) } -pub(crate) fn resolve_user_settings( - file: &SettingsLayer, -) -> anyhow::Result { - fabro_config::UserSettings::from_layer(file).map_err(anyhow::Error::from) -} - -pub(crate) fn resolve_cli_settings(file: &SettingsLayer) -> anyhow::Result { - resolve_user_settings(file).map(|settings| settings.cli) -} - pub(crate) fn apply_storage_dir_override( mut layer: SettingsLayer, storage_dir: Option<&Path>, @@ -68,7 +58,7 @@ fn cli_target_from_settings(settings: &CliNamespace) -> Option { } fn configured_server_target(settings: &SettingsLayer) -> Result> { - let user_settings = resolve_user_settings(settings)?; + let user_settings = fabro_config::UserSettings::from_layer(settings)?; let Some(value) = cli_target_from_settings(&user_settings.cli) else { return Ok(None); }; diff --git a/lib/crates/fabro-config/src/lib.rs b/lib/crates/fabro-config/src/lib.rs index dc654cbeb..24af07e32 100644 --- a/lib/crates/fabro-config/src/lib.rs +++ b/lib/crates/fabro-config/src/lib.rs @@ -2,10 +2,8 @@ clippy::disallowed_methods, reason = "sync config loading utilities used at startup; not on a Tokio path" )] -//! Settings resolution entrypoints are owner-first context types: -//! [`ServerSettings`] for current server/runtime config and [`UserSettings`] -//! for current CLI/user config. Stored `SettingsLayer` artifacts still use the -//! per-namespace `resolve_*_from_file` helpers. +//! Resolved settings entrypoints: [`ServerSettings`] for the running server and +//! [`UserSettings`] for the CLI/user perspective. extern crate self as fabro_config; diff --git a/lib/crates/fabro-server/src/canonical_origin.rs b/lib/crates/fabro-server/src/canonical_origin.rs index 05bd7793c..203a86abe 100644 --- a/lib/crates/fabro-server/src/canonical_origin.rs +++ b/lib/crates/fabro-server/src/canonical_origin.rs @@ -1,10 +1,10 @@ -use fabro_types::settings::ServerNamespace as ResolvedServerSettings; +use fabro_types::settings::ServerNamespace; use url::Url; use crate::server::EnvLookup; pub(crate) fn resolve_canonical_origin( - resolved: &ResolvedServerSettings, + resolved: &ServerNamespace, env_lookup: &EnvLookup, ) -> Result { let value = resolved diff --git a/lib/crates/fabro-server/src/jwt_auth.rs b/lib/crates/fabro-server/src/jwt_auth.rs index 4b2b53d54..b332cb797 100644 --- a/lib/crates/fabro-server/src/jwt_auth.rs +++ b/lib/crates/fabro-server/src/jwt_auth.rs @@ -2,7 +2,7 @@ use anyhow::{Result, anyhow}; use axum::extract::FromRequestParts; use axum::http::header; use axum::http::request::Parts; -use fabro_types::settings::{ServerAuthMethod, ServerNamespace as ResolvedServerSettings}; +use fabro_types::settings::{ServerAuthMethod, ServerNamespace}; use fabro_types::{IdpIdentity, RunAuthMethod}; use fabro_util::dev_token::validate_dev_token_format; use hmac::{Hmac, Mac}; @@ -51,14 +51,11 @@ pub enum AuthMode { Disabled, } -pub fn resolve_auth_mode(settings: &ResolvedServerSettings) -> Result { +pub fn resolve_auth_mode(settings: &ServerNamespace) -> Result { resolve_auth_mode_with_lookup(settings, |name| std::env::var(name).ok()) } -pub fn resolve_auth_mode_with_lookup( - settings: &ResolvedServerSettings, - lookup: F, -) -> Result +pub fn resolve_auth_mode_with_lookup(settings: &ServerNamespace, lookup: F) -> Result where F: Fn(&str) -> Option, { @@ -124,7 +121,7 @@ where })) } -fn resolve_jwt_issuer(settings: &ResolvedServerSettings, lookup: &F) -> String +fn resolve_jwt_issuer(settings: &ServerNamespace, lookup: &F) -> String where F: Fn(&str) -> Option, { @@ -389,7 +386,7 @@ mod tests { use tracing_subscriber::{Layer, Registry}; use super::*; - fn settings(source: &str) -> ResolvedServerSettings { + fn settings(source: &str) -> ServerNamespace { let file = parse_settings_layer(source).expect("fixture should parse"); resolve_server_from_file(&file).expect("fixture should resolve") } diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs index 0055eb108..137928875 100644 --- a/lib/crates/fabro-server/src/serve.rs +++ b/lib/crates/fabro-server/src/serve.rs @@ -8,14 +8,14 @@ use clap::Args; use fabro_config::bind::{self, Bind, BindRequest}; use fabro_config::merge::combine_files; use fabro_config::user::load_settings_config; -use fabro_config::{ServerSettings as CurrentServerSettings, Storage}; +use fabro_config::{ServerSettings, Storage}; use fabro_sandbox::SandboxProvider; use fabro_types::settings::server::{ GithubIntegrationStrategy, ServerLayer, ServerListenLayer, WebhookStrategy, }; use fabro_types::settings::{ GithubIntegrationSettings, InterpString, ObjectStoreSettings, ServerListenSettings, - ServerNamespace as ResolvedServerSettings, SettingsLayer, + ServerNamespace, SettingsLayer, }; use fabro_util::terminal::Styles; use object_store::ObjectStore; @@ -142,12 +142,12 @@ fn apply_runtime_settings( settings } -fn router_web_enabled(settings: &ResolvedServerSettings) -> bool { +fn router_web_enabled(settings: &ServerNamespace) -> bool { settings.web.enabled } async fn resolve_github_webhook_ip_allowlist( - resolved_server_settings: &ResolvedServerSettings, + resolved_server_settings: &ServerNamespace, github_meta_resolver: &GitHubMetaResolver, ) -> anyhow::Result> { let config = resolve_ip_allowlist_config( @@ -167,7 +167,7 @@ async fn resolve_github_webhook_ip_allowlist( } async fn resolve_startup_github_webhook_ip_allowlist( - resolved_server_settings: &ResolvedServerSettings, + resolved_server_settings: &ServerNamespace, github_meta_resolver: &GitHubMetaResolver, webhook_secret_present: bool, ) -> anyhow::Result>> { @@ -228,7 +228,7 @@ fn resolve_webhook_preconditions( } async fn start_webhook_strategy( - resolved_server_settings: &ResolvedServerSettings, + resolved_server_settings: &ServerNamespace, state: &Arc, bind_addr: &Bind, webhook_secret_present: bool, @@ -349,8 +349,8 @@ fn build_object_store_from_settings( } } -fn resolve_server_settings(file: &SettingsLayer) -> anyhow::Result { - CurrentServerSettings::from_layer(file) +fn resolve_server_settings(file: &SettingsLayer) -> anyhow::Result { + ServerSettings::from_layer(file) .map(|settings| settings.server) .map_err(anyhow::Error::from) } @@ -391,7 +391,7 @@ fn bind_override_layer(bind: BindRequest) -> SettingsLayer { } fn resolved_bind_request( - resolved_server_settings: &ResolvedServerSettings, + resolved_server_settings: &ServerNamespace, ) -> anyhow::Result { match &resolved_server_settings.listen { ServerListenSettings::Unix { path } => Ok(BindRequest::Unix(resolve_interp_path(path)?)), @@ -411,7 +411,7 @@ fn resolve_interp_path(value: &InterpString) -> anyhow::Result { } pub fn build_artifact_object_store( - settings: &ResolvedServerSettings, + settings: &ServerNamespace, ) -> anyhow::Result<(Arc, String)> { let prefix = resolve_interp(&settings.artifacts.prefix)?; let object_store = build_object_store_from_settings(&settings.artifacts.store)?; @@ -419,7 +419,7 @@ pub fn build_artifact_object_store( } fn build_slatedb_store( - settings: &ResolvedServerSettings, + settings: &ServerNamespace, ) -> anyhow::Result<(Arc, String, Duration, bool)> { let prefix = resolve_interp(&settings.slatedb.prefix)?; let object_store = build_object_store_from_settings(&settings.slatedb.store)?; diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index 6d2526cb1..c0fe087b8 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -40,7 +40,7 @@ pub use fabro_api::types::{ }; use fabro_auth::parse_credential_secret; use fabro_config::daemon::ServerDaemon; -use fabro_config::{ServerSettings as CurrentServerSettings, Storage}; +use fabro_config::{ServerSettings, Storage}; use fabro_interview::{ Answer, ControlInterviewer, Interviewer, Question, QuestionType, WorkerControlEnvelope, }; @@ -573,7 +573,7 @@ pub struct AppState { pub(crate) server_secrets: ServerSecrets, pub(crate) provider_credentials: ProviderCredentials, pub(crate) settings: Arc>, - pub(crate) server_settings: RwLock>, + pub(crate) server_settings: RwLock>, pub(crate) env_lookup: EnvLookup, http_client: Option, shutting_down: AtomicBool, @@ -638,7 +638,7 @@ fn accumulate_model_billing(entry: &mut ModelBillingTotals, usage: &BilledModelU } impl AppState { - pub(crate) fn server_settings(&self) -> Arc { + pub(crate) fn server_settings(&self) -> Arc { Arc::clone( &self .server_settings @@ -780,7 +780,7 @@ impl AppState { } pub(crate) fn replace_settings(&self, settings: SettingsLayer) -> anyhow::Result<()> { - let resolved = Arc::new(CurrentServerSettings::from_layer(&settings)?); + let resolved = Arc::new(ServerSettings::from_layer(&settings)?); resolve_canonical_origin(&resolved.server, &self.env_lookup).map_err(anyhow::Error::msg)?; *self.settings.write().expect("settings lock poisoned") = settings; @@ -1656,7 +1656,7 @@ fn system_sandbox_provider(settings: &SettingsLayer) -> String { } fn resolved_storage_dir(settings: &SettingsLayer) -> Result { - let resolved = CurrentServerSettings::from_layer(settings).map_err(|err| err.to_string())?; + let resolved = ServerSettings::from_layer(settings).map_err(|err| err.to_string())?; resolved .server .storage @@ -1672,7 +1672,7 @@ fn resolved_storage_dir(settings: &SettingsLayer) -> Result { } fn resolved_github_settings(settings: &SettingsLayer) -> Result { - let resolved = CurrentServerSettings::from_layer(settings).map_err(|err| err.to_string())?; + let resolved = ServerSettings::from_layer(settings).map_err(|err| err.to_string())?; Ok(resolved.server.integrations.github) } @@ -2589,7 +2589,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result Date: Wed, 22 Apr 2026 21:48:20 -0400 Subject: [PATCH 05/13] lint: fix clippy absolute_paths & disallowed_methods after merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Import serde::de::Error trait so the `custom` fn pointer uses `D::Error` instead of the absolute `serde::de::Error::custom` path. - Import `fabro_api::types::ServerSettings` / `fabro_config::UserSettings` directly rather than through absolute paths. - Gate sync `std::fs::write` fixture setup in new config resolver tests with a file-level `#![expect(clippy::disallowed_methods, …)]`. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/crates/fabro-cli/src/commands/config/mod.rs | 6 ++++-- lib/crates/fabro-client/src/client.rs | 6 ++---- lib/crates/fabro-config/tests/resolve_cli.rs | 5 +++++ lib/crates/fabro-config/tests/resolve_server.rs | 5 +++++ lib/crates/fabro-types/src/settings/server.rs | 3 ++- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/crates/fabro-cli/src/commands/config/mod.rs b/lib/crates/fabro-cli/src/commands/config/mod.rs index 670cca826..1535bfa0e 100644 --- a/lib/crates/fabro-cli/src/commands/config/mod.rs +++ b/lib/crates/fabro-cli/src/commands/config/mod.rs @@ -9,6 +9,8 @@ use std::io::Write; +use fabro_api::types::ServerSettings; +use fabro_config::UserSettings; use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; @@ -20,8 +22,8 @@ use crate::shared::print_json_pretty; #[derive(Serialize)] struct RenderedConfig { - user: fabro_config::UserSettings, - server: fabro_api::types::ServerSettings, + user: UserSettings, + server: ServerSettings, } async fn rendered_config( diff --git a/lib/crates/fabro-client/src/client.rs b/lib/crates/fabro-client/src/client.rs index f674ee474..582426a00 100644 --- a/lib/crates/fabro-client/src/client.rs +++ b/lib/crates/fabro-client/src/client.rs @@ -490,16 +490,14 @@ impl Client { } } - pub async fn retrieve_resolved_server_settings( - &self, - ) -> Result { + pub async fn retrieve_resolved_server_settings(&self) -> Result { let url = format!("{}/api/v1/settings", self.base_url()); let response = self .send_http(|http_client| async move { http_client.get(&url).send().await }) .await?; response - .json::() + .json::() .await .context("server returned invalid JSON for server settings") } diff --git a/lib/crates/fabro-config/tests/resolve_cli.rs b/lib/crates/fabro-config/tests/resolve_cli.rs index f6dc06005..01c05b60a 100644 --- a/lib/crates/fabro-config/tests/resolve_cli.rs +++ b/lib/crates/fabro-config/tests/resolve_cli.rs @@ -1,3 +1,8 @@ +#![expect( + clippy::disallowed_methods, + reason = "sync test fixture setup; not on a Tokio path" +)] + use fabro_config::{parse_settings_layer, resolve_cli_from_file}; use fabro_types::settings::cli::{CliTargetSettings, OutputFormat, OutputVerbosity}; use fabro_types::settings::run::AgentPermissions; diff --git a/lib/crates/fabro-config/tests/resolve_server.rs b/lib/crates/fabro-config/tests/resolve_server.rs index 11681904d..445fb834f 100644 --- a/lib/crates/fabro-config/tests/resolve_server.rs +++ b/lib/crates/fabro-config/tests/resolve_server.rs @@ -1,3 +1,8 @@ +#![expect( + clippy::disallowed_methods, + reason = "sync test fixture setup; not on a Tokio path" +)] + use fabro_config::parse_settings_layer; use fabro_config::user::default_storage_dir; use fabro_types::settings::server::{ diff --git a/lib/crates/fabro-types/src/settings/server.rs b/lib/crates/fabro-types/src/settings/server.rs index ef10b7b3b..9e4d300e7 100644 --- a/lib/crates/fabro-types/src/settings/server.rs +++ b/lib/crates/fabro-types/src/settings/server.rs @@ -10,6 +10,7 @@ use std::net::SocketAddr; use std::time::Duration as StdDuration; use ipnet::IpNet; +use serde::de::Error as _; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::duration::Duration as DurationLayer; @@ -287,7 +288,7 @@ where D: Deserializer<'de>, { let value = String::deserialize(deserializer)?; - value.parse().map_err(serde::de::Error::custom) + value.parse().map_err(D::Error::custom) } fn serialize_std_duration(value: &StdDuration, serializer: S) -> Result From 28a9f036d89e5b9ad16ba98c80f07059d8c5630d Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Wed, 22 Apr 2026 23:15:56 -0400 Subject: [PATCH 06/13] fix: server-side settings authority + unit tests that never hit live S3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related correctness bugs surfaced by the failing test suite: 1. Server-owned settings didn't flow into run settings, and the few server-only fields that did leak in made run snapshots bulky and let callers re-resolve server state from the run layer. - effective_settings::materialize_settings_layer now treats the server's run/features stanzas as base defaults (client layers still win where set), and enforce_server_authority keeps the original cherry-pick of storage/scheduler/artifacts/web/api but no longer lets the rest of the server namespace propagate. auth, listen, ip_allowlist, slatedb, logging, and integrations stay on the server, where AppState::server_settings() already has them. - run_preflight, the scheduler start-path, and operations::start now read GitHub integrations from state.server_settings() (or StartServices::github_permissions, which the server populates) instead of re-resolving the server namespace from the run's settings layer. - create_app_state{_with_options,_with_env_lookup,_with_options_and_registry_factory} and create_app_state_with_store_and_env_lookup all route through ensure_test_auth_methods so the strict resolver accepts SettingsLayer::default() in tests. - Fixed the start_run_persists_full_settings_snapshot assertion that expected server.integrations.github.app_id in the run's persisted settings — the new design deliberately omits it. 2. Unit and integration tests were hitting live AWS S3. - Added a NoProxyReqwestConnector (behind a dedicated reqwest 0.12 dep aliased as object_store_reqwest) and wired it through AmazonS3Builder::with_http_connector. macOS SystemConfiguration proxy discovery in the default reqwest client was blowing past nextest's 20s kill timeout on serve.rs's S3 builder unit tests; the no-proxy connector brings them under 15ms. - InstallAppState::for_test_with_paths now sets FABRO_TEST_IN_MEMORY_STORE=1 so /install/finish's artifact-metadata sentinel write short-circuits to the in-memory object store and never contacts AWS. The install integration tests verify persistence/redaction, not S3 reachability. `cargo nextest run --workspace`: 4495/4495 passing. `cargo +nightly-2026-04-14 fmt --check --all`: clean. `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`: clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 1 + .../fabro-cli/src/commands/run/runner.rs | 2 + .../fabro-config/src/effective_settings.rs | 54 +++++--- lib/crates/fabro-server/Cargo.toml | 4 + lib/crates/fabro-server/src/install.rs | 11 ++ lib/crates/fabro-server/src/run_manifest.rs | 12 +- lib/crates/fabro-server/src/serve.rs | 52 ++++++++ lib/crates/fabro-server/src/server.rs | 115 +++++++----------- .../fabro-workflow/src/operations/start.rs | 48 +++----- 9 files changed, 173 insertions(+), 126 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d410b4ea5..a6834259a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2052,6 +2052,7 @@ dependencies = [ "percent-encoding", "rand 0.9.4", "regex", + "reqwest 0.12.28", "semver", "serde", "serde_json", diff --git a/lib/crates/fabro-cli/src/commands/run/runner.rs b/lib/crates/fabro-cli/src/commands/run/runner.rs index 898ce27ae..b587fb3a1 100644 --- a/lib/crates/fabro-cli/src/commands/run/runner.rs +++ b/lib/crates/fabro-cli/src/commands/run/runner.rs @@ -4,6 +4,7 @@ std::io::BufReader; not on a Tokio path" )] +use std::collections::HashMap; use std::io::{BufRead as StdBufRead, BufReader as StdBufReader}; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -107,6 +108,7 @@ pub(crate) async fn execute( artifact_sink, run_control: Some(run_control), github_app, + github_permissions: HashMap::new(), vault, on_node: None, registry_override: None, diff --git a/lib/crates/fabro-config/src/effective_settings.rs b/lib/crates/fabro-config/src/effective_settings.rs index bf69160ec..9a42750b8 100644 --- a/lib/crates/fabro-config/src/effective_settings.rs +++ b/lib/crates/fabro-config/src/effective_settings.rs @@ -43,6 +43,25 @@ impl EffectiveSettingsLayers { /// Materialize layered configuration down to a single effective /// [`SettingsLayer`]. +/// +/// Precedence, lowest to highest: +/// +/// 1. `server_settings.run` / `server_settings.features` — the server's +/// `~/.fabro/settings.toml` contributes its run-level defaults (for example, +/// a server-wide `run.execution.mode = "dry_run"`). Client layers win when +/// they set the same field. +/// 2. `user` — the manifest's `User` configs. +/// 3. `project` — the manifest's `Project` configs, with `cli`/`server` +/// stripped. +/// 4. `workflow` — the manifest's workflow config, with `cli`/`server` +/// stripped. +/// 5. `args` — process-local CLI overrides. +/// 6. A small subset of `server_settings.server` (storage, scheduler, +/// artifacts, web, api) plus `server_settings.features` are applied +/// authoritatively on top. The remaining server-ops fields (listen, auth, +/// ip_allowlist, slatedb, logging, integrations) stay on the server and do +/// not flow into the run's persisted settings — callers that need them +/// should consult the server's resolved settings directly. pub fn materialize_settings_layer( layers: EffectiveSettingsLayers, server_settings: Option<&SettingsLayer>, @@ -61,20 +80,20 @@ pub fn materialize_settings_layer( strip_owner_domains(&mut workflow); strip_owner_domains(&mut project); - let combined = combine_files(combine_files(combine_files(user, project), workflow), args); - let mut settings = enforce_server_authority(combined, server_settings); + // Server's run/features stanzas act as base defaults; client layers win. + // Server's server/cli stanzas are handled authoritatively below. + let mut server_defaults = server_settings.clone(); + server_defaults.cli = None; + server_defaults.server = None; - // Storage root always comes from the server's local ~/.fabro/settings.toml, - // never from the client. - if let Some(server_root) = server_settings - .server - .as_ref() - .and_then(|server| server.storage.as_ref()) - .cloned() - { - let server = settings.server.get_or_insert_with(ServerLayer::default); - server.storage = Some(server_root); - } + let combined = combine_files( + combine_files( + combine_files(combine_files(server_defaults, user), project), + workflow, + ), + args, + ); + let settings = enforce_server_authority(combined, server_settings); Ok(apply_builtin_defaults(settings)) } @@ -84,10 +103,13 @@ fn strip_owner_domains(file: &mut SettingsLayer) { file.server = None; } -/// Enforce server-owned fields on a client-layered [`SettingsLayer`]. +/// Apply server-owned fields on top of a client-combined [`SettingsLayer`]. /// -/// A subset of server-owned fields unconditionally override any client-side -/// values. Client-controlled run-level fields are left alone. +/// Only the fields runs genuinely need are copied from the server: +/// `storage`, `scheduler`, `artifacts`, `web`, `api`. Operational config +/// (`listen`, `auth`, `ip_allowlist`, `slatedb`, `logging`, `integrations`) +/// stays on the server — callers that need those fields should read +/// `AppState::server_settings()` rather than re-resolving from run settings. fn enforce_server_authority(mut settings: SettingsLayer, server: &SettingsLayer) -> SettingsLayer { if let Some(server_layer) = server.server.clone() { let client = settings.server.get_or_insert_with(ServerLayer::default); diff --git a/lib/crates/fabro-server/Cargo.toml b/lib/crates/fabro-server/Cargo.toml index c74a8da7e..a7851a872 100644 --- a/lib/crates/fabro-server/Cargo.toml +++ b/lib/crates/fabro-server/Cargo.toml @@ -67,6 +67,10 @@ rand.workspace = true bytes = "1" tempfile = "3" object_store.workspace = true +# reqwest 0.12 is a direct dep only so `NoProxyReqwestConnector` can build a +# `reqwest::Client` of the version object_store accepts. Rest of the crate +# talks to HTTP via fabro-http (reqwest 0.13). +object_store_reqwest = { package = "reqwest", version = "0.12", default-features = false, features = ["rustls-tls-native-roots"] } mime_guess.workspace = true regex.workspace = true semver.workspace = true diff --git a/lib/crates/fabro-server/src/install.rs b/lib/crates/fabro-server/src/install.rs index d88cc2563..e1dbe6969 100644 --- a/lib/crates/fabro-server/src/install.rs +++ b/lib/crates/fabro-server/src/install.rs @@ -108,7 +108,18 @@ impl InstallAppState { } #[must_use] + #[expect( + unsafe_code, + reason = "test-only: set FABRO_TEST_IN_MEMORY_STORE to a constant so install tests \ + don't hang on real S3; parallel tests race on the same value" + )] pub fn for_test_with_paths(token: &str, storage_dir: &Path, config_path: &Path) -> Self { + // Install-flow tests verify persistence and redaction, not S3 + // reachability. Force the in-memory object store shortcut so + // /install/finish can't hang on an unreachable bucket. + unsafe { + std::env::set_var("FABRO_TEST_IN_MEMORY_STORE", "1"); + } Self { install_token: Arc::from(token), pending_install: Arc::new(Mutex::new(PendingInstall::default())), diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs index 96d5db9b6..f4ed75a3d 100644 --- a/lib/crates/fabro-server/src/run_manifest.rs +++ b/lib/crates/fabro-server/src/run_manifest.rs @@ -362,8 +362,8 @@ async fn build_preflight_report( ); let resolved_run = fabro_config::resolve_run_from_file(&materialized) .map_err(|errors| anyhow!(render_resolve_errors(&errors)))?; - let resolved_server = fabro_config::resolve_server_from_file(settings) - .map_err(|errors| anyhow!(render_resolve_errors(&errors)))?; + let server_settings = state.server_settings(); + let github_integration = &server_settings.server.integrations.github; let sandbox_provider = resolve_sandbox_provider(&resolved_run)?; let sandbox_provider = if resolved_run.execution.mode == RunMode::DryRun && !sandbox_provider.is_local() { @@ -371,11 +371,11 @@ async fn build_preflight_report( } else { sandbox_provider }; - let needs_github_credentials = sandbox_provider == SandboxProvider::Daytona - || !resolved_server.integrations.github.permissions.is_empty(); + let needs_github_credentials = + sandbox_provider == SandboxProvider::Daytona || !github_integration.permissions.is_empty(); let github_app = if needs_github_credentials { state - .github_credentials(&resolved_server.integrations.github) + .github_credentials(github_integration) .unwrap_or_default() } else { None @@ -399,7 +399,7 @@ async fn build_preflight_report( &configured_providers, ) .await; - run_github_token_check(&mut checks, prepared, &resolved_server, github_app).await; + run_github_token_check(&mut checks, prepared, &server_settings.server, github_app).await; let checks_ok = sandbox_ok && llm_ok; diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs index f969e9a60..d35b1a46f 100644 --- a/lib/crates/fabro-server/src/serve.rs +++ b/lib/crates/fabro-server/src/serve.rs @@ -20,6 +20,7 @@ use fabro_types::settings::{ }; use fabro_util::terminal::Styles; use object_store::aws::{AmazonS3Builder, AmazonS3ConfigKey}; +use object_store::client::{HttpClient, HttpConnector}; use object_store::local::LocalFileSystem; use object_store::memory::InMemory; use object_store::{ClientOptions, ObjectStore, RetryConfig}; @@ -58,6 +59,56 @@ impl Default for ObjectStoreBuildOptions { } } +/// `HttpConnector` that builds a `reqwest::Client` with `.no_proxy()`. +/// +/// The object_store default `ReqwestConnector` calls `reqwest::Client::new()`, +/// which on macOS probes `SystemConfiguration` for proxies every time it runs. +/// That probe can stall long enough to blow past test timeouts. S3/MinIO +/// traffic goes directly to the configured endpoint, so skipping proxy +/// discovery is safe and keeps startup predictable. +#[derive(Debug)] +struct NoProxyReqwestConnector; + +impl HttpConnector for NoProxyReqwestConnector { + #[expect( + clippy::disallowed_methods, + reason = "object_store pins reqwest 0.12 and object_store::HttpClient::new requires \ + that exact version; we can't route through fabro_http (reqwest 0.13)" + )] + fn connect(&self, options: &ClientOptions) -> object_store::Result { + let mut builder = object_store_reqwest::Client::builder().no_proxy(); + if let Some(raw) = options.get_config_value(&object_store::ClientConfigKey::Timeout) { + if let Some(duration) = parse_config_duration(&raw) { + builder = builder.timeout(duration); + } + } + if let Some(raw) = options.get_config_value(&object_store::ClientConfigKey::ConnectTimeout) + { + if let Some(duration) = parse_config_duration(&raw) { + builder = builder.connect_timeout(duration); + } + } + let client = builder + .build() + .map_err(|err| object_store::Error::Generic { + store: "object_store", + source: Box::new(err), + })?; + Ok(HttpClient::new(client)) + } +} + +fn parse_config_duration(raw: &str) -> Option { + let raw = raw.trim(); + if let Some(ms) = raw.strip_suffix("ms") { + return ms.trim().parse::().ok().map(Duration::from_millis); + } + if let Some(s) = raw.strip_suffix('s') { + return s.trim().parse::().ok().map(Duration::from_secs); + } + None +} + #[derive(Clone, Copy)] enum ServerTitlePhase { Boot, @@ -424,6 +475,7 @@ where path_style, } => { let mut builder = AmazonS3Builder::new() + .with_http_connector(NoProxyReqwestConnector) .with_bucket_name(resolve_interp(bucket)?) .with_region(resolve_interp(region)?) .with_virtual_hosted_style_request(!*path_style); diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index 3edeec31d..7dec3ca1b 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -1657,27 +1657,6 @@ fn system_sandbox_provider(settings: &SettingsLayer) -> String { ) } -fn resolved_storage_dir(settings: &SettingsLayer) -> Result { - let resolved = ServerSettings::from_layer(settings).map_err(|err| err.to_string())?; - resolved - .server - .storage - .root - .resolve(|name| std::env::var(name).ok()) - .map(|value| PathBuf::from(value.value)) - .map_err(|err| { - format!( - "failed to resolve {}: {err}", - resolved.server.storage.root.as_source() - ) - }) -} - -fn resolved_github_settings(settings: &SettingsLayer) -> Result { - let resolved = ServerSettings::from_layer(settings).map_err(|err| err.to_string())?; - Ok(resolved.server.integrations.github) -} - fn parse_system_duration(raw: &str) -> anyhow::Result { let raw = raw.trim(); anyhow::ensure!(!raw.is_empty(), "empty duration string"); @@ -2396,11 +2375,9 @@ pub fn create_app_state_with_options_and_registry_factory( registry_factory_override: impl Fn(Arc) -> HandlerRegistry + Send + Sync + 'static, ) -> Arc { let env_lookup = default_env_lookup(); - let mut config = default_test_app_state_config( - Arc::new(RwLock::new(settings)), - max_concurrent_runs, - env_lookup, - ); + let settings = Arc::new(RwLock::new(settings)); + ensure_test_auth_methods(&settings); + let mut config = default_test_app_state_config(settings, max_concurrent_runs, env_lookup); config.registry_factory_override = Some(Box::new(registry_factory_override)); build_app_state(config).expect("test app state should build") } @@ -2410,9 +2387,11 @@ pub fn create_app_state_with_options( settings: SettingsLayer, max_concurrent_runs: usize, ) -> Arc { + let settings = Arc::new(RwLock::new(settings)); + ensure_test_auth_methods(&settings); let env_lookup = default_env_lookup(); build_app_state(default_test_app_state_config( - Arc::new(RwLock::new(settings)), + settings, max_concurrent_runs, env_lookup, )) @@ -2427,11 +2406,9 @@ pub fn create_app_state_with_env_lookup( ) -> Arc { let (store, artifact_store) = test_store_bundle(); let env_lookup: EnvLookup = Arc::new(env_lookup); - let mut config = default_test_app_state_config( - Arc::new(RwLock::new(settings)), - max_concurrent_runs, - env_lookup, - ); + let settings = Arc::new(RwLock::new(settings)); + ensure_test_auth_methods(&settings); + let mut config = default_test_app_state_config(settings, max_concurrent_runs, env_lookup); config.store = store; config.artifact_store = artifact_store; build_app_state(config).expect("test app state should build") @@ -2549,6 +2526,7 @@ fn create_app_state_with_store_and_env_lookup( artifact_store: ArtifactStore, env_lookup: &EnvLookup, ) -> Arc { + ensure_test_auth_methods(&settings); let mut config = default_test_app_state_config(settings, max_concurrent_runs, Arc::clone(env_lookup)); config.store = store; @@ -4238,26 +4216,17 @@ async fn start_run( } } - let Some(run_spec) = run_state.spec.as_ref() else { + if run_state.spec.is_none() { return ApiError::new( StatusCode::INTERNAL_SERVER_ERROR, "run spec missing from store", ) .into_response(); - }; - let run_dir = match resolved_storage_dir(&run_spec.settings) { - Ok(storage_dir) => Storage::new(storage_dir) - .run_scratch(&id) - .root() - .to_path_buf(), - Err(err) => { - return ApiError::new( - StatusCode::INTERNAL_SERVER_ERROR, - format!("invalid persisted server storage settings: {err}"), - ) - .into_response(); - } - }; + } + let run_dir = Storage::new(state.server_storage_dir()) + .run_scratch(&id) + .root() + .to_path_buf(); let dot_source = run_state.graph_source.unwrap_or_default(); if let Err(err) = workflow_event::append_event(&run_store, &id, &workflow_event::Event::RunQueued).await @@ -4424,22 +4393,8 @@ async fn execute_run_in_process(state: Arc, run_id: RunId) { return; } }; - let github_settings = match resolved_github_settings(&persisted.run_spec().settings) { - Ok(settings) => settings, - Err(err) => { - tracing::error!(run_id = %run_id, error = %err, "Invalid GitHub integration config"); - let mut runs = state.runs.lock().expect("runs lock poisoned"); - if let Some(managed_run) = runs.get_mut(&run_id) { - managed_run.status = RunStatus::Failed { - reason: FailureReason::WorkflowError, - }; - managed_run.error = Some(format!("Invalid GitHub integration config: {err}")); - clear_live_run_state(managed_run); - } - state.scheduler_notify.notify_one(); - return; - } - }; + let server_settings = state.server_settings(); + let github_settings = &server_settings.server.integrations.github; let github_app_result = match fabro_config::resolve_run_from_file( &persisted.run_spec().settings, ) { @@ -4448,10 +4403,10 @@ async fn execute_run_in_process(state: Arc, run_id: RunId) { && settings.sandbox.provider == "daytona") || !github_settings.permissions.is_empty(); if required_github_credentials { - state.github_credentials(&github_settings) + state.github_credentials(github_settings) } else if settings.execution.mode != RunMode::DryRun && settings.pull_request.is_some() { - match state.github_credentials(&github_settings) { + match state.github_credentials(github_settings) { Ok(github_app) => Ok(github_app), Err(err) => { tracing::warn!( @@ -4484,6 +4439,16 @@ async fn execute_run_in_process(state: Arc, run_id: RunId) { return; } }; + let github_permissions = github_settings + .permissions + .iter() + .map(|(name, value)| { + let resolved = value + .resolve(|env| std::env::var(env).ok()) + .map_or_else(|_| value.as_source(), |resolved| resolved.value); + (name.clone(), resolved) + }) + .collect(); let services = operations::StartServices { run_id, cancel_token: Some(Arc::clone(&cancel_token)), @@ -4494,6 +4459,7 @@ async fn execute_run_in_process(state: Arc, run_id: RunId) { artifact_sink: Some(ArtifactSink::Store(state.artifact_store.clone())), run_control: None, github_app, + github_permissions, vault: Some(Arc::clone(&state.vault)), on_node: None, registry_override, @@ -9979,7 +9945,6 @@ level = "debug" .spec .expect("run spec should exist"); let resolved_run = fabro_config::resolve_run_from_file(&run_spec.settings).unwrap(); - let resolved_server = fabro_config::resolve_server_from_file(&run_spec.settings).unwrap(); // Verify a sampling of the persisted v2 settings, including inherited // run execution mode from server settings. @@ -10005,16 +9970,18 @@ level = "debug" .as_deref(), Some("claude-sonnet-4-5"), ); - assert_eq!( - resolved_server + + // Server-operational fields (auth, integrations, etc.) deliberately + // do not flow into the run's persisted settings — they live on the + // server and are read via AppState::server_settings(). + assert!(run_spec.settings.server.as_ref().is_none_or(|server| { + server .integrations - .github - .app_id .as_ref() - .map(fabro_types::settings::InterpString::as_source) - .as_deref(), - Some("12345"), - ); + .and_then(|integrations| integrations.github.as_ref()) + .and_then(|github| github.app_id.as_ref()) + .is_none() + })); } #[tokio::test] diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs index f294340f1..bcd030dcd 100644 --- a/lib/crates/fabro-workflow/src/operations/start.rs +++ b/lib/crates/fabro-workflow/src/operations/start.rs @@ -83,18 +83,21 @@ struct RunSession { } pub struct StartServices { - pub run_id: RunId, - pub cancel_token: Option>, - pub emitter: Arc, - pub interviewer: Arc, - pub run_store: RunStoreHandle, - pub event_sink: RunEventSink, - pub artifact_sink: Option, - pub run_control: Option>, - pub github_app: Option, - pub vault: Option>>, - pub on_node: crate::OnNodeCallback, - pub registry_override: Option>, + pub run_id: RunId, + pub cancel_token: Option>, + pub emitter: Arc, + pub interviewer: Arc, + pub run_store: RunStoreHandle, + pub event_sink: RunEventSink, + pub artifact_sink: Option, + pub run_control: Option>, + pub github_app: Option, + /// Server-resolved GitHub integration permissions to inject into the + /// sandbox env. Empty when github integration has no permissions. + pub github_permissions: HashMap, + pub vault: Option>>, + pub on_node: crate::OnNodeCallback, + pub registry_override: Option>, } pub struct Started { @@ -379,24 +382,8 @@ impl RunSession { .iter() .map(|(k, v)| (k.clone(), resolve_interp(v))) .collect(); - let resolved_server = fabro_config::ServerSettings::from_layer(settings) - .map_err(|err| Error::Precondition(err.to_string()))?; - let github_permissions: Option> = (!resolved_server - .server - .integrations - .github - .permissions - .is_empty()) - .then(|| { - resolved_server - .server - .integrations - .github - .permissions - .iter() - .map(|(k, v)| (k.clone(), resolve_interp(v))) - .collect() - }); + let github_permissions: Option> = + (!services.github_permissions.is_empty()).then(|| services.github_permissions.clone()); let sandbox_env = SandboxEnvSpec { devcontainer_env: HashMap::new(), toml_env, @@ -1089,6 +1076,7 @@ mod tests { artifact_sink: None, run_control: None, github_app: None, + github_permissions: HashMap::new(), vault: None, on_node: None, registry_override: Some(registry), From a6e755c200755405ac71de1ffd89f61fb57b6e11 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Wed, 22 Apr 2026 23:47:00 -0400 Subject: [PATCH 07/13] simplify: dedupe storage-root override + cache demo settings - Promote `apply_storage_dir_override` to `fabro_config::user` so the serve startup path stops carrying its own copy of the storage-root mutation that already lived in `fabro-cli/user_config.rs`. - Inline the `load_settings` and `router_web_enabled` one-liner wrappers in `fabro-server/src/serve.rs` and drop the dead `let _ = CliLayer::default()` marker. - Cache the demo `server_settings()` JSON in a `OnceLock` so the demo mode stops re-parsing TOML, re-resolving, and re-serializing the same static fixture on every `GET /api/v1/settings` request. - Standardize the four `state.settings.read().unwrap()` callsites in `fabro-server/src/server.rs` on `.expect("settings lock poisoned")` to match the existing convention. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/crates/fabro-cli/src/user_config.rs | 17 ------------ lib/crates/fabro-config/src/user.rs | 20 ++++++++++++++ lib/crates/fabro-server/src/demo/mod.rs | 25 +++++++++++------- lib/crates/fabro-server/src/serve.rs | 35 ++++++------------------- lib/crates/fabro-server/src/server.rs | 28 ++++++++++++++------ 5 files changed, 64 insertions(+), 61 deletions(-) diff --git a/lib/crates/fabro-cli/src/user_config.rs b/lib/crates/fabro-cli/src/user_config.rs index 03429dbb4..774d3c8a5 100644 --- a/lib/crates/fabro-cli/src/user_config.rs +++ b/lib/crates/fabro-cli/src/user_config.rs @@ -30,23 +30,6 @@ pub(crate) fn load_settings_with_config_and_storage_dir( Ok(apply_storage_dir_override(layer, storage_dir)) } -pub(crate) fn apply_storage_dir_override( - mut layer: SettingsLayer, - storage_dir: Option<&Path>, -) -> SettingsLayer { - use fabro_types::settings::interp::InterpString; - use fabro_types::settings::server::{ServerLayer, ServerStorageLayer}; - if let Some(dir) = storage_dir { - let server = layer.server.get_or_insert_with(ServerLayer::default); - let storage = server - .storage - .get_or_insert_with(ServerStorageLayer::default); - storage.root = Some(InterpString::parse(&dir.display().to_string())); - } - - layer -} - /// Pull the resolved CLI target configuration out of `[cli.target]`. /// Returns either an http(s) URL or a unix socket path. fn cli_target_from_settings(settings: &CliNamespace) -> Option { diff --git a/lib/crates/fabro-config/src/user.rs b/lib/crates/fabro-config/src/user.rs index 951ae75aa..b561c361b 100644 --- a/lib/crates/fabro-config/src/user.rs +++ b/lib/crates/fabro-config/src/user.rs @@ -63,6 +63,26 @@ fn load_v2_layer_from_path(path: &Path) -> Result { load_settings_path(path) } +/// Override the resolved storage root in a settings layer with a runtime path. +/// +/// Used at server startup and by CLI commands that accept `--storage-dir`. +pub fn apply_storage_dir_override( + mut layer: SettingsLayer, + storage_dir: Option<&Path>, +) -> SettingsLayer { + use fabro_types::settings::interp::InterpString; + use fabro_types::settings::server::{ServerLayer, ServerStorageLayer}; + if let Some(dir) = storage_dir { + let server = layer.server.get_or_insert_with(ServerLayer::default); + let storage = server + .storage + .get_or_insert_with(ServerStorageLayer::default); + storage.root = Some(InterpString::parse(&dir.display().to_string())); + } + + layer +} + #[cfg(test)] mod tests { use super::{ diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs index d52044c2b..3709bd62d 100644 --- a/lib/crates/fabro-server/src/demo/mod.rs +++ b/lib/crates/fabro-server/src/demo/mod.rs @@ -1554,9 +1554,14 @@ mod insights { } mod settings { + use std::sync::OnceLock; + pub(super) fn server_settings() -> serde_json::Value { - let settings = fabro_config::parse_settings_layer( - r#" + static CACHED: OnceLock = OnceLock::new(); + CACHED + .get_or_init(|| { + let settings = fabro_config::parse_settings_layer( + r#" _version = 1 [server.listen] @@ -1592,13 +1597,15 @@ slug = "fabro-dev" [features] session_sandboxes = false "#, - ) - .expect("demo settings fixture should parse"); + ) + .expect("demo settings fixture should parse"); - serde_json::to_value( - fabro_config::ServerSettings::from_layer(&settings) - .expect("demo settings fixture should resolve"), - ) - .expect("demo settings should serialize") + serde_json::to_value( + fabro_config::ServerSettings::from_layer(&settings) + .expect("demo settings fixture should resolve"), + ) + .expect("demo settings should serialize") + }) + .clone() } } diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs index d35b1a46f..3705d5437 100644 --- a/lib/crates/fabro-server/src/serve.rs +++ b/lib/crates/fabro-server/src/serve.rs @@ -7,7 +7,7 @@ use anyhow::Context; use clap::Args; use fabro_config::bind::{self, Bind, BindRequest}; use fabro_config::merge::combine_files; -use fabro_config::user::load_settings_config; +use fabro_config::user::{apply_storage_dir_override, load_settings_config}; use fabro_config::{ServerSettings, Storage}; use fabro_install::{OBJECT_STORE_ACCESS_KEY_ID_ENV, OBJECT_STORE_SECRET_ACCESS_KEY_ENV}; use fabro_sandbox::SandboxProvider; @@ -159,12 +159,7 @@ pub struct ServeArgs { pub watch_web: bool, } -fn load_settings(path: Option<&Path>) -> anyhow::Result { - Ok(load_settings_config(path)?) -} - fn apply_serve_overrides(base: &SettingsLayer, args: &ServeArgs) -> SettingsLayer { - use fabro_types::settings::cli::CliLayer; use fabro_types::settings::interp::InterpString; use fabro_types::settings::run::{RunLayer, RunModelLayer, RunSandboxLayer}; use fabro_types::settings::server::{ServerLayer, ServerWebLayer}; @@ -189,8 +184,6 @@ fn apply_serve_overrides(base: &SettingsLayer, args: &ServeArgs) -> SettingsLaye let sandbox_layer = run.sandbox.get_or_insert_with(RunSandboxLayer::default); sandbox_layer.provider = Some(sandbox.to_string()); } - // CliLayer is namespaced; nothing to populate from flag overrides today. - let _ = CliLayer::default(); settings } @@ -199,19 +192,7 @@ fn apply_runtime_settings( args: &ServeArgs, data_dir: &Path, ) -> SettingsLayer { - use fabro_types::settings::interp::InterpString; - use fabro_types::settings::server::{ServerLayer, ServerStorageLayer}; - let mut settings = apply_serve_overrides(base, args); - let server = settings.server.get_or_insert_with(ServerLayer::default); - let storage = server - .storage - .get_or_insert_with(ServerStorageLayer::default); - storage.root = Some(InterpString::parse(&data_dir.to_string_lossy())); - settings -} - -fn router_web_enabled(settings: &ServerNamespace) -> bool { - settings.web.enabled + apply_storage_dir_override(apply_serve_overrides(base, args), Some(data_dir)) } async fn resolve_github_webhook_ip_allowlist( @@ -625,7 +606,7 @@ where #[cfg(debug_assertions)] let watch_web = args.watch_web; let config_path = args.config.clone(); - let disk_settings = load_settings(config_path.as_deref())?; + let disk_settings = load_settings_config(config_path.as_deref())?; let disk_server_settings = resolve_server_settings(&disk_settings)?; let data_dir = match storage_dir_override { Some(path) => path, @@ -652,7 +633,7 @@ where let max_concurrent_runs = resolved_server_settings.scheduler.max_concurrent_runs; (auth_mode, max_concurrent_runs) }; - let web_enabled = router_web_enabled(&resolved_server_settings); + let web_enabled = resolved_server_settings.web.enabled; let github_meta_resolver = GitHubMetaResolver::from_cache_dir(&storage.cache_dir())?; let (object_store, slatedb_prefix, flush_interval, disk_cache) = @@ -759,7 +740,7 @@ where interval.tick().await; // skip first immediate tick loop { interval.tick().await; - match load_settings(config_path_for_poll.as_deref()) { + match load_settings_config(config_path_for_poll.as_deref()) { Ok(new_disk_settings) => { let effective = apply_runtime_settings( &new_disk_settings, @@ -1073,8 +1054,8 @@ mod tests { bind_tcp_host_with_fallback, build_local_object_store_with_preference, build_object_store_from_settings_with_lookup, build_slatedb_store, resolve_bind_request_from_settings, resolve_github_webhook_ip_allowlist, - resolve_server_settings, resolve_startup_github_webhook_ip_allowlist, router_web_enabled, - server_bind_title, server_title, + resolve_server_settings, resolve_startup_github_webhook_ip_allowlist, server_bind_title, + server_title, }; use crate::server::create_app_state_with_options; @@ -1278,7 +1259,7 @@ strategy = "token" let resolved = resolve_server_settings(&base).expect("settings should resolve"); - assert!(router_web_enabled(&resolved)); + assert!(resolved.web.enabled); } #[test] diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index 7dec3ca1b..fc3ee36f3 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -1316,7 +1316,11 @@ async fn get_system_info( _auth: AuthenticatedService, State(state): State>, ) -> Response { - let settings = state.settings.read().unwrap().clone(); + let settings = state + .settings + .read() + .expect("settings lock poisoned") + .clone(); let (total_runs, active_runs) = { let runs = state.runs.lock().expect("runs lock poisoned"); let active = runs @@ -3996,7 +4000,10 @@ async fn create_run( Ok(req) => req, Err(err) => return ApiError::bad_request(err.to_string()).into_response(), }; - let prepared = match run_manifest::prepare_manifest(&state.settings.read().unwrap(), &req) { + let prepared = match run_manifest::prepare_manifest( + &state.settings.read().expect("settings lock poisoned"), + &req, + ) { Ok(prepared) => prepared, Err(err) => return ApiError::bad_request(err.to_string()).into_response(), }; @@ -4098,7 +4105,10 @@ async fn run_preflight( State(state): State>, Json(req): Json, ) -> Response { - let prepared = match run_manifest::prepare_manifest(&state.settings.read().unwrap(), &req) { + let prepared = match run_manifest::prepare_manifest( + &state.settings.read().expect("settings lock poisoned"), + &req, + ) { Ok(prepared) => prepared, Err(err) => return ApiError::bad_request(err.to_string()).into_response(), }; @@ -4124,11 +4134,13 @@ async fn render_graph_from_manifest( State(state): State>, Json(req): Json, ) -> Response { - let prepared = - match run_manifest::prepare_manifest(&state.settings.read().unwrap(), &req.manifest) { - Ok(prepared) => prepared, - Err(err) => return ApiError::bad_request(err.to_string()).into_response(), - }; + let prepared = match run_manifest::prepare_manifest( + &state.settings.read().expect("settings lock poisoned"), + &req.manifest, + ) { + Ok(prepared) => prepared, + Err(err) => return ApiError::bad_request(err.to_string()).into_response(), + }; let validated = match run_manifest::validate_prepared_manifest(&prepared) { Ok(validated) => validated, Err(err) => return ApiError::bad_request(err.to_string()).into_response(), From 2dfefe36e910a0d3d9891919a441dab543d3752d Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Wed, 22 Apr 2026 23:54:05 -0400 Subject: [PATCH 08/13] simplify: dedupe render_resolve_errors across crates Three byte-identical copies of `render_resolve_errors` had drifted into `fabro-server/src/run_manifest.rs`, `fabro-workflow/src/operations/start.rs`, and `fabro-workflow/src/operations/create.rs`. Each one folded a `&[ResolveError]` into a semicolon-separated string for surfacing through `anyhow!` / `Error::Precondition` envelopes. Promote the helper to `fabro_config::render_resolve_errors` (it lives next to `ResolveError`, the type it acts on) and rewrite the four call sites in workflow ops plus the one in run_manifest to call the shared version. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/crates/fabro-config/src/lib.rs | 8 ++++---- lib/crates/fabro-config/src/resolve/mod.rs | 11 +++++++++++ lib/crates/fabro-server/src/run_manifest.rs | 10 +--------- .../fabro-workflow/src/operations/create.rs | 16 ++++------------ .../fabro-workflow/src/operations/start.rs | 10 +--------- 5 files changed, 21 insertions(+), 34 deletions(-) diff --git a/lib/crates/fabro-config/src/lib.rs b/lib/crates/fabro-config/src/lib.rs index 24af07e32..451de1e82 100644 --- a/lib/crates/fabro-config/src/lib.rs +++ b/lib/crates/fabro-config/src/lib.rs @@ -37,10 +37,10 @@ pub use load::{ }; pub use parse::{ParseError, parse_settings_layer}; pub use resolve::{ - ResolveError, dev_token_auth_enabled, resolve_cli, resolve_cli_from_file, resolve_features, - resolve_features_from_file, resolve_project, resolve_project_from_file, resolve_run, - resolve_run_from_file, resolve_server, resolve_server_from_file, resolve_storage_root, - resolve_workflow, resolve_workflow_from_file, + ResolveError, dev_token_auth_enabled, render_resolve_errors, resolve_cli, + resolve_cli_from_file, resolve_features, resolve_features_from_file, resolve_project, + resolve_project_from_file, resolve_run, resolve_run_from_file, resolve_server, + resolve_server_from_file, resolve_storage_root, resolve_workflow, resolve_workflow_from_file, }; use serde::de::DeserializeOwned; pub use storage::{RunScratch, RuntimeDirectory, Storage}; diff --git a/lib/crates/fabro-config/src/resolve/mod.rs b/lib/crates/fabro-config/src/resolve/mod.rs index c72ca5930..a93f2faf2 100644 --- a/lib/crates/fabro-config/src/resolve/mod.rs +++ b/lib/crates/fabro-config/src/resolve/mod.rs @@ -100,6 +100,17 @@ pub fn resolve_workflow_from_file( } } +/// Render a list of [`ResolveError`]s as a single semicolon-separated message +/// suitable for surfacing through `anyhow!` / `Error::Precondition` / similar +/// human-facing error envelopes. +pub fn render_resolve_errors(errors: &[ResolveError]) -> String { + errors + .iter() + .map(ToString::to_string) + .collect::>() + .join("; ") +} + pub(crate) fn require_interp( value: Option<&InterpString>, path: &str, diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs index f4ed75a3d..cae7fd3e3 100644 --- a/lib/crates/fabro-server/src/run_manifest.rs +++ b/lib/crates/fabro-server/src/run_manifest.rs @@ -361,7 +361,7 @@ async fn build_preflight_report( &configured_providers, ); let resolved_run = fabro_config::resolve_run_from_file(&materialized) - .map_err(|errors| anyhow!(render_resolve_errors(&errors)))?; + .map_err(|errors| anyhow!(fabro_config::render_resolve_errors(&errors)))?; let server_settings = state.server_settings(); let github_integration = &server_settings.server.integrations.github; let sandbox_provider = resolve_sandbox_provider(&resolved_run)?; @@ -701,14 +701,6 @@ fn resolve_model_provider( } } -fn render_resolve_errors(errors: &[fabro_config::ResolveError]) -> String { - errors - .iter() - .map(ToString::to_string) - .collect::>() - .join("; ") -} - fn runtime_daytona_config(settings: &DaytonaSettings) -> DaytonaConfig { DaytonaConfig { auto_stop_interval: settings.auto_stop_interval, diff --git a/lib/crates/fabro-workflow/src/operations/create.rs b/lib/crates/fabro-workflow/src/operations/create.rs index 0b26760d1..ba6ced33b 100644 --- a/lib/crates/fabro-workflow/src/operations/create.rs +++ b/lib/crates/fabro-workflow/src/operations/create.rs @@ -284,23 +284,15 @@ fn store_error(err: impl std::fmt::Display) -> Error { Error::engine(err.to_string()) } -fn render_resolve_errors(errors: &[fabro_config::ResolveError]) -> String { - errors - .iter() - .map(ToString::to_string) - .collect::>() - .join("; ") -} - fn resolve_settings_tree(settings: &SettingsLayer) -> Result { Ok(ResolvedSettingsTree { server_storage_root: fabro_config::resolve_storage_root(settings), project: fabro_config::resolve_project_from_file(settings) - .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?, + .map_err(|errors| Error::Precondition(fabro_config::render_resolve_errors(&errors)))?, workflow: fabro_config::resolve_workflow_from_file(settings) - .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?, + .map_err(|errors| Error::Precondition(fabro_config::render_resolve_errors(&errors)))?, run: fabro_config::resolve_run_from_file(settings) - .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?, + .map_err(|errors| Error::Precondition(fabro_config::render_resolve_errors(&errors)))?, }) } @@ -313,7 +305,7 @@ fn combined_labels(settings: &ResolvedSettingsTree) -> HashMap { fn validate_sandbox_provider(settings: &SettingsLayer) -> Result<(), Error> { let resolved = fabro_config::resolve_run_from_file(settings) - .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?; + .map_err(|errors| Error::Precondition(fabro_config::render_resolve_errors(&errors)))?; resolved .sandbox .provider diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs index bcd030dcd..7a087b34a 100644 --- a/lib/crates/fabro-workflow/src/operations/start.rs +++ b/lib/crates/fabro-workflow/src/operations/start.rs @@ -310,7 +310,7 @@ impl RunSession { .map_or((None, None), |(url, branch)| (Some(url), branch)); let resolved = fabro_config::resolve_run_from_file(settings) - .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?; + .map_err(|errors| Error::Precondition(fabro_config::render_resolve_errors(&errors)))?; let sandbox_provider = resolve_sandbox_provider(&resolved)?; let sandbox_provider = @@ -518,14 +518,6 @@ fn resolve_fallback_chain( Catalog::builtin().build_fallback_chain(provider, model, &by_provider) } -fn render_resolve_errors(errors: &[fabro_config::ResolveError]) -> String { - errors - .iter() - .map(ToString::to_string) - .collect::>() - .join("; ") -} - fn runtime_mcp_server(settings: &ResolvedMcpServerSettings) -> McpServerSettings { McpServerSettings { name: settings.name.clone(), From 52d24531df6ad74f1701eddaf3b7aa68afb21336 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 23 Apr 2026 00:18:20 -0400 Subject: [PATCH 09/13] simplify: introduce Resolver to apply settings defaults once Previously, each per-namespace `resolve_*_from_file` helper, together with `resolve_storage_root` and the `*Settings::from_layer` constructors, called `apply_builtin_defaults(file.clone())` independently. That meant every batch resolve cloned the entire `SettingsLayer` (including hooks, MCPs, sandbox config, etc.) and merged the static defaults layer once per call. The worst offender, `fabro_workflow::operations::create::resolve_settings_tree`, ran that pipeline four times back-to-back per `create_run` request. Add `fabro_config::Resolver`, which applies builtin defaults exactly once on construction and exposes per-namespace methods (`server`, `cli`, `features`, `project`, `run`, `workflow`, `storage_root`) plus low-level `*_into(&mut errors)` variants for callers that want to merge errors across multiple namespaces. The standalone `resolve_*_from_file` helpers and `resolve_storage_root` remain on the public API, but each is now a one-liner that delegates to `Resolver::from_file(...)` so single-namespace callers see no behavior change. Migrate the multi-namespace consumers: - `ServerSettings::from_layer` and `UserSettings::from_layer` build one `Resolver` and call the `*_into` pair, preserving the original "surface all errors from both namespaces" semantics. - `resolve_settings_tree` builds one `Resolver` and pulls all four namespaces from it, dropping three redundant defaulting+clone passes. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/crates/fabro-config/src/context.rs | 20 ++- lib/crates/fabro-config/src/lib.rs | 2 +- lib/crates/fabro-config/src/resolve/mod.rs | 68 ++-------- .../fabro-config/src/resolve/resolver.rs | 122 ++++++++++++++++++ lib/crates/fabro-config/src/resolve/server.rs | 9 -- .../fabro-workflow/src/operations/create.rs | 14 +- 6 files changed, 150 insertions(+), 85 deletions(-) create mode 100644 lib/crates/fabro-config/src/resolve/resolver.rs diff --git a/lib/crates/fabro-config/src/context.rs b/lib/crates/fabro-config/src/context.rs index 1197a4d99..33f0f9788 100644 --- a/lib/crates/fabro-config/src/context.rs +++ b/lib/crates/fabro-config/src/context.rs @@ -1,9 +1,9 @@ use fabro_types::settings::{CliNamespace, FeaturesNamespace, ServerNamespace, SettingsLayer}; use serde::{Deserialize, Serialize}; -use crate::resolve::{resolve_cli, resolve_features, resolve_server}; +use crate::resolve::Resolver; use crate::user::load_settings_config; -use crate::{Error, Result, apply_builtin_defaults}; +use crate::{Error, Result}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ServerSettings { @@ -13,12 +13,10 @@ pub struct ServerSettings { impl ServerSettings { pub fn from_layer(layer: &SettingsLayer) -> Result { - let layer = apply_builtin_defaults(layer.clone()); + let resolver = Resolver::from_file(layer); let mut errors = Vec::new(); - let server_layer = layer.server.clone().unwrap_or_default(); - let features_layer = layer.features.clone().unwrap_or_default(); - let server = resolve_server(&server_layer, &mut errors); - let features = resolve_features(&features_layer, &mut errors); + let server = resolver.server_into(&mut errors); + let features = resolver.features_into(&mut errors); if errors.is_empty() { Ok(Self { server, features }) } else { @@ -40,12 +38,10 @@ pub struct UserSettings { impl UserSettings { pub fn from_layer(layer: &SettingsLayer) -> Result { - let layer = apply_builtin_defaults(layer.clone()); + let resolver = Resolver::from_file(layer); let mut errors = Vec::new(); - let cli_layer = layer.cli.clone().unwrap_or_default(); - let features_layer = layer.features.clone().unwrap_or_default(); - let cli = resolve_cli(&cli_layer, &mut errors); - let features = resolve_features(&features_layer, &mut errors); + let cli = resolver.cli_into(&mut errors); + let features = resolver.features_into(&mut errors); if errors.is_empty() { Ok(Self { cli, features }) } else { diff --git a/lib/crates/fabro-config/src/lib.rs b/lib/crates/fabro-config/src/lib.rs index 451de1e82..ed5772707 100644 --- a/lib/crates/fabro-config/src/lib.rs +++ b/lib/crates/fabro-config/src/lib.rs @@ -37,7 +37,7 @@ pub use load::{ }; pub use parse::{ParseError, parse_settings_layer}; pub use resolve::{ - ResolveError, dev_token_auth_enabled, render_resolve_errors, resolve_cli, + ResolveError, Resolver, dev_token_auth_enabled, render_resolve_errors, resolve_cli, resolve_cli_from_file, resolve_features, resolve_features_from_file, resolve_project, resolve_project_from_file, resolve_run, resolve_run_from_file, resolve_server, resolve_server_from_file, resolve_storage_root, resolve_workflow, resolve_workflow_from_file, diff --git a/lib/crates/fabro-config/src/resolve/mod.rs b/lib/crates/fabro-config/src/resolve/mod.rs index a93f2faf2..b10f244be 100644 --- a/lib/crates/fabro-config/src/resolve/mod.rs +++ b/lib/crates/fabro-config/src/resolve/mod.rs @@ -2,6 +2,7 @@ mod cli; mod error; mod features; mod project; +mod resolver; mod run; mod server; mod workflow; @@ -14,90 +15,45 @@ use fabro_types::settings::{ }; pub use features::resolve_features; pub use project::resolve_project; +pub use resolver::Resolver; pub use run::resolve_run; -pub use server::{dev_token_auth_enabled, resolve_server, resolve_storage_root}; +pub use server::{dev_token_auth_enabled, resolve_server}; pub use workflow::resolve_workflow; -use crate::apply_builtin_defaults; +pub fn resolve_storage_root(file: &SettingsLayer) -> InterpString { + Resolver::from_file(file).storage_root() +} pub fn resolve_cli_from_file(file: &SettingsLayer) -> Result> { - let file = apply_builtin_defaults(file.clone()); - let mut errors = Vec::new(); - let cli_layer = file.cli.clone().unwrap_or_default(); - let cli = resolve_cli(&cli_layer, &mut errors); - if errors.is_empty() { - Ok(cli) - } else { - Err(errors) - } + Resolver::from_file(file).cli() } pub fn resolve_server_from_file( file: &SettingsLayer, ) -> Result> { - let file = apply_builtin_defaults(file.clone()); - let mut errors = Vec::new(); - let server_layer = file.server.clone().unwrap_or_default(); - let server = resolve_server(&server_layer, &mut errors); - if errors.is_empty() { - Ok(server) - } else { - Err(errors) - } + Resolver::from_file(file).server() } pub fn resolve_project_from_file( file: &SettingsLayer, ) -> Result> { - let file = apply_builtin_defaults(file.clone()); - let mut errors = Vec::new(); - let project_layer = file.project.clone().unwrap_or_default(); - let project = resolve_project(&project_layer, &mut errors); - if errors.is_empty() { - Ok(project) - } else { - Err(errors) - } + Resolver::from_file(file).project() } pub fn resolve_features_from_file( file: &SettingsLayer, ) -> Result> { - let file = apply_builtin_defaults(file.clone()); - let mut errors = Vec::new(); - let features_layer = file.features.clone().unwrap_or_default(); - let features = resolve_features(&features_layer, &mut errors); - if errors.is_empty() { - Ok(features) - } else { - Err(errors) - } + Resolver::from_file(file).features() } pub fn resolve_run_from_file(file: &SettingsLayer) -> Result> { - let file = apply_builtin_defaults(file.clone()); - let mut errors = Vec::new(); - let run_layer = file.run.clone().unwrap_or_default(); - let run = resolve_run(&run_layer, &mut errors); - if errors.is_empty() { - Ok(run) - } else { - Err(errors) - } + Resolver::from_file(file).run() } pub fn resolve_workflow_from_file( file: &SettingsLayer, ) -> Result> { - let file = apply_builtin_defaults(file.clone()); - let mut errors = Vec::new(); - let workflow_layer = file.workflow.clone().unwrap_or_default(); - let workflow = resolve_workflow(&workflow_layer, &mut errors); - if errors.is_empty() { - Ok(workflow) - } else { - Err(errors) - } + Resolver::from_file(file).workflow() } /// Render a list of [`ResolveError`]s as a single semicolon-separated message diff --git a/lib/crates/fabro-config/src/resolve/resolver.rs b/lib/crates/fabro-config/src/resolve/resolver.rs new file mode 100644 index 000000000..2081c27f1 --- /dev/null +++ b/lib/crates/fabro-config/src/resolve/resolver.rs @@ -0,0 +1,122 @@ +//! Cache builtin defaults across multiple per-namespace resolutions. +//! +//! [`resolve_storage_root`] and the per-namespace `resolve_*_from_file` +//! helpers each call [`apply_builtin_defaults`], which clones both the input +//! layer and the embedded defaults layer before merging them. Callers that +//! need more than one namespace would otherwise pay that cost N times. +//! +//! [`Resolver`] applies defaults once on construction, then exposes per- +//! namespace methods that work against the materialized layer. It is the +//! shared backend for the standalone `resolve_*_from_file` helpers and the +//! preferred entrypoint when more than one namespace is needed. + +use fabro_types::settings::{ + CliNamespace, FeaturesNamespace, InterpString, ProjectNamespace, RunNamespace, ServerNamespace, + SettingsLayer, WorkflowNamespace, +}; + +use super::{ + ResolveError, default_interp, resolve_cli, resolve_features, resolve_project, resolve_run, + resolve_server, resolve_workflow, +}; +use crate::apply_builtin_defaults; +use crate::user::default_storage_dir; + +pub struct Resolver { + layer: SettingsLayer, +} + +impl Resolver { + #[must_use] + pub fn from_file(file: &SettingsLayer) -> Self { + Self { + layer: apply_builtin_defaults(file.clone()), + } + } + + pub fn cli(&self) -> Result> { + let mut errors = Vec::new(); + let value = self.cli_into(&mut errors); + finish(value, errors) + } + + pub fn server(&self) -> Result> { + let mut errors = Vec::new(); + let value = self.server_into(&mut errors); + finish(value, errors) + } + + pub fn project(&self) -> Result> { + let mut errors = Vec::new(); + let value = self.project_into(&mut errors); + finish(value, errors) + } + + pub fn features(&self) -> Result> { + let mut errors = Vec::new(); + let value = self.features_into(&mut errors); + finish(value, errors) + } + + pub fn run(&self) -> Result> { + let mut errors = Vec::new(); + let value = self.run_into(&mut errors); + finish(value, errors) + } + + pub fn workflow(&self) -> Result> { + let mut errors = Vec::new(); + let value = self.workflow_into(&mut errors); + finish(value, errors) + } + + /// Resolved storage root, defaulting to [`default_storage_dir`] when the + /// input layer doesn't pin one. + #[must_use] + pub fn storage_root(&self) -> InterpString { + self.layer + .server + .as_ref() + .and_then(|server| server.storage.as_ref()) + .and_then(|storage| storage.root.clone()) + .unwrap_or_else(|| default_interp(default_storage_dir())) + } + + pub fn cli_into(&self, errors: &mut Vec) -> CliNamespace { + let layer = self.layer.cli.clone().unwrap_or_default(); + resolve_cli(&layer, errors) + } + + pub fn server_into(&self, errors: &mut Vec) -> ServerNamespace { + let layer = self.layer.server.clone().unwrap_or_default(); + resolve_server(&layer, errors) + } + + pub fn project_into(&self, errors: &mut Vec) -> ProjectNamespace { + let layer = self.layer.project.clone().unwrap_or_default(); + resolve_project(&layer, errors) + } + + pub fn features_into(&self, errors: &mut Vec) -> FeaturesNamespace { + let layer = self.layer.features.clone().unwrap_or_default(); + resolve_features(&layer, errors) + } + + pub fn run_into(&self, errors: &mut Vec) -> RunNamespace { + let layer = self.layer.run.clone().unwrap_or_default(); + resolve_run(&layer, errors) + } + + pub fn workflow_into(&self, errors: &mut Vec) -> WorkflowNamespace { + let layer = self.layer.workflow.clone().unwrap_or_default(); + resolve_workflow(&layer, errors) + } +} + +fn finish(value: T, errors: Vec) -> Result> { + if errors.is_empty() { + Ok(value) + } else { + Err(errors) + } +} diff --git a/lib/crates/fabro-config/src/resolve/server.rs b/lib/crates/fabro-config/src/resolve/server.rs index 1ceb10092..dc73c8da3 100644 --- a/lib/crates/fabro-config/src/resolve/server.rs +++ b/lib/crates/fabro-config/src/resolve/server.rs @@ -17,15 +17,6 @@ use fabro_util::Home; use super::{ResolveError, default_interp, parse_socket_addr, require_interp}; use crate::user::default_storage_dir; -pub fn resolve_storage_root(file: &SettingsLayer) -> InterpString { - let file = crate::apply_builtin_defaults(file.clone()); - file.server - .as_ref() - .and_then(|server| server.storage.as_ref()) - .and_then(|storage| storage.root.clone()) - .unwrap_or_else(|| default_interp(default_storage_dir())) -} - pub fn dev_token_auth_enabled(layer: &SettingsLayer) -> bool { layer .server diff --git a/lib/crates/fabro-workflow/src/operations/create.rs b/lib/crates/fabro-workflow/src/operations/create.rs index ba6ced33b..0df712887 100644 --- a/lib/crates/fabro-workflow/src/operations/create.rs +++ b/lib/crates/fabro-workflow/src/operations/create.rs @@ -285,14 +285,14 @@ fn store_error(err: impl std::fmt::Display) -> Error { } fn resolve_settings_tree(settings: &SettingsLayer) -> Result { + let resolver = fabro_config::Resolver::from_file(settings); + let to_error = + |errors: Vec<_>| Error::Precondition(fabro_config::render_resolve_errors(&errors)); Ok(ResolvedSettingsTree { - server_storage_root: fabro_config::resolve_storage_root(settings), - project: fabro_config::resolve_project_from_file(settings) - .map_err(|errors| Error::Precondition(fabro_config::render_resolve_errors(&errors)))?, - workflow: fabro_config::resolve_workflow_from_file(settings) - .map_err(|errors| Error::Precondition(fabro_config::render_resolve_errors(&errors)))?, - run: fabro_config::resolve_run_from_file(settings) - .map_err(|errors| Error::Precondition(fabro_config::render_resolve_errors(&errors)))?, + server_storage_root: resolver.storage_root(), + project: resolver.project().map_err(to_error)?, + workflow: resolver.workflow().map_err(to_error)?, + run: resolver.run().map_err(to_error)?, }) } From ec18c1864b33b18ef5e4994a1f1043acb4b3ad33 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 23 Apr 2026 00:21:43 -0400 Subject: [PATCH 10/13] =?UTF-8?q?simplify:=20rename=20Resolver::from=5Ffil?= =?UTF-8?q?e=20=E2=86=92=20from=5Flayer=20and=20trim=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fresh `Resolver` API takes a `&SettingsLayer`, not a file path, so its constructor should match the existing `ServerSettings::from_layer` and `UserSettings::from_layer` naming. The `*_from_file` suffix on the older free helpers is a legacy choice (their input was historically loaded from a file); leave those names alone since they're a stable public API used across many call sites. Also drop two doc-comment references to specific call sites (the simplify guidelines treat those as rot bait — call sites move, the doc lies). Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/crates/fabro-config/src/context.rs | 4 ++-- lib/crates/fabro-config/src/resolve/mod.rs | 17 ++++++++--------- lib/crates/fabro-config/src/resolve/resolver.rs | 4 ++-- lib/crates/fabro-config/src/user.rs | 2 -- .../fabro-workflow/src/operations/create.rs | 2 +- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/lib/crates/fabro-config/src/context.rs b/lib/crates/fabro-config/src/context.rs index 33f0f9788..5cddd9e6a 100644 --- a/lib/crates/fabro-config/src/context.rs +++ b/lib/crates/fabro-config/src/context.rs @@ -13,7 +13,7 @@ pub struct ServerSettings { impl ServerSettings { pub fn from_layer(layer: &SettingsLayer) -> Result { - let resolver = Resolver::from_file(layer); + let resolver = Resolver::from_layer(layer); let mut errors = Vec::new(); let server = resolver.server_into(&mut errors); let features = resolver.features_into(&mut errors); @@ -38,7 +38,7 @@ pub struct UserSettings { impl UserSettings { pub fn from_layer(layer: &SettingsLayer) -> Result { - let resolver = Resolver::from_file(layer); + let resolver = Resolver::from_layer(layer); let mut errors = Vec::new(); let cli = resolver.cli_into(&mut errors); let features = resolver.features_into(&mut errors); diff --git a/lib/crates/fabro-config/src/resolve/mod.rs b/lib/crates/fabro-config/src/resolve/mod.rs index b10f244be..f35282ed7 100644 --- a/lib/crates/fabro-config/src/resolve/mod.rs +++ b/lib/crates/fabro-config/src/resolve/mod.rs @@ -21,44 +21,43 @@ pub use server::{dev_token_auth_enabled, resolve_server}; pub use workflow::resolve_workflow; pub fn resolve_storage_root(file: &SettingsLayer) -> InterpString { - Resolver::from_file(file).storage_root() + Resolver::from_layer(file).storage_root() } pub fn resolve_cli_from_file(file: &SettingsLayer) -> Result> { - Resolver::from_file(file).cli() + Resolver::from_layer(file).cli() } pub fn resolve_server_from_file( file: &SettingsLayer, ) -> Result> { - Resolver::from_file(file).server() + Resolver::from_layer(file).server() } pub fn resolve_project_from_file( file: &SettingsLayer, ) -> Result> { - Resolver::from_file(file).project() + Resolver::from_layer(file).project() } pub fn resolve_features_from_file( file: &SettingsLayer, ) -> Result> { - Resolver::from_file(file).features() + Resolver::from_layer(file).features() } pub fn resolve_run_from_file(file: &SettingsLayer) -> Result> { - Resolver::from_file(file).run() + Resolver::from_layer(file).run() } pub fn resolve_workflow_from_file( file: &SettingsLayer, ) -> Result> { - Resolver::from_file(file).workflow() + Resolver::from_layer(file).workflow() } /// Render a list of [`ResolveError`]s as a single semicolon-separated message -/// suitable for surfacing through `anyhow!` / `Error::Precondition` / similar -/// human-facing error envelopes. +/// for human-facing error envelopes. pub fn render_resolve_errors(errors: &[ResolveError]) -> String { errors .iter() diff --git a/lib/crates/fabro-config/src/resolve/resolver.rs b/lib/crates/fabro-config/src/resolve/resolver.rs index 2081c27f1..893aa41ac 100644 --- a/lib/crates/fabro-config/src/resolve/resolver.rs +++ b/lib/crates/fabro-config/src/resolve/resolver.rs @@ -28,9 +28,9 @@ pub struct Resolver { impl Resolver { #[must_use] - pub fn from_file(file: &SettingsLayer) -> Self { + pub fn from_layer(layer: &SettingsLayer) -> Self { Self { - layer: apply_builtin_defaults(file.clone()), + layer: apply_builtin_defaults(layer.clone()), } } diff --git a/lib/crates/fabro-config/src/user.rs b/lib/crates/fabro-config/src/user.rs index b561c361b..298cf2f48 100644 --- a/lib/crates/fabro-config/src/user.rs +++ b/lib/crates/fabro-config/src/user.rs @@ -64,8 +64,6 @@ fn load_v2_layer_from_path(path: &Path) -> Result { } /// Override the resolved storage root in a settings layer with a runtime path. -/// -/// Used at server startup and by CLI commands that accept `--storage-dir`. pub fn apply_storage_dir_override( mut layer: SettingsLayer, storage_dir: Option<&Path>, diff --git a/lib/crates/fabro-workflow/src/operations/create.rs b/lib/crates/fabro-workflow/src/operations/create.rs index 0df712887..149e799d1 100644 --- a/lib/crates/fabro-workflow/src/operations/create.rs +++ b/lib/crates/fabro-workflow/src/operations/create.rs @@ -285,7 +285,7 @@ fn store_error(err: impl std::fmt::Display) -> Error { } fn resolve_settings_tree(settings: &SettingsLayer) -> Result { - let resolver = fabro_config::Resolver::from_file(settings); + let resolver = fabro_config::Resolver::from_layer(settings); let to_error = |errors: Vec<_>| Error::Precondition(fabro_config::render_resolve_errors(&errors)); Ok(ResolvedSettingsTree { From 7fd8f5a57b0860af20c16c219650164cedfe83e2 Mon Sep 17 00:00:00 2001 From: "fabro-releases[bot]" Date: Thu, 23 Apr 2026 09:45:03 +0000 Subject: [PATCH 11/13] Bump version to 0.212.0-nightly.0 --- Cargo.lock | 76 +++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a6834259a..8d1b03b93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1505,7 +1505,7 @@ dependencies = [ [[package]] name = "fabro-agent" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -1541,7 +1541,7 @@ dependencies = [ [[package]] name = "fabro-api" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "chrono", "fabro-config", @@ -1561,7 +1561,7 @@ dependencies = [ [[package]] name = "fabro-auth" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -1582,7 +1582,7 @@ dependencies = [ [[package]] name = "fabro-checkpoint" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "chrono", "fabro-store", @@ -1597,7 +1597,7 @@ dependencies = [ [[package]] name = "fabro-cli" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -1687,7 +1687,7 @@ dependencies = [ [[package]] name = "fabro-client" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -1714,7 +1714,7 @@ dependencies = [ [[package]] name = "fabro-config" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -1737,7 +1737,7 @@ dependencies = [ [[package]] name = "fabro-core" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "async-trait", "fabro-types", @@ -1752,7 +1752,7 @@ dependencies = [ [[package]] name = "fabro-devcontainer" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "fabro-http", "fabro-util", @@ -1768,7 +1768,7 @@ dependencies = [ [[package]] name = "fabro-github" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "base64", "chrono", @@ -1784,7 +1784,7 @@ dependencies = [ [[package]] name = "fabro-graphviz" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -1798,7 +1798,7 @@ dependencies = [ [[package]] name = "fabro-hooks" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "async-trait", "fabro-agent", @@ -1821,7 +1821,7 @@ dependencies = [ [[package]] name = "fabro-http" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "http", "reqwest 0.13.2", @@ -1830,7 +1830,7 @@ dependencies = [ [[package]] name = "fabro-install" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -1844,7 +1844,7 @@ dependencies = [ [[package]] name = "fabro-interview" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "async-trait", "dialoguer", @@ -1858,7 +1858,7 @@ dependencies = [ [[package]] name = "fabro-llm" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -1888,7 +1888,7 @@ dependencies = [ [[package]] name = "fabro-macros" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "proc-macro2", "quote", @@ -1897,7 +1897,7 @@ dependencies = [ [[package]] name = "fabro-mcp" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "fabro-config", @@ -1913,7 +1913,7 @@ dependencies = [ [[package]] name = "fabro-model" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "insta", "serde", @@ -1923,7 +1923,7 @@ dependencies = [ [[package]] name = "fabro-oauth" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "axum", "base64", @@ -1942,7 +1942,7 @@ dependencies = [ [[package]] name = "fabro-proc" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "cc", "libc", @@ -1951,7 +1951,7 @@ dependencies = [ [[package]] name = "fabro-retro" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -1969,7 +1969,7 @@ dependencies = [ [[package]] name = "fabro-sandbox" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2001,7 +2001,7 @@ dependencies = [ [[package]] name = "fabro-server" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2079,7 +2079,7 @@ dependencies = [ [[package]] name = "fabro-slack" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "fabro-http", "fabro-interview", @@ -2098,14 +2098,14 @@ dependencies = [ [[package]] name = "fabro-spa" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "rust-embed", ] [[package]] name = "fabro-store" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "async-trait", "bytes", @@ -2131,7 +2131,7 @@ dependencies = [ [[package]] name = "fabro-telemetry" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2156,7 +2156,7 @@ dependencies = [ [[package]] name = "fabro-template" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "fabro-util", @@ -2168,7 +2168,7 @@ dependencies = [ [[package]] name = "fabro-test" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "assert_cmd", "axum", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "fabro-tracker" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "async-trait", "fabro-github", @@ -2203,7 +2203,7 @@ dependencies = [ [[package]] name = "fabro-types" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "chrono", "clap", @@ -2224,7 +2224,7 @@ dependencies = [ [[package]] name = "fabro-util" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "aho-corasick", "anyhow", @@ -2246,7 +2246,7 @@ dependencies = [ [[package]] name = "fabro-validate" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "fabro-graphviz", "fabro-model", @@ -2256,7 +2256,7 @@ dependencies = [ [[package]] name = "fabro-vault" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "chrono", "serde", @@ -2267,7 +2267,7 @@ dependencies = [ [[package]] name = "fabro-workflow" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -6912,7 +6912,7 @@ dependencies = [ [[package]] name = "twin-github" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "axum", "base64", @@ -6931,7 +6931,7 @@ dependencies = [ [[package]] name = "twin-openai" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" dependencies = [ "anyhow", "async-stream", diff --git a/Cargo.toml b/Cargo.toml index 49ad412a2..a4f711760 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.211.0-nightly.1" +version = "0.212.0-nightly.0" license = "MIT" [workspace.dependencies] From cb9b762119dd398e189f4bad3239b1df5bd17ef6 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 23 Apr 2026 07:48:03 -0400 Subject: [PATCH 12/13] feat(cli): rename store dump to dump --- docs-internal/run-directory-keys.md | 2 +- docs/agents/outputs.mdx | 4 +- docs/agents/prompts.mdx | 2 +- docs/execution/observability.mdx | 4 +- docs/execution/retros.mdx | 2 +- docs/reference/cli.mdx | 6 +- docs/reference/run-directory.mdx | 6 +- lib/crates/fabro-cli/src/args.rs | 22 ++----- .../fabro-cli/src/commands/store/dump.rs | 6 +- .../fabro-cli/src/commands/store/mod.rs | 18 ----- lib/crates/fabro-cli/src/main.rs | 17 +++-- .../tests/it/cmd/{store_dump.rs => dump.rs} | 65 ++++++++++--------- lib/crates/fabro-cli/tests/it/cmd/fabro.rs | 2 +- lib/crates/fabro-cli/tests/it/cmd/mod.rs | 3 +- lib/crates/fabro-cli/tests/it/cmd/store.rs | 29 --------- .../tests/it/workflow/command_agent_mixed.rs | 6 +- .../tests/it/workflow/command_pipeline.rs | 6 +- .../fabro-cli/tests/it/workflow/full_stack.rs | 6 +- lib/crates/fabro-cli/tests/it/workflow/mod.rs | 7 +- 19 files changed, 77 insertions(+), 136 deletions(-) rename lib/crates/fabro-cli/tests/it/cmd/{store_dump.rs => dump.rs} (87%) delete mode 100644 lib/crates/fabro-cli/tests/it/cmd/store.rs diff --git a/docs-internal/run-directory-keys.md b/docs-internal/run-directory-keys.md index 4d5adc8e7..990e57bfe 100644 --- a/docs-internal/run-directory-keys.md +++ b/docs-internal/run-directory-keys.md @@ -33,7 +33,7 @@ These paths are local runtime state, not canonical event projections. These names are still real, but they are no longer live scratch files by default: - Metadata branch files such as `run.json`, `start.json`, `checkpoint.json`, and `retro.json` -- `fabro store dump` exports such as `run.json`, `start.json`, `status.json`, `checkpoint.json`, `conclusion.json`, `retro.json`, `events.jsonl`, and per-node prompt/response/status/stdout/stderr files +- `fabro dump` exports such as `run.json`, `start.json`, `status.json`, `checkpoint.json`, `conclusion.json`, `retro.json`, `events.jsonl`, and per-node prompt/response/status/stdout/stderr files - Retro-agent temp uploads named `progress.jsonl`, `checkpoint.json`, `run.json`, and `start.json` inside the retro sandbox ## Notes diff --git a/docs/agents/outputs.mdx b/docs/agents/outputs.mdx index e375ff405..3e95af24d 100644 --- a/docs/agents/outputs.mdx +++ b/docs/agents/outputs.mdx @@ -7,7 +7,7 @@ When an agent or prompt node finishes, Fabro captures its response text and prod ## Response capture -After an agent or prompt node completes, Fabro captures the full response text and persists it to `stages/{node_id}@{visit}/response.md` in metadata snapshots and `fabro store dump` output. It also writes the final outcome (status, context updates, routing directives) to `stages/{node_id}@{visit}/status.json`. +After an agent or prompt node completes, Fabro captures the full response text and persists it to `stages/{node_id}@{visit}/response.md` in metadata snapshots and `fabro dump` output. It also writes the final outcome (status, context updates, routing directives) to `stages/{node_id}@{visit}/status.json`. ## Context updates @@ -92,7 +92,7 @@ review -> approve [label="Approve"] ## Output logging -Fabro writes several files per stage to `stages/{node_id}@{visit}/` in metadata snapshots and `fabro store dump` output: +Fabro writes several files per stage to `stages/{node_id}@{visit}/` in metadata snapshots and `fabro dump` output: | File | Contents | |---|---| diff --git a/docs/agents/prompts.mdx b/docs/agents/prompts.mdx index 69404aa79..61cc58dc1 100644 --- a/docs/agents/prompts.mdx +++ b/docs/agents/prompts.mdx @@ -295,4 +295,4 @@ Use prompt nodes for analysis, classification, and summarization tasks where too ## Prompt logging -Fabro persists the assembled prompt to `stages/{node_id}@{visit}/prompt.md` in metadata snapshots and `fabro store dump` output for every agent and prompt stage. This includes the preamble (if any) and the expanded prompt text. Use these files for debugging when an agent behaves unexpectedly. +Fabro persists the assembled prompt to `stages/{node_id}@{visit}/prompt.md` in metadata snapshots and `fabro dump` output for every agent and prompt stage. This includes the preamble (if any) and the expanded prompt text. Use these files for debugging when an agent behaves unexpectedly. diff --git a/docs/execution/observability.mdx b/docs/execution/observability.mdx index 80eff8ed8..d10a5ec91 100644 --- a/docs/execution/observability.mdx +++ b/docs/execution/observability.mdx @@ -81,7 +81,7 @@ jq '{from: .properties.from_node, to: .properties.to_node, label: .properties.la <(fabro logs 01JKXYZ...) | head ``` -If you need files on disk for offline analysis, `fabro store dump` exports `events.jsonl` plus run-state projections. +If you need files on disk for offline analysis, `fabro dump` exports `events.jsonl` plus run-state projections. ## Event categories @@ -132,6 +132,6 @@ Post-run analysis surfaces include: |---|---| | `fabro logs ` | Full event envelope stream as NDJSON | | `fabro inspect ` | Current durable run state, including run/start/checkpoint/conclusion records | -| `fabro store dump --output ` | Exported `events.jsonl` plus reconstructed JSON and node files | +| `fabro dump --output ` | Exported `events.jsonl` plus reconstructed JSON and node files | See [retros](/execution/retros), [stages](/api-reference/run-internals/list-run-stages), and [turns](/api-reference/run-internals/list-stage-turns) for higher-level analysis views built on top of this event stream. diff --git a/docs/execution/retros.mdx b/docs/execution/retros.mdx index 879190803..e5a8437f4 100644 --- a/docs/execution/retros.mdx +++ b/docs/execution/retros.mdx @@ -143,4 +143,4 @@ Retros are also available via the REST API. See the [list retros](/api-reference ## Storage -Retros are stored in durable run state. If you need files on disk, `fabro store dump` materializes retro text under `stages/retro/` alongside `run.json`, stage files, and the rest of the exported run data. +Retros are stored in durable run state. If you need files on disk, `fabro dump` materializes retro text under `stages/retro/` alongside `run.json`, stage files, and the rest of the exported run data. diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx index 8a1f33e6e..719a2fbbc 100644 --- a/docs/reference/cli.mdx +++ b/docs/reference/cli.mdx @@ -947,13 +947,13 @@ fabro secret rm ANTHROPIC_API_KEY --- -## `fabro store dump` +## `fabro dump` Export the contents of a run's store-backed state to a directory for debugging and inspection. ```bash -fabro store dump -fabro store dump abc123 -o ./debug-output +fabro dump +fabro dump abc123 -o ./debug-output ``` | Argument / Flag | Description | diff --git a/docs/reference/run-directory.mdx b/docs/reference/run-directory.mdx index d707afa35..f463697b3 100644 --- a/docs/reference/run-directory.mdx +++ b/docs/reference/run-directory.mdx @@ -30,18 +30,18 @@ These paths are local runtime state and caches, not the canonical run state. - **`runtime/`** — Local runtime files. Today this is mainly materialized blob payloads under `runtime/blobs/`. - **`nodes/{manager_node}_{visit}/child/`** — Nested scratch directories for manager-loop child workflows. -Large durable values, event streams, checkpoints, diffs, conclusions, and retros are no longer projected into live scratch by default. Use `fabro logs`, `fabro inspect`, the API, or `fabro store dump` for those surfaces. +Large durable values, event streams, checkpoints, diffs, conclusions, and retros are no longer projected into live scratch by default. Use `fabro logs`, `fabro inspect`, the API, or `fabro dump` for those surfaces. ## Reconstructed and export-only layouts -Reconstructed metadata branches and `fabro store dump` exports now use the same core layout: +Reconstructed metadata branches and `fabro dump` exports now use the same core layout: - `run.json` for the current projection snapshot, including the current checkpoint - `graph.fabro` for workflow source - `stages/retro/*.md` for retro prompt/response text - `stages/{node_id}@{visit}/...` for per-stage prompt, response, status, diff, stdout, and stderr files -`fabro store dump` adds export-only history surfaces on top of that shared layout: +`fabro dump` adds export-only history surfaces on top of that shared layout: - `events.jsonl` for the durable event stream - `checkpoints/*.json` for checkpoint history snapshots diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs index 44e4160e3..67ccdc5eb 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -514,7 +514,7 @@ pub(crate) struct InspectArgs { } #[derive(Args)] -pub(crate) struct StoreDumpArgs { +pub(crate) struct DumpArgs { #[command(flatten)] pub(crate) server: ServerTargetArgs, @@ -1001,8 +1001,8 @@ pub(crate) enum Commands { Parse(ParseArgs), /// Inspect and copy run artifacts (screenshots, reports, traces) Artifact(ArtifactNamespace), - /// Export store-backed run state for debugging - Store(StoreNamespace), + /// Export a run's durable state to a directory + Dump(DumpArgs), #[command(flatten)] RunsCmd(RunsCommands), /// List and test LLM models @@ -1085,9 +1085,7 @@ impl Commands { ArtifactCommand::List(_) => "artifact list", ArtifactCommand::Cp(_) => "artifact cp", }, - Self::Store(ns) => match &ns.command { - StoreCommand::Dump(_) => "store dump", - }, + Self::Dump(_) => "dump", Self::Exec(_) => "exec", Self::RunCmd(cmd) => cmd.name(), Self::Preflight(_) => "preflight", @@ -1199,18 +1197,6 @@ pub(crate) enum ArtifactCommand { Cp(ArtifactCpArgs), } -#[derive(Args)] -pub(crate) struct StoreNamespace { - #[command(subcommand)] - pub(crate) command: StoreCommand, -} - -#[derive(Subcommand)] -pub(crate) enum StoreCommand { - /// Export a run's durable state to a directory - Dump(StoreDumpArgs), -} - #[derive(Args)] pub(crate) struct SecretNamespace { #[command(flatten)] diff --git a/lib/crates/fabro-cli/src/commands/store/dump.rs b/lib/crates/fabro-cli/src/commands/store/dump.rs index df1cf1f6a..bc5d27453 100644 --- a/lib/crates/fabro-cli/src/commands/store/dump.rs +++ b/lib/crates/fabro-cli/src/commands/store/dump.rs @@ -1,6 +1,6 @@ #![expect( clippy::disallowed_methods, - reason = "CLI `store dump` command: sync file I/O for dump outputs" + reason = "CLI `dump` command: sync file I/O for dump outputs" )] use std::io::ErrorKind; @@ -21,13 +21,13 @@ use serde::de::DeserializeOwned; use tokio::task::spawn_blocking; use super::run_export::StoreRunExport; -use crate::args::StoreDumpArgs; +use crate::args::DumpArgs; use crate::command_context::CommandContext; use crate::server_client::Client; use crate::shared::{absolute_or_current, print_json_pretty}; pub(crate) async fn dump_command( - args: &StoreDumpArgs, + args: &DumpArgs, cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/store/mod.rs b/lib/crates/fabro-cli/src/commands/store/mod.rs index 2c885231b..b384d7c30 100644 --- a/lib/crates/fabro-cli/src/commands/store/mod.rs +++ b/lib/crates/fabro-cli/src/commands/store/mod.rs @@ -1,21 +1,3 @@ pub(crate) mod dump; pub(crate) mod rebuild; mod run_export; - -use anyhow::Result; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::CliLayer; -use fabro_util::printer::Printer; - -use crate::args::{StoreCommand, StoreNamespace}; - -pub(crate) async fn dispatch( - ns: StoreNamespace, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { - match ns.command { - StoreCommand::Dump(args) => dump::dump_command(&args, cli, cli_layer, printer).await, - } -} diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 6e54808ff..7efe0e1ed 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -247,8 +247,9 @@ async fn main_inner() -> (String, Result<()>) { Commands::Artifact(ns) => { commands::artifact::dispatch(ns, &cli_settings, &cli_layer, printer).await?; } - Commands::Store(ns) => { - commands::store::dispatch(ns, &cli_settings, &cli_layer, printer).await?; + Commands::Dump(args) => { + commands::store::dump::dump_command(&args, &cli_settings, &cli_layer, printer) + .await?; } Commands::RunsCmd(cmd) => { commands::runs::dispatch(cmd, &cli_settings, &cli_layer, printer).await?; @@ -505,7 +506,7 @@ async fn prepare_server_bootstrap( mod tests { use args::{ AuthCommand, AuthNamespace, Commands, InstallGitHubStrategyArg, ModelsCommand, - ProviderCommand, ProviderNamespace, StoreCommand, StoreNamespace, + ProviderCommand, ProviderNamespace, }; use tokio::runtime::Runtime; @@ -940,13 +941,11 @@ level = "warn" } #[test] - fn parse_store_dump_command() { - let cli = Cli::try_parse_from(["fabro", "store", "dump", "ABC123", "-o", "./out"]) - .expect("should parse"); + fn parse_dump_command() { + let cli = + Cli::try_parse_from(["fabro", "dump", "ABC123", "-o", "./out"]).expect("should parse"); match *cli.command.unwrap() { - Commands::Store(StoreNamespace { - command: StoreCommand::Dump(args), - }) => { + Commands::Dump(args) => { assert_eq!(args.run, "ABC123"); assert_eq!(args.output, std::path::PathBuf::from("./out")); } diff --git a/lib/crates/fabro-cli/tests/it/cmd/store_dump.rs b/lib/crates/fabro-cli/tests/it/cmd/dump.rs similarity index 87% rename from lib/crates/fabro-cli/tests/it/cmd/store_dump.rs rename to lib/crates/fabro-cli/tests/it/cmd/dump.rs index 1fedcf9e0..97a5d89f3 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/store_dump.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/dump.rs @@ -16,14 +16,14 @@ use crate::support::{LightweightCli, unique_run_id}; fn help() { let context = test_context!(); let mut cmd = context.command(); - cmd.args(["store", "dump", "--help"]); + cmd.args(["dump", "--help"]); fabro_snapshot!(context.filters(), cmd, @" success: true exit_code: 0 ----- stdout ----- Export a run's durable state to a directory - Usage: fabro store dump [OPTIONS] --output + Usage: fabro dump [OPTIONS] --output Arguments: Run ID prefix or workflow name @@ -42,7 +42,27 @@ fn help() { } #[test] -fn store_dump_accepts_server_target_from_separate_home() { +fn old_store_dump_command_is_rejected() { + let context = test_context!(); + let mut cmd = context.command(); + cmd.args(["store", "dump", "--help"]); + fabro_snapshot!(context.filters(), cmd, @" + success: false + exit_code: 2 + ----- stdout ----- + ----- stderr ----- + error: unrecognized subcommand 'store' + + tip: some similar subcommands exist: 'server', 'secret', 'system', 'start' + + Usage: fabro [OPTIONS] [COMMAND] + + For more information, try '--help'. + "); +} + +#[test] +fn dump_accepts_server_target_from_separate_home() { let context = test_context!(); let run = setup_completed_dry_run(&context); let cli = LightweightCli::new(); @@ -51,7 +71,6 @@ fn store_dump_accepts_server_target_from_separate_home() { let mut cmd = cli.command(); cmd.args([ - "store", "dump", "--server", &server, @@ -63,10 +82,10 @@ fn store_dump_accepts_server_target_from_separate_home() { cmd.env("FABRO_DEV_TOKEN", dev_token); } - let output = cmd.output().expect("store dump should execute"); + let output = cmd.output().expect("dump should execute"); assert!( output.status.success(), - "store dump via remote server target failed\nstdout:\n{}\nstderr:\n{}", + "dump via remote server target failed\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); @@ -74,7 +93,7 @@ fn store_dump_accepts_server_target_from_separate_home() { } #[test] -fn store_dump_exports_large_command_output_backed_by_blob_refs() { +fn dump_exports_large_command_output_backed_by_blob_refs() { let context = test_context!(); let workflow = context.temp_dir.join("large-output.fabro"); fs::write( @@ -130,17 +149,11 @@ fn store_dump_exports_large_command_output_backed_by_blob_refs() { let output_dir = context.temp_dir.join("export"); let mut dump_cmd = context.command(); - dump_cmd.args([ - "store", - "dump", - "--output", - output_dir.to_str().unwrap(), - &run_id, - ]); - let dump_output = dump_cmd.output().expect("store dump should execute"); + dump_cmd.args(["dump", "--output", output_dir.to_str().unwrap(), &run_id]); + let dump_output = dump_cmd.output().expect("dump should execute"); assert!( dump_output.status.success(), - "store dump failed\nstdout:\n{}\nstderr:\n{}", + "dump failed\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&dump_output.stdout), String::from_utf8_lossy(&dump_output.stderr) ); @@ -153,7 +166,7 @@ fn store_dump_exports_large_command_output_backed_by_blob_refs() { } #[test] -fn store_dump_exports_blob_refs_and_artifacts_together() { +fn dump_exports_blob_refs_and_artifacts_together() { let context = test_context!(); let workspace_dir = context.temp_dir.join("mixed-export"); fs::create_dir_all(&workspace_dir).unwrap(); @@ -233,17 +246,11 @@ include = ["assets/**"] let output_dir = context.temp_dir.join("export-mixed"); let mut dump_cmd = context.command(); - dump_cmd.args([ - "store", - "dump", - "--output", - output_dir.to_str().unwrap(), - &run_id, - ]); - let dump_output = dump_cmd.output().expect("store dump should execute"); + dump_cmd.args(["dump", "--output", output_dir.to_str().unwrap(), &run_id]); + let dump_output = dump_cmd.output().expect("dump should execute"); assert!( dump_output.status.success(), - "store dump failed\nstdout:\n{}\nstderr:\n{}", + "dump failed\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&dump_output.stdout), String::from_utf8_lossy(&dump_output.stderr) ); @@ -260,14 +267,13 @@ include = ["assets/**"] } #[test] -fn store_dump_exports_completed_run_snapshot() { +fn dump_exports_completed_run_snapshot() { let context = test_context!(); let run = setup_completed_dry_run(&context); let output_dir = context.temp_dir.join("export"); let mut cmd = context.command(); cmd.args([ - "store", "dump", "--output", output_dir.to_str().unwrap(), @@ -298,7 +304,7 @@ fn store_dump_exports_completed_run_snapshot() { } #[test] -fn store_dump_rejects_non_empty_output_dir() { +fn dump_rejects_non_empty_output_dir() { let context = test_context!(); let run = setup_completed_dry_run(&context); let output_dir = context.temp_dir.join("nonempty"); @@ -307,7 +313,6 @@ fn store_dump_rejects_non_empty_output_dir() { let mut cmd = context.command(); cmd.args([ - "store", "dump", "--output", output_dir.to_str().unwrap(), diff --git a/lib/crates/fabro-cli/tests/it/cmd/fabro.rs b/lib/crates/fabro-cli/tests/it/cmd/fabro.rs index ee823c0f5..a8fbc255d 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/fabro.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/fabro.rs @@ -25,7 +25,7 @@ fn help() { validate Validate a workflow graph Render a workflow graph as SVG artifact Inspect and copy run artifacts (screenshots, reports, traces) - store Export store-backed run state for debugging + dump Export a run's durable state to a directory rm Remove one or more workflow runs inspect Show detailed information about a workflow run archive Mark terminal runs as archived (reviewed, no further action needed). Archived runs are hidden from default listings diff --git a/lib/crates/fabro-cli/tests/it/cmd/mod.rs b/lib/crates/fabro-cli/tests/it/cmd/mod.rs index 8cae23120..880ef3d99 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/mod.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/mod.rs @@ -9,6 +9,7 @@ mod diff; mod discord; mod docs; mod doctor; +mod dump; mod exec; mod fabro; mod fork; @@ -53,8 +54,6 @@ mod server_start; mod server_status; mod server_stop; mod start; -mod store; -mod store_dump; pub(crate) mod support; mod system; mod system_df; diff --git a/lib/crates/fabro-cli/tests/it/cmd/store.rs b/lib/crates/fabro-cli/tests/it/cmd/store.rs deleted file mode 100644 index 082ce6ba3..000000000 --- a/lib/crates/fabro-cli/tests/it/cmd/store.rs +++ /dev/null @@ -1,29 +0,0 @@ -use fabro_test::{fabro_snapshot, test_context}; - -#[test] -fn help() { - let context = test_context!(); - let mut cmd = context.command(); - cmd.args(["store", "--help"]); - fabro_snapshot!(context.filters(), cmd, @" - success: true - exit_code: 0 - ----- stdout ----- - Export store-backed run state for debugging - - Usage: fabro store [OPTIONS] - - Commands: - dump Export a run's durable state to a directory - help Print this message or the help of the given subcommand(s) - - Options: - --json Output as JSON [env: FABRO_JSON=] - --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] - --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] - --quiet Suppress non-essential output [env: FABRO_QUIET=] - --verbose Enable verbose output [env: FABRO_VERBOSE=] - -h, --help Print help - ----- stderr ----- - "); -} diff --git a/lib/crates/fabro-cli/tests/it/workflow/command_agent_mixed.rs b/lib/crates/fabro-cli/tests/it/workflow/command_agent_mixed.rs index ee9ba5be8..d0fbabe4f 100644 --- a/lib/crates/fabro-cli/tests/it/workflow/command_agent_mixed.rs +++ b/lib/crates/fabro-cli/tests/it/workflow/command_agent_mixed.rs @@ -6,8 +6,8 @@ use fabro_test::test_context; use super::{ - completed_nodes, find_run_dir, fixture, read_conclusion, run_id_for, sandbox_tests, - store_dump_export, timeout_for, + completed_nodes, dump_export, find_run_dir, fixture, read_conclusion, run_id_for, + sandbox_tests, timeout_for, }; sandbox_tests!(command_agent_mixed, keys = ["ANTHROPIC_API_KEY"]); @@ -48,7 +48,7 @@ fn scenario_command_agent_mixed(sandbox: &str) { "verify should be completed" ); - let export_dir = store_dump_export(&context, &run_id_for(&run_dir)); + let export_dir = dump_export(&context, &run_id_for(&run_dir)); let stdout = std::fs::read_to_string(export_dir.join("stages/verify@1/stdout.log")) .expect("verify stdout.log should exist"); assert!( diff --git a/lib/crates/fabro-cli/tests/it/workflow/command_pipeline.rs b/lib/crates/fabro-cli/tests/it/workflow/command_pipeline.rs index 5a46d92e4..7c1d7eae7 100644 --- a/lib/crates/fabro-cli/tests/it/workflow/command_pipeline.rs +++ b/lib/crates/fabro-cli/tests/it/workflow/command_pipeline.rs @@ -6,8 +6,8 @@ use fabro_test::test_context; use super::{ - completed_nodes, find_run_dir, fixture, read_conclusion, run_id_for, sandbox_tests, - store_dump_export, timeout_for, + completed_nodes, dump_export, find_run_dir, fixture, read_conclusion, run_id_for, + sandbox_tests, timeout_for, }; sandbox_tests!(command_pipeline); @@ -47,7 +47,7 @@ fn scenario_command_pipeline(sandbox: &str) { "step2 should be completed" ); - let export_dir = store_dump_export(&context, &run_id_for(&run_dir)); + let export_dir = dump_export(&context, &run_id_for(&run_dir)); let stdout1 = std::fs::read_to_string(export_dir.join("stages/step1@1/stdout.log")) .expect("step1 stdout.log should exist"); assert!( diff --git a/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs b/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs index 4b03f1155..a2a1feff4 100644 --- a/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs +++ b/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs @@ -6,8 +6,8 @@ use fabro_test::test_context; use super::{ - completed_nodes, find_run_dir, fixture, has_event, read_conclusion, read_run_spec, run_id_for, - sandbox_tests, store_dump_export, timeout_for, + completed_nodes, dump_export, find_run_dir, fixture, has_event, read_conclusion, read_run_spec, + run_id_for, sandbox_tests, timeout_for, }; sandbox_tests!(full_stack, keys = ["ANTHROPIC_API_KEY"]); @@ -73,7 +73,7 @@ fn scenario_full_stack(sandbox: &str) { } // Verify node stdout should contain PASS - let export_dir = store_dump_export(&context, &run_id_for(&run_dir)); + let export_dir = dump_export(&context, &run_id_for(&run_dir)); let stdout = std::fs::read_to_string(export_dir.join("stages/verify@1/stdout.log")) .expect("verify stdout.log should exist"); assert!( diff --git a/lib/crates/fabro-cli/tests/it/workflow/mod.rs b/lib/crates/fabro-cli/tests/it/workflow/mod.rs index 5cf1005a4..b7911837a 100644 --- a/lib/crates/fabro-cli/tests/it/workflow/mod.rs +++ b/lib/crates/fabro-cli/tests/it/workflow/mod.rs @@ -59,17 +59,16 @@ pub(super) fn has_event(run_dir: &Path, event_name: &str) -> bool { .any(|event| event.event.event_name() == event_name) } -pub(super) fn store_dump_export(context: &TestContext, run_id: &str) -> PathBuf { - let output_dir = context.temp_dir.join(format!("store-dump-{run_id}")); +pub(super) fn dump_export(context: &TestContext, run_id: &str) -> PathBuf { + let output_dir = context.temp_dir.join(format!("dump-{run_id}")); context .command() .args([ - "store", "dump", "--output", output_dir .to_str() - .expect("store dump output path should be valid UTF-8"), + .expect("dump output path should be valid UTF-8"), run_id, ]) .assert() From a8623fb996b9b5aea7a3e5147c91fb850a9fadbf Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 23 Apr 2026 07:58:26 -0400 Subject: [PATCH 13/13] simplify: flatten commands/store/ after dump rename The `fabro store dump` -> `fabro dump` rename left `commands/store/` as a vestigial directory with a stale one-line `StoreRunExport` alias. Move `dump.rs` and `rebuild.rs` up to `commands/`, import `RunDump` directly, rename `dump::dump_command` -> `dump::run`, and clean up stale docs and a noise test that only asserted clap's default error output. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs-internal/cli-workflow-coupling-audit.md | 2 +- docs/changelog/2026-03-29.mdx | 8 ++++---- docs/reference/cli.mdx | 2 +- .../src/commands/{store => }/dump.rs | 8 ++++---- lib/crates/fabro-cli/src/commands/mod.rs | 3 ++- .../fabro-cli/src/commands/pr/create.rs | 2 +- .../src/commands/{store => }/rebuild.rs | 0 lib/crates/fabro-cli/src/commands/run/fork.rs | 2 +- .../fabro-cli/src/commands/run/rewind.rs | 2 +- .../fabro-cli/src/commands/store/mod.rs | 3 --- .../src/commands/store/run_export.rs | 1 - lib/crates/fabro-cli/src/main.rs | 3 +-- lib/crates/fabro-cli/tests/it/cmd/dump.rs | 20 ------------------- 13 files changed, 16 insertions(+), 40 deletions(-) rename lib/crates/fabro-cli/src/commands/{store => }/dump.rs (99%) rename lib/crates/fabro-cli/src/commands/{store => }/rebuild.rs (100%) delete mode 100644 lib/crates/fabro-cli/src/commands/store/mod.rs delete mode 100644 lib/crates/fabro-cli/src/commands/store/run_export.rs diff --git a/docs-internal/cli-workflow-coupling-audit.md b/docs-internal/cli-workflow-coupling-audit.md index 00f9c295b..a58c2c828 100644 --- a/docs-internal/cli-workflow-coupling-audit.md +++ b/docs-internal/cli-workflow-coupling-audit.md @@ -34,7 +34,7 @@ | Path | Direct dependency | Why it still exists | Suggested handling | | --- | --- | --- | --- | -| `lib/crates/fabro-cli/src/commands/store/dump.rs` test module | `event::{Event, append_event}` | Unit tests synthesize workflow events directly. | Low priority; keep until a lighter-weight event fixture helper exists. | +| `lib/crates/fabro-cli/src/commands/dump.rs` test module | `event::{Event, append_event}` | Unit tests synthesize workflow events directly. | Low priority; keep until a lighter-weight event fixture helper exists. | | `lib/crates/fabro-cli/src/commands/run/wait.rs` test module | `outcome::StageStatus`, `records::Conclusion`, `run_status::RunStatusRecord` | Output tests construct workflow-owned records directly. | Replace with shared fixture builders once status/conclusion DTOs move out. | | `lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs` test module | `event::{Event, RunNoticeLevel, to_run_event, to_run_event_at}`, `outcome::billed_model_usage_from_llm` | Progress tests build engine events directly. | Replace with shared event fixture helpers after event DTO extraction. | | `lib/crates/fabro-cli/src/commands/run/run_progress/event.rs` test module | `event::{Event, to_run_event}` | Event rendering tests depend on engine event constructors. | Replace with shared event fixture helpers after event DTO extraction. | diff --git a/docs/changelog/2026-03-29.mdx b/docs/changelog/2026-03-29.mdx index e6e82bdcf..f89bbadab 100644 --- a/docs/changelog/2026-03-29.mdx +++ b/docs/changelog/2026-03-29.mdx @@ -1,14 +1,14 @@ --- -title: "Store dump export command" +title: "Dump export command" date: "2026-03-29" --- -## `fabro store dump` +## `fabro dump` -A new `fabro store dump` command exports the contents of the run store to a human-readable format for debugging and inspection. This is useful for diagnosing issues with run state, verifying data integrity after migrations, or extracting run data for external analysis. +A new `fabro dump` command exports the contents of the run store to a human-readable format for debugging and inspection. This is useful for diagnosing issues with run state, verifying data integrity after migrations, or extracting run data for external analysis. ```bash -fabro store dump +fabro dump ``` ## More diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx index 719a2fbbc..bac80bc2e 100644 --- a/docs/reference/cli.mdx +++ b/docs/reference/cli.mdx @@ -949,7 +949,7 @@ fabro secret rm ANTHROPIC_API_KEY ## `fabro dump` -Export the contents of a run's store-backed state to a directory for debugging and inspection. +Export the contents of a run's durable state to a directory for debugging and inspection. ```bash fabro dump diff --git a/lib/crates/fabro-cli/src/commands/store/dump.rs b/lib/crates/fabro-cli/src/commands/dump.rs similarity index 99% rename from lib/crates/fabro-cli/src/commands/store/dump.rs rename to lib/crates/fabro-cli/src/commands/dump.rs index bc5d27453..2dc98b4b4 100644 --- a/lib/crates/fabro-cli/src/commands/store/dump.rs +++ b/lib/crates/fabro-cli/src/commands/dump.rs @@ -15,18 +15,18 @@ use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_types::{RunBlobId, RunId}; use fabro_util::printer::Printer; +use fabro_workflow::run_dump::RunDump; use futures::future::BoxFuture; #[cfg(test)] use serde::de::DeserializeOwned; use tokio::task::spawn_blocking; -use super::run_export::StoreRunExport; use crate::args::DumpArgs; use crate::command_context::CommandContext; use crate::server_client::Client; use crate::shared::{absolute_or_current, print_json_pretty}; -pub(crate) async fn dump_command( +pub(crate) async fn run( args: &DumpArgs, cli: &CliNamespace, cli_layer: &CliLayer, @@ -222,7 +222,7 @@ async fn export_run_from_source( .with_context(|| format!("failed to create {}", staging_parent.display()))?; let staging_dir = tempfile::Builder::new() - .prefix(".fabro-store-dump-") + .prefix(".fabro-dump-") .tempdir_in(staging_parent) .with_context(|| { format!( @@ -248,7 +248,7 @@ async fn write_run_dump( output_dir: &Path, ) -> Result { let events = source.list_events().await?; - let mut dump = StoreRunExport::from_store_state_and_events(state, &events)?; + let mut dump = RunDump::from_store_state_and_events(state, &events)?; dump.hydrate_referenced_blobs_with_reader(|blob_id| source.read_blob(blob_id)) .await?; diff --git a/lib/crates/fabro-cli/src/commands/mod.rs b/lib/crates/fabro-cli/src/commands/mod.rs index 68eece40f..f9408d15b 100644 --- a/lib/crates/fabro-cli/src/commands/mod.rs +++ b/lib/crates/fabro-cli/src/commands/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod artifact; pub(crate) mod auth; pub(crate) mod config; pub(crate) mod doctor; +pub(crate) mod dump; pub(crate) mod exec; pub(crate) mod graph; pub(crate) mod install; @@ -10,6 +11,7 @@ pub(crate) mod parse; pub(crate) mod pr; pub(crate) mod preflight; pub(crate) mod provider; +pub(crate) mod rebuild; pub(crate) mod render_graph; pub(crate) mod repo; pub(crate) mod run; @@ -17,7 +19,6 @@ pub(crate) mod runs; pub(crate) mod sandbox; pub(crate) mod secret; pub(crate) mod server; -pub(crate) mod store; pub(crate) mod system; pub(crate) mod uninstall; pub(crate) mod upgrade; diff --git a/lib/crates/fabro-cli/src/commands/pr/create.rs b/lib/crates/fabro-cli/src/commands/pr/create.rs index e42c633e2..3f3c8c2d2 100644 --- a/lib/crates/fabro-cli/src/commands/pr/create.rs +++ b/lib/crates/fabro-cli/src/commands/pr/create.rs @@ -16,7 +16,7 @@ use tracing::info; use crate::args::PrCreateArgs; use crate::command_context::CommandContext; -use crate::commands::store::rebuild::rebuild_run_store; +use crate::commands::rebuild::rebuild_run_store; use crate::shared::print_json_pretty; use crate::shared::repo::ensure_matching_repo_origin; use crate::user_config; diff --git a/lib/crates/fabro-cli/src/commands/store/rebuild.rs b/lib/crates/fabro-cli/src/commands/rebuild.rs similarity index 100% rename from lib/crates/fabro-cli/src/commands/store/rebuild.rs rename to lib/crates/fabro-cli/src/commands/rebuild.rs diff --git a/lib/crates/fabro-cli/src/commands/run/fork.rs b/lib/crates/fabro-cli/src/commands/run/fork.rs index 70d5ebba6..ba988198e 100644 --- a/lib/crates/fabro-cli/src/commands/run/fork.rs +++ b/lib/crates/fabro-cli/src/commands/run/fork.rs @@ -9,7 +9,7 @@ use git2::Repository; use crate::args::ForkArgs; use crate::command_context::CommandContext; -use crate::commands::store::rebuild::rebuild_run_store; +use crate::commands::rebuild::rebuild_run_store; use crate::shared::print_json_pretty; use crate::shared::repo::ensure_matching_repo_origin; diff --git a/lib/crates/fabro-cli/src/commands/run/rewind.rs b/lib/crates/fabro-cli/src/commands/run/rewind.rs index a57e0f9d0..2793cbdcf 100644 --- a/lib/crates/fabro-cli/src/commands/run/rewind.rs +++ b/lib/crates/fabro-cli/src/commands/run/rewind.rs @@ -17,7 +17,7 @@ use serde::Serialize; use crate::args::RewindArgs; use crate::command_context::CommandContext; -use crate::commands::store::rebuild::rebuild_run_store; +use crate::commands::rebuild::rebuild_run_store; use crate::server_client::Client; use crate::shared::repo::ensure_matching_repo_origin; use crate::shared::{color_if, print_json_pretty}; diff --git a/lib/crates/fabro-cli/src/commands/store/mod.rs b/lib/crates/fabro-cli/src/commands/store/mod.rs deleted file mode 100644 index b384d7c30..000000000 --- a/lib/crates/fabro-cli/src/commands/store/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod dump; -pub(crate) mod rebuild; -mod run_export; diff --git a/lib/crates/fabro-cli/src/commands/store/run_export.rs b/lib/crates/fabro-cli/src/commands/store/run_export.rs deleted file mode 100644 index 42738384d..000000000 --- a/lib/crates/fabro-cli/src/commands/store/run_export.rs +++ /dev/null @@ -1 +0,0 @@ -pub(super) use fabro_workflow::run_dump::RunDump as StoreRunExport; diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 7efe0e1ed..816a9ac73 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -248,8 +248,7 @@ async fn main_inner() -> (String, Result<()>) { commands::artifact::dispatch(ns, &cli_settings, &cli_layer, printer).await?; } Commands::Dump(args) => { - commands::store::dump::dump_command(&args, &cli_settings, &cli_layer, printer) - .await?; + commands::dump::run(&args, &cli_settings, &cli_layer, printer).await?; } Commands::RunsCmd(cmd) => { commands::runs::dispatch(cmd, &cli_settings, &cli_layer, printer).await?; diff --git a/lib/crates/fabro-cli/tests/it/cmd/dump.rs b/lib/crates/fabro-cli/tests/it/cmd/dump.rs index 97a5d89f3..355dc7f0c 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/dump.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/dump.rs @@ -41,26 +41,6 @@ fn help() { "); } -#[test] -fn old_store_dump_command_is_rejected() { - let context = test_context!(); - let mut cmd = context.command(); - cmd.args(["store", "dump", "--help"]); - fabro_snapshot!(context.filters(), cmd, @" - success: false - exit_code: 2 - ----- stdout ----- - ----- stderr ----- - error: unrecognized subcommand 'store' - - tip: some similar subcommands exist: 'server', 'secret', 'system', 'start' - - Usage: fabro [OPTIONS] [COMMAND] - - For more information, try '--help'. - "); -} - #[test] fn dump_accepts_server_target_from_separate_home() { let context = test_context!();