refactor settings API entrypoints

This commit is contained in:
Bryan Helmkamp 2026-04-22 18:58:47 -04:00
parent 10a9038dcc
commit ebb8bf7add
No known key found for this signature in database
157 changed files with 2318 additions and 2408 deletions

11
Cargo.lock generated
View file

@ -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"

View file

@ -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<string, unknown>;
export type RunSettingsLayer = Record<string, unknown>;
export interface WorkflowScheduleSummary {
expression: string;
@ -35,6 +33,6 @@ export interface WorkflowDetailResponse {
slug: string;
description: string;
filename: string;
settings: RunSettings;
settings: RunSettingsLayer;
graph: string;
}

View file

@ -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<PaginatedRunStageList>(`/runs/${params.id}/stages`, { request }),
apiJson<RunSettings>(`/runs/${params.id}/settings`, { request }),
apiJson<RunSettingsLayer>(`/runs/${params.id}/settings`, { request }),
]);
const stages: Stage[] = apiStages.filter((s) => isVisibleStage(s.id)).map((s) => ({
id: s.id,

View file

@ -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<string, unknown>;
export function meta({}: any) {
return [{ title: "Settings — Fabro" }];
}

View file

@ -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<string, unknown>`.
// 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<string, unknown>`.
export const workflowData: Record<string, WorkflowEntry> = {
fix_build: {
name: "Fix Build",

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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());

View file

@ -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,
};

View file

@ -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::<ApiServerSettings, ServerSettings>();
assert_same_type::<ApiServerNamespace, ServerNamespace>();
assert_same_type::<ApiFeaturesNamespace, FeaturesNamespace>();
assert_same_type::<ApiObjectStoreSettings, ObjectStoreSettings>();
}
#[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<T: 'static, U: 'static>() {
assert_eq!(
TypeId::of::<T>(),
TypeId::of::<U>(),
"{} should be the same type as {}",
type_name::<T>(),
type_name::<U>()
);
}

View file

@ -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<PathBuf>,
}
#[derive(Args)]

View file

@ -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<Arc<Client>>,
}
@ -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> {
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> {
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> {
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<Self> {
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
}

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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<u32>,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<(RunId, Client, Vec<ArtifactEntry>)> {
@ -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<()> {

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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<EffectiveSettingsLayers> {
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<serde_json::Value> {
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<fabro_config::ResolveError>) -> anyhow::Error {
anyhow::anyhow!(
"failed to resolve local settings:\n{}",
errors
.into_iter()
.map(|error| error.to_string())
.collect::<Vec<_>>()
.join("\n")
)
}
fn resolve_local_settings_value(file: &SettingsLayer) -> anyhow::Result<serde_json::Value> {
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<serde_json::Value> {
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<()> {

View file

@ -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<i32, anyhow::Error> {

View file

@ -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;

View file

@ -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,

View file

@ -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<ResolveError>) -> anyhow::Error {
anyhow::anyhow!(
"failed to resolve server settings:\n{}",
errors
.into_iter()
.map(|error| error.to_string())
.collect::<Vec<_>>()
.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<InstallCommand>,
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
{

View file

@ -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<ModelsCommand>,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {

View file

@ -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())
}

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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<GitHubCredentials> {
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::<Vec<_>>()
.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)> {

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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,

View file

@ -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,

View file

@ -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<Vec<String>> {
pub(crate) fn run_deinit(cli: &CliNamespace, printer: Printer) -> Result<Vec<String>> {
let repo_root = super::init::git_repo_root()?;
let mut removed = Vec::new();

View file

@ -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<PathBuf> {
pub(crate) async fn run_init(
args: &RepoInitArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<Vec<String>> {
@ -159,7 +159,7 @@ draft = true
async fn check_github_app_installation(
target: &ServerTargetArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) {

View file

@ -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<()> {

View file

@ -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<ExitCode> {
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<ExitCode> {
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();

View file

@ -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?;

View file

@ -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<CopyDirection> {
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)> {

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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?;

View file

@ -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,

View file

@ -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?;

View file

@ -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<()> {

View file

@ -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,

View file

@ -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<()> {

View file

@ -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;

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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;

View file

@ -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,

View file

@ -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<Utc>, now: DateTime<Utc>) -> String {
pub(super) async fn list_command(
client: &Client,
_args: &SecretListArgs,
cli: &CliSettings,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
let secrets = client.list_secrets().await?;

View file

@ -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<()> {

View file

@ -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?;

View file

@ -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<String> {
pub(super) async fn set_command(
client: &Client,
args: &SecretSetArgs,
cli: &CliSettings,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
let value = resolve_value(args).await?;

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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;

View file

@ -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,

View file

@ -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<()> {

View file

@ -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<()> {

View file

@ -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()?;

View file

@ -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();

View file

@ -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),

View file

@ -27,8 +27,8 @@ pub(crate) fn bind_request(
}
pub(crate) fn auth_methods(settings: &SettingsLayer) -> Vec<ServerAuthMethod> {
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()
}

View file

@ -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]

View file

@ -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<fabro_config::ResolveError>) -> anyhow::Error {
anyhow::anyhow!(
"failed to resolve cli settings:\n{}",
errors
.into_iter()
.map(|error| error.to_string())
.collect::<Vec<_>>()
.join("\n")
)
pub(crate) fn resolve_user_settings(
file: &SettingsLayer,
) -> anyhow::Result<fabro_config::UserSettings> {
fabro_config::UserSettings::from_layer(file).map_err(anyhow::Error::from)
}
pub(crate) fn resolve_cli_settings(file: &SettingsLayer) -> anyhow::Result<CliSettings> {
fabro_config::resolve_cli_from_file(file).map_err(render_resolve_errors)
pub(crate) fn resolve_cli_settings(file: &SettingsLayer) -> anyhow::Result<CliNamespace> {
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<String> {
fn cli_target_from_settings(settings: &CliNamespace) -> Option<String> {
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<String> {
}
fn configured_server_target(settings: &SettingsLayer) -> Result<Option<ServerTarget>> {
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)

View file

@ -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<String, serde_json::Value> {
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<String, serde_json::Value> {
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<String> {
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 <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!();

View file

@ -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")
}

View file

@ -490,26 +490,18 @@ impl Client {
}
}
pub async fn retrieve_resolved_server_settings(&self) -> Result<serde_json::Value> {
let url = format!("{}/api/v1/settings?view=resolved", self.base_url());
pub async fn retrieve_resolved_server_settings(
&self,
) -> Result<fabro_api::types::ServerSettings> {
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::<serde_json::Value>()
.json::<fabro_api::types::ServerSettings>()
.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<RunId> {

View file

@ -37,3 +37,4 @@ ulid.workspace = true
[dev-dependencies]
toml.workspace = true
fabro-types = { path = "../fabro-types", features = ["test-support"] }
temp-env = "0.3"

View file

@ -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<Self> {
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<Self> {
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<Self> {
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<Self> {
let layer = load_settings_config(None)?;
Self::from_layer(&layer)
}
}

View file

@ -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<SettingsLayer> {
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

View file

@ -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<Settings> {
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

View file

@ -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<ResolveError>) -> CliSettings {
CliSettings {
pub fn resolve_cli(layer: &CliLayer, errors: &mut Vec<ResolveError>) -> CliNamespace {
CliNamespace {
target: resolve_target(layer.target.as_ref(), errors),
auth: CliAuthSettings {
strategy: layer.auth.as_ref().and_then(|auth| auth.strategy),

View file

@ -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<ResolveError>,
) -> FeaturesSettings {
FeaturesSettings {
) -> FeaturesNamespace {
FeaturesNamespace {
session_sandboxes: layer
.session_sandboxes
.expect("defaults.toml should provide features.session_sandboxes"),

View file

@ -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<Settings, Vec<ResolveError>> {
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<CliSettings, Vec<ResolveError>> {
pub fn resolve_cli_from_file(file: &SettingsLayer) -> Result<CliNamespace, Vec<ResolveError>> {
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<CliSettings, Vec<Re
}
}
pub fn resolve_server_from_file(file: &SettingsLayer) -> Result<ServerSettings, Vec<ResolveError>> {
pub fn resolve_server_from_file(
file: &SettingsLayer,
) -> Result<ServerNamespace, Vec<ResolveError>> {
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<ServerSettings,
pub fn resolve_project_from_file(
file: &SettingsLayer,
) -> Result<ProjectSettings, Vec<ResolveError>> {
) -> Result<ProjectNamespace, Vec<ResolveError>> {
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<FeaturesSettings, Vec<ResolveError>> {
) -> Result<FeaturesNamespace, Vec<ResolveError>> {
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<RunSettings, Vec<ResolveError>> {
pub fn resolve_run_from_file(file: &SettingsLayer) -> Result<RunNamespace, Vec<ResolveError>> {
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<RunSettings, Vec<Re
pub fn resolve_workflow_from_file(
file: &SettingsLayer,
) -> Result<WorkflowSettings, Vec<ResolveError>> {
) -> Result<WorkflowNamespace, Vec<ResolveError>> {
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"))

View file

@ -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<ResolveError>) -> ProjectSettings {
ProjectSettings {
pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec<ResolveError>) -> ProjectNamespace {
ProjectNamespace {
name: layer.name.clone(),
description: layer.description.clone(),
directory: layer

View file

@ -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<ResolveError>) -> RunSettings {
RunSettings {
pub fn resolve_run(layer: &RunLayer, errors: &mut Vec<ResolveError>) -> RunNamespace {
RunNamespace {
goal: resolve_goal(layer.goal.as_ref()),
working_dir: layer.working_dir.clone(),
metadata: layer.metadata.clone(),

View file

@ -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<ResolveError>) -> ServerSettings {
pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec<ResolveError>) -> 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<ResolveError>) -> 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()),

View file

@ -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<ResolveError>,
) -> WorkflowSettings {
WorkflowSettings {
) -> WorkflowNamespace {
WorkflowNamespace {
name: layer.name.clone(),
description: layer.description.clone(),
graph: layer

View file

@ -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");
}

View file

@ -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(

View file

@ -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::<Vec<_>>()
.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())
);
}

View file

@ -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(

View file

@ -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())

View file

@ -225,7 +225,6 @@ mod tests {
server::create_test_app_state_with_session_key(
SettingsLayer::default(),
Some(SESSION_SECRET),
false,
)
}

View file

@ -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;

View file

@ -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<T: serde::Serialize>(
items: Vec<T>,
@ -602,22 +601,8 @@ pub(crate) async fn list_query_history(
pub(crate) async fn get_server_settings(
_auth: AuthenticatedService,
State(_state): State<Arc<AppState>>,
Query(query): Query<settings_view::SettingsQuery>,
) -> 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")
}
}

Some files were not shown because too many files have changed in this diff Show more