`resolve_auth_mode_with_lookup` now returns `anyhow::Result<AuthMode>`
and refuses to return success when `server.auth` resolves to zero
enabled strategies. Startup propagates the error via `?` and aborts
with a descriptive message pointing at the three configuration
escape hatches.
Previously the resolver logged a warning and returned
`AuthMode::Strategies(empty)`, which meant an unconfigured server
would start and then reject every request — accidental
misconfigurations produced a silently-broken process rather than a
clean startup failure. The new behavior matches the implementation
plan's explicit guidance: "if `server.auth` is absent or resolves to
no enabled API or web auth configuration, normal server startup
must refuse to start. Demo and test helpers may continue to inject
explicit insecure settings, but insecure startup must be opt-in
rather than accidental."
The single opt-in path is the `FABRO_LOCAL_NO_AUTH` env var set to
the literal string `"1"`, now hoisted into a module-level
`FABRO_LOCAL_NO_AUTH_ENV` constant. `fabro server start --bind
<unix-socket>` already sets this implicitly in `start.rs:232-234`,
so local daemon usage is unchanged. TCP binds now require either
real auth config or an explicit `FABRO_LOCAL_NO_AUTH=1` — arguably
a security improvement for TCP.
Detailed error message lists the three configuration options:
Configure at least one of the following in `[server.auth]`:
- `[server.auth.api.jwt]` (requires `FABRO_JWT_PUBLIC_KEY` env)
- `[server.auth.api.mtls]` (requires `[server.listen.tls]` ...)
- `SESSION_SECRET` env (enables cookie-based web auth)
Adds six new unit tests covering the full decision matrix:
- `fail_closed_when_server_auth_absent`
- `fail_closed_when_all_strategies_disabled`
- `opt_in_insecure_startup_via_env`
- `insecure_startup_flag_any_other_value_still_fails_closed`
- `cookie_strategy_alone_unlocks_startup`
- `mtls_strategy_resolves_when_enabled_with_listen_tls`
Also adds `#[derive(Debug)]` to `AuthMode` and `AuthStrategy` so the
tests can `expect_err()` on the resolver result.
Two existing `fabro-cli` integration tests for TCP bind resolution
(`start_with_tcp_host_only_bind_resolves_to_host_and_port` and
`start_with_tcp_host_only_bind_warns_and_falls_back_when_default_port_is_unavailable`)
now set `FABRO_LOCAL_NO_AUTH=1` in the test environment. They were
exercising bind-address resolution, not auth, so opting into
insecure startup explicitly keeps their focus narrow.
3,764 workspace tests pass (was 3,758, +6 new). `cargo fmt
--check --all` and `cargo clippy --workspace -- -D warnings` are
clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`TlsSettings` and its `from_settings(&SettingsFile)` constructor
lived in `jwt_auth.rs` as a historical artifact from the Stage 6.6g
rewrite — the auth resolver only needs to know *whether* TLS is
present (for mTLS support), not the contents of the triple. The
type is really a listen-side concern that belongs next to the
rustls builder.
Moves the type into a new `fabro-server/src/tls_config.rs` module
(35 LOC). Updates three importers:
- `jwt_auth.rs` — imports `TlsSettings` from `crate::tls_config`;
drops the `std::path::PathBuf` / `InterpString` / `ServerListenLayer`
/ `serde::Deserialize` imports that are no longer used after the
type moved.
- `serve.rs` — splits the multi-item `use crate::jwt_auth::{...}`
line so `TlsSettings` comes from `crate::tls_config`.
- `tls.rs` — same split.
- `tests/it/api/mtls.rs` — same split.
Pure relocation; no behavioral change. 156 fabro-server tests pass,
`cargo fmt --check --all` and `cargo clippy --workspace -- -D warnings`
are clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Final mechanical pass: replaces every remaining
`fabro_types::settings::v2::*` import path with
`fabro_types::settings::*` (or the appropriate submodule) across 53
files in 10 crates, then deletes the transitional
`pub mod v2 { pub use super::*; }` alias from
`fabro-types/src/settings/mod.rs`.
No functional changes — all touches are `sed s|settings::v2::|settings::|g`
on import statements and fully-qualified type paths. The v2
namespace is now fully gone; the authoritative module path is
`fabro_types::settings::{accessors, cli, duration, features, interp,
model_ref, project, run, server, size, splice_array, tree, version,
workflow}`.
All 3,758 workspace tests pass. `cargo fmt --check --all` and
`cargo clippy --workspace -- -D warnings` are clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the `build_legacy_api_settings` + `resolve_auth_mode_with_lookup(&ApiSettings, &[String], lookup)`
shim path with a direct `resolve_auth_mode_with_lookup(&SettingsFile, lookup)`
that walks the v2 `server.auth.api.{jwt,mtls}` and
`server.auth.web.allowed_usernames` subtrees directly:
- Each strategy subtree is considered enabled when present unless
`enabled = false` is explicit (R52).
- `allowed_usernames` is read from `server.auth.web.allowed_usernames`
instead of a separate caller-supplied `&[String]` slice.
- The FABRO_LOCAL_NO_AUTH escape hatch and
"no strategies configured; rejecting everything" warnings are
preserved.
Deletes the `ApiAuthStrategy` and `ApiSettings` transitional shim
types from `fabro-server/src/jwt_auth.rs`. `TlsSettings` survives
(it's the resolved `(cert, key, ca)` triple that `tls.rs`'s rustls
builder still consumes), with a new
`TlsSettings::from_settings(&SettingsFile)` constructor that
projects `server.listen.tls` into the runtime shape.
`serve.rs` drops its `build_legacy_api_settings` helper entirely
(~60 LOC). The serve bootstrap now calls
`resolve_auth_mode_with_lookup(&cfg_file, ...)` directly and uses
`TlsSettings::from_settings(&cfg_file)` for the TCP-vs-Unix branch.
The `build_legacy_api_settings` TODO-2 from handoff-2 is resolved.
TlsSettings uses `is_some_and` instead of `map_or(false, ...)` to
satisfy the clippy `unnecessary_map_or` lint.
All 3,758 workspace tests pass. `cargo fmt --check --all` and
`cargo clippy --workspace -- -D warnings` are clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
**6.3b finishing touch:** relocates the last three transitional server
runtime types (`ApiAuthStrategy`, `TlsSettings`, `ApiSettings`) out of
`fabro-types` into `fabro-server/src/jwt_auth.rs` — the only crate
that consumes them. `serve.rs`, `tls.rs`, and the mTLS integration
test now import from `crate::jwt_auth` / `fabro_server::jwt_auth`
instead of `fabro_types::settings::server`.
`lib/crates/fabro-types/src/settings/server.rs` (the legacy one) and
the `pub mod server_config { pub use fabro_types::settings::server::*; }`
block in `fabro-server/src/lib.rs` are both deleted. The legacy
runtime type module tree under `fabro-types/src/settings/{hook,
mcp, project, run, sandbox, server, user}.rs` is now fully gone —
nothing left to promote.
**6.5b flatten:** `git mv` the fourteen v2 modules up one directory:
- `settings/v2/accessors.rs` → `settings/accessors.rs`
- `settings/v2/cli.rs` → `settings/cli.rs`
- `settings/v2/duration.rs` → `settings/duration.rs`
- `settings/v2/features.rs` → `settings/features.rs`
- `settings/v2/interp.rs` → `settings/interp.rs`
- `settings/v2/model_ref.rs` → `settings/model_ref.rs`
- `settings/v2/project.rs` → `settings/project.rs`
- `settings/v2/run.rs` → `settings/run.rs`
- `settings/v2/server.rs` → `settings/server.rs` (name no longer
collides with the deleted legacy `server.rs`)
- `settings/v2/size.rs` → `settings/size.rs`
- `settings/v2/splice_array.rs` → `settings/splice_array.rs`
- `settings/v2/tree.rs` → `settings/tree.rs`
- `settings/v2/version.rs` → `settings/version.rs`
- `settings/v2/workflow.rs` → `settings/workflow.rs`
- `settings/v2/mod.rs` — deleted (its `pub mod` / `pub use` block
moved into `settings/mod.rs`).
`settings/mod.rs` picks up those `pub mod` declarations and the
accompanying `pub use <module>::*` re-exports, plus a transitional
`pub mod v2 { pub use super::*; }` alias so that existing
`fabro_types::settings::v2::*` import paths across the workspace
keep compiling. A follow-up sweep will drop the `::v2::` prefix from
every consumer and then the alias can go away.
3,758 workspace tests pass. `cargo fmt --check --all` and
`cargo clippy --workspace -- -D warnings` are clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prunes `fabro-types/src/settings/server.rs` down to just the three
types that still have live consumers:
- `ApiAuthStrategy` — used by `fabro-server::jwt_auth::resolve_auth_mode_with_lookup`
- `TlsSettings` — used by `fabro-server::tls::*` and the mTLS integration test
- `ApiSettings` — the shim struct built by
`fabro-server::serve::build_legacy_api_settings` so the pre-v2
`resolve_auth_mode_with_lookup` signature still compiles
Deletes the rest as dead code (all unreferenced in the workspace):
`AuthProvider`, `AuthSettings`, `GitProvider`, `GitSettings`,
`GitAuthorSettings`, `WebSettings`, `WebhookSettings`,
`WebhookStrategy`, `SlackSettings`, `FeaturesSettings`, `LogSettings`,
`ArtifactStorageBackend`, `ArtifactStorageSettings`. Trims the
`ApiSettings` struct itself to just the two fields the auth resolver
reads; drops the never-used `base_url` field and the
`build_legacy_api_settings` lines that were computing it.
Drops `pub use settings::{ArtifactStorageBackend, ArtifactStorageSettings}`
from `fabro-types/src/lib.rs`.
Also deletes the dead `Combine` trait machinery alongside its only
remaining consumers:
- `lib/crates/fabro-types/src/combine.rs` — deleted.
- `pub mod combine;` / `pub use fabro_macros::Combine;` removed from
`fabro-types/src/lib.rs`.
- `#[proc_macro_derive(Combine)] fn derive_combine` — deleted from
`fabro-macros/src/lib.rs` along with its `syn::{Data, DeriveInput,
Fields}` imports. The `e2e_test` proc-macro is untouched.
The seven legacy runtime type modules
(`hook`, `mcp`, `project`, `run`, `sandbox`, `user`, plus now the
bulk of `server`) are effectively all gone. Only a tiny `server.rs`
remains as a transitional home for the three auth-resolver types
until Stage 6.6g rewrites `resolve_auth_mode_with_lookup` to walk
the v2 `server.auth.api` subtree directly.
3,758 workspace tests pass. `cargo fmt --check --all` and
`cargo clippy --workspace -- -D warnings` are clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moves the only actively-used types from
`fabro-types/src/settings/run.rs` — `PullRequestSettings`,
`MergeStrategy`, `ArtifactsSettings` — into a new
`fabro-workflow/src/config.rs` module. The other types in that file
(`LlmSettings`, `SetupSettings`, `CheckpointSettings`,
`GitHubSettings`) had no remaining consumers in the workspace and
are deleted outright.
`bridge_pull_request`, `bridge_merge_strategy`, and
`bridge_run_artifacts` move along with them into
`fabro-workflow/src/config.rs`. That empties
`fabro-types/src/settings/v2/to_runtime.rs`, so the file is deleted
and its `pub mod` declaration removed from `v2/mod.rs`. Stage 6.2's
"narrow runtime-type conversion helpers" module is completely gone.
Consumer updates:
- `fabro-workflow/src/lib.rs` exposes `pub mod config`.
- `fabro-workflow/src/operations/start.rs` imports
`PullRequestSettings` and `bridge_pull_request` from
`crate::config`.
- `fabro-workflow/src/pipeline/types.rs` imports
`PullRequestSettings` from `crate::config`.
- `fabro-workflow/src/pipeline/pull_request.rs` imports
`MergeStrategy` from `crate::config`.
`fabro-types/src/settings/mod.rs` drops `pub mod run` and the
corresponding `pub use run::{ArtifactsSettings, ...}` re-export.
Six of the seven legacy runtime type modules are now gone; only
`server.rs` remains. 3,758 workspace tests pass. `cargo fmt
--check --all` and `cargo clippy --workspace -- -D warnings` are
clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moves the sandbox runtime types from `fabro-types/src/settings/sandbox.rs`
into a new `fabro-sandbox/src/config.rs` module:
- `SandboxSettings`, `LocalSandboxSettings`, `DaytonaSettings`,
`DaytonaSnapshotSettings`, `DaytonaNetwork`, `DockerfileSource`,
`WorktreeMode` (with the custom serde `DaytonaNetwork`
serialize/deserialize impls intact).
- `bridge_sandbox` and `bridge_worktree_mode` (v2
`RunSandboxLayer` → `SandboxSettings` converters) also move from
`fabro-types/src/settings/v2/to_runtime.rs` into the new config
module.
`fabro-sandbox/src/daytona/mod.rs` and `sandbox_spec.rs` update to
import from the crate-local `config` module instead of
`fabro_types::settings::sandbox`. The daytona module still re-exports
`DaytonaSettings as DaytonaConfig` etc., so no breaking changes for
callers of `fabro_sandbox::daytona::*`.
Consumer updates:
- `fabro-workflow/src/operations/start.rs` and `pipeline/types.rs`
now import `WorktreeMode`, `SandboxSettings` (as `sandbox_config`
alias), `bridge_sandbox`, and `bridge_worktree_mode` from
`fabro_sandbox::config`.
- `fabro-server/src/run_manifest.rs` imports `bridge_sandbox` from
`fabro_sandbox::config`.
`to_runtime.rs` in fabro-types shrinks to just the three remaining
helpers tied to the legacy `run.rs` module types
(`bridge_merge_strategy`, `bridge_pull_request`, `bridge_run_artifacts`).
Those move out in the next 6.3b pass when the `run.rs` module itself
moves.
Five of the seven legacy runtime type modules are now gone; two
remain (run, server). 3,758 workspace tests pass. `cargo fmt
--check --all` and `cargo clippy --workspace -- -D warnings` are
clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moves `McpServerEntry`, `McpServerSettings`, `McpTransport`, plus the
`default_startup_timeout_secs` / `default_tool_timeout_secs` helpers
from `fabro-types/src/settings/mcp.rs` into
`fabro-mcp/src/config.rs`. fabro-mcp was already the only crate that
re-exported them, so this deletes the `fabro-types` module entirely
and drops the `pub use mcp::*` re-export from `settings/mod.rs`.
`bridge_mcps` / `bridge_mcp_entry` (v2 `McpEntryLayer` → runtime
`McpServerEntry` converters) also move to `fabro-mcp/src/config.rs`.
`fabro-workflow::operations::start` and `fabro-cli::commands::exec`
now import `bridge_mcp_entry` from `fabro_mcp::config::bridge_mcp_entry`
instead of the v2 `to_runtime` module.
Four of the seven legacy runtime type modules are now gone; three
remain (run, sandbox, server). The `to_runtime.rs` module is down to
just sandbox, pull-request, merge-strategy, artifacts, and
worktree-mode helpers.
3,758 workspace tests pass. `cargo fmt --check --all` and
`cargo clippy --workspace -- -D warnings` are clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two more legacy runtime type modules deleted from `fabro-types`:
**project.rs** (19 LOC): `ProjectSettings` was a trivial one-field
struct with a `pub use` re-export in `fabro-config/src/project.rs`.
Nothing else referenced it. Deleted outright; `fabro-config/src/project.rs`
drops the re-export and fixes up a `v2::` import path.
**hook.rs** (230 LOC): `HookDefinition`, `HookEvent`, `HookSettings`,
`HookType`, `TlsMode` plus the `resolved_hook_type` / `is_blocking`
/ `timeout` / `runs_in_sandbox` / `effective_name` behavior methods
are **moved** (not just re-exported) into
`fabro-hooks/src/config.rs`. They're runtime shapes owned by the
hook executor, so they belong in the consumer crate.
`bridge_hook` (and its private `resolve_hook_type` /
`bridge_hook_event` helpers) also moved from
`fabro-types/src/settings/v2/to_runtime.rs` into
`fabro-hooks/src/config.rs`, because the target type is now local
to `fabro-hooks`. `fabro-workflow/src/operations/start.rs` now
imports `bridge_hook` from `fabro_hooks::config::bridge_hook`
instead of the v2 `to_runtime` module.
`fabro-hooks/src/types.rs` re-export of `HookEvent` switches from
the deleted `fabro_types::settings::hook` path to the new
crate-local `crate::config::HookEvent`.
`settings/mod.rs` drops `pub mod {hook, project}` and the
corresponding `pub use` re-exports. Three of the seven legacy
runtime type modules are now gone; four remain (mcp, run, sandbox,
server).
3,758 workspace tests pass. `cargo fmt --check --all` and
`cargo clippy --workspace -- -D warnings` are clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
First consumer migration pass. Deletes
`lib/crates/fabro-types/src/settings/user.rs` outright:
- `OutputFormat`, `PermissionLevel`: moved into `fabro-agent/src/cli.rs`
where they are actually consumed as `AgentArgs` fields. They carry
clap `ValueEnum` derives so `fabro-cli` keeps importing them via the
`fabro_agent::cli::{OutputFormat, PermissionLevel}` public path.
- `ClientTlsSettings`: moved into `fabro-cli/src/user_config.rs` as a
crate-private struct. Only `fabro-cli` references it (via
`cli_target_from_v2` when building the HTTP client).
- `ExecSettings`, legacy `ServerSettings` (from `settings::user`):
deleted outright — no callers remained.
Also removes the now-dead `From<&GitAuthorSettings> for GitAuthor`
impl in `fabro-checkpoint/src/author.rs`. The v2 `GitAuthorLayer`
conversion is the only path `fabro-workflow::git_author_from_settings`
uses. Drops the `fabro_types::settings::server::GitAuthorSettings`
import along with it.
`settings/mod.rs` drops the `pub mod user` declaration and the
`pub use user::*` re-export line. One of the seven legacy runtime
type modules is now gone; six remain.
3,758 workspace tests pass. `cargo fmt --check --all` and
`cargo clippy --workspace -- -D warnings` are clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Deletes `fabro_types::Settings` — the ~65-field legacy flat view that
has been read-only since Stage 6.1 migrated all production read sites
to the v2 `SettingsFile`.
The last remaining readers all fall out of this commit:
- `fabro-server/src/demo/mod.rs` — the two demo settings fixtures
(`runs::settings()` and `settings::server_settings()`) are rewritten
as `serde_json::json!(...)` literals in the v2 `SettingsFile` shape.
They produce the same wire bytes as the real handlers now return, so
the demo page keeps rendering identically.
- `fabro-server/src/lib.rs::server_config` — drops the
`pub use fabro_types::Settings` re-export. Only the inner
`fabro_types::settings::server::*` module (still around until the
full runtime-type cleanup) remains.
- `fabro-server/tests/it/openapi_conformance.rs` — drops the
`server_settings_keys_match_openapi_spec` schema-drift test and all
of its legacy type imports. The new freeform-object DTO in the spec
(`type: object, additionalProperties: true`) has no `properties` to
diff against, so the test was already a no-op. Leaves
`all_spec_routes_are_routable` in place.
- `fabro-store/src/run_state.rs` — test fixture was building a
`Settings::default()` JSON payload; switched to `SettingsFile::default()`.
- `fabro-types/src/run_event/mod.rs` — two `EventBody::RunCreated`
round-trip tests were constructing `Settings::default()`; switched
to `SettingsFile::default()`.
- `fabro-workflow/tests/it/integration.rs` — the two
`hook_toml_*_parsing` tests decoded top-level `[[hooks]]` into a
legacy `Settings`. That parse path was removed in Stage 6.1; the
tests are deleted and replaced with a comment pointing at the v2
`settings::v2::tree::tests` fixtures that cover the same ground.
The legacy flat struct's module-level doc comment in
`settings/mod.rs` is updated to explain the transitional runtime
shapes that still live under `hook`, `mcp`, `project`, `run`,
`sandbox`, `server`, and `user` — a follow-up pass will either
promote them into their consumer crates or inline them at the call
sites so the whole `settings/*.rs` file set can go away and 6.5b
flattening can happen.
3,758 workspace tests pass. `cargo fmt --check --all` and
`cargo clippy --workspace -- -D warnings` are clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mostly consolidation of code added in the recent schema v2 work:
- Share a single ActorRef::user() constructor between server control
actions and workflow provenance conversions.
- Share StageScope::from_context() between current_stage_scope and
StageScope::for_handler so the 4-field construction lives in one place.
- Collapse RunEvent::to_value's if-let chain into an insert_opt helper.
- Use Value::String(id.to_string()) instead of serde_json::to_value for
StageId/ParallelBranchId when seeding the parallel branch context.
- Share parse_event_envelopes via tests/it/support/mod.rs instead of
duplicating the parsing block in two CLI run_events helpers.
Also fix parallel-branch git.commit to emit via emit_scoped with a
branch-specific StageScope so it carries stage_id / parallel_group_id /
parallel_branch_id alongside the other stage-scoped events.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the Stage 6.2 stopgap `strip_nulls(serde_json::to_value(full
SettingsFile))` path in `get_server_settings` with an explicit
redaction pass in the new `fabro_server::settings_view` module.
The redaction drops the narrow set of fields that leak operational
secrets or host filesystem layout:
- `server.listen.*` (bind + TLS material)
- `server.auth.api.jwt.{issuer, audience}` (auth topology)
- `server.auth.api.mtls.ca` (filesystem path)
- `server.auth.web.providers.github.client_secret`
Every other field is preserved. `InterpString` values that reference
`${env.NAME}` already serialize to their unresolved template form, so
no additional env-provenance walk is needed in this pass.
Implements the real `/api/v1/runs/:id/settings` handler — previously
wired to `not_implemented` — by opening the run reader, reading the
persisted `RunRecord.settings`, running it through the same
redaction, and serializing. The demo route still points at
`demo::get_run_settings`, unchanged.
Updates `fabro-cli` to deserialize the new wire shape as
`SettingsFile` directly:
- `server_client::retrieve_server_settings` now returns
`SettingsFile` (no longer the legacy flat `Settings`) by decoding
the progenitor `types::ServerSettings` newtype map into a
`serde_json::Value` and then into `SettingsFile`.
- `commands/config/mod.rs::legacy_settings_to_v2` shim (TODO-1)
**deleted**; `merged_config` passes the v2 file straight into
`effective_settings::resolve_settings`.
- The `fabro-cli` integration tests rewrite their mock `/api/v1/settings`
payloads as v2 TOML via `ConfigLayer::parse` instead of hand-rolling
the legacy TOML shape.
All 3,761 workspace tests pass. `cargo fmt --check --all` and
`cargo clippy --workspace -- -D warnings` are clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the legacy flat `ServerSettings` / `RunSettings` schemas in
`docs/api-reference/fabro-api.yaml` and 20+ supporting nested type
schemas (LlmSettings, SandboxSettings, HookDefinition, WebSettings,
ApiSettings, GitSettings, McpServerEntry, etc.) with two simple
`type: object, additionalProperties: true` schemas that declare the
wire shape as the v2 `SettingsFile` tree with secret-bearing subtrees
dropped before serialization.
Regenerates the Rust progenitor and TypeScript Axios clients against
the new spec. The progenitor generates `RunSettings` / `ServerSettings`
as `#[serde(transparent)]` newtypes over `serde_json::Map<String,
Value>`; the openapi-generator emits `{ [key: string]: any; }` inlined
into the API method signatures and no longer exports named model
types.
Updates fabro-web to define local `type ServerSettings =
Record<string, unknown>` / `type RunSettings = Record<string,
unknown>` aliases since the generated client no longer exports them.
The UI only `JSON.stringify`s these payloads into a CollapsibleFile,
so the opaque shape is fine.
All 3,756 workspace tests remain green. The OpenAPI conformance test
`server_settings_keys_match_openapi_spec` still passes because
`compare_schema` short-circuits on pure-map schemas (no `properties`);
it becomes a no-op that will be removed entirely when Stage 6.3b
deletes the legacy flat `Settings` struct it still builds.
Unblocks the server handler + CLI migration in the next commits of
Stage 6.6.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per the schema v2 spec (docs-internal/fabro-event-schema-v2-concrete-shape.md:208-229),
`actor` is expected on control actions like `run.cancel.requested` to
identify the user who initiated the request. Before this commit, the
three Event::Run{Cancel,Pause,Unpause}Requested variants were bare
unit variants and the cancel/pause/unpause HTTP handlers used the
_auth: AuthenticatedService ZST extractor which discards user
identity.
- fabro-workflow/src/event.rs: add `actor: Option<ActorRef>` to
Event::RunCancelRequested, Event::RunPauseRequested,
Event::RunUnpauseRequested. Add a stored_event_fields_for_variant
match arm that copies the actor into the envelope. Update
event_body_from_event, event_name, and the trace! debug arm to
ignore the new field via `{ .. }`.
- fabro-server/src/server.rs: switch cancel_run, pause_run,
unpause_run from _auth: AuthenticatedService to
subject: AuthenticatedSubject (which handles cookie/JWT/mTLS
identity uniformly via lib/crates/fabro-server/src/jwt_auth.rs).
Add an actor_from_subject helper that mirrors the existing
actor_from_provenance in fabro-workflow -- both produce an
ActorRef { kind: User, id: login, display: login }.
append_control_request takes a new Option<ActorRef> argument and
constructs the variants with it. Test call sites pass None.
Test: new unit test control_action_events_carry_actor_in_envelope
in event.rs covering cancel/pause/unpause with Some(actor) and
unpause with None. Run mode AuthMode::Disabled returns
subject.login = None, so actor ends up None in that path -- matches
the spec's "actor is optional" guidance.
Wire format is backward compatible: actor uses
#[serde(default, skip_serializing_if = "Option::is_none")] so old
persisted events without the field still parse cleanly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Populate stage_id / parallel_group_id / parallel_branch_id on every
event tied to a concrete stage execution, per the spec at
docs-internal/fabro-event-schema-v2-concrete-shape.md:223-279.
Before this commit, stored_event_fields() only set stage_id for the
four Event::Stage* variants and Event::Agent -- the only variants
that carried visit/parallel_group_id/parallel_branch_id in their
payload. Every other stage-scoped event (Checkpoint*, PromptCompleted,
Command*, AgentCli*, Prompt, Interview*, Failover, StallWatchdog,
GitCommit, ArtifactCaptured) fell through to node_stored_fields()
and left stage_id as None.
New approach: scope is carried alongside the event, not on the
variant.
- fabro-workflow/src/event.rs: new StageScope type
{ node_id, visit, parallel_group_id, parallel_branch_id }. New
Emitter::emit_scoped(&event, &scope) for stage-level emission.
to_run_event_at and stored_event_fields take an
Option<&StageScope> that merges into the returned envelope
fields. StageScope::for_handler(context, node_id) is the
canonical handler-side constructor -- prefers
context.current_stage_scope() set by the fidelity lifecycle,
falls back to a scope synthesized from the node_id + context
visit count for tests that don't go through the full lifecycle.
- fabro-workflow/src/context.rs: new
WorkflowContext::current_stage_scope() method reads CURRENT_NODE,
internal.node_visit_count, internal.parallel_group_id,
internal.parallel_branch_id from the context.
- Remove the now-redundant visit/parallel_group_id/parallel_branch_id
fields from Event::Stage{Started,Completed,Failed,Retrying} and
the parallel_* fields from Event::Agent. These existed only to
feed stored_event_fields() and are obsolete once scope is
threaded through the emitter.
Emission site migration (all stage-scoped handlers now use
emit_scoped):
- lifecycle/event.rs: StageStarted, StageCompleted, StageFailed,
StageRetrying, CheckpointCompleted, GitCommit (from on_checkpoint)
- lifecycle/git.rs: CheckpointFailed
- lifecycle/artifact.rs: ArtifactCaptured
- handler/command.rs: CommandStarted, CommandCompleted
- handler/prompt.rs: Prompt, PromptCompleted
- handler/agent.rs: Prompt, PromptCompleted
- handler/fan_in.rs: Prompt, PromptCompleted
- handler/human.rs: InterviewStarted, InterviewTimeout,
InterviewInterrupted, InterviewCompleted
- handler/llm/api.rs: Failover, Agent (via spawn_event_forwarder
which now carries a StageScope across the tokio::spawn boundary)
- handler/llm/cli.rs: AgentCliStarted, AgentCliCompleted
- handler/parallel.rs: ParallelBranchStarted, ParallelBranchCompleted
StallWatchdogTimeout stays on plain emit() because the watchdog
fires from an error path without a live stage context.
Deleted the local StageEventScope struct + current_stage_event_scope
helper from handler/llm/api.rs; it's generalized into StageScope.
Tests: two new unit tests in event.rs --
stage_scope_populates_stage_id_on_non_stage_events verifies
CommandStarted / Prompt / GitCommit all pick up stage_id from scope,
run_level_events_without_scope_leave_stage_id_absent confirms
run.* events still get no stage scope. Updated all test fixtures
across fabro-workflow, fabro-cli to drop the removed Event variant
fields. Accepted two insta snapshot updates in
fabro-cli/tests/it/cmd/{attach,run}.rs that now include the
formerly-missing stage_id fields on checkpoint and interview events.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stage 6.5 can't flatten the `settings::v2::*` module tree onto
`settings::*` files wholesale because the v2 submodules
(`project.rs`, `run.rs`, `server.rs`) share filenames with the legacy
flat type modules that are still required by the OpenAPI legacy
`ServerSettings` response path (Stage 6.3 / 6.6 deletes them).
As the feasible piece of Stage 6.5 work:
- Re-export the v2 top-level type aliases from `fabro_types::settings`
so consumers can write `fabro_types::settings::SettingsFile`,
`fabro_types::settings::InterpString`, `fabro_types::settings::Duration`,
etc. without the `::v2::` prefix.
- The re-export covers the whole public v2 surface:
`{CURRENT_VERSION, CliLayer, Duration, FeaturesLayer, InterpString,
ModelRef, ParseDurationError, ParseError, ParseModelRefError,
ParseSizeError, ProjectLayer, Provenance, ResolveEnvError, Resolved,
ResolvedModelRef, RunLayer, SchemaVersion, ServerLayer, SettingsFile,
Size, SpliceArray, SpliceArrayError, VersionError, WorkflowLayer,
parse_settings_file, validate_version}`.
The `v2` module itself stays in place to host the submodule tree
(accessors, to_runtime, run::*, cli::*, server::*, interp, etc.) until
Stage 6.3 finishes deleting the conflicting legacy files, at which
point the v2/ directory can be promoted to replace them.
Build, clippy, fmt, and 3756 / 3756 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fabro-config no longer carries the legacy pass-through shims that
forwarded type re-exports from `fabro_types::settings::{hook,mcp,sandbox,
server,user,run}`. Consumers now import the runtime types directly
from `fabro_types::settings::*`, which is the only definitional
location.
Deleted files:
- `fabro-config/src/hook.rs` (1 LOC glob re-export)
- `fabro-config/src/mcp.rs` (1 LOC glob re-export)
- `fabro-config/src/sandbox.rs` (~8 LOC re-export list)
- `fabro-config/src/server.rs` (re-exports + `resolve_storage_dir`;
the `resolve_storage_dir` helper moved to `fabro_config`'s crate root
and takes `&SettingsFile` directly)
Shrunk files:
- `fabro-config/src/run.rs` lost the `ArtifactsSettings` /
`CheckpointSettings` / `GitHubSettings` / `LlmSettings` /
`MergeStrategy` / `PullRequestSettings` / `SetupSettings` re-export
block and the unused `resolve_env_refs` helper. What remains is just
the workflow TOML loader helpers (`parse_run_config`, `load_run_config`,
`resolve_graph_path`).
- `fabro-config/src/user.rs` lost the `ClientTlsSettings` /
`ExecSettings` / `OutputFormat` / `PermissionLevel` /
`ServerSettings` re-export block. The settings-path helpers and
legacy-config warning logic stay. `fabro-cli/src/user_config.rs`
now imports `ClientTlsSettings` directly from fabro_types.
Callers updated to use the canonical paths:
- `fabro-agent/src/cli.rs` imports `{OutputFormat, PermissionLevel}`
from `fabro_types::settings::user`; added `fabro-types` dep.
- `fabro-hooks/src/{config,types}.rs` re-export from
`fabro_types::settings::hook`.
- `fabro-mcp/src/config.rs` re-exports from `fabro_types::settings::mcp`.
- `fabro-sandbox/src/daytona/mod.rs` re-exports from
`fabro_types::settings::sandbox`.
- `fabro-server/src/{lib,jwt_auth,tls,serve,demo}.rs` +
`tests/it/openapi_conformance.rs` import server types from
`fabro_types::settings::server` and call `fabro_config::resolve_storage_dir`
from the crate root.
- `fabro-workflow/src/{operations/start,pipeline/types,pipeline/pull_request}.rs`
import sandbox / pull_request types from `fabro_types::settings::*`.
Build, clippy, fmt, and 3756 / 3756 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stage 6.3 closes out the dead code that Stage 6.1 left behind:
fabro-types
- Delete the inherent helpers on the legacy flat `Settings` struct
(`app_id`, `slug`, `client_id`, `git_author`, `sandbox_settings`,
`setup_settings`, `setup_commands`, `setup_timeout_ms`,
`preserve_sandbox_enabled`, `github_permissions`, `mcp_server_entries`,
`verbose_enabled`, `prevent_idle_sleep_enabled`, `upgrade_check_enabled`,
`dry_run_enabled`, `auto_approve_enabled`, `no_retro_enabled`,
`storage_dir`, `slack_settings`). Nothing reads them anymore --
consumers now use `SettingsFile` accessors (`github_app_id_str()`,
`run_sandbox()`, `dry_run_enabled()`, `storage_dir()`, etc.). The
`Settings` struct itself stays alive for the remaining legacy
OpenAPI response path and a handful of demo-route payloads; Stage
6.6 finishes the deletion alongside the OpenAPI spec rewrite.
- Delete the `#[cfg(test)] mod tests` block that only covered the
deleted `storage_dir()` helper.
fabro-cli/commands/install.rs
- `merge_server_settings` now writes a v2 TOML file (with
`[server.{api,listen.tls,web,auth.api.{jwt,mtls},auth.web}]` stanzas)
instead of the legacy v1 top-level `[web]`/`[api]`/`[git]` shape.
The generated file previously failed to parse as v2 on next startup;
now it round-trips through `ConfigLayer::parse`.
- Tests rewritten to parse the generated TOML through
`fabro_config::ConfigLayer::parse` and assert against the v2 tree
(`server.auth.web.allowed_usernames`, `server.auth.api.{jwt,mtls}.enabled`,
`server.listen.tls.{cert,key,ca}`). The `merge_server_settings_preserves_existing_*`
tests collapsed into a single `preserves_existing_top_level_sections`
test since the old tests were asserting v1 `[git]` / `[api]` keys
that no longer make sense.
Build, clippy, fmt, and tests all green: 3756 / 3756 pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bridge.rs (818 LOC) is gone. Production consumers no longer produce a
full legacy `Settings` from v2 state; every read path walks the v2 tree
directly or uses one of the narrow v2->runtime helpers in the new
`settings::v2::to_runtime` module.
Core moves:
fabro-types
- Delete `settings::v2::bridge::bridge_to_old` and the whole bridge.rs
file.
- Relocate the narrow v2->runtime helpers (`bridge_sandbox`,
`bridge_mcp_entry`, `bridge_mcps`, `bridge_hook`, `bridge_worktree_mode`,
`bridge_merge_strategy`, `bridge_pull_request`, `bridge_run_artifacts`)
into a new `settings::v2::to_runtime` module. Each helper takes a
single v2 subtree and produces the corresponding runtime shape;
nothing assembles a full legacy `Settings` anymore.
- `settings/mod.rs` doc comment rewritten to describe `Settings` as a
runtime shape, not a resolved parse target. Stage 6.3 deletes it.
fabro-config
- `ConfigLayer::resolve` is gone along with the `TryFrom<ConfigLayer>
for Settings` impls. Consumers call `.into()` for a `SettingsFile`,
or `.as_v2()` to borrow one.
- `fabro_config::server::resolve_storage_dir` now takes `&SettingsFile`.
fabro-server
- `api_server_settings` emits the v2 `SettingsFile` JSON shape
directly instead of bridging to the legacy flat DTO. Stage 6.6
replaces the shape again with an explicit allow-list DTO.
- `serve.rs`: `load_settings` returns `SettingsFile`;
`apply_serve_overrides` / `apply_runtime_settings` mutate v2
subtrees directly; `build_artifact_object_store` walks
`server.artifacts`; `build_legacy_api_settings` projects the v2
auth/listen/api subtrees down to the legacy `ApiSettings` shape for
the (still-legacy) auth resolver.
- `diagnostics::check_crypto` walks `server.auth.api.{jwt,mtls}` and
`server.listen.tls` directly.
- `web_auth.rs` oauth / register / setup-status / auth-me flows all
read `server.web`, `server.integrations.github`, and
`server.auth.web` directly via the v2 accessors. `merge_settings_keys`
now writes v2 TOML (with `[server.web]`, `[server.integrations.github]`,
etc.) instead of the legacy v1 top-level keys, and the register
handler re-parses the freshly-written file back into the in-memory
`SettingsFile` state.
fabro-cli
- `CommandContext::machine_settings` returns `&SettingsFile`.
- `user_config::load_settings` and friends return `SettingsFile`.
- `user_config::resolve_server_target` / `exec_server_target` /
`configured_server_target` walk `cli.target.{http,unix}` directly.
Tests rewritten against v2 TOML fixtures.
- `main.rs` logging init reads `cli.logging.level` / `server.logging.level`
via v2 accessors.
- `commands/exec.rs` reads `cli.exec.{model,agent}` and builds mcps
from `cli.exec.agent.mcps` (falling back to `run.agent.mcps`) via
`to_runtime::bridge_mcp_entry`.
- `commands/pr/mod.rs` calls `github_app_id_str()`.
- `commands/run/create.rs` drops the legacy `.resolve()` call and uses
`Into::<SettingsFile>::into(...)`.
- `commands/config/mod.rs::legacy_settings_to_v2` is now a real
reverse-mapping helper that covers `storage`, `scheduler`,
`integrations.{github,slack}`, `run.model`, `run.inputs`, and
`cli.output.verbosity`. Stage 6.6 deletes it when the API client
returns v2 natively.
- `tests/it/cmd/config.rs` tests now walk the v2 tree directly (via
`cfg.run_model_name_str()`, `cfg.run_inputs()`, `cfg.run_sandbox()`,
`cfg.run_hooks()`, `cfg.run_agent_mcps()`, `cfg.run_prepare_commands()`,
`cfg.server_storage_root_str()`, etc.). The `bridge_to_old` test
helper is gone.
- `tests/it/api/settings.rs` asserts against the v2 JSON shape.
Build, test, and quality gates all green:
- `cargo build --workspace --tests`
- `cargo clippy --workspace -- -D warnings`
- `cargo fmt --check --all`
- `cargo nextest run --workspace`: 3758 / 3758 passed, 182 skipped.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The typescript-axios generator was collapsing EventEnvelope's
allOf([inline_object, $ref: RunEvent]) to a bare `type
EventEnvelope = RunEvent` alias, losing the `seq` field at the
type level. TypeScript consumers could write `envelope.seq` and
get `any` (via RunEvent's additionalProperties index signature),
but had no type-level guarantee that seq was present.
Extract `EventSeq` as a named component schema and switch
EventEnvelope's allOf to two $refs. typescript-axios now
generates `export type EventEnvelope = EventSeq & RunEvent`,
which makes `envelope.seq: number` a typed property.
The Rust progenitor client is unchanged: it still flattens the
allOf into a single EventEnvelope struct with `seq: i64` inline,
exactly as before. Wire JSON is byte-identical on both sides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Completes the fabro-cli test migration for Stage 6.1. Every test in
`cargo nextest run --workspace` now passes (3,764 passed / 0 failed).
Changes:
- cmd/support.rs: compact_inspect / compact_git_inspect now walk the
v2 tree (/settings/run/goal, /settings/run/sandbox/provider,
/settings/run/model/provider) and derive `dry_run` from the v2
execution mode.
- cmd/attach.rs: the event-log filter strips _version and redacts
settings.cli.target.path to [CLI_SOCKET] so randomized tempdir
sockets don't pollute the snapshot. Insta snapshot accepted.
- cmd/run.rs: same cli.target redaction in the run event filter.
dry_run_persists_event_history_in_store and
json_run_implies_auto_approve_for_human_gates check for
`settings.run.execution.approval == "auto"` instead of
`settings.auto_approve == true`. Insta snapshot accepted.
- cmd/config.rs: parse_settings bridges the v2 YAML output back down
to the legacy flat Settings shape so the existing helper assertions
keep working. settings_fetches_server_settings_and_merges_with_local_config
now asserts the v2 R22 behavior (run.inputs replaces wholesale, so
server-side `server_only` is dropped in favor of project's vars).
settings_uses_fabro_home_for_home_config_resolution walks the v2
JSON paths (cli.output.verbosity, run.model.name).
create_explicit_workflow_path_uses_project_config_relative_to_workflow
asserts against the v2 run-record shape.
- fabro-cli/commands/config/mod.rs: legacy_settings_to_v2 is now a
real (if narrow) reverse bridge covering storage, scheduler, github
integration, slack integration, run.model, run.inputs, and cli
verbosity. Stage 6.6 still replaces this when the API client returns
v2 types natively, but for now the server-side defaults round-trip
through the resolver with enough fidelity to keep the settings
command integration tests honest.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extends the stage 6.1 WIP into a compiling state across the workspace.
Most crates and their unit/integration tests now read run.* / cli.* /
server.* v2 layers directly or through targeted bridge helpers.
Key moves in this commit:
fabro-server
- AppState.settings: Arc<RwLock<SettingsFile>> -- all helpers,
create_app_state_with_* factories, and tests updated.
- api_server_settings bridges SettingsFile -> legacy Settings via the
transitional bridge so /api/v1/settings still emits the legacy DTO
shape until Stage 6.6 replaces it with an allow-list DTO.
- get_system_info, get_system_df, get_github_repo, webhook startup, and
other read sites use the v2 accessors (github_app_id_str,
server_web, run_sandbox, run_model_*).
- web_auth.rs wraps each oauth / register / setup-status handler in a
local `bridged` helper that produces a legacy Settings from the v2
state, so the complex oauth mutation flow keeps working until its
Stage 6.6 rewrite.
- diagnostics::check_github_app reads via github_*_str accessors;
check_crypto bridges to the legacy shape inline.
- serve.rs: load_settings returns SettingsFile; apply_serve_overrides /
apply_runtime_settings mutate v2 subtrees directly; the config poll
loop and TLS/webhook startup use bridged() for legacy-shape reads.
- Tests in tests/it/{helpers,api/*,scenario/*} rewritten to construct
SettingsFile via ConfigLayer::parse or v2 struct literals.
fabro-workflow
- Every test fixture in pipeline/{finalize,initialize,pull_request,retro,
execute,persist}, operations/{create,rebuild_meta,start}, run_lookup,
runtime_store, handler/manager_loop, and tests/it/{integration,
daytona_integration}.rs now uses SettingsFile.
- start.rs hooks into the bridge helpers directly via use-imports.
- run_graph / run_graph_from_checkpoint / initialize / finalize /
pull_request calls are Box::pin'd to stay under clippy's large-future
threshold after the v2 tree brought RunOptions size up.
- resolve_run_settings writes resolved model/provider back into
run.model as InterpStrings; tests assert via run_model_*_str().
- preprocess_and_validate pulls vars from run_inputs_as_strings().
fabro-cli
- manifest_builder uses ConfigLayer.combine(...).into() to get a v2
SettingsFile for the manifest goal resolution path; file-based
goal_file handling is deferred to 6.6 when the manifest schema catches
up.
- runner::maybe_build_github_app_credentials and
tests/it/cmd/{create,runner}.rs read from v2 accessors.
- commands/config/mod.rs::merged_config returns SettingsFile; the
server-side retrieve_server_settings is bridged via a stopgap
legacy_settings_to_v2 shim that Stage 6.6 replaces.
- commands/store/dump.rs sample_run_record constructs SettingsFile.
fabro-store, fabro-checkpoint
- Test fixtures constructing RunRecord values updated to SettingsFile.
- fabro-checkpoint/src/author.rs stays (v2 From impl landed in a
previous additive commit).
fabro-config
- effective_settings.rs rewrite compiles and passes its unit tests.
- project::resolve_working_directory takes &SettingsFile.
Build status: `cargo build --workspace --tests`, `cargo clippy
--workspace -- -D warnings`, and `cargo fmt --check --all` all pass.
`cargo nextest run --workspace` passes 3,749 of 3,764 tests; the 15
remaining failures are fabro-cli integration tests whose snapshot +
TOML fixture shapes still need manual updates:
- cmd::config::* (seven tests): fixture TOML files still use v1
top-level keys and the snapshot outputs expect the legacy flat JSON
shape.
- cmd::inspect::* (four tests): run-record JSON snapshots embed the
flat Settings shape.
- cmd::run::dry_run_persists_event_history_in_store and
json_run_implies_auto_approve_for_human_gates: check `settings.dry_run
== Some(true)` directly on the v2 file; should assert
dry_run_enabled() instead.
- cmd::attach::attach_json_errors_without_prompting_for_human_input:
unrelated insta snapshot drift caused by the new SettingsFile JSON
shape leaking into an events-log snapshot.
Follow-up work for this stage also includes:
- Rewriting web_auth.rs register flow to emit v2 TOML directly and to
re-parse the written file back into state.settings so in-memory
state doesn't lag the on-disk file.
- Removing the legacy_settings_to_v2 shim in fabro-cli/config once
the server-side settings endpoint returns v2 shapes (Stage 6.6).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Partial Stage 6.1 migration of consumers off the legacy flat Settings
shape to v2 SettingsFile. Commits the in-flight work so subsequent
sessions can resume from here. Workspace currently does NOT build --
fabro-server still has ~60 consumer sites that reference state.settings
as legacy Settings, and fabro-cli is entirely untouched.
Landed in this commit:
fabro-types
- RunRecord.settings: Settings -> SettingsFile
- RunCreatedProps.settings: Settings -> SettingsFile
fabro-config
- effective_settings: full rewrite. resolve_settings now returns
SettingsFile; apply_server_defaults / apply_local_daemon_overrides
are v2-native and use the v2 merge matrix for server-owned domains.
- project::resolve_working_directory takes &SettingsFile and reads
run.working_dir as an InterpString.
fabro-workflow
- start.rs, create.rs, source.rs, validate.rs, run_options.rs, git.rs,
initialize.rs, manager_loop.rs all migrated to &SettingsFile reads.
- resolve_sandbox_provider / resolve_worktree_mode / resolve_daytona_config
/ resolve_fallback_chain walk v2 trees using the bridge helper fns.
- LifecycleOptions built from run_prepare_commands() / run_prepare_timeout_ms().
- Hooks built via bridge_hook on v2 HookEntry.
- MCPs built via bridge_mcp_entry on v2 McpEntryLayer.
- resolve_run_settings writes resolved model/provider back into
run.model (InterpString), not the flat llm struct.
- preprocess_and_validate pulls var expansion from run_inputs_as_strings.
fabro-server/run_manifest.rs
- PreparedManifest.settings -> SettingsFile.
- prepare_manifest_with_mode takes &SettingsFile.
- build_preflight_report / run_llm_check / resolve_model_provider /
run_github_token_check / resolve_sandbox_provider / resolve_daytona_config
all migrated.
- Tests rewritten to use v2 fixtures via ConfigLayer::parse.
fabro-server/server.rs
- AppState.settings type changed to Arc<RwLock<SettingsFile>>.
- github_app_credentials call site uses settings.github_app_id_str()
accessor instead of the flat app_id().
Known remaining errors:
- fabro-server/server.rs: ~60 state.settings.read() sites still
reference legacy Settings fields (llm, sandbox, setup, git, etc.).
- fabro-server/web_auth.rs: heavy git settings usage, tests.
- fabro-server/serve.rs: state mutation of flat llm/sandbox fields.
- fabro-server/diagnostics.rs: app_id / api auth strategies.
- fabro-cli: manifest_builder, commands, tests all untouched.
- Test fixtures across the workspace still construct Settings literals.
- insta snapshots will need bulk-accept after the runtime shape stabilizes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stage 6.1 prep follow-ups that consumers need when walking v2 directly:
- `bridge::bridge_sandbox`, `bridge_mcp_entry`, `bridge_mcps`,
`bridge_hook`, `bridge_exec`, `bridge_worktree_mode`,
`bridge_merge_strategy` are now `pub`, so callers can lift the
runtime shape they need out of the v2 tree without round-tripping
through the full `bridge_to_old` legacy Settings builder.
- New `bridge::bridge_pull_request` and `bridge::bridge_run_artifacts`
helpers extract their respective runtime shapes from v2 layers.
- `SettingsFile::run_prepare_commands()` / `run_prepare_timeout_ms()`
flatten `run.prepare.steps` into the legacy script-string vector
shape consumers pass to `LifecycleOptions::setup_commands`.
- `SettingsFile::run_inputs_as_strings()` stringifies `run.inputs`
TOML values for var-expansion call sites.
- `fabro_checkpoint::GitAuthor` now has `From<&v2::run::GitAuthorLayer>`
so consumers can construct a runtime author directly from the v2
subtree without going through the legacy flat `GitAuthorSettings`.
All changes are additive. `bridge_to_old` still exists and nothing has
migrated off the flat `Settings` shape yet -- those moves land in
follow-up commits once each consumer crate is converted independently.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Promote RunEvent.stage_id / parallel_group_id / parallel_branch_id
and the internal Event enum's matching fields from stringly-typed
Option<String> to Option<StageId> / Option<ParallelBranchId>. The
wire contract is now self-enforcing: malformed strings are rejected
at the serde seam, not quietly round-tripped, and the three
StageId::new(...).to_string() calls in stored_event_fields() just
drop the .to_string() since the newtypes flow straight through.
- fabro-types/src/stage_id.rs: new ParallelBranchId { group: StageId,
index: u32 } mirroring StageId's Display / FromStr / serde string
form. "{group}:{index}" (e.g. "fanout@2:0"). Tests for round-trip
and parse rejections.
- fabro-types/src/lib.rs: re-export ParallelBranchId.
- fabro-types/src/run_event/mod.rs: RunEvent, RunEventRaw, and
RunEventParts take Option<StageId> / Option<ParallelBranchId>.
from_ref gains a small generic opt_field<T: Deserialize> helper
that also replaces the bespoke actor null-handling branch. to_value
uses serde_json::to_value(value) for the three typed fields.
- fabro-workflow/src/event.rs: Event::Stage{Started,Completed,
Failed,Retrying} and Event::Agent take Option<StageId> /
Option<ParallelBranchId>. Event::ParallelBranch{Started,Completed}
take the required (non-Option) typed forms. StoredEventFields
and stored_event_fields() plumb the newtypes end-to-end.
- fabro-workflow/src/context.rs: WorkflowContext::parallel_group_id()
returns Option<StageId>, parallel_branch_id() returns
Option<ParallelBranchId>. Read via serde_json::from_value which
validates the shape on the way out.
- fabro-workflow/src/handler/parallel.rs: builds typed values
directly, stores in context via serde_json::to_value (still
produces a JSON string through the custom Serialize). BranchSetup
holds a ParallelBranchId.
- fabro-workflow/src/handler/llm/api.rs: StageEventScope holds
typed ids.
- fabro-workflow/src/lifecycle/event.rs: stage_parallel_ids returns
typed tuple.
Wire JSON is byte-identical before and after (StageId serializes as
"{node_id}@{visit}", ParallelBranchId as "{node_id}@{visit}:{index}",
matching the existing spec). Progenitor-generated types and OpenAPI
schema untouched. Existing None-only fixtures in runtime_store,
git, pipeline, error, run_state, rewind, pr_view, and store/dump
didn't need any edit because None fits any Option<T>.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the hand-written to_wire_value / from_wire_value helpers
and the wire_event_envelope_from_generated bridge with
#[serde(flatten)] on EventEnvelope.payload. Derived serde now
produces and accepts the wire shape natively:
{ "seq": 42, "id": "...", "event": "...", ... }
instead of the nested { "seq": 42, "payload": { ... } } the
derive would otherwise emit. #[serde(flatten)] composes fine with
the #[serde(transparent)] EventPayload(Value) wrapper, so the
inner payload object is merged into the outer map on both sides.
- fabro-store/src/types.rs: add #[serde(flatten)]; delete the two
wire helpers (33 lines of Value-map poking); update the
round-trip test to assert the shape is actually flat.
- fabro-server/src/server.rs: sse_event_from_store serializes
the envelope directly; api_event_envelope_from_store pipelines
to_value into from_value.
- fabro-cli/src/server_client.rs: buffer_sse_events parses
straight into EventEnvelope via serde_json::from_str;
list_run_events uses the existing convert_type helper in place
of the deleted wire_event_envelope_from_generated bridge.
- fabro-cli tests: helpers that called from_wire_value now call
serde_json::from_value.
Drops the shape check that from_wire_value used to perform on
parse (id/ts/run_id/event must exist as strings): that check
extracted run_id from the payload and then validated it against
itself, so it only guaranteed presence, not correctness.
EventPayload::new(value, expected_run_id) still runs the same
check where a caller has a real external run_id to cross-match.
Generated code and the OpenAPI allOf(seq, RunEvent) schema are
untouched; the wire JSON is byte-identical before and after.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Drop the fabro_types::Combine re-export from fabro-config/lib.rs
(unused externally after Stage 3 replaced the legacy Combine-based
merge with the v2 merge matrix)
- Replace absolute `fabro_types::settings::v2::InterpString` paths in
fabro-config/src/config.rs and merge.rs test blocks with a scoped
`use` import, satisfying clippy::absolute_paths
- fabro-config/src/merge.rs tests: use `!contains_key`, drop redundant
closures around InterpString::as_source, prefer indexing over
get().unwrap() on the notifications HashMap
- fabro-config/src/project.rs tests: switch the run.execution.retros
fixture off raw string literal hashes (only simple content inside)
and use ToString::to_string in the error-chain join expression
Quality cleanup on top of the v2 envelope commits:
- fabro-workflow/src/event.rs: add ActorKind/ActorRef/RunProvenance
to the existing ::fabro_types import block so call sites can use
unqualified names (restores CLAUDE.md import style). Extract a
node_stored_fields helper to collapse 4 near-identical match arms
in stored_event_fields. Drop the no-op ..default() from the Agent
arm where all 9 fields are set explicitly.
- fabro-types/src/run_event/mod.rs: collapse 9 copies of the
obj.get/as_str/to_string chain in from_ref behind an opt_str
closure.
- fabro-server/src/server.rs: dedupe the two identical error
closures in api_event_envelope_from_store. Skip the typed
ApiEventEnvelope roundtrip in sse_event_from_store so streamed
events go straight from the wire Value to a JSON string.
- fabro-workflow/src/handler/llm/api.rs: inline current_visit into
its sole caller current_stage_event_scope.
Also fixes pre-existing test compile breakage carried in by the
v2 commits: restore the fabro_types::RunId import in support.rs
(removed by 44def786 but still referenced by find_run_dir), and
thread parallel_group_id/parallel_branch_id: None through 9
Event::Stage*/Event::Agent constructors in run_progress and
store/dump tests that 28d28c59 missed.
No behavior change aside from the SSE hot path avoiding one full
strong-type deserialize + reserialize per event.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stage 6 initial cleanup. Removes two fabro-config shim modules that no
longer hold any code and adds a module-level comment to
fabro-types/src/settings/mod.rs documenting the transitional seam
between the flat legacy Settings shape and the authoritative v2
namespaced schema in fabro_types::settings::v2.
Deleted:
- fabro-config/src/combine.rs: was a one-line re-export of
fabro_types::combine::Combine; nothing imports it anymore
- fabro-config/src/settings.rs: was reduced to a header comment
after Stage 3 replaced TryFrom<ConfigLayer> for Settings with
ConfigLayer::resolve via the v2 bridge
Stage 6 full deletion (legacy flat Settings type, the bridge, the
old settings/{hook,mcp,project,run,sandbox,server,user}.rs modules,
plus the Combine trait derive) is scheduled for a follow-up PR that
migrates every consumer call site from the flat settings.llm /
.vars / .sandbox / .setup / .hooks / .mcp_servers / .goal / .work_dir
/ .github / .git / .pull_request / .checkpoint / .artifacts fields to
the v2 SettingsFile tree. That touches ~128 call sites across ~15
files and is a mechanical but large follow-up; the current bridge is
the safe intermediate state.
Centralize flattened EventEnvelope conversion in fabro-store so the CLI,
server, and test helpers reuse one wire-shape path. Also thread parallel
group and branch ids through nested stage and agent events so the new
envelope fields stay populated inside parallel branches.
Close out consumer migration with targeted behavior fixes and the
remaining integration-test fixture rewrites. The full workspace
nextest run now reports 3,760 passed / 0 failed / 182 skipped.
Runtime fixes:
- effective_settings::apply_server_defaults now propagates the full
server-side Settings shape (llm, sandbox, setup, checkpoint,
pull_request, artifacts, hooks, mcp_servers, github, slack, fabro)
into the resolved CLI settings, matching the pre-Stage-3 'merge
everything server' behavior for RemoteServer/LocalDaemon modes
- fabro-cli commands/run/overrides: route --verbose through
cli.output.verbosity = verbose instead of a run.metadata stash,
so it resolves to settings.verbose via the bridge
- fabro-server run_manifest manifest_args_layer: same — emit a
CliLayer with cli.output.verbosity rather than stuffing the flag
into run.metadata
- fabro-test settings_storage_dir: detect the managed marker and
return None instead of parsing the injected server.storage.root,
so isolated_server correctly spins up a new storage dir
- fabro-server run_manifest_local_daemon test now passes with full
server-side settings snapshot propagation
Test fixture + assertion updates:
- cmd::config::settings_local_explicit_workflow_path_uses_workflow_project_layers:
assertion updated for v2 R30 whole-list replacement of
run.prepare.steps across layers (only workflow-setup survives)
- cmd::config::create_explicit_workflow_path_uses_project_config_relative_to_workflow:
same correction for the persisted run.settings.setup.commands
- cmd::attach::attach_json_errors_without_prompting_for_human_input
and cmd::run::json_run_implies_auto_approve_for_human_gates: strip
the bridge-emitted settings.server and settings.version fields from
the JSON snapshot so the randomised unix-socket path does not flap
the insta snapshot
- cmd::server_start::concurrent_autostart_converges_on_one_shared_daemon_and_cleans_up:
rewrite the injected settings.toml to v2 shape with
[server.storage] root and [cli.target] type = unix path
- scenario::smoke::attach_smoke_covers_arg_validation_and_remote_server_behaviors:
two [server] target fixtures rewritten to [cli.target]
type = http url
Accepted insta snapshots for attach and run JSON outputs. Workspace
build + clippy both clean under -D warnings.
Wire EventEnvelope now inlines the RunEvent payload fields alongside
seq at the top level of the JSON object. The internal Rust
EventEnvelope { seq, payload } stays structurally unchanged; only the
API/SSE serialization layer flattens for clients.
- OpenAPI spec: add stage_id, parallel_group_id, parallel_branch_id,
tool_call_id, actor to RunEvent; model EventEnvelope as allOf(seq,
RunEvent); introduce ActorRef/ActorKind schemas.
- fabro-server: rewrite api_event_envelope_from_store to merge seq
into the payload JSON value before returning the generated flat
type; remove the now-unused nested ApiRunEvent conversion helper.
- fabro-cli server_client: add wire_event_envelope_into_store helper
that turns flat wire JSON back into fabro_store::EventEnvelope
{ seq, payload } for internal consumers.
- Regenerate progenitor Rust types and typescript-axios client.
- Update demo stubs, SSE tests, CLI test helpers, and insta
snapshots to expect the flattened shape and the new stage_id field.
Incidental: the typescript regeneration also picked up prior-merged
spec fields (ApiQuestion stage/timeout/context, upload manifest
batches, web-settings) that were stale in the TS client.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Populates stage_id, parallel_group_id, parallel_branch_id,
tool_call_id, and actor on RunEvent from the internal Event
variants:
- stage_id on stage.* events ("{node_id}@{visit}")
- parallel_group_id on parallel.* events ("{node_id}@{visit}")
- parallel_group_id + parallel_branch_id on parallel.branch.*
- tool_call_id + stage_id on agent.tool.* events
- actor=User from run.created provenance.subject.login
- actor=Agent{session_id, model} on agent.message events
Adds unit tests covering each extraction path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LocalDaemon and RemoteServer modes were stripping owner-specific
domains (cli, server) from the user layer as well as from fabro.toml
and workflow.toml. Per the plan's trust boundary rule, owner-specific
domains should only be consumed from ~/.fabro/settings.toml, so the
user layer is the one place they MUST survive. Strip only the
workflow and project layers.
- effective_settings server_defaults_layer: drop Result wrapper since
the body never fails after the v2 switch
- merge.rs: allow needless_pass_by_value module-wide since every
merge helper consumes both sides by design
- fabro-cli overrides.rs: replace &Option<String> sigs with
Option<&str>, collapse Default-plus-assignment into struct literal
(avoid clippy::field_reassign_with_default), and box the metadata
HashMap inline
- fabro-cli manifest_builder.rs: pull DaytonaDockerfileLayer into
scope so the pattern match stays absolute-path-clean
- fabro-cli main.rs + commands/config/mod.rs: Box::pin the settings
subcommand future so clippy::large_futures stays happy
Adds visit: u32 to Event::StageStarted/Completed/Failed/Retrying so
stored_event_fields() can derive stage_id = "{node_id}@{visit}".
Adds parallel_group_id/parallel_branch_id to ParallelBranchStarted/
Completed Events, computed once in handler/parallel.rs from the
parent parallel node id + visit_from_context + branch index.
Emission sites in lifecycle/event.rs populate visit from
state.node_visits via a new stage_visit helper.
Stored_event_fields() still leaves stage_id and parallel ids None
pending the extraction pass in the next commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The old HookDefinition struct has HookType flattened via
#[serde(flatten)], so emitting hook_type = Some(HookType::Command {...})
produces an inner 'command' key at the same level as the outer
HookDefinition.command shorthand field. Round-tripping through YAML
then fails with 'duplicate field command'.
Bridge script/command hooks via the HookDefinition.command shorthand
instead, leaving hook_type = None. Also: sandbox FABRO_CONFIG in the
manifest_builder unit test so it doesn't pick up the developer's real
~/.fabro/settings.toml, and update settings_local_merges_cli_and_project_defaults
to reflect v2 R22 semantics: run.inputs replaces wholesale across
layers rather than merging by key, while daytona.labels stays a sticky
merge-by-key map per R71.
Adds stage_id, parallel_group_id, parallel_branch_id, tool_call_id,
and actor to RunEvent per the v2 concrete-shape proposal. Introduces
ActorRef/ActorKind types. Serialization omits absent fields rather
than writing null. Stubs StoredEventFields with matching defaults;
population in stored_event_fields() follows in a later commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stage 3 of the settings TOML redesign. Switches the core parse/merge/
resolve path to the v2 namespaced schema while keeping the legacy flat
Settings shape accessible via the bridge for not-yet-migrated consumers.
Parser and layering:
- ConfigLayer is now a newtype around v2 SettingsFile. Loading via
ConfigLayer::parse/load/settings/for_workflow/project now hard-fails
on legacy top-level keys (version, llm, vars, sandbox, etc.) with
targeted rename hints emitted by fabro_types::settings::v2::tree
- new fabro_config::merge module encodes the merge matrix directly:
replace-by-default maps, sticky merge for run.sandbox.env and
provider-native labels, splice-aware string arrays for
run.model.fallbacks and notification route events, whole-list
replacement for run.prepare.steps, field-merge keyed objects for
notifications/MCPs/web-auth providers, and ordered hook id-aware
replacement
- ConfigLayer::resolve delegates to fabro_types::settings::v2::bridge
so consumers keep reading through the legacy Settings shape until
Stage 4 migrates them off it
- effective_settings::resolve_settings now treats project/workflow/
run/features as shared layered domains and strips cli/server from
non-local layers before merging, fulfilling the owner-first trust
boundary rule
Consumer migration (Stage 4 preview, kept to the files that block
the workspace build):
- fabro-server run_manifest builds v2 RunLayer from ManifestArgs and
resolves manifest dockerfile references through the v2 sandbox
daytona snapshot tree
- fabro-cli manifest_builder consults run.goal via v2; user_config
writes the v2 server.storage.root field under the CLI storage-dir
override; run/overrides constructs a v2 RunLayer from RunArgs
- fabro-cli scaffolds (repo init, workflow create) emit _version = 1
with project.directory/workflow.graph/run.sandbox etc.
fabro-config / fabro-types legacy parse-time types (ProjectConfig,
LlmConfig, SandboxConfig, PullRequestConfig, ExecConfig, SettingsFile
try_into, etc.) are deleted from the parse path; the resolved type
re-exports (LlmSettings, SandboxSettings, etc.) remain as shims so
unmigrated consumers keep compiling.
fabro-test helper: settings.toml fixtures now use _version = 1 plus
[server.storage] root and [cli.target] type = "unix" path. Legacy
flat storage_dir/server.target handling removed from the sync path.
Known Stage 4/5 follow-ups:
- fabro-cli integration test fixtures still use legacy-shape TOML
(version = 1, [llm], [sandbox], [vars], [exec], [fabro], etc.);
tests currently fail to parse against the v2 schema as intended.
Migrating them is the bulk of Stage 4 and lands in subsequent
commits.
- OpenAPI ServerSettings schema, generated clients, apps/fabro-web
workflowData fallback, and docs/reference examples are unchanged
and land in Stage 5.
Stage 2 of the settings TOML redesign. Completes the v2 resolved
settings tree and adds a temporary internal bridge so callers can
migrate incrementally during stages 3 and 4.
- run subtree: model (with splice-aware fallbacks), git author,
prepare steps (script xor command), execution (mode, approval,
retros as positive-form), checkpoint, sandbox (with local/daytona
provider leaves and sticky env), notifications (keyed routes with
slack/discord/teams subtables), interviews (provider + subtables),
agent (permissions + mcps map), hooks (id-aware ordered list), scm
(with github leaf), pull_request, artifacts
- cli subtree: target (http/unix), auth (strategy), exec (model,
agent, prevent_idle_sleep), output (format, verbosity), updates,
logging
- server subtree: listen (tcp/unix with tls), api, web, auth (api
jwt/mtls, web providers), storage, artifacts (local/s3 provider
leaves), slatedb (local/s3 provider leaves), scheduler, logging,
integrations (github/slack/discord/teams)
- closed ObjectStoreProvider enum so unknown providers hard-fail
schema validation
- provider-specific subtables use enumerated known providers rather
than flatten+HashMap so strict deny_unknown_fields still holds
- bridge module (settings::v2::bridge) with bridge_to_old() mapping
the v2 resolved tree back to the legacy flat Settings shape for
fields that current consumers read. Env interpolation emits raw
source form; resolution is a Stage 3 concern
- representative_full_tree_parses integration test exercises the
canonical example from the brainstorm document end-to-end
- 140 tests passing; workspace clippy-clean under -D warnings
Stage 1 of the settings TOML redesign. Introduces the namespaced v2
schema module alongside the existing flat Settings shape so the
workspace still builds while the new parser architecture comes online.
- value-language helpers with full unit-test coverage:
- Duration: single-unit suffixes (ms, s, m, h, d); rejects composed
values like '1h30m'; canonical renderer picks the largest unit
- Size: decimal (KB, MB, GB, TB) and binary (KiB, MiB, GiB, TiB)
units; bare integers default to GB; canonical renderer picks the
largest decimal unit
- ModelRef: bare vs qualified forms with a ModelRegistry trait for
later ambiguity resolution
- InterpString: ${env.NAME} tokens with whole-value, substring, and
multi-token support; provenance tagging for outward-facing redaction
- SpliceArray: '...' marker with append, prepend, and single-marker
enforcement
- SchemaVersion pre-validation: missing defaults to 1, legacy 'version'
key hard-fails with a rename hint, unsupported higher versions
hard-fail with an upgrade hint
- SettingsFile top-level sparse parse tree with strict unknown-key
rejection and targeted rename hints for every legacy top-level
section (llm, vars, exec, fabro, setup, sandbox, etc.)
- Skeleton ProjectLayer/WorkflowLayer/RunLayer/CliLayer/ServerLayer/
FeaturesLayer with deny_unknown_fields; full subtree fleshed out in
Stage 2
65 new unit tests all passing. fabro-types is clippy-clean under
-D warnings.
Stage captured artifacts in per-attempt tempdirs and persist them through an
explicit artifact sink instead of writing into run scratch cache.
Server-managed and test-owned runs now write directly to ArtifactStore, while
CLI worker runs keep the staged upload path. The local run summary now prints
durable artifact identifiers and copy hints rather than scratch-cache paths,
and the run-directory docs and integration coverage were updated to match.
Use a synthetic .map path instead of scanning apps/fabro-web/dist at
runtime, which requires a prior bun build and breaks on fresh checkouts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7 IT tests in cmd/uninstall.rs covering:
- help snapshot
- not-installed detection (plain + JSON)
- dry-run preview without deleting
- --yes removes ~/.fabro/
- --json inventory output (dry-run + execute)
Also fixes the "not installed" check to use marker files
(settings.toml, certs/, storage/) instead of directory existence,
since the CLI's logging startup may auto-create the directory.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>