diff --git a/Cargo.lock b/Cargo.lock index a4d73154b..846f1a106 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1544,6 +1544,7 @@ name = "fabro-api" version = "0.211.0-nightly.1" dependencies = [ "chrono", + "fabro-config", "fabro-types", "openapiv3", "prettyplease", @@ -1726,6 +1727,7 @@ dependencies = [ "serde", "serde_json", "strsim 0.11.1", + "temp-env", "tempfile", "thiserror 2.0.18", "toml 0.8.23", @@ -6419,6 +6421,15 @@ dependencies = [ "xattr", ] +[[package]] +name = "temp-env" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050" +dependencies = [ + "parking_lot", +] + [[package]] name = "tempfile" version = "3.26.0" diff --git a/apps/fabro-web/app/lib/workflow-api.ts b/apps/fabro-web/app/lib/workflow-api.ts index e73d79ed0..6ee4ecb97 100644 --- a/apps/fabro-web/app/lib/workflow-api.ts +++ b/apps/fabro-web/app/lib/workflow-api.ts @@ -1,12 +1,10 @@ import type { PaginationMeta } from "@qltysh/fabro-api-client"; /** - * Opaque settings payload returned by `/api/v1/runs/:id/settings`. Mirrors the - * v2 `SettingsFile` shape in `lib/crates/fabro-types/src/settings/tree.rs`, - * with secret-bearing subtrees dropped before serialization. Treated as a - * loose JSON object on the web side — consumers only render it. + * Opaque persisted `SettingsLayer` payload returned by `/api/v1/runs/:id/settings`. + * Treated as a loose JSON object on the web side — consumers only render it. */ -export type RunSettings = Record; +export type RunSettingsLayer = Record; export interface WorkflowScheduleSummary { expression: string; @@ -35,6 +33,6 @@ export interface WorkflowDetailResponse { slug: string; description: string; filename: string; - settings: RunSettings; + settings: RunSettingsLayer; graph: string; } diff --git a/apps/fabro-web/app/routes/run-settings.tsx b/apps/fabro-web/app/routes/run-settings.tsx index 9bfa09f99..e66f4bbeb 100644 --- a/apps/fabro-web/app/routes/run-settings.tsx +++ b/apps/fabro-web/app/routes/run-settings.tsx @@ -6,14 +6,14 @@ import { apiJson } from "../api"; import { isVisibleStage } from "../data/runs"; import { formatDurationSecs } from "../lib/format"; import type { PaginatedRunStageList } from "@qltysh/fabro-api-client"; -import type { RunSettings } from "../lib/workflow-api"; +import type { RunSettingsLayer } from "../lib/workflow-api"; export const handle = { wide: true }; export async function loader({ request, params }: any) { const [{ data: apiStages }, settings] = await Promise.all([ apiJson(`/runs/${params.id}/stages`, { request }), - apiJson(`/runs/${params.id}/settings`, { request }), + apiJson(`/runs/${params.id}/settings`, { request }), ]); const stages: Stage[] = apiStages.filter((s) => isVisibleStage(s.id)).map((s) => ({ id: s.id, diff --git a/apps/fabro-web/app/routes/settings.tsx b/apps/fabro-web/app/routes/settings.tsx index e557d622a..dc5cb53ce 100644 --- a/apps/fabro-web/app/routes/settings.tsx +++ b/apps/fabro-web/app/routes/settings.tsx @@ -1,13 +1,7 @@ +import type { ServerSettings } from "@qltysh/fabro-api-client"; import { apiJson } from "../api"; import { CollapsibleFile } from "../components/collapsible-file"; -/** - * Opaque server settings payload returned by `/api/v1/settings`. Mirrors the - * v2 `SettingsFile` shape with secret-bearing subtrees dropped before - * serialization. The UI only renders it as JSON. - */ -type ServerSettings = Record; - export function meta({}: any) { return [{ title: "Settings — Fabro" }]; } diff --git a/apps/fabro-web/app/routes/workflow-detail.tsx b/apps/fabro-web/app/routes/workflow-detail.tsx index f7951413e..0999ef984 100644 --- a/apps/fabro-web/app/routes/workflow-detail.tsx +++ b/apps/fabro-web/app/routes/workflow-detail.tsx @@ -1,22 +1,24 @@ import { ChevronRightIcon } from "@heroicons/react/20/solid"; import { Link, Outlet, useLocation, useParams } from "react-router"; import { apiJsonOrNull } from "../api"; -import type { RunSettings, WorkflowDetailResponse as ApiWorkflowDetail } from "../lib/workflow-api"; +import type { + RunSettingsLayer, + WorkflowDetailResponse as ApiWorkflowDetail, +} from "../lib/workflow-api"; export interface WorkflowEntry { name: string; slug: string; description: string; filename: string; - settings: RunSettings; + settings: RunSettingsLayer; graph: string; } // Static sample data used by the `workflow-definition` index route for the -// hardcoded showcase workflows. Shape mirrors the v2 `SettingsFile` JSON -// returned by `/api/v1/runs/:id/settings` (see the Rust -// `fabro_types::settings::SettingsFile` type). Fields are opaque to the -// `RunSettings` TypeScript type, which is a bare `Record`. +// hardcoded showcase workflows. Shape mirrors the persisted `SettingsLayer` +// JSON returned by `/api/v1/runs/:id/settings`. Fields are opaque to the +// `RunSettingsLayer` TypeScript type, which is a bare `Record`. export const workflowData: Record = { fix_build: { name: "Fix Build", diff --git a/bin/dev/check-boundary.sh b/bin/dev/check-boundary.sh index d1cfb3f50..7771d58ed 100755 --- a/bin/dev/check-boundary.sh +++ b/bin/dev/check-boundary.sh @@ -6,6 +6,7 @@ cd "$(dirname "$0")/../.." symbol_allowlist=( "lib/crates/fabro-cli/src/local_server.rs" "lib/crates/fabro-cli/src/commands/install.rs" + "lib/crates/fabro-cli/src/commands/uninstall.rs" "lib/crates/fabro-cli/src/commands/run/runner.rs" "lib/crates/fabro-cli/src/commands/pr/mod.rs" "lib/crates/fabro-cli/src/commands/pr/create.rs" @@ -53,7 +54,7 @@ while IFS= read -r path; do echo "boundary check failed: gated server symbol used outside allowlist: $path" >&2 fail=1 fi -done < <(find_matches 'fabro_config::resolve_server_from_file|fabro_config::resolve_server\b|Storage::new') +done < <(find_matches 'fabro_config::resolve_server_from_file|fabro_config::resolve_server\b|fabro_config::ServerSettings::from_layer\b|fabro_config::ServerSettings::resolve\b|ServerSettings::from_layer\b|ServerSettings::resolve\b|Storage::new') while IFS= read -r path; do [[ -z "$path" ]] && continue diff --git a/docs/api-reference/fabro-api.yaml b/docs/api-reference/fabro-api.yaml index 9eefde011..9439eb1e6 100644 --- a/docs/api-reference/fabro-api.yaml +++ b/docs/api-reference/fabro-api.yaml @@ -1314,7 +1314,7 @@ paths: operationId: retrieveRunSettings tags: [Run Internals] summary: Retrieve Run Settings - description: Returns the structured settings used to launch this run. + description: Returns the persisted `SettingsLayer` used to launch this run. parameters: - $ref: "#/components/parameters/RunId" responses: @@ -1323,7 +1323,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/RunSettings" + $ref: "#/components/schemas/RunSettingsLayer" "404": description: Run not found content: @@ -1950,22 +1950,11 @@ paths: tags: [Settings] summary: Retrieve Server Settings description: > - Returns the server settings view selected by the optional `view` query - parameter. `view=layer` (the default) returns the current sparse - redacted `SettingsLayer` payload. `view=resolved` returns the server's - dense resolved settings payload after applying the same redaction - policy. - parameters: - - $ref: "#/components/parameters/SettingsView" + Returns the server's current in-memory settings view as the typed + `ServerSettings` payload. responses: "200": description: Server settings - headers: - X-Fabro-Settings-View: - description: Present with value `resolved` when the response body is the dense resolved settings view. - schema: - type: string - enum: [resolved] content: application/json: schema: @@ -2007,16 +1996,6 @@ components: type: string example: nightly-build - SettingsView: - name: view - in: query - required: false - description: Selects the server settings representation to return. - schema: - type: string - enum: [layer, resolved] - default: layer - StageId: name: stageId in: path @@ -4895,32 +4874,328 @@ components: # ── Settings Schemas ───────────────────────────────────────────────── ServerSettings: - description: | - Redacted server settings payload. - - The `/api/v1/settings` endpoint supports two response shapes: - - - `view=layer` (default): the sparse redacted `SettingsLayer` shape - - `view=resolved`: the dense resolved `Settings` shape - - Both views drop the same exact operational path: - - - `server.listen` - - For non-redacted `InterpString` fields, the wire payload preserves the - unresolved source/template string rather than any environment-resolved - secret value. + description: Current in-memory server settings view. type: object - additionalProperties: true + required: [server, features] + properties: + server: + $ref: "#/components/schemas/ServerNamespace" + features: + $ref: "#/components/schemas/FeaturesNamespace" - RunSettings: + ServerNamespace: + type: object + required: + - listen + - api + - web + - auth + - ip_allowlist + - storage + - artifacts + - slatedb + - scheduler + - logging + - integrations + properties: + listen: + $ref: "#/components/schemas/ServerListenSettings" + api: + $ref: "#/components/schemas/ServerApiSettings" + web: + $ref: "#/components/schemas/ServerWebSettings" + auth: + $ref: "#/components/schemas/ServerAuthSettings" + ip_allowlist: + $ref: "#/components/schemas/ServerIpAllowlistSettings" + storage: + $ref: "#/components/schemas/ServerStorageSettings" + artifacts: + $ref: "#/components/schemas/ServerArtifactsSettings" + slatedb: + $ref: "#/components/schemas/ServerSlateDbSettings" + scheduler: + $ref: "#/components/schemas/ServerSchedulerSettings" + logging: + $ref: "#/components/schemas/ServerLoggingSettings" + integrations: + $ref: "#/components/schemas/ServerIntegrationsSettings" + + FeaturesNamespace: + type: object + required: [session_sandboxes] + properties: + session_sandboxes: + type: boolean + + ServerListenSettings: + oneOf: + - $ref: "#/components/schemas/ServerListenTcpSettings" + - $ref: "#/components/schemas/ServerListenUnixSettings" + + ServerListenTcpSettings: + type: object + required: [type, address] + properties: + type: + type: string + enum: [tcp] + address: + type: string + + ServerListenUnixSettings: + type: object + required: [type, path] + properties: + type: + type: string + enum: [unix] + path: + type: string + + ServerApiSettings: + type: object + required: [url] + properties: + url: + type: ["string", "null"] + + ServerWebSettings: + type: object + required: [enabled, url] + properties: + enabled: + type: boolean + url: + type: string + + ServerAuthSettings: + type: object + required: [methods, github] + properties: + methods: + type: array + items: + $ref: "#/components/schemas/ServerAuthMethod" + github: + $ref: "#/components/schemas/ServerAuthGithubSettings" + + ServerAuthMethod: + type: string + enum: [dev-token, github] + + ServerAuthGithubSettings: + type: object + required: [allowed_usernames] + properties: + allowed_usernames: + type: array + items: + type: string + + ServerIpAllowlistSettings: + type: object + required: [entries, trusted_proxy_count] + properties: + entries: + type: array + items: + $ref: "#/components/schemas/IpAllowEntry" + trusted_proxy_count: + type: integer + + ServerIpAllowlistOverrideSettings: + type: object + required: [entries, trusted_proxy_count] + properties: + entries: + type: ["array", "null"] + items: + $ref: "#/components/schemas/IpAllowEntry" + trusted_proxy_count: + type: ["integer", "null"] + + IpAllowEntry: + oneOf: + - $ref: "#/components/schemas/LiteralIpAllowEntry" + - $ref: "#/components/schemas/GitHubMetaHooksEntry" + + LiteralIpAllowEntry: + type: object + required: [Literal] + properties: + Literal: + type: string + + GitHubMetaHooksEntry: + type: string + enum: [GitHubMetaHooks] + + ServerStorageSettings: + type: object + required: [root] + properties: + root: + type: string + + ServerArtifactsSettings: + type: object + required: [prefix, store] + properties: + prefix: + type: string + store: + $ref: "#/components/schemas/ObjectStoreSettings" + + ServerSlateDbSettings: + type: object + required: [prefix, store, flush_interval, disk_cache] + properties: + prefix: + type: string + store: + $ref: "#/components/schemas/ObjectStoreSettings" + flush_interval: + type: string + disk_cache: + type: boolean + + ObjectStoreSettings: + oneOf: + - $ref: "#/components/schemas/ObjectStoreLocalSettings" + - $ref: "#/components/schemas/ObjectStoreS3Settings" + + ObjectStoreLocalSettings: + type: object + required: [type, root] + properties: + type: + type: string + enum: [local] + root: + type: string + + ObjectStoreS3Settings: + type: object + required: [type, bucket, region, endpoint, path_style] + properties: + type: + type: string + enum: [s3] + bucket: + type: string + region: + type: string + endpoint: + type: ["string", "null"] + path_style: + type: boolean + + ServerSchedulerSettings: + type: object + required: [max_concurrent_runs] + properties: + max_concurrent_runs: + type: integer + + ServerLoggingSettings: + type: object + required: [level] + properties: + level: + type: ["string", "null"] + + ServerIntegrationsSettings: + type: object + required: [github, slack, discord, teams] + properties: + github: + $ref: "#/components/schemas/GithubIntegrationSettings" + slack: + $ref: "#/components/schemas/SlackIntegrationSettings" + discord: + $ref: "#/components/schemas/DiscordIntegrationSettings" + teams: + $ref: "#/components/schemas/TeamsIntegrationSettings" + + GithubIntegrationSettings: + type: object + required: + - enabled + - strategy + - app_id + - client_id + - slug + - permissions + - webhooks + properties: + enabled: + type: boolean + strategy: + $ref: "#/components/schemas/GithubIntegrationStrategy" + app_id: + type: ["string", "null"] + client_id: + type: ["string", "null"] + slug: + type: ["string", "null"] + permissions: + type: object + additionalProperties: + type: string + webhooks: + oneOf: + - $ref: "#/components/schemas/IntegrationWebhooksSettings" + - type: "null" + + GithubIntegrationStrategy: + type: string + enum: [token, app] + + SlackIntegrationSettings: + type: object + required: [enabled, default_channel] + properties: + enabled: + type: boolean + default_channel: + type: ["string", "null"] + + DiscordIntegrationSettings: + type: object + required: [enabled] + properties: + enabled: + type: boolean + + TeamsIntegrationSettings: + type: object + required: [enabled] + properties: + enabled: + type: boolean + + IntegrationWebhooksSettings: + type: object + required: [strategy, ip_allowlist] + properties: + strategy: + oneOf: + - $ref: "#/components/schemas/WebhookStrategy" + - type: "null" + ip_allowlist: + oneOf: + - $ref: "#/components/schemas/ServerIpAllowlistOverrideSettings" + - type: "null" + + WebhookStrategy: + type: string + enum: [tailscale_funnel, server_url] + + RunSettingsLayer: description: | - The merged, persisted v2 `[run]` subtree for a specific run, serialized - as the wrapping `SettingsFile` shape (so `settings.run.*` holds the run - config). Matches `fabro_types::settings::SettingsFile` minus secret - subtrees, identical to ServerSettings' redaction rules. - - See `lib/crates/fabro-types/src/settings/run.rs` for the full type. + The persisted `SettingsLayer` used for a specific run, serialized as-is. + This matches the stored run manifest shape rather than a resolved view. type: object additionalProperties: true diff --git a/lib/crates/fabro-api/Cargo.toml b/lib/crates/fabro-api/Cargo.toml index cf3335ea6..fe476f2c2 100644 --- a/lib/crates/fabro-api/Cargo.toml +++ b/lib/crates/fabro-api/Cargo.toml @@ -15,6 +15,7 @@ wildcard_imports = "warn" [dependencies] chrono = { workspace = true, features = ["serde"] } +fabro-config = { path = "../fabro-config" } fabro-types = { path = "../fabro-types" } progenitor-client = "0.13" regress = "0.10" diff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs index 168fe8bb9..86a7fb474 100644 --- a/lib/crates/fabro-api/build.rs +++ b/lib/crates/fabro-api/build.rs @@ -177,6 +177,132 @@ fn main() { "fabro_types::status::RunStatusRecord", &[], ), + ("ServerSettings", "fabro_config::ServerSettings", &[]), + ( + "ServerNamespace", + "fabro_types::settings::ServerNamespace", + &[], + ), + ( + "FeaturesNamespace", + "fabro_types::settings::FeaturesNamespace", + &[], + ), + ( + "ServerListenSettings", + "fabro_types::settings::server::ServerListenSettings", + &[], + ), + ( + "ServerApiSettings", + "fabro_types::settings::server::ServerApiSettings", + &[], + ), + ( + "ServerWebSettings", + "fabro_types::settings::server::ServerWebSettings", + &[], + ), + ( + "ServerAuthSettings", + "fabro_types::settings::server::ServerAuthSettings", + &[], + ), + ( + "ServerAuthMethod", + "fabro_types::settings::server::ServerAuthMethod", + &[], + ), + ( + "ServerAuthGithubSettings", + "fabro_types::settings::server::ServerAuthGithubSettings", + &[], + ), + ( + "ServerIpAllowlistSettings", + "fabro_types::settings::server::ServerIpAllowlistSettings", + &[], + ), + ( + "ServerIpAllowlistOverrideSettings", + "fabro_types::settings::server::ServerIpAllowlistOverrideSettings", + &[], + ), + ( + "IpAllowEntry", + "fabro_types::settings::server::IpAllowEntry", + &[], + ), + ( + "ServerStorageSettings", + "fabro_types::settings::server::ServerStorageSettings", + &[], + ), + ( + "ServerArtifactsSettings", + "fabro_types::settings::server::ServerArtifactsSettings", + &[], + ), + ( + "ServerSlateDbSettings", + "fabro_types::settings::server::ServerSlateDbSettings", + &[], + ), + ( + "ObjectStoreSettings", + "fabro_types::settings::server::ObjectStoreSettings", + &[], + ), + ( + "ServerSchedulerSettings", + "fabro_types::settings::server::ServerSchedulerSettings", + &[], + ), + ( + "ServerLoggingSettings", + "fabro_types::settings::server::ServerLoggingSettings", + &[], + ), + ( + "ServerIntegrationsSettings", + "fabro_types::settings::server::ServerIntegrationsSettings", + &[], + ), + ( + "GithubIntegrationSettings", + "fabro_types::settings::server::GithubIntegrationSettings", + &[], + ), + ( + "GithubIntegrationStrategy", + "fabro_types::settings::server::GithubIntegrationStrategy", + &[], + ), + ( + "SlackIntegrationSettings", + "fabro_types::settings::server::SlackIntegrationSettings", + &[], + ), + ( + "DiscordIntegrationSettings", + "fabro_types::settings::server::DiscordIntegrationSettings", + &[], + ), + ( + "TeamsIntegrationSettings", + "fabro_types::settings::server::TeamsIntegrationSettings", + &[], + ), + ( + "IntegrationWebhooksSettings", + "fabro_types::settings::server::IntegrationWebhooksSettings", + &[], + ), + ( + "WebhookStrategy", + "fabro_types::settings::server::WebhookStrategy", + &[], + ), ]; for (name, path, impls) in replacements { settings.with_replacement(*name, *path, impls.iter().copied()); diff --git a/lib/crates/fabro-api/src/lib.rs b/lib/crates/fabro-api/src/lib.rs index b039d6c12..949d0e215 100644 --- a/lib/crates/fabro-api/src/lib.rs +++ b/lib/crates/fabro-api/src/lib.rs @@ -14,6 +14,17 @@ mod generated { include!(concat!(env!("OUT_DIR"), "/codegen.rs")); } pub mod types { + pub use fabro_config::ServerSettings; + pub use fabro_types::settings::server::{ + DiscordIntegrationSettings, GithubIntegrationSettings, GithubIntegrationStrategy, + IntegrationWebhooksSettings, IpAllowEntry, ObjectStoreSettings, ServerApiSettings, + ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, + ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings, + ServerListenSettings, ServerLoggingSettings, ServerSchedulerSettings, + ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, + TeamsIntegrationSettings, WebhookStrategy, + }; + pub use fabro_types::settings::{FeaturesNamespace, ServerNamespace}; pub use fabro_types::status::{ BlockedReason, RunControlAction, RunStatus, RunStatusRecord, StatusReason, }; diff --git a/lib/crates/fabro-api/tests/server_settings_round_trip.rs b/lib/crates/fabro-api/tests/server_settings_round_trip.rs new file mode 100644 index 000000000..487c37f77 --- /dev/null +++ b/lib/crates/fabro-api/tests/server_settings_round_trip.rs @@ -0,0 +1,78 @@ +use std::any::{TypeId, type_name}; + +use fabro_api::types::{ + FeaturesNamespace as ApiFeaturesNamespace, ObjectStoreSettings as ApiObjectStoreSettings, + ServerNamespace as ApiServerNamespace, ServerSettings as ApiServerSettings, +}; +use fabro_config::{ServerSettings, parse_settings_layer}; +use fabro_types::settings::server::ObjectStoreSettings; +use fabro_types::settings::{FeaturesNamespace, ServerNamespace}; + +#[test] +fn server_settings_family_reuses_domain_types() { + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); +} + +#[test] +fn server_settings_json_matches_openapi_shape() { + let layer = parse_settings_layer( + r#" +_version = 1 + +[server.listen] +type = "tcp" +address = "127.0.0.1:32276" + +[server.api] +url = "https://api.fabro.example.com" + +[server.web] +enabled = true +url = "https://fabro.example.com" + +[server.auth] +methods = ["dev-token", "github"] + +[server.auth.github] +allowed_usernames = ["alice"] + +[server.storage] +root = "/srv/fabro" + +[server.integrations.github] +enabled = true +strategy = "app" +app_id = "12345" +client_id = "Iv1.abcdef" +slug = "fabro-dev" + +[features] +session_sandboxes = true +"#, + ) + .expect("settings fixture should parse"); + let settings = ServerSettings::from_layer(&layer).expect("settings should resolve"); + + let json = serde_json::to_value(&settings).expect("server settings should serialize"); + assert_eq!(json["server"]["listen"]["type"], "tcp"); + assert_eq!(json["server"]["listen"]["address"], "127.0.0.1:32276"); + assert_eq!(json["server"]["storage"]["root"], "/srv/fabro"); + assert_eq!(json["features"]["session_sandboxes"], true); + + let round_trip: ApiServerSettings = + serde_json::from_value(json).expect("server settings should deserialize"); + assert_eq!(round_trip, settings); +} + +fn assert_same_type() { + assert_eq!( + TypeId::of::(), + TypeId::of::(), + "{} should be the same type as {}", + type_name::(), + type_name::() + ); +} diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs index b8c92fddb..44e4160e3 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -712,13 +712,6 @@ pub(crate) struct SystemEventsArgs { pub(crate) struct SettingsArgs { #[command(flatten)] pub(crate) target: ServerTargetArgs, - - /// Show only locally resolved settings and skip the server call - #[arg(long, conflicts_with = "server")] - pub(crate) local: bool, - - /// Optional workflow name, .fabro path, or .toml run config to overlay - pub(crate) workflow: Option, } #[derive(Args)] diff --git a/lib/crates/fabro-cli/src/command_context.rs b/lib/crates/fabro-cli/src/command_context.rs index c8f6635b8..ac508f5ed 100644 --- a/lib/crates/fabro-cli/src/command_context.rs +++ b/lib/crates/fabro-cli/src/command_context.rs @@ -2,9 +2,10 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{Context as _, Result, bail}; +use fabro_config::UserSettings; use fabro_config::merge::combine_files; use fabro_types::settings::cli::CliLayer; -use fabro_types::settings::{CliSettings, SettingsLayer}; +use fabro_types::settings::{CliNamespace, SettingsLayer}; use fabro_util::printer::Printer; use tokio::sync::OnceCell; @@ -33,7 +34,8 @@ pub(crate) struct CommandContext { cwd: PathBuf, base_config_path: PathBuf, machine_settings: SettingsLayer, - cli_settings: CliSettings, + user_settings: UserSettings, + cli_settings: CliNamespace, server_mode: ServerMode, server: OnceCell>, } @@ -41,7 +43,7 @@ pub(crate) struct CommandContext { impl CommandContext { pub(crate) fn base( printer: Printer, - cli_settings: CliSettings, + cli_settings: CliNamespace, cli_layer: &CliLayer, ) -> Result { Self::new(printer, ServerMode::None, cli_settings, cli_layer) @@ -50,7 +52,7 @@ impl CommandContext { pub(crate) fn for_target( args: &ServerTargetArgs, printer: Printer, - cli_settings: CliSettings, + cli_settings: CliNamespace, cli_layer: &CliLayer, ) -> Result { Self::new( @@ -66,7 +68,7 @@ impl CommandContext { pub(crate) fn for_connection( args: &ServerConnectionArgs, printer: Printer, - cli_settings: CliSettings, + cli_settings: CliNamespace, cli_layer: &CliLayer, ) -> Result { Self::new( @@ -83,7 +85,7 @@ impl CommandContext { fn new( printer: Printer, server_mode: ServerMode, - cli_settings: CliSettings, + cli_settings: CliNamespace, cli_layer: &CliLayer, ) -> Result { let cwd = std::env::current_dir().context("Failed to get current directory")?; @@ -99,12 +101,14 @@ impl CommandContext { cli: Some(cli_layer.clone()), ..SettingsLayer::default() }); + let user_settings = user_config::resolve_user_settings(&machine_settings)?; Ok(Self { printer, cwd, base_config_path, machine_settings, + user_settings, cli_settings, server_mode, server: OnceCell::new(), @@ -123,15 +127,15 @@ impl CommandContext { &self.cwd } - pub(crate) fn base_config_path(&self) -> &Path { - &self.base_config_path - } - pub(crate) fn machine_settings(&self) -> &SettingsLayer { &self.machine_settings } - pub(crate) fn cli_settings(&self) -> &CliSettings { + pub(crate) fn user_settings(&self) -> &UserSettings { + &self.user_settings + } + + pub(crate) fn cli_settings(&self) -> &CliNamespace { &self.cli_settings } diff --git a/lib/crates/fabro-cli/src/commands/artifact/cp.rs b/lib/crates/fabro-cli/src/commands/artifact/cp.rs index a465af4f7..9b87dd6a1 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/cp.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/cp.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; @@ -16,7 +16,7 @@ use crate::shared::{print_json_pretty, split_run_path}; pub(super) async fn cp_command( args: &ArtifactCpArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/artifact/list.rs b/lib/crates/fabro-cli/src/commands/artifact/list.rs index fefe8b61c..0637f7a5b 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/list.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/list.rs @@ -1,7 +1,7 @@ use anyhow::Result; use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Style, Table}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -10,7 +10,7 @@ use crate::args::ArtifactListArgs; pub(super) async fn list_command( args: &ArtifactListArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/artifact/mod.rs b/lib/crates/fabro-cli/src/commands/artifact/mod.rs index 56cfa6ad8..571f3cdb0 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/mod.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/mod.rs @@ -2,7 +2,7 @@ mod cp; mod list; use anyhow::{Context, Result}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_types::{RunId, StageId}; use fabro_util::printer::Printer; @@ -26,7 +26,7 @@ pub(super) async fn resolve_artifacts( run_selector: &str, node: Option<&str>, retry: Option, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<(RunId, Client, Vec)> { @@ -65,7 +65,7 @@ pub(super) async fn resolve_artifacts( pub(crate) async fn dispatch( ns: ArtifactNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/auth/login.rs b/lib/crates/fabro-cli/src/commands/auth/login.rs index 8ccf866ca..5d3d69a30 100644 --- a/lib/crates/fabro-cli/src/commands/auth/login.rs +++ b/lib/crates/fabro-cli/src/commands/auth/login.rs @@ -4,7 +4,7 @@ use anyhow::{Context as _, Result, bail}; use chrono::{DateTime, Utc}; use fabro_client::{AuthEntry, AuthStore, StoredSubject}; use fabro_http::header::CONTENT_TYPE; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::browser; use fabro_util::printer::Printer; @@ -36,7 +36,7 @@ struct CliTokenSubject { pub(super) async fn login_command( args: AuthLoginArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/auth/logout.rs b/lib/crates/fabro-cli/src/commands/auth/logout.rs index 4b72ef5c1..eda47e51b 100644 --- a/lib/crates/fabro-cli/src/commands/auth/logout.rs +++ b/lib/crates/fabro-cli/src/commands/auth/logout.rs @@ -1,7 +1,7 @@ use anyhow::{Result, bail}; use fabro_client::{AuthEntry, AuthStore}; use fabro_http::header::AUTHORIZATION; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; @@ -12,7 +12,7 @@ use crate::user_config::ServerTarget; pub(super) async fn logout_command( args: AuthLogoutArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/auth/mod.rs b/lib/crates/fabro-cli/src/commands/auth/mod.rs index 0cb79d6b4..1d5a4546e 100644 --- a/lib/crates/fabro-cli/src/commands/auth/mod.rs +++ b/lib/crates/fabro-cli/src/commands/auth/mod.rs @@ -3,7 +3,7 @@ mod logout; mod status; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; @@ -11,7 +11,7 @@ use crate::args::{AuthCommand, AuthNamespace}; pub(crate) async fn dispatch( ns: AuthNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/auth/status.rs b/lib/crates/fabro-cli/src/commands/auth/status.rs index de81e60c2..86962b52a 100644 --- a/lib/crates/fabro-cli/src/commands/auth/status.rs +++ b/lib/crates/fabro-cli/src/commands/auth/status.rs @@ -1,7 +1,7 @@ use anyhow::Result; use chrono::{DateTime, Utc}; use fabro_client::{AuthEntry, AuthStore}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::dev_token::{read_dev_token_file, validate_dev_token_format}; use fabro_util::printer::Printer; @@ -43,7 +43,7 @@ struct StatusOutput { pub(super) fn status_command( args: &AuthStatusArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/config/mod.rs b/lib/crates/fabro-cli/src/commands/config/mod.rs index b81434d66..1f329af84 100644 --- a/lib/crates/fabro-cli/src/commands/config/mod.rs +++ b/lib/crates/fabro-cli/src/commands/config/mod.rs @@ -8,151 +8,41 @@ )] use std::io::Write; -use std::path::Path; -use fabro_config::effective_settings::{ - EffectiveSettingsLayers, EffectiveSettingsMode, materialize_settings_layer, -}; -use fabro_config::{load_settings_project, project}; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_types::settings::{CliSettings, SettingsLayer}; use fabro_util::printer::Printer; -use serde_json::json; +use serde::Serialize; use crate::args::SettingsArgs; use crate::command_context::CommandContext; use crate::shared::print_json_pretty; -use crate::user_config; -fn config_layers( - ctx: &CommandContext, - workflow: Option<&Path>, -) -> anyhow::Result { - let cwd = ctx.cwd(); - let (workflow_layer, project_layer) = match workflow { - Some(path) => workflow_and_project_layers(path, cwd)?, - None => (SettingsLayer::default(), load_settings_project(cwd)?), - }; - let user_layer = - user_config::load_settings_with_config_and_storage_dir(Some(ctx.base_config_path()), None)?; - Ok(EffectiveSettingsLayers::new( - SettingsLayer::default(), - workflow_layer, - project_layer, - user_layer, - )) -} - -fn workflow_and_project_layers( - path: &Path, - cwd: &Path, -) -> anyhow::Result<(SettingsLayer, SettingsLayer)> { - let resolution = project::resolve_workflow_path(path, cwd)?; - if resolution.workflow_config.is_none() && !resolution.resolved_workflow_path.is_file() { - anyhow::bail!( - "Workflow not found: {}", - resolution.resolved_workflow_path.display() - ); - } - - let workflow_layer = resolution.workflow_config.unwrap_or_default(); - let project_layer = project::discover_project_config( - resolution - .resolved_workflow_path - .parent() - .unwrap_or_else(|| Path::new(".")), - )? - .map(|(_, config)| config) - .unwrap_or_default(); - - Ok((workflow_layer, project_layer)) -} - -fn strip_nulls(value: &mut serde_json::Value) { - match value { - serde_json::Value::Object(map) => { - for child in map.values_mut() { - strip_nulls(child); - } - map.retain(|_, child| !child.is_null()); - } - serde_json::Value::Array(values) => { - for child in values { - strip_nulls(child); - } - } - _ => {} - } -} - -fn local_settings_value( - args: &SettingsArgs, - cli: &CliSettings, - cli_layer: &CliLayer, - printer: Printer, -) -> anyhow::Result { - let base_ctx = CommandContext::base(printer, cli.clone(), cli_layer)?; - let layers = config_layers(&base_ctx, args.workflow.as_deref())?; - let local_settings = - materialize_settings_layer(layers, None, EffectiveSettingsMode::LocalOnly)?; - let mut value = resolve_local_settings_value(&local_settings)?; - strip_nulls(&mut value); - Ok(value) -} - -fn render_resolve_errors(errors: Vec) -> anyhow::Error { - anyhow::anyhow!( - "failed to resolve local settings:\n{}", - errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n") - ) -} - -fn resolve_local_settings_value(file: &SettingsLayer) -> anyhow::Result { - let file = fabro_config::apply_builtin_defaults(file.clone()); - - let project = fabro_config::resolve_project_from_file(&file).map_err(render_resolve_errors)?; - let workflow = - fabro_config::resolve_workflow_from_file(&file).map_err(render_resolve_errors)?; - let run = fabro_config::resolve_run_from_file(&file).map_err(render_resolve_errors)?; - let cli = fabro_config::resolve_cli_from_file(&file).map_err(render_resolve_errors)?; - let features = - fabro_config::resolve_features_from_file(&file).map_err(render_resolve_errors)?; - - Ok(json!({ - "project": project, - "workflow": workflow, - "run": run, - "cli": cli, - "features": features, - })) +#[derive(Serialize)] +struct RenderedConfig { + user: fabro_config::UserSettings, + server: fabro_api::types::ServerSettings, } async fn rendered_config( args: &SettingsArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result { - if args.local { - return local_settings_value(args, cli, cli_layer, printer); - } - if args.workflow.is_some() { - anyhow::bail!("WORKFLOW requires --local; use `fabro settings --local WORKFLOW`"); - } let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; - ctx.server() + let user = fabro_config::UserSettings::resolve()?; + let server = ctx + .server() .await? .retrieve_resolved_server_settings() - .await + .await?; + serde_json::to_value(RenderedConfig { user, server }).map_err(Into::into) } pub(crate) async fn execute( args: &SettingsArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/doctor.rs b/lib/crates/fabro-cli/src/commands/doctor.rs index cdf9931ac..fb25bb147 100644 --- a/lib/crates/fabro-cli/src/commands/doctor.rs +++ b/lib/crates/fabro-cli/src/commands/doctor.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use anyhow::Result; use fabro_api::types as api_types; use fabro_config::user::active_settings_path; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; pub(crate) use fabro_util::check_report::{ CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus, @@ -143,7 +143,7 @@ fn render_report(report: &CheckReport, styles: &Styles, verbose: bool, printer: pub(crate) async fn run_doctor( args: &DoctorArgs, verbose: bool, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result { diff --git a/lib/crates/fabro-cli/src/commands/exec.rs b/lib/crates/fabro-cli/src/commands/exec.rs index 9680e605b..4048915e5 100644 --- a/lib/crates/fabro-cli/src/commands/exec.rs +++ b/lib/crates/fabro-cli/src/commands/exec.rs @@ -15,7 +15,7 @@ use fabro_llm::types::{ use fabro_mcp::config::{McpServerSettings, McpTransport}; use fabro_types::settings::cli::OutputFormat as SettingsOutputFormat; use fabro_types::settings::run::McpEntryLayer; -use fabro_types::settings::{CliSettings, InterpString}; +use fabro_types::settings::{CliNamespace, InterpString}; use fabro_util::exit::{ErrorExt, ExitClass}; use fabro_util::printer::Printer; use futures::stream; @@ -358,7 +358,7 @@ impl ProviderAdapter for AuthenticatedFabroServerAdapter { pub(crate) async fn execute( mut args: ExecArgs, - cli: &CliSettings, + cli: &CliNamespace, _printer: Printer, ) -> AnyResult<()> { use fabro_agent::cli::PermissionLevel as AgentPermissionLevel; diff --git a/lib/crates/fabro-cli/src/commands/graph.rs b/lib/crates/fabro-cli/src/commands/graph.rs index 7f7b21e93..0df39a2e1 100644 --- a/lib/crates/fabro-cli/src/commands/graph.rs +++ b/lib/crates/fabro-cli/src/commands/graph.rs @@ -14,7 +14,7 @@ use fabro_api::types; use fabro_config::load::load_settings_user; use fabro_config::user::active_settings_path; use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_types::settings::{CliSettings, SettingsLayer}; +use fabro_types::settings::{CliNamespace, SettingsLayer}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use tracing::debug; @@ -28,7 +28,7 @@ use crate::shared::{absolute_or_current, print_diagnostics, print_json_pretty, r pub(crate) async fn run( args: &GraphArgs, styles: &Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/install.rs b/lib/crates/fabro-cli/src/commands/install.rs index d7f2742ed..886524266 100644 --- a/lib/crates/fabro-cli/src/commands/install.rs +++ b/lib/crates/fabro-cli/src/commands/install.rs @@ -24,7 +24,7 @@ use fabro_auth::{AuthCredential, AuthMethod, codex_oauth_config, credential_id_f use fabro_config::bind::Bind; use fabro_config::daemon::ServerDaemon; use fabro_config::user::{SETTINGS_CONFIG_FILENAME, default_storage_dir}; -use fabro_config::{ResolveError, Storage, envfile}; +use fabro_config::{Storage, envfile}; use fabro_install::{ InstallListenConfig, generate_jwt_keypair, merge_server_settings as merge_server_settings_impl, write_github_app_settings, write_token_settings, @@ -34,7 +34,7 @@ use fabro_server::serve; use fabro_store::ArtifactStore; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_types::settings::server::ServerAuthMethod; -use fabro_types::settings::{CliSettings, SettingsLayer}; +use fabro_types::settings::{CliNamespace, SettingsLayer}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_util::version::FABRO_VERSION; @@ -1273,24 +1273,13 @@ fn persist_github_install_changes( Ok(()) } -fn render_server_resolve_errors(errors: Vec) -> anyhow::Error { - anyhow::anyhow!( - "failed to resolve server settings:\n{}", - errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n") - ) -} - async fn write_artifact_store_metadata( settings: &SettingsLayer, fabro_version: &str, ) -> Result<()> { let resolved = - fabro_config::resolve_server_from_file(settings).map_err(render_server_resolve_errors)?; - let (object_store, prefix) = serve::build_artifact_object_store(&resolved)?; + fabro_config::ServerSettings::from_layer(settings).map_err(anyhow::Error::from)?; + let (object_store, prefix) = serve::build_artifact_object_store(&resolved.server)?; let artifact_store = ArtifactStore::new(object_store, prefix); artifact_store.write_metadata(fabro_version).await?; Ok(()) @@ -1427,7 +1416,7 @@ where pub(crate) async fn execute( args: &InstallArgs, command: Option, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, @@ -1443,7 +1432,7 @@ pub(crate) async fn execute( async fn run_install_github_command( args: &InstallArgs, github_args: &InstallGithubArgs, - cli: &CliSettings, + cli: &CliNamespace, process_local_json: bool, printer: Printer, ) -> Result<()> { @@ -1606,7 +1595,7 @@ async fn run_install_github_inner( pub(crate) async fn run_install( args: &InstallArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, @@ -1632,7 +1621,7 @@ pub(crate) async fn run_install( async fn run_install_inner( args: &InstallArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { @@ -1808,8 +1797,7 @@ async fn run_install_inner( .context("failed to parse generated settings.toml")?, args.storage_dir.as_deref(), ); - fabro_config::resolve_server_from_file(&install_settings) - .map_err(render_server_resolve_errors)?; + fabro_config::ServerSettings::from_layer(&install_settings).map_err(anyhow::Error::from)?; // Secrets and auth material { diff --git a/lib/crates/fabro-cli/src/commands/model.rs b/lib/crates/fabro-cli/src/commands/model.rs index 61b22b490..c96b3c349 100644 --- a/lib/crates/fabro-cli/src/commands/model.rs +++ b/lib/crates/fabro-cli/src/commands/model.rs @@ -3,7 +3,7 @@ use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; use fabro_api::types as api_types; use fabro_model::{Catalog, Model, Provider}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -42,7 +42,7 @@ struct ModelTestOutput { pub(crate) async fn execute( command: Option, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/parse.rs b/lib/crates/fabro-cli/src/commands/parse.rs index bc0a21306..e14d970ce 100644 --- a/lib/crates/fabro-cli/src/commands/parse.rs +++ b/lib/crates/fabro-cli/src/commands/parse.rs @@ -11,13 +11,13 @@ use std::io::Write; use fabro_config::project::resolve_workflow; use fabro_graphviz::parser::parse_ast; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_util::printer::Printer; use crate::args::ParseArgs; use crate::shared::read_workflow_file; -pub(crate) fn run(args: &ParseArgs, _cli: &CliSettings, _printer: Printer) -> anyhow::Result<()> { +pub(crate) fn run(args: &ParseArgs, _cli: &CliNamespace, _printer: Printer) -> anyhow::Result<()> { let stdout = std::io::stdout(); run_to(args, stdout.lock()) } diff --git a/lib/crates/fabro-cli/src/commands/pr/close.rs b/lib/crates/fabro-cli/src/commands/pr/close.rs index 1732c6556..0cc9a4a39 100644 --- a/lib/crates/fabro-cli/src/commands/pr/close.rs +++ b/lib/crates/fabro-cli/src/commands/pr/close.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::info; @@ -9,7 +9,7 @@ use crate::shared::print_json_pretty; pub(super) async fn close_command( args: PrCloseArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/pr/create.rs b/lib/crates/fabro-cli/src/commands/pr/create.rs index 3e269ba79..d26b8080b 100644 --- a/lib/crates/fabro-cli/src/commands/pr/create.rs +++ b/lib/crates/fabro-cli/src/commands/pr/create.rs @@ -5,7 +5,7 @@ use fabro_auth::configured_providers_from_process_env; use fabro_config::Storage; use fabro_model::Catalog; use fabro_sandbox::daytona::detect_repo_info; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_vault::Vault; @@ -27,7 +27,7 @@ use crate::user_config; )] pub(super) async fn create_command( args: PrCreateArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/pr/list.rs b/lib/crates/fabro-cli/src/commands/pr/list.rs index bcd497a12..2e0f51f1b 100644 --- a/lib/crates/fabro-cli/src/commands/pr/list.rs +++ b/lib/crates/fabro-cli/src/commands/pr/list.rs @@ -1,7 +1,7 @@ use anyhow::Result; use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -25,7 +25,7 @@ struct PrRow { pub(super) async fn list_command( args: PrListArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/pr/merge.rs b/lib/crates/fabro-cli/src/commands/pr/merge.rs index 7e68240c8..be7ce9f61 100644 --- a/lib/crates/fabro-cli/src/commands/pr/merge.rs +++ b/lib/crates/fabro-cli/src/commands/pr/merge.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::info; @@ -9,7 +9,7 @@ use crate::shared::print_json_pretty; pub(super) async fn merge_command( args: PrMergeArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/pr/mod.rs b/lib/crates/fabro-cli/src/commands/pr/mod.rs index bf7a3671b..8d1933f66 100644 --- a/lib/crates/fabro-cli/src/commands/pr/mod.rs +++ b/lib/crates/fabro-cli/src/commands/pr/mod.rs @@ -9,7 +9,7 @@ use fabro_config::Storage; use fabro_github::GitHubCredentials; use fabro_types::PullRequestRecord; use fabro_types::settings::cli::CliLayer; -use fabro_types::settings::{CliSettings, InterpString}; +use fabro_types::settings::{CliNamespace, InterpString}; use fabro_util::printer::Printer; use crate::args::{PrCommand, PrNamespace, ServerTargetArgs}; @@ -22,7 +22,7 @@ const GITHUB_CREDENTIALS_REQUIRED: &str = pub(crate) async fn dispatch( ns: PrNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { @@ -42,28 +42,20 @@ pub(crate) async fn dispatch( reason = "boundary-exempt(pr-api): remove with follow-up #1 when PR ops move server-side" )] fn load_github_credentials_required( - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result { let ctx = CommandContext::base(printer, cli.clone(), cli_layer)?; - let server_settings = - fabro_config::resolve_server_from_file(ctx.machine_settings()).map_err(|errors| { - anyhow!( - "failed to resolve server settings:\n{}", - errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n") - ) - })?; + let server_settings = fabro_config::ServerSettings::from_layer(ctx.machine_settings()) + .map_err(anyhow::Error::from)?; let vault = user_config::storage_dir(ctx.machine_settings()) .ok() .and_then(|dir| fabro_vault::Vault::load(Storage::new(&dir).secrets_path()).ok()); let creds = build_github_credentials( - server_settings.integrations.github.strategy, + server_settings.server.integrations.github.strategy, server_settings + .server .integrations .github .app_id @@ -79,7 +71,7 @@ fn load_github_credentials_required( pub(crate) async fn load_pr_record( server: &ServerTargetArgs, run_id: &str, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<(PullRequestRecord, fabro_types::RunId)> { diff --git a/lib/crates/fabro-cli/src/commands/pr/view.rs b/lib/crates/fabro-cli/src/commands/pr/view.rs index f093838dc..79eabadeb 100644 --- a/lib/crates/fabro-cli/src/commands/pr/view.rs +++ b/lib/crates/fabro-cli/src/commands/pr/view.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::info; @@ -9,7 +9,7 @@ use crate::shared::print_json_pretty; pub(super) async fn view_command( args: PrViewArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/preflight.rs b/lib/crates/fabro-cli/src/commands/preflight.rs index 6ebba6168..03dd4304a 100644 --- a/lib/crates/fabro-cli/src/commands/preflight.rs +++ b/lib/crates/fabro-cli/src/commands/preflight.rs @@ -1,7 +1,7 @@ use anyhow::bail; use fabro_config::load::load_settings_user; use fabro_config::user::active_settings_path; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -17,7 +17,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn execute( mut args: PreflightArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/provider/login.rs b/lib/crates/fabro-cli/src/commands/provider/login.rs index 45b33e9f9..1c613936d 100644 --- a/lib/crates/fabro-cli/src/commands/provider/login.rs +++ b/lib/crates/fabro-cli/src/commands/provider/login.rs @@ -1,7 +1,7 @@ use anyhow::Result; use fabro_api::types; use fabro_auth::credential_id_for; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -12,7 +12,7 @@ use crate::shared::provider_auth; pub(super) async fn login_command( args: ProviderLoginArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/provider/mod.rs b/lib/crates/fabro-cli/src/commands/provider/mod.rs index 10db120ae..85dd085a8 100644 --- a/lib/crates/fabro-cli/src/commands/provider/mod.rs +++ b/lib/crates/fabro-cli/src/commands/provider/mod.rs @@ -1,7 +1,7 @@ mod login; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; @@ -9,7 +9,7 @@ use crate::args::{ProviderCommand, ProviderNamespace}; pub(crate) async fn dispatch( ns: ProviderNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/repo/deinit.rs b/lib/crates/fabro-cli/src/commands/repo/deinit.rs index 6d7ccd294..8ed8ae707 100644 --- a/lib/crates/fabro-cli/src/commands/repo/deinit.rs +++ b/lib/crates/fabro-cli/src/commands/repo/deinit.rs @@ -1,9 +1,9 @@ use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; -pub(crate) fn run_deinit(cli: &CliSettings, printer: Printer) -> Result> { +pub(crate) fn run_deinit(cli: &CliNamespace, printer: Printer) -> Result> { let repo_root = super::init::git_repo_root()?; let mut removed = Vec::new(); diff --git a/lib/crates/fabro-cli/src/commands/repo/init.rs b/lib/crates/fabro-cli/src/commands/repo/init.rs index 136eb2bff..6b3d717f3 100644 --- a/lib/crates/fabro-cli/src/commands/repo/init.rs +++ b/lib/crates/fabro-cli/src/commands/repo/init.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tokio::process::Command as TokioCommand; @@ -36,7 +36,7 @@ pub(super) fn git_repo_root() -> Result { pub(crate) async fn run_init( args: &RepoInitArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result> { @@ -159,7 +159,7 @@ draft = true async fn check_github_app_installation( target: &ServerTargetArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) { diff --git a/lib/crates/fabro-cli/src/commands/repo/mod.rs b/lib/crates/fabro-cli/src/commands/repo/mod.rs index 30ce0fd41..5ddeaa95b 100644 --- a/lib/crates/fabro-cli/src/commands/repo/mod.rs +++ b/lib/crates/fabro-cli/src/commands/repo/mod.rs @@ -2,7 +2,7 @@ pub(crate) mod deinit; pub(crate) mod init; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; @@ -11,7 +11,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn dispatch( ns: RepoNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs index 0c634fbd9..bad2a27dd 100644 --- a/lib/crates/fabro-cli/src/commands/run/attach.rs +++ b/lib/crates/fabro-cli/src/commands/run/attach.rs @@ -19,7 +19,6 @@ use anyhow::Result; use fabro_api::types; use fabro_interview::{AnswerValue, ConsoleInterviewer, Question, QuestionOption, QuestionType}; use fabro_store::EventEnvelope; -use fabro_types::settings::cli::OutputVerbosity; use fabro_types::settings::run::ApprovalMode; use fabro_types::{EventBody, RunId}; use fabro_util::json::normalize_json_value; @@ -49,6 +48,7 @@ pub(crate) async fn attach_run( kill_on_detach: bool, styles: &'static Styles, json_output: bool, + live_verbose: bool, ) -> Result { let inferred_storage_dir = infer_storage_dir(run_dir); let inferred_run_id = infer_run_id(run_dir); @@ -63,6 +63,7 @@ pub(crate) async fn attach_run( kill_on_detach, styles, json_output, + live_verbose, Printer::Default, )) .await; @@ -79,6 +80,7 @@ pub(crate) async fn attach_run_with_client( kill_on_detach: bool, styles: &'static Styles, json_output: bool, + live_verbose: bool, printer: Printer, ) -> Result { let state = client.get_run_state(run_id).await?; @@ -86,10 +88,6 @@ pub(crate) async fn attach_run_with_client( fabro_config::resolve_run_from_file(&record.settings) .is_ok_and(|settings| settings.execution.approval == ApprovalMode::Auto) }); - let verbose = state.spec.as_ref().is_some_and(|record| { - fabro_config::resolve_cli_from_file(&record.settings) - .is_ok_and(|settings| settings.output.verbosity == OutputVerbosity::Verbose) - }); let events = client.list_run_events(run_id, None, None).await?; let replay_events = events.clone(); let next_seq = events.last().map_or(1, |event| event.seq.saturating_add(1)); @@ -98,7 +96,7 @@ pub(crate) async fn attach_run_with_client( if state_is_terminal(&state) || initial_exit_code.is_some() { return replay_run_with_client( - verbose, + live_verbose, events, initial_exit_code .or(state_exit_code) @@ -116,7 +114,7 @@ pub(crate) async fn attach_run_with_client( styles, AttachOptions { auto_approve, - verbose, + verbose: live_verbose, kill_on_detach, json_output, }, @@ -542,6 +540,7 @@ mod tests { false, no_color_styles(), false, + false, )) .await .unwrap_err(); diff --git a/lib/crates/fabro-cli/src/commands/run/command.rs b/lib/crates/fabro-cli/src/commands/run/command.rs index ffb83ce03..1f63dbef5 100644 --- a/lib/crates/fabro-cli/src/commands/run/command.rs +++ b/lib/crates/fabro-cli/src/commands/run/command.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -11,7 +11,7 @@ use crate::user_config::load_settings_with_storage_dir; pub(crate) async fn execute( mut args: RunArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { @@ -64,6 +64,7 @@ pub(crate) async fn execute( true, styles, json, + ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose, printer, )) .await?; diff --git a/lib/crates/fabro-cli/src/commands/run/cp.rs b/lib/crates/fabro-cli/src/commands/run/cp.rs index 732312219..a757ac81c 100644 --- a/lib/crates/fabro-cli/src/commands/run/cp.rs +++ b/lib/crates/fabro-cli/src/commands/run/cp.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tokio::fs; @@ -28,7 +28,7 @@ enum CopyDirection { pub(crate) async fn cp_command( args: CpArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { @@ -128,7 +128,7 @@ fn parse_direction(src: &str, dst: &str) -> Result { async fn resolve_client_and_run_id( server: &ServerTargetArgs, run_prefix: &str, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<(Client, fabro_types::RunId)> { diff --git a/lib/crates/fabro-cli/src/commands/run/diff.rs b/lib/crates/fabro-cli/src/commands/run/diff.rs index 32951fff1..138c54162 100644 --- a/lib/crates/fabro-cli/src/commands/run/diff.rs +++ b/lib/crates/fabro-cli/src/commands/run/diff.rs @@ -10,7 +10,7 @@ use std::io::{self, IsTerminal, Write}; use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::{debug, info}; @@ -22,7 +22,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn run( args: DiffArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/run/fork.rs b/lib/crates/fabro-cli/src/commands/run/fork.rs index b4f8c81e4..e08b934b0 100644 --- a/lib/crates/fabro-cli/src/commands/run/fork.rs +++ b/lib/crates/fabro-cli/src/commands/run/fork.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; use fabro_checkpoint::git::Store; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -16,7 +16,7 @@ use crate::shared::repo::ensure_matching_repo_origin; pub(crate) async fn run( args: &ForkArgs, styles: &Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/run/logs.rs b/lib/crates/fabro-cli/src/commands/run/logs.rs index a732bc023..fbaca77cd 100644 --- a/lib/crates/fabro-cli/src/commands/run/logs.rs +++ b/lib/crates/fabro-cli/src/commands/run/logs.rs @@ -13,7 +13,7 @@ use std::time::Duration; use anyhow::{Context, Result, bail}; use chrono::{DateTime, Utc}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::json::normalize_json_value; use fabro_util::printer::Printer; @@ -32,7 +32,7 @@ const FOLLOW_TERMINAL_GRACE: Duration = Duration::from_millis(500); pub(crate) async fn run( args: &LogsArgs, styles: &Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/run/mod.rs b/lib/crates/fabro-cli/src/commands/run/mod.rs index b9a8eaec0..ff0641f29 100644 --- a/lib/crates/fabro-cli/src/commands/run/mod.rs +++ b/lib/crates/fabro-cli/src/commands/run/mod.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; +use fabro_types::settings::CliNamespace; +use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -29,7 +29,7 @@ pub(crate) mod wait; pub(crate) async fn dispatch( cmd: RunCommands, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, _process_local_json: bool, printer: Printer, @@ -77,6 +77,7 @@ pub(crate) async fn dispatch( false, styles, cli.output.format == OutputFormat::Json, + ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose, printer, )) .await?; diff --git a/lib/crates/fabro-cli/src/commands/run/preview.rs b/lib/crates/fabro-cli/src/commands/run/preview.rs index 55a9f8c0d..1bcb5f6b2 100644 --- a/lib/crates/fabro-cli/src/commands/run/preview.rs +++ b/lib/crates/fabro-cli/src/commands/run/preview.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::info; @@ -10,7 +10,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn run( args: PreviewArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/run/resume.rs b/lib/crates/fabro-cli/src/commands/run/resume.rs index 85d801474..fb175cb51 100644 --- a/lib/crates/fabro-cli/src/commands/run/resume.rs +++ b/lib/crates/fabro-cli/src/commands/run/resume.rs @@ -1,5 +1,5 @@ -use fabro_types::settings::CliSettings; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; +use fabro_types::settings::CliNamespace; +use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -15,7 +15,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn resume_command( args: ResumeArgs, styles: &'static Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result<()> { @@ -39,6 +39,7 @@ pub(crate) async fn resume_command( true, styles, json, + ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose, printer, )) .await?; diff --git a/lib/crates/fabro-cli/src/commands/run/rewind.rs b/lib/crates/fabro-cli/src/commands/run/rewind.rs index 1645dacfa..6e65a5f98 100644 --- a/lib/crates/fabro-cli/src/commands/run/rewind.rs +++ b/lib/crates/fabro-cli/src/commands/run/rewind.rs @@ -3,7 +3,7 @@ use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; use fabro_checkpoint::git::Store; use fabro_types::run_event::{CheckpointCompletedProps, RunRewoundProps, RunSubmittedProps}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_types::{EventBody, RunEvent}; use fabro_util::printer::Printer; @@ -33,7 +33,7 @@ pub(crate) struct TimelineEntryJson { pub(crate) async fn run( args: &RewindArgs, styles: &Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/run/ssh.rs b/lib/crates/fabro-cli/src/commands/run/ssh.rs index 4e9268de1..f4465d499 100644 --- a/lib/crates/fabro-cli/src/commands/run/ssh.rs +++ b/lib/crates/fabro-cli/src/commands/run/ssh.rs @@ -1,5 +1,5 @@ use anyhow::{Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::info; @@ -10,7 +10,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn run( args: SshArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/crates/fabro-cli/src/commands/run/wait.rs index 49e8557a7..639960771 100644 --- a/lib/crates/fabro-cli/src/commands/run/wait.rs +++ b/lib/crates/fabro-cli/src/commands/run/wait.rs @@ -11,7 +11,7 @@ use std::io::Write; use anyhow::{Result, bail}; use fabro_types::RunId; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -27,7 +27,7 @@ use crate::shared::{format_duration_ms, format_usd_micros}; pub(crate) async fn run( args: &WaitArgs, styles: &Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/runs/archive.rs b/lib/crates/fabro-cli/src/commands/runs/archive.rs index 4a0627759..6052d9457 100644 --- a/lib/crates/fabro-cli/src/commands/runs/archive.rs +++ b/lib/crates/fabro-cli/src/commands/runs/archive.rs @@ -1,5 +1,5 @@ use anyhow::{Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; @@ -11,7 +11,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn archive_command( args: &RunsArchiveArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { @@ -28,7 +28,7 @@ pub(crate) async fn archive_command( pub(crate) async fn unarchive_command( args: &RunsUnarchiveArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { @@ -66,7 +66,7 @@ async fn run_bulk( action: Action, identifiers: &[String], client: &server_client::Client, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let json = cli.output.format == OutputFormat::Json; diff --git a/lib/crates/fabro-cli/src/commands/runs/inspect.rs b/lib/crates/fabro-cli/src/commands/runs/inspect.rs index 2c3b7911d..7858260f1 100644 --- a/lib/crates/fabro-cli/src/commands/runs/inspect.rs +++ b/lib/crates/fabro-cli/src/commands/runs/inspect.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; use fabro_workflow::run_status::RunStatus; @@ -23,7 +23,7 @@ pub(crate) struct InspectOutput { pub(crate) async fn run( args: &InspectArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/runs/list.rs b/lib/crates/fabro-cli/src/commands/runs/list.rs index 5864f4da0..8b81b5f7b 100644 --- a/lib/crates/fabro-cli/src/commands/runs/list.rs +++ b/lib/crates/fabro-cli/src/commands/runs/list.rs @@ -4,7 +4,7 @@ use anyhow::Result; use chrono::Utc; use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -20,7 +20,7 @@ use crate::shared::{color_if, format_duration_ms, tilde_path}; pub(crate) async fn list_command( args: &RunsListArgs, styles: &Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/runs/mod.rs b/lib/crates/fabro-cli/src/commands/runs/mod.rs index f3900a94e..fb62b7f2a 100644 --- a/lib/crates/fabro-cli/src/commands/runs/mod.rs +++ b/lib/crates/fabro-cli/src/commands/runs/mod.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -13,7 +13,7 @@ pub(crate) mod rm; pub(crate) async fn dispatch( cmd: RunsCommands, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/runs/rm.rs b/lib/crates/fabro-cli/src/commands/runs/rm.rs index becdc9969..eb1047332 100644 --- a/lib/crates/fabro-cli/src/commands/runs/rm.rs +++ b/lib/crates/fabro-cli/src/commands/runs/rm.rs @@ -1,5 +1,5 @@ use anyhow::{Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; @@ -11,7 +11,7 @@ use crate::shared::print_json_pretty; pub(crate) async fn remove_command( args: &RunsRemoveArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { @@ -22,7 +22,7 @@ pub(crate) async fn remove_command( async fn remove_from( args: &RunsRemoveArgs, client: &server_client::Client, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let json = cli.output.format == OutputFormat::Json; diff --git a/lib/crates/fabro-cli/src/commands/sandbox/mod.rs b/lib/crates/fabro-cli/src/commands/sandbox/mod.rs index c584f3e81..3adc7bd13 100644 --- a/lib/crates/fabro-cli/src/commands/sandbox/mod.rs +++ b/lib/crates/fabro-cli/src/commands/sandbox/mod.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; @@ -7,7 +7,7 @@ use crate::args::SandboxCommand; pub(crate) async fn dispatch( command: SandboxCommand, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, process_local_json: bool, printer: Printer, diff --git a/lib/crates/fabro-cli/src/commands/secret/list.rs b/lib/crates/fabro-cli/src/commands/secret/list.rs index 033afdf17..25d7f7680 100644 --- a/lib/crates/fabro-cli/src/commands/secret/list.rs +++ b/lib/crates/fabro-cli/src/commands/secret/list.rs @@ -2,7 +2,7 @@ use anyhow::Result; use chrono::{DateTime, Utc}; use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Style, Table}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -25,7 +25,7 @@ fn format_age(dt: DateTime, now: DateTime) -> String { pub(super) async fn list_command( client: &Client, _args: &SecretListArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let secrets = client.list_secrets().await?; diff --git a/lib/crates/fabro-cli/src/commands/secret/mod.rs b/lib/crates/fabro-cli/src/commands/secret/mod.rs index d0148c60e..59146f0a8 100644 --- a/lib/crates/fabro-cli/src/commands/secret/mod.rs +++ b/lib/crates/fabro-cli/src/commands/secret/mod.rs @@ -3,7 +3,7 @@ mod rm; mod set; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; @@ -12,7 +12,7 @@ use crate::command_context::CommandContext; pub(crate) async fn dispatch( ns: SecretNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/secret/rm.rs b/lib/crates/fabro-cli/src/commands/secret/rm.rs index 43c466501..253ca09d0 100644 --- a/lib/crates/fabro-cli/src/commands/secret/rm.rs +++ b/lib/crates/fabro-cli/src/commands/secret/rm.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; @@ -10,7 +10,7 @@ use crate::shared::print_json_pretty; pub(super) async fn rm_command( client: &Client, args: &SecretRmArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { client.delete_secret_by_name(&args.key).await?; diff --git a/lib/crates/fabro-cli/src/commands/secret/set.rs b/lib/crates/fabro-cli/src/commands/secret/set.rs index a8c88debd..1ec66418e 100644 --- a/lib/crates/fabro-cli/src/commands/secret/set.rs +++ b/lib/crates/fabro-cli/src/commands/secret/set.rs @@ -11,7 +11,7 @@ use std::io::{IsTerminal, Read as _}; use anyhow::{Context as _, Result, bail}; use fabro_api::types; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use tokio::task::spawn_blocking; @@ -60,7 +60,7 @@ async fn resolve_value(args: &SecretSetArgs) -> Result { pub(super) async fn set_command( client: &Client, args: &SecretSetArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let value = resolve_value(args).await?; diff --git a/lib/crates/fabro-cli/src/commands/store/dump.rs b/lib/crates/fabro-cli/src/commands/store/dump.rs index 700e94783..1713ce6b2 100644 --- a/lib/crates/fabro-cli/src/commands/store/dump.rs +++ b/lib/crates/fabro-cli/src/commands/store/dump.rs @@ -11,7 +11,7 @@ use bytes::Bytes; #[cfg(test)] use fabro_store::{ArtifactStore, RunDatabase}; use fabro_store::{EventEnvelope, RunProjection, StageId}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_types::{RunBlobId, RunId}; use fabro_util::printer::Printer; @@ -28,7 +28,7 @@ use crate::shared::{absolute_or_current, print_json_pretty}; pub(crate) async fn dump_command( args: &StoreDumpArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/store/mod.rs b/lib/crates/fabro-cli/src/commands/store/mod.rs index f4cab294d..2c885231b 100644 --- a/lib/crates/fabro-cli/src/commands/store/mod.rs +++ b/lib/crates/fabro-cli/src/commands/store/mod.rs @@ -3,7 +3,7 @@ pub(crate) mod rebuild; mod run_export; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; @@ -11,7 +11,7 @@ use crate::args::{StoreCommand, StoreNamespace}; pub(crate) async fn dispatch( ns: StoreNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/system/df.rs b/lib/crates/fabro-cli/src/commands/system/df.rs index ae6e30343..f5cebe0b4 100644 --- a/lib/crates/fabro-cli/src/commands/system/df.rs +++ b/lib/crates/fabro-cli/src/commands/system/df.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Style, Table}; use fabro_api::types; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; @@ -13,7 +13,7 @@ use crate::shared::{format_size, print_json_pretty}; pub(super) async fn df_command( args: &DfArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/system/events.rs b/lib/crates/fabro-cli/src/commands/system/events.rs index da5bdad57..7e94cfa1d 100644 --- a/lib/crates/fabro-cli/src/commands/system/events.rs +++ b/lib/crates/fabro-cli/src/commands/system/events.rs @@ -1,6 +1,6 @@ use anyhow::Result; use fabro_client::sse; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use futures::StreamExt; @@ -10,7 +10,7 @@ use crate::command_context::CommandContext; pub(super) async fn events_command( args: &SystemEventsArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/system/info.rs b/lib/crates/fabro-cli/src/commands/system/info.rs index 17668bfe5..a1c584453 100644 --- a/lib/crates/fabro-cli/src/commands/system/info.rs +++ b/lib/crates/fabro-cli/src/commands/system/info.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; @@ -9,7 +9,7 @@ use crate::shared::print_json_pretty; pub(super) async fn info_command( args: &SystemInfoArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/system/mod.rs b/lib/crates/fabro-cli/src/commands/system/mod.rs index 86d35e367..45f35dee6 100644 --- a/lib/crates/fabro-cli/src/commands/system/mod.rs +++ b/lib/crates/fabro-cli/src/commands/system/mod.rs @@ -4,7 +4,7 @@ mod info; mod prune; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; pub(crate) use prune::parse_duration; @@ -13,7 +13,7 @@ use crate::args::{SystemCommand, SystemNamespace}; pub(crate) async fn dispatch( ns: SystemNamespace, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/system/prune.rs b/lib/crates/fabro-cli/src/commands/system/prune.rs index 1ed6d684d..f7242b023 100644 --- a/lib/crates/fabro-cli/src/commands/system/prune.rs +++ b/lib/crates/fabro-cli/src/commands/system/prune.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use anyhow::{Context, Result, bail}; use fabro_api::types; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::{debug, info}; @@ -13,7 +13,7 @@ use crate::shared::{format_size, print_json_pretty}; pub(super) async fn prune_command( args: &RunsPruneArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/uninstall.rs b/lib/crates/fabro-cli/src/commands/uninstall.rs index ed358496d..5d19aa459 100644 --- a/lib/crates/fabro-cli/src/commands/uninstall.rs +++ b/lib/crates/fabro-cli/src/commands/uninstall.rs @@ -15,7 +15,7 @@ use std::time::Duration; use anyhow::{Context, Result}; use fabro_config::Storage; use fabro_config::daemon::ServerDaemon; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::Home; use fabro_util::printer::Printer; @@ -45,7 +45,7 @@ struct Inventory { )] pub(crate) async fn run_uninstall( args: &UninstallArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let json = cli.output.format == OutputFormat::Json; diff --git a/lib/crates/fabro-cli/src/commands/upgrade.rs b/lib/crates/fabro-cli/src/commands/upgrade.rs index ddfaaf659..53390cfd5 100644 --- a/lib/crates/fabro-cli/src/commands/upgrade.rs +++ b/lib/crates/fabro-cli/src/commands/upgrade.rs @@ -13,7 +13,7 @@ use std::io::{IsTerminal, Write}; use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use semver::Version; @@ -436,7 +436,7 @@ impl UpgradeCheckState { pub(crate) async fn run_upgrade( args: UpgradeArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let current_exe = std::env::current_exe() @@ -599,7 +599,7 @@ pub(crate) async fn run_upgrade( fn run_upgrade_brew( args: &UpgradeArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, channel: BrewChannel, ) -> Result<()> { @@ -1153,7 +1153,7 @@ mod tests { #[test] fn run_upgrade_brew_refuses_by_default() { - let cli = CliSettings::default(); + let cli = CliNamespace::default(); let err = run_upgrade_brew( &brew_args(None, false, false, false), &cli, @@ -1169,7 +1169,7 @@ mod tests { #[test] fn run_upgrade_brew_dry_run_returns_ok() { - let cli = CliSettings::default(); + let cli = CliNamespace::default(); let result = run_upgrade_brew( &brew_args(None, false, false, true), &cli, @@ -1181,7 +1181,7 @@ mod tests { #[test] fn run_upgrade_brew_rejects_version_flag() { - let cli = CliSettings::default(); + let cli = CliNamespace::default(); let err = run_upgrade_brew( &brew_args(Some("0.1.0"), false, false, false), &cli, @@ -1194,7 +1194,7 @@ mod tests { #[test] fn run_upgrade_brew_rejects_prerelease_flag() { - let cli = CliSettings::default(); + let cli = CliNamespace::default(); let err = run_upgrade_brew( &brew_args(None, true, false, false), &cli, @@ -1207,7 +1207,7 @@ mod tests { #[test] fn run_upgrade_brew_rejects_force_flag() { - let cli = CliSettings::default(); + let cli = CliNamespace::default(); let err = run_upgrade_brew( &brew_args(None, false, true, false), &cli, diff --git a/lib/crates/fabro-cli/src/commands/validate.rs b/lib/crates/fabro-cli/src/commands/validate.rs index a530e5470..95691b74e 100644 --- a/lib/crates/fabro-cli/src/commands/validate.rs +++ b/lib/crates/fabro-cli/src/commands/validate.rs @@ -2,7 +2,7 @@ use anyhow::bail; use fabro_config::load::load_settings_user; use fabro_config::user::active_settings_path; use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_types::settings::{CliSettings, SettingsLayer}; +use fabro_types::settings::{CliNamespace, SettingsLayer}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -15,7 +15,7 @@ use crate::shared::{print_diagnostics, print_json_pretty, relative_path}; pub(crate) async fn run( args: &ValidateArgs, styles: &Styles, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/version.rs b/lib/crates/fabro-cli/src/commands/version.rs index a58e0541f..826ec108b 100644 --- a/lib/crates/fabro-cli/src/commands/version.rs +++ b/lib/crates/fabro-cli/src/commands/version.rs @@ -6,7 +6,7 @@ use std::io::IsTerminal; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use serde_json::{Map, Value, json}; @@ -18,7 +18,7 @@ use crate::user_config::{self, ServerTarget}; pub(crate) async fn version_command( args: &VersionArgs, - cli: &CliSettings, + cli: &CliNamespace, cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { diff --git a/lib/crates/fabro-cli/src/commands/workflow/create.rs b/lib/crates/fabro-cli/src/commands/workflow/create.rs index 845508a2a..6a4f7b5f8 100644 --- a/lib/crates/fabro-cli/src/commands/workflow/create.rs +++ b/lib/crates/fabro-cli/src/commands/workflow/create.rs @@ -7,7 +7,7 @@ use std::path::Path; use anyhow::{Context, Result, bail}; use fabro_config::project::{discover_project_config, resolve_fabro_root}; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; @@ -16,7 +16,7 @@ use crate::shared::{print_json_pretty, relative_path}; pub(super) fn create_command( args: &WorkflowCreateArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let cwd = std::env::current_dir()?; diff --git a/lib/crates/fabro-cli/src/commands/workflow/list.rs b/lib/crates/fabro-cli/src/commands/workflow/list.rs index 44eee6f46..40da91203 100644 --- a/lib/crates/fabro-cli/src/commands/workflow/list.rs +++ b/lib/crates/fabro-cli/src/commands/workflow/list.rs @@ -5,7 +5,7 @@ use fabro_config::project::{ WorkflowInfo, WorkflowSource, discover_project_config, list_workflows_detailed, resolve_fabro_root, }; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -17,7 +17,7 @@ const GOAL_MAX_LEN: usize = 60; pub(super) fn list_command( _args: &WorkflowListArgs, - cli: &CliSettings, + cli: &CliNamespace, printer: Printer, ) -> Result<()> { let styles = Styles::detect_stderr(); diff --git a/lib/crates/fabro-cli/src/commands/workflow/mod.rs b/lib/crates/fabro-cli/src/commands/workflow/mod.rs index 46478dd2c..44de6cf4e 100644 --- a/lib/crates/fabro-cli/src/commands/workflow/mod.rs +++ b/lib/crates/fabro-cli/src/commands/workflow/mod.rs @@ -2,12 +2,12 @@ mod create; mod list; use anyhow::Result; -use fabro_types::settings::CliSettings; +use fabro_types::settings::CliNamespace; use fabro_util::printer::Printer; use crate::args::{WorkflowCommand, WorkflowNamespace}; -pub(crate) fn dispatch(ns: WorkflowNamespace, cli: &CliSettings, printer: Printer) -> Result<()> { +pub(crate) fn dispatch(ns: WorkflowNamespace, cli: &CliNamespace, printer: Printer) -> Result<()> { match ns.command { WorkflowCommand::List(args) => list::list_command(&args, cli, printer), WorkflowCommand::Create(args) => create::create_command(&args, cli, printer), diff --git a/lib/crates/fabro-cli/src/local_server.rs b/lib/crates/fabro-cli/src/local_server.rs index 6ba1a01aa..cdba3b556 100644 --- a/lib/crates/fabro-cli/src/local_server.rs +++ b/lib/crates/fabro-cli/src/local_server.rs @@ -27,8 +27,8 @@ pub(crate) fn bind_request( } pub(crate) fn auth_methods(settings: &SettingsLayer) -> Vec { - fabro_config::resolve_server_from_file(settings) - .map(|resolved| resolved.auth.methods) + fabro_config::ServerSettings::from_layer(settings) + .map(|resolved| resolved.server.auth.methods) .unwrap_or_default() } diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 5a5ffd7fb..bd2cd9e06 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -1077,36 +1077,22 @@ level = "warn" assert_eq!(cli.command.as_ref().unwrap().name(), "settings"); match *cli.command.unwrap() { Commands::Settings(args) => { - assert!(!args.local); assert!(args.target.server.is_none()); - assert!(args.workflow.is_none()); } _ => panic!("unexpected command variant"), } } #[test] - fn parse_settings_with_workflow() { - let cli = Cli::try_parse_from(["fabro", "settings", "demo"]).expect("should parse"); - match *cli.command.unwrap() { - Commands::Settings(args) => { - assert_eq!(args.workflow, Some(std::path::PathBuf::from("demo"))); - } - _ => panic!("unexpected command variant"), - } + fn parse_settings_rejects_workflow_argument() { + let result = Cli::try_parse_from(["fabro", "settings", "demo"]); + assert!(result.is_err(), "should reject settings workflow argument"); } #[test] - fn parse_settings_local_mode() { - let cli = - Cli::try_parse_from(["fabro", "settings", "--local", "demo"]).expect("should parse"); - match *cli.command.unwrap() { - Commands::Settings(args) => { - assert!(args.local); - assert_eq!(args.workflow, Some(std::path::PathBuf::from("demo"))); - } - _ => panic!("unexpected command variant"), - } + fn parse_settings_rejects_local_flag() { + let result = Cli::try_parse_from(["fabro", "settings", "--local"]); + assert!(result.is_err(), "should reject settings --local"); } #[test] diff --git a/lib/crates/fabro-cli/src/user_config.rs b/lib/crates/fabro-cli/src/user_config.rs index 76fbc9507..687831cc4 100644 --- a/lib/crates/fabro-cli/src/user_config.rs +++ b/lib/crates/fabro-cli/src/user_config.rs @@ -5,7 +5,7 @@ use anyhow::Result; pub(crate) use fabro_client::ServerTarget; pub(crate) use fabro_config::user::*; use fabro_types::settings::cli::CliTargetSettings; -use fabro_types::settings::{CliSettings, SettingsLayer}; +use fabro_types::settings::{CliNamespace, SettingsLayer}; use fabro_util::version::FABRO_VERSION; use tracing::debug; @@ -30,19 +30,14 @@ pub(crate) fn load_settings_with_config_and_storage_dir( Ok(apply_storage_dir_override(layer, storage_dir)) } -fn render_resolve_errors(errors: Vec) -> anyhow::Error { - anyhow::anyhow!( - "failed to resolve cli settings:\n{}", - errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n") - ) +pub(crate) fn resolve_user_settings( + file: &SettingsLayer, +) -> anyhow::Result { + fabro_config::UserSettings::from_layer(file).map_err(anyhow::Error::from) } -pub(crate) fn resolve_cli_settings(file: &SettingsLayer) -> anyhow::Result { - fabro_config::resolve_cli_from_file(file).map_err(render_resolve_errors) +pub(crate) fn resolve_cli_settings(file: &SettingsLayer) -> anyhow::Result { + resolve_user_settings(file).map(|settings| settings.cli) } pub(crate) fn apply_storage_dir_override( @@ -64,7 +59,7 @@ pub(crate) fn apply_storage_dir_override( /// Pull the resolved CLI target configuration out of `[cli.target]`. /// Returns either an http(s) URL or a unix socket path. -fn cli_target_from_settings(settings: &CliSettings) -> Option { +fn cli_target_from_settings(settings: &CliNamespace) -> Option { let target = settings.target.as_ref()?; match target { CliTargetSettings::Http { url } => Some(url.as_source()), @@ -73,8 +68,8 @@ fn cli_target_from_settings(settings: &CliSettings) -> Option { } fn configured_server_target(settings: &SettingsLayer) -> Result> { - let cli_settings = resolve_cli_settings(settings)?; - let Some(value) = cli_target_from_settings(&cli_settings) else { + let user_settings = resolve_user_settings(settings)?; + let Some(value) = cli_target_from_settings(&user_settings.cli) else { return Ok(None); }; parse_server_target(&value).map(Some) diff --git a/lib/crates/fabro-cli/tests/it/cmd/config.rs b/lib/crates/fabro-cli/tests/it/cmd/config.rs index ddf031666..28dc1db55 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/config.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/config.rs @@ -40,93 +40,10 @@ fn parse_settings(stdout: &[u8]) -> serde_json::Value { serde_yaml::from_slice(stdout).expect("stdout should be valid YAML settings") } -fn parse_settings_json(stdout: &[u8]) -> serde_json::Value { - serde_json::from_slice(stdout).expect("stdout should be valid JSON settings") -} - -fn run_goal_inline(settings: &serde_json::Value) -> Option<&str> { - let goal = settings.get("run")?.get("goal")?; - (goal.get("type")?.as_str() == Some("inline")) - .then(|| goal.get("value")?.as_str()) - .flatten() -} - -fn run_model_name(settings: &serde_json::Value) -> Option<&str> { - settings.get("run")?.get("model")?.get("name")?.as_str() -} - -fn run_model_provider(settings: &serde_json::Value) -> Option<&str> { - settings.get("run")?.get("model")?.get("provider")?.as_str() -} - -fn run_inputs(settings: &serde_json::Value) -> &serde_json::Map { - settings - .get("run") - .and_then(|run| run.get("inputs")) - .and_then(serde_json::Value::as_object) - .expect("run.inputs") -} - -fn run_sandbox(settings: &serde_json::Value) -> &serde_json::Value { - settings - .get("run") - .and_then(|run| run.get("sandbox")) - .expect("run.sandbox") -} - -fn run_checkpoint(settings: &serde_json::Value) -> &serde_json::Value { - settings - .get("run") - .and_then(|run| run.get("checkpoint")) - .expect("run.checkpoint") -} - -fn run_hooks(settings: &serde_json::Value) -> &[serde_json::Value] { - settings - .get("run") - .and_then(|run| run.get("hooks")) - .and_then(serde_json::Value::as_array) - .expect("run.hooks") -} - -fn run_agent_mcps(settings: &serde_json::Value) -> &serde_json::Map { - settings - .get("run") - .and_then(|run| run.get("agent")) - .and_then(|agent| agent.get("mcps")) - .and_then(serde_json::Value::as_object) - .expect("run.agent.mcps") -} - -fn auto_approve_enabled(settings: &serde_json::Value) -> bool { - settings - .get("run") - .and_then(|run| run.get("execution")) - .and_then(|execution| execution.get("approval")) - .and_then(serde_json::Value::as_str) - == Some("auto") -} - -fn run_prepare_commands(settings: &serde_json::Value) -> Vec { - settings - .get("run") - .and_then(|run| run.get("prepare")) - .and_then(|prepare| prepare.get("commands")) - .and_then(serde_json::Value::as_array) - .expect("run.prepare.commands") - .iter() - .map(|value| { - value - .as_str() - .expect("command should be a string") - .to_string() - }) - .collect() -} - fn server_storage_root(settings: &serde_json::Value) -> &str { settings .get("server") + .and_then(|server| server.get("server")) .and_then(|server| server.get("storage")) .and_then(|storage| storage.get("root")) .and_then(serde_json::Value::as_str) @@ -157,7 +74,7 @@ shared = "server" } fn resolved_server_settings_fixture() -> serde_json::Value { - let settings = fabro_config::resolve(&server_settings_layer_fixture()) + let settings = fabro_config::ServerSettings::from_layer(&server_settings_layer_fixture()) .expect("server settings fixture should resolve"); serde_json::to_value(settings).expect("resolved settings payload should serialize") } @@ -389,155 +306,6 @@ script = "workflow-setup" // Tests // --------------------------------------------------------------------------- -#[test] -fn settings_local_merges_cli_and_project_defaults() { - let context = test_context!(); - let project = setup_settings_fixture(&context); - - let output = context - .settings() - .arg("--local") - .current_dir(project.path()) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let cfg = parse_settings(&output); - assert!(cfg.get("_version").is_none()); - assert_eq!(cfg["project"]["directory"].as_str(), Some(".")); - assert_eq!(cfg["workflow"]["graph"].as_str(), Some("workflow.fabro")); - assert_eq!(cfg["run"]["execution"]["approval"].as_str(), Some("prompt")); - assert_eq!(cfg["run"]["sandbox"]["provider"].as_str(), Some("daytona")); - assert_eq!(run_model_name(&cfg), Some("project-model")); - assert_eq!(run_model_provider(&cfg), Some("openai")); - assert_eq!(run_goal_inline(&cfg), None); - - // v2 R22: run.inputs replaces the inherited map wholesale rather than - // merging by key, so the project layer wipes out the CLI layer's inputs. - let vars = run_inputs(&cfg); - assert_eq!( - vars.get("project_only").and_then(serde_json::Value::as_str), - Some("1") - ); - assert_eq!( - vars.get("shared").and_then(serde_json::Value::as_str), - Some("project") - ); - assert!( - !vars.contains_key("cli_only"), - "run.inputs should replace across layers, not merge by key" - ); - - // v2 R71: provider-native maps such as run.sandbox.daytona.labels remain - // sticky merge-by-key, so CLI labels persist under the project layer. - let sandbox = run_sandbox(&cfg); - let labels = &sandbox["daytona"]["labels"]; - assert_eq!(labels["cli_only"].as_str(), Some("1")); - assert_eq!(labels["shared"].as_str(), Some("cli")); -} - -#[test] -fn settings_local_workflow_name_applies_run_overlay_and_deep_merges() { - let context = test_context!(); - let project = setup_settings_fixture(&context); - - let output = context - .settings() - .current_dir(project.path()) - .args(["--local", "demo"]) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let cfg = parse_settings(&output); - assert_eq!(run_goal_inline(&cfg), Some("demo goal")); - assert_eq!(run_model_name(&cfg), Some("run-model")); - assert_eq!(run_model_provider(&cfg), Some("anthropic")); - - // v2 R22: run.inputs replaces wholesale, so the workflow layer wins - // over project and cli. - let vars = run_inputs(&cfg); - assert_eq!(vars.get("run_only").and_then(|v| v.as_str()), Some("1")); - assert_eq!(vars.get("shared").and_then(|v| v.as_str()), Some("run")); - - // checkpoint.exclude_globs is a security/policy list: replace by default. - let checkpoint = run_checkpoint(&cfg); - assert_eq!( - checkpoint["exclude_globs"], - serde_json::json!(["run-only", "shared"]) - ); - - // Hooks: id-based replacement. The "shared" hook appears in both cli and - // workflow layers and resolves to the workflow entry; project and run-only - // contribute the other two ids. - let hooks = run_hooks(&cfg); - assert!(hooks.len() >= 2); - let shared_hook = hooks - .iter() - .find(|hook| hook["name"].as_str() == Some("shared")) - .expect("shared hook"); - assert_eq!(shared_hook["command"].as_str(), Some("echo run")); - assert!( - hooks - .iter() - .any(|hook| hook["name"].as_str() == Some("run-only")) - ); - - let mcps = run_agent_mcps(&cfg); - let shared = mcps.get("shared").expect("shared mcp"); - assert_eq!(shared["transport"]["type"].as_str(), Some("stdio")); - assert_eq!( - shared["transport"]["command"], - serde_json::json!(["echo", "run"]) - ); - assert!(mcps.contains_key("run_only")); - - // run.sandbox.daytona.labels stays sticky merge-by-key per R71. - let sandbox = run_sandbox(&cfg); - let labels = &sandbox["daytona"]["labels"]; - assert_eq!(labels["run_only"].as_str(), Some("1")); - assert_eq!(labels["shared"].as_str(), Some("run")); - - // run.sandbox.env stays sticky merge-by-key per R71. - let env = &sandbox["env"]; - assert_eq!(env["CLI_ONLY"].as_str(), Some("1")); - assert_eq!(env["RUN_ONLY"].as_str(), Some("1")); - assert_eq!(env["SHARED"].as_str(), Some("run")); -} - -#[test] -fn settings_local_explicit_workflow_path_uses_workflow_project_layers() { - let mut context = test_context!(); - let (project, _storage_dir) = setup_external_workflow_fixture(&mut context); - let cwd = tempfile::tempdir().unwrap(); - let workflow = project.path().join("workflow.toml"); - - // Remove FABRO_STORAGE_DIR so the CLI uses storage_dir from settings.toml - let output = context - .settings() - .env_remove("FABRO_STORAGE_DIR") - .current_dir(cwd.path()) - .args(["--local", workflow.to_str().unwrap()]) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let cfg = parse_settings(&output); - assert!(auto_approve_enabled(&cfg)); - // v2 R30: run.prepare.steps replaces the whole ordered list across layers. - // The highest-precedence layer (workflow) wins. - assert_eq!(run_prepare_commands(&cfg), vec![ - "workflow-setup".to_string() - ]); - assert_eq!(run_sandbox(&cfg)["preserve"].as_bool(), Some(true)); -} - #[test] fn create_explicit_workflow_path_uses_project_config_relative_to_workflow() { let mut context = test_context!(); @@ -608,254 +376,6 @@ fn create_explicit_workflow_path_uses_project_config_relative_to_workflow() { ); } -#[test] -fn settings_fabro_path_matches_ambient_defaults() { - let context = test_context!(); - let project = setup_settings_fixture(&context); - - let ambient = context - .settings() - .arg("--local") - .current_dir(project.path()) - .assert() - .success() - .get_output() - .stdout - .clone(); - let graph = context - .settings() - .current_dir(project.path()) - .args(["--local", "standalone.fabro"]) - .assert() - .success() - .get_output() - .stdout - .clone(); - - assert_eq!(parse_settings(&graph), parse_settings(&ambient)); -} - -#[test] -fn settings_missing_run_config_errors() { - let context = test_context!(); - let project = setup_settings_fixture(&context); - - let mut cmd = context.settings(); - cmd.current_dir(project.path()); - cmd.args(["--local", "missing.toml"]); - let output = cmd.output().expect("command should execute"); - assert!(!output.status.success()); - assert!(String::from_utf8_lossy(&output.stdout).trim().is_empty()); - let stderr = String::from_utf8_lossy(&output.stderr); - assert!( - stderr.contains("workflow not found:"), - "stderr should report missing workflow path, got:\n{stderr}" - ); - assert!( - stderr.contains("missing.toml"), - "stderr should include missing workflow filename, got:\n{stderr}" - ); -} - -#[test] -fn settings_legacy_cli_config_is_silently_ignored() { - let context = test_context!(); - let project = tempfile::tempdir().unwrap(); - - context.write_home( - ".fabro/cli.toml", - r#" -_version = 1 - -[cli.output] -verbosity = "verbose" - -[run.model] -name = "legacy-model" -"#, - ); - - let assert = context - .settings() - .arg("--local") - .current_dir(project.path()) - .assert() - .success(); - - assert!( - assert.get_output().stderr.is_empty(), - "settings should not warn about legacy config files: {}", - String::from_utf8_lossy(&assert.get_output().stderr) - ); - - let cfg = parse_settings(&assert.get_output().stdout); - assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal")); - assert!( - cfg["run"]["model"].get("name").is_none(), - "resolved dense settings should omit an unset run.model.name" - ); -} - -#[test] -fn settings_legacy_user_config_is_silently_ignored() { - let context = test_context!(); - let project = tempfile::tempdir().unwrap(); - - context.write_home( - ".fabro/user.toml", - r#" -_version = 1 - -[cli.output] -verbosity = "verbose" - -[run.model] -name = "legacy-model" -"#, - ); - - let assert = context - .settings() - .arg("--local") - .current_dir(project.path()) - .assert() - .success(); - - assert!( - assert.get_output().stderr.is_empty(), - "settings should not warn about legacy config files: {}", - String::from_utf8_lossy(&assert.get_output().stderr) - ); - - let cfg = parse_settings(&assert.get_output().stdout); - assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal")); - assert!( - cfg["run"]["model"].get("name").is_none(), - "resolved dense settings should omit an unset run.model.name" - ); -} - -#[test] -fn settings_legacy_server_config_is_silently_ignored() { - let context = test_context!(); - let project = tempfile::tempdir().unwrap(); - - context.write_home( - ".fabro/server.toml", - r#" -_version = 1 - -[cli.output] -verbosity = "verbose" - -[run.model] -name = "legacy-model" -"#, - ); - - let assert = context - .settings() - .arg("--local") - .current_dir(project.path()) - .assert() - .success(); - - assert!( - assert.get_output().stderr.is_empty(), - "settings should not warn about legacy config files: {}", - String::from_utf8_lossy(&assert.get_output().stderr) - ); - - let cfg = parse_settings(&assert.get_output().stdout); - assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal")); - assert!( - cfg["run"]["model"].get("name").is_none(), - "resolved dense settings should omit an unset run.model.name" - ); -} - -#[test] -fn settings_user_config_wins_over_legacy_cli_config() { - let context = test_context!(); - let project = setup_settings_fixture(&context); - context.write_home( - ".fabro/cli.toml", - r#" -_version = 1 - -[run.model] -name = "legacy-model" - -[run.inputs] -shared = "legacy" -"#, - ); - - let assert = context - .settings() - .arg("--local") - .current_dir(project.path()) - .assert() - .success(); - - assert!( - assert.get_output().stderr.is_empty(), - "settings should not warn about legacy config files: {}", - String::from_utf8_lossy(&assert.get_output().stderr) - ); - - let cfg = parse_settings(&assert.get_output().stdout); - assert_eq!(run_model_name(&cfg), Some("project-model")); - let vars = run_inputs(&cfg); - assert_eq!( - vars.get("shared").and_then(serde_json::Value::as_str), - Some("project") - ); -} - -#[test] -fn settings_uses_fabro_home_for_home_config_resolution() { - let context = test_context!(); - let fabro_home = tempfile::tempdir().unwrap(); - - std::fs::write( - fabro_home.path().join("settings.toml"), - r#" -_version = 1 - -[cli.output] -verbosity = "verbose" - -[run.model] -name = "from-fabro-home" -"#, - ) - .unwrap(); - - let output = context - .settings() - .args(["--local", "--json"]) - .env("FABRO_HOME", fabro_home.path()) - .env_remove("FABRO_STORAGE_DIR") - .output() - .expect("command should execute"); - - assert!( - output.status.success(), - "settings command failed:\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - - let cfg = parse_settings_json(&output.stdout); - assert!(cfg.get("_version").is_none()); - assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("verbose")); - assert_eq!( - cfg["run"]["model"]["name"].as_str(), - Some("from-fabro-home") - ); -} - #[test] fn settings_rejects_server_url_flag() { let context = test_context!(); @@ -883,32 +403,27 @@ fn settings_rejects_storage_dir_flag() { } #[test] -fn settings_rejects_local_and_server_combination() { +fn settings_rejects_local_flag() { let context = test_context!(); context .settings() - .args(["--local", "--server", "https://cli.example.com"]) + .arg("--local") .assert() .failure() .stderr(predicate::str::contains( - "the argument '--local' cannot be used with '--server '", + "unexpected argument '--local' found", )); } #[test] -fn settings_rejects_workflow_without_local() { +fn settings_rejects_workflow_argument() { let context = test_context!(); - let project = setup_settings_fixture(&context); - context .settings() - .current_dir(project.path()) .arg("demo") .assert() .failure() - .stderr(predicate::str::contains( - "WORKFLOW requires --local; use `fabro settings --local WORKFLOW`", - )); + .stderr(predicate::str::contains("unexpected argument 'demo' found")); } #[test] @@ -918,12 +433,9 @@ fn settings_fetches_server_resolved_settings() { let server = MockServer::start(); let server_settings = resolved_server_settings_fixture(); let mock = server.mock(|when, then| { - when.method("GET") - .path("/api/v1/settings") - .query_param("view", "resolved"); + when.method("GET").path("/api/v1/settings"); then.status(200) .header("Content-Type", "application/json") - .header("X-Fabro-Settings-View", "resolved") .body(server_settings_body(&server_settings)); }); context.write_home( @@ -962,30 +474,25 @@ shared = "cli" mock.assert(); let cfg = parse_settings(&output); - assert!(cfg.get("_version").is_none()); - assert_eq!(cfg["project"]["directory"].as_str(), Some(".")); - assert_eq!(cfg["workflow"]["graph"].as_str(), Some("workflow.fabro")); - assert_eq!(cfg["run"]["execution"]["approval"].as_str(), Some("prompt")); - assert_eq!(run_model_name(&cfg), Some("server-model")); - assert_eq!(run_model_provider(&cfg), Some("openai")); + assert_eq!( + cfg["user"]["cli"]["output"]["verbosity"].as_str(), + Some("verbose") + ); + assert_eq!( + cfg["user"]["features"]["session_sandboxes"].as_bool(), + Some(false) + ); + assert_eq!( + cfg["server"]["server"]["auth"]["methods"][0].as_str(), + Some("dev-token") + ); assert_eq!(server_storage_root(&cfg), "/srv/fabro-server"); - assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal")); - - // Server-backed mode now returns the selected server's own dense resolved - // settings; local project/user overlays are not merged into the output. - let vars = run_inputs(&cfg); assert_eq!( - vars.get("server_only").and_then(serde_json::Value::as_str), - Some("1") - ); - assert_eq!( - vars.get("shared").and_then(serde_json::Value::as_str), - Some("server") - ); - assert!( - !vars.contains_key("project_only"), - "server-backed settings output must not include local workflow/project overlays" + cfg["server"]["server"]["artifacts"]["store"]["type"].as_str(), + Some("local") ); + assert!(cfg.get("run").is_none()); + assert!(cfg.get("project").is_none()); } #[test] @@ -994,21 +501,16 @@ fn settings_cli_server_target_overrides_configured_server_target() { let project = setup_settings_fixture(&context); let configured_server = MockServer::start(); let configured_mock = configured_server.mock(|when, then| { - when.method("GET") - .path("/api/v1/settings") - .query_param("view", "resolved"); + when.method("GET").path("/api/v1/settings"); then.status(500) .body("configured-server-should-not-be-used"); }); let cli_server = MockServer::start(); let cli_server_settings = resolved_server_settings_fixture(); let cli_mock = cli_server.mock(|when, then| { - when.method("GET") - .path("/api/v1/settings") - .query_param("view", "resolved"); + when.method("GET").path("/api/v1/settings"); then.status(200) .header("Content-Type", "application/json") - .header("X-Fabro-Settings-View", "resolved") .body(server_settings_body(&cli_server_settings)); }); context.write_home( @@ -1044,46 +546,6 @@ verbosity = "verbose" assert_eq!(server_storage_root(&cfg), "/srv/fabro-server"); } -#[test] -fn settings_errors_when_server_lacks_resolved_view_marker() { - let context = test_context!(); - let project = setup_settings_fixture(&context); - let server = MockServer::start(); - let server_settings = resolved_server_settings_fixture(); - let mock = server.mock(|when, then| { - when.method("GET") - .path("/api/v1/settings") - .query_param("view", "resolved"); - then.status(200) - .header("Content-Type", "application/json") - .body(server_settings_body(&server_settings)); - }); - context.write_home( - ".fabro/settings.toml", - format!( - r#" -_version = 1 - -[cli.target] -type = "http" -url = "{}/api/v1" -"#, - server.base_url() - ), - ); - - context - .settings() - .current_dir(project.path()) - .assert() - .failure() - .stderr(predicate::str::contains( - "server does not support resolved settings view; upgrade the server or use --local", - )); - - mock.assert(); -} - #[test] fn settings_unreachable_http_target_fails_clearly() { let context = test_context!(); diff --git a/lib/crates/fabro-cli/tests/it/cmd/create.rs b/lib/crates/fabro-cli/tests/it/cmd/create.rs index d09845eaa..1de7d5f97 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/create.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/create.rs @@ -8,7 +8,7 @@ use crate::support::{fabro_json_snapshot, unique_run_id}; fn resolved_run( settings: &fabro_types::settings::SettingsLayer, -) -> fabro_types::settings::RunSettings { +) -> fabro_types::settings::RunNamespace { fabro_config::resolve_run_from_file(settings).expect("run settings should resolve") } diff --git a/lib/crates/fabro-client/src/client.rs b/lib/crates/fabro-client/src/client.rs index 61cbace50..f674ee474 100644 --- a/lib/crates/fabro-client/src/client.rs +++ b/lib/crates/fabro-client/src/client.rs @@ -490,26 +490,18 @@ impl Client { } } - pub async fn retrieve_resolved_server_settings(&self) -> Result { - let url = format!("{}/api/v1/settings?view=resolved", self.base_url()); + pub async fn retrieve_resolved_server_settings( + &self, + ) -> Result { + let url = format!("{}/api/v1/settings", self.base_url()); let response = self .send_http(|http_client| async move { http_client.get(&url).send().await }) .await?; - let marker = response - .headers() - .get("x-fabro-settings-view") - .and_then(|value| value.to_str().ok()); - if marker != Some("resolved") { - bail!( - "server does not support resolved settings view; upgrade the server or use --local" - ); - } - response - .json::() + .json::() .await - .context("server returned invalid JSON for the resolved settings view") + .context("server returned invalid JSON for server settings") } pub async fn create_run_from_manifest(&self, manifest: types::RunManifest) -> Result { diff --git a/lib/crates/fabro-config/Cargo.toml b/lib/crates/fabro-config/Cargo.toml index c0a220b9f..a4ec98f7a 100644 --- a/lib/crates/fabro-config/Cargo.toml +++ b/lib/crates/fabro-config/Cargo.toml @@ -37,3 +37,4 @@ ulid.workspace = true [dev-dependencies] toml.workspace = true fabro-types = { path = "../fabro-types", features = ["test-support"] } +temp-env = "0.3" diff --git a/lib/crates/fabro-config/src/context.rs b/lib/crates/fabro-config/src/context.rs new file mode 100644 index 000000000..1197a4d99 --- /dev/null +++ b/lib/crates/fabro-config/src/context.rs @@ -0,0 +1,60 @@ +use fabro_types::settings::{CliNamespace, FeaturesNamespace, ServerNamespace, SettingsLayer}; +use serde::{Deserialize, Serialize}; + +use crate::resolve::{resolve_cli, resolve_features, resolve_server}; +use crate::user::load_settings_config; +use crate::{Error, Result, apply_builtin_defaults}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ServerSettings { + pub server: ServerNamespace, + pub features: FeaturesNamespace, +} + +impl ServerSettings { + pub fn from_layer(layer: &SettingsLayer) -> Result { + let layer = apply_builtin_defaults(layer.clone()); + let mut errors = Vec::new(); + let server_layer = layer.server.clone().unwrap_or_default(); + let features_layer = layer.features.clone().unwrap_or_default(); + let server = resolve_server(&server_layer, &mut errors); + let features = resolve_features(&features_layer, &mut errors); + if errors.is_empty() { + Ok(Self { server, features }) + } else { + Err(Error::resolve("failed to resolve server settings", errors)) + } + } + + pub fn resolve() -> Result { + let layer = load_settings_config(None)?; + Self::from_layer(&layer) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub struct UserSettings { + pub cli: CliNamespace, + pub features: FeaturesNamespace, +} + +impl UserSettings { + pub fn from_layer(layer: &SettingsLayer) -> Result { + let layer = apply_builtin_defaults(layer.clone()); + let mut errors = Vec::new(); + let cli_layer = layer.cli.clone().unwrap_or_default(); + let features_layer = layer.features.clone().unwrap_or_default(); + let cli = resolve_cli(&cli_layer, &mut errors); + let features = resolve_features(&features_layer, &mut errors); + if errors.is_empty() { + Ok(Self { cli, features }) + } else { + Err(Error::resolve("failed to resolve user settings", errors)) + } + } + + pub fn resolve() -> Result { + let layer = load_settings_config(None)?; + Self::from_layer(&layer) + } +} diff --git a/lib/crates/fabro-config/src/effective_settings.rs b/lib/crates/fabro-config/src/effective_settings.rs index 685bf9bae..bf69160ec 100644 --- a/lib/crates/fabro-config/src/effective_settings.rs +++ b/lib/crates/fabro-config/src/effective_settings.rs @@ -5,7 +5,7 @@ //! across all three config files (settings.toml, .fabro/project.toml, //! workflow.toml). //! Owner-specific domains (`cli`, `server`) are consumed only from the local -//! `~/.fabro/settings.toml` plus explicit process-local overrides — their +//! `~/.fabro/settings.toml` plus explicit process-local overrides. Their //! stanzas in `.fabro/project.toml` and `workflow.toml` remain schema-valid but //! inert. @@ -16,13 +16,6 @@ use fabro_types::settings::server::ServerLayer; use crate::merge::combine_files; use crate::{Error, Result, apply_builtin_defaults}; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum EffectiveSettingsMode { - LocalOnly, - RemoteServer, - LocalDaemon, -} - #[derive(Clone, Debug, Default)] pub struct EffectiveSettingsLayers { pub args: SettingsLayer, @@ -53,7 +46,6 @@ impl EffectiveSettingsLayers { pub fn materialize_settings_layer( layers: EffectiveSettingsLayers, server_settings: Option<&SettingsLayer>, - mode: EffectiveSettingsMode, ) -> Result { let EffectiveSettingsLayers { args, @@ -61,47 +53,28 @@ pub fn materialize_settings_layer( mut project, user, } = layers; + let server_settings = server_settings.ok_or(Error::MissingServerSettings)?; - let settings = match mode { - EffectiveSettingsMode::LocalOnly => { - combine_files(combine_files(combine_files(user, project), workflow), args) - } - EffectiveSettingsMode::RemoteServer | EffectiveSettingsMode::LocalDaemon => { - let server_settings = server_settings.ok_or(Error::MissingServerSettings)?; - // Owner-specific domains (cli, server) may only come from the - // local ~/.fabro/settings.toml, never from .fabro/project.toml or - // workflow.toml. The user layer keeps its cli/server fields. - strip_owner_domains(&mut workflow); - strip_owner_domains(&mut project); + // Owner-specific domains (cli, server) may only come from the local + // ~/.fabro/settings.toml, never from .fabro/project.toml or workflow.toml. + // The user layer keeps its cli/server fields. + strip_owner_domains(&mut workflow); + strip_owner_domains(&mut project); - let server_defaults = server_settings.clone(); + let combined = combine_files(combine_files(combine_files(user, project), workflow), args); + let mut settings = enforce_server_authority(combined, server_settings); - let combined = - combine_files(combine_files(combine_files(user, project), workflow), args); - - let mut settings = match mode { - EffectiveSettingsMode::RemoteServer => { - apply_server_defaults(combined, &server_defaults) - } - EffectiveSettingsMode::LocalDaemon => { - apply_local_daemon_overrides(combined, &server_defaults) - } - EffectiveSettingsMode::LocalOnly => unreachable!(), - }; - // Storage root always comes from the server's local - // ~/.fabro/settings.toml, never from the client. - if let Some(server_root) = server_settings - .server - .as_ref() - .and_then(|s| s.storage.as_ref()) - .cloned() - { - let server = settings.server.get_or_insert_with(ServerLayer::default); - server.storage = Some(server_root); - } - settings - } - }; + // Storage root always comes from the server's local ~/.fabro/settings.toml, + // never from the client. + if let Some(server_root) = server_settings + .server + .as_ref() + .and_then(|server| server.storage.as_ref()) + .cloned() + { + let server = settings.server.get_or_insert_with(ServerLayer::default); + server.storage = Some(server_root); + } Ok(apply_builtin_defaults(settings)) } @@ -111,30 +84,11 @@ fn strip_owner_domains(file: &mut SettingsLayer) { file.server = None; } -/// Apply server-side defaults to a client-layered [`SettingsLayer`]. +/// Enforce server-owned fields on a client-layered [`SettingsLayer`]. /// -/// Server-owned domains (`server`, `features`, and parts of `run`) flow from -/// the server's local `~/.fabro/settings.toml` when the corresponding client -/// value is absent. Run-shaped defaults (model, prepare, sandbox, checkpoint, -/// hooks, agent mcps, etc.) also flow from server to client so the persisted -/// run spec matches the server's local configuration. -fn apply_server_defaults(mut settings: SettingsLayer, server: &SettingsLayer) -> SettingsLayer { - // Server-owned domains: server-side always wins when client left blank. - // Use the v2 merge matrix with the server layer in lower precedence so - // that client-supplied values still dominate when present. - settings = combine_files(server.clone(), settings); - settings -} - -/// Apply server-side overrides in LocalDaemon mode. -/// -/// In LocalDaemon mode, a subset of server-owned fields unconditionally -/// override any client-side values. Client-controlled run-level fields are -/// left alone. -fn apply_local_daemon_overrides( - mut settings: SettingsLayer, - server: &SettingsLayer, -) -> SettingsLayer { +/// A subset of server-owned fields unconditionally override any client-side +/// values. Client-controlled run-level fields are left alone. +fn enforce_server_authority(mut settings: SettingsLayer, server: &SettingsLayer) -> SettingsLayer { if let Some(server_layer) = server.server.clone() { let client = settings.server.get_or_insert_with(ServerLayer::default); if let Some(storage) = server_layer.storage { @@ -173,7 +127,7 @@ mod tests { use fabro_types::settings::server::{ServerLayer, ServerSchedulerLayer, ServerStorageLayer}; use fabro_types::settings::{InterpString, SettingsLayer}; - use super::{EffectiveSettingsLayers, EffectiveSettingsMode, materialize_settings_layer}; + use super::{EffectiveSettingsLayers, materialize_settings_layer}; use crate::parse::parse_settings_layer; fn layer(source: &str) -> SettingsLayer { @@ -181,7 +135,7 @@ mod tests { } #[test] - fn local_only_merges_project_and_user_layers() { + fn materialize_settings_layer_merges_layers_and_applies_server_authority() { let settings = materialize_settings_layer( EffectiveSettingsLayers::new( SettingsLayer::default(), @@ -214,8 +168,17 @@ shared = "user" "#, ), ), - None, - EffectiveSettingsMode::LocalOnly, + Some(&layer( + r#" +_version = 1 + +[server.storage] +root = "/srv/fabro" + +[server.scheduler] +max_concurrent_runs = 7 +"#, + )), ) .unwrap(); @@ -229,7 +192,7 @@ shared = "user" .as_deref(), Some("project-model") ); - // Per R22, run.inputs replaces wholesale — the winning layer is the + // Per R22, run.inputs replaces wholesale. The winning layer is the // highest-precedence layer that sets `inputs` (project here, since it // wins over user). let inputs = settings @@ -239,13 +202,31 @@ shared = "user" .unwrap(); assert!(inputs.contains_key("project_only")); assert_eq!( - inputs.get("shared").and_then(|v| v.as_str()), + inputs.get("shared").and_then(|value| value.as_str()), Some("project") ); assert!( !inputs.contains_key("user_only"), "project.inputs should replace user.inputs wholesale" ); + assert_eq!( + settings + .server + .as_ref() + .and_then(|server| server.storage.as_ref()) + .and_then(|storage| storage.root.as_ref()) + .map(InterpString::as_source) + .as_deref(), + Some("/srv/fabro") + ); + assert_eq!( + settings + .server + .as_ref() + .and_then(|server| server.scheduler.as_ref()) + .and_then(|scheduler| scheduler.max_concurrent_runs), + Some(7) + ); assert_eq!( settings .project @@ -271,7 +252,7 @@ shared = "user" } #[test] - fn local_only_merges_workflow_project_user() { + fn materialize_settings_layer_preserves_client_values_with_empty_server_layer() { let settings = materialize_settings_layer( EffectiveSettingsLayers::new( SettingsLayer::default(), @@ -303,16 +284,13 @@ provider = "openai" "#, ), ), - None, - EffectiveSettingsMode::LocalOnly, + Some(&SettingsLayer::default()), ) .unwrap(); assert_eq!( match settings.run.as_ref().and_then(|run| run.goal.as_ref()) { - Some(RunGoalLayer::Inline(value)) => { - Some(value.as_source()) - } + Some(RunGoalLayer::Inline(value)) => Some(value.as_source()), _ => None, } .as_deref(), @@ -341,83 +319,7 @@ provider = "openai" } #[test] - fn cli_and_server_domains_from_fabro_toml_are_inert_under_remote_mode() { - let server_settings = SettingsLayer { - server: Some(ServerLayer { - storage: Some(ServerStorageLayer { - root: Some(InterpString::parse("/srv/fabro")), - }), - scheduler: Some(ServerSchedulerLayer { - max_concurrent_runs: Some(9), - }), - ..ServerLayer::default() - }), - ..SettingsLayer::default() - }; - - let project_with_server = layer( - r#" -_version = 1 - -[run] -goal = "project goal" - -[server.storage] -root = "/tmp/should-be-inert" -"#, - ); - - let settings = materialize_settings_layer( - EffectiveSettingsLayers::new( - SettingsLayer::default(), - SettingsLayer::default(), - project_with_server, - SettingsLayer::default(), - ), - Some(&server_settings), - EffectiveSettingsMode::RemoteServer, - ) - .unwrap(); - - assert_eq!( - settings - .server - .as_ref() - .and_then(|server| server.storage.as_ref()) - .and_then(|storage| storage.root.as_ref()) - .map(InterpString::as_source) - .as_deref(), - Some("/srv/fabro") - ); - assert_eq!( - match settings.run.as_ref().and_then(|run| run.goal.as_ref()) { - Some(RunGoalLayer::Inline(value)) => { - Some(value.as_source()) - } - _ => None, - } - .as_deref(), - Some("project goal") - ); - assert_eq!( - settings - .workflow - .as_ref() - .and_then(|workflow| workflow.graph.as_deref()), - Some("workflow.fabro") - ); - assert_eq!( - settings - .run - .as_ref() - .and_then(|run| run.sandbox.as_ref()) - .and_then(|sandbox| sandbox.provider.as_deref()), - Some("local") - ); - } - - #[test] - fn local_daemon_mode_only_applies_server_owned_overrides() { + fn materialize_settings_layer_applies_server_owned_overrides() { let server_settings = SettingsLayer { server: Some(ServerLayer { storage: Some(ServerStorageLayer { @@ -431,12 +333,9 @@ root = "/tmp/should-be-inert" ..SettingsLayer::default() }; - let settings = materialize_settings_layer( - EffectiveSettingsLayers::default(), - Some(&server_settings), - EffectiveSettingsMode::LocalDaemon, - ) - .unwrap(); + let settings = + materialize_settings_layer(EffectiveSettingsLayers::default(), Some(&server_settings)) + .unwrap(); assert_eq!( settings diff --git a/lib/crates/fabro-config/src/lib.rs b/lib/crates/fabro-config/src/lib.rs index a405fdac7..dc654cbeb 100644 --- a/lib/crates/fabro-config/src/lib.rs +++ b/lib/crates/fabro-config/src/lib.rs @@ -2,9 +2,14 @@ clippy::disallowed_methods, reason = "sync config loading utilities used at startup; not on a Tokio path" )] +//! Settings resolution entrypoints are owner-first context types: +//! [`ServerSettings`] for current server/runtime config and [`UserSettings`] +//! for current CLI/user config. Stored `SettingsLayer` artifacts still use the +//! per-namespace `resolve_*_from_file` helpers. extern crate self as fabro_config; +pub mod context; mod defaults; pub mod bind; @@ -24,9 +29,9 @@ pub mod user; use std::path::Path; +pub use context::{ServerSettings, UserSettings}; pub use defaults::{apply_builtin_defaults, defaults_layer}; pub use error::{Error, Result}; -use fabro_types::settings::{Settings, SettingsLayer}; pub use fabro_util::path::expand_tilde; pub use home::Home; pub use load::{ @@ -34,23 +39,14 @@ pub use load::{ }; pub use parse::{ParseError, parse_settings_layer}; pub use resolve::{ - ResolveError, dev_token_auth_enabled, resolve, resolve_cli, resolve_cli_from_file, - resolve_features, resolve_features_from_file, resolve_project, resolve_project_from_file, - resolve_run, resolve_run_from_file, resolve_server, resolve_server_from_file, - resolve_storage_root, resolve_workflow, resolve_workflow_from_file, + ResolveError, dev_token_auth_enabled, resolve_cli, resolve_cli_from_file, resolve_features, + resolve_features_from_file, resolve_project, resolve_project_from_file, resolve_run, + resolve_run_from_file, resolve_server, resolve_server_from_file, resolve_storage_root, + resolve_workflow, resolve_workflow_from_file, }; use serde::de::DeserializeOwned; pub use storage::{RunScratch, RuntimeDirectory, Storage}; -pub fn load_and_resolve( - layers: effective_settings::EffectiveSettingsLayers, - server_settings: Option<&SettingsLayer>, - mode: effective_settings::EffectiveSettingsMode, -) -> Result { - let layer = effective_settings::materialize_settings_layer(layers, server_settings, mode)?; - resolve(&layer).map_err(|errors| Error::resolve("failed to resolve settings", errors)) -} - /// Load a TOML config from an explicit path or `~/.fabro/{filename}`. /// /// Returns `T::default()` when no explicit path is given and the default file diff --git a/lib/crates/fabro-config/src/resolve/cli.rs b/lib/crates/fabro-config/src/resolve/cli.rs index 4905359d1..2c7e09345 100644 --- a/lib/crates/fabro-config/src/resolve/cli.rs +++ b/lib/crates/fabro-config/src/resolve/cli.rs @@ -1,13 +1,13 @@ use fabro_types::settings::cli::{ CliAuthSettings, CliExecAgentSettings, CliExecLayer, CliExecModelSettings, CliExecSettings, - CliLayer, CliLoggingSettings, CliOutputSettings, CliSettings, CliTargetLayer, + CliLayer, CliLoggingSettings, CliNamespace, CliOutputSettings, CliTargetLayer, CliTargetSettings, CliUpdatesSettings, }; use super::{ResolveError, require_interp}; -pub fn resolve_cli(layer: &CliLayer, errors: &mut Vec) -> CliSettings { - CliSettings { +pub fn resolve_cli(layer: &CliLayer, errors: &mut Vec) -> CliNamespace { + CliNamespace { target: resolve_target(layer.target.as_ref(), errors), auth: CliAuthSettings { strategy: layer.auth.as_ref().and_then(|auth| auth.strategy), diff --git a/lib/crates/fabro-config/src/resolve/features.rs b/lib/crates/fabro-config/src/resolve/features.rs index e6c8eeced..a9a2d1254 100644 --- a/lib/crates/fabro-config/src/resolve/features.rs +++ b/lib/crates/fabro-config/src/resolve/features.rs @@ -1,12 +1,12 @@ -use fabro_types::settings::features::{FeaturesLayer, FeaturesSettings}; +use fabro_types::settings::features::{FeaturesLayer, FeaturesNamespace}; use super::ResolveError; pub fn resolve_features( layer: &FeaturesLayer, _errors: &mut Vec, -) -> FeaturesSettings { - FeaturesSettings { +) -> FeaturesNamespace { + FeaturesNamespace { session_sandboxes: layer .session_sandboxes .expect("defaults.toml should provide features.session_sandboxes"), diff --git a/lib/crates/fabro-config/src/resolve/mod.rs b/lib/crates/fabro-config/src/resolve/mod.rs index 9032464a0..c72ca5930 100644 --- a/lib/crates/fabro-config/src/resolve/mod.rs +++ b/lib/crates/fabro-config/src/resolve/mod.rs @@ -9,8 +9,8 @@ mod workflow; pub use cli::resolve_cli; pub use error::ResolveError; use fabro_types::settings::{ - CliSettings, FeaturesSettings, InterpString, ProjectSettings, RunSettings, ServerSettings, - Settings, SettingsLayer, WorkflowSettings, + CliNamespace, FeaturesNamespace, InterpString, ProjectNamespace, RunNamespace, ServerNamespace, + SettingsLayer, WorkflowNamespace, }; pub use features::resolve_features; pub use project::resolve_project; @@ -20,33 +20,7 @@ pub use workflow::resolve_workflow; use crate::apply_builtin_defaults; -pub fn resolve(file: &SettingsLayer) -> Result> { - let file = apply_builtin_defaults(file.clone()); - let mut errors = Vec::new(); - let project_layer = file.project.clone().unwrap_or_default(); - let workflow_layer = file.workflow.clone().unwrap_or_default(); - let run_layer = file.run.clone().unwrap_or_default(); - let cli_layer = file.cli.clone().unwrap_or_default(); - let server_layer = file.server.clone().unwrap_or_default(); - let features_layer = file.features.clone().unwrap_or_default(); - - let settings = Settings { - project: resolve_project(&project_layer, &mut errors), - workflow: resolve_workflow(&workflow_layer, &mut errors), - run: resolve_run(&run_layer, &mut errors), - cli: resolve_cli(&cli_layer, &mut errors), - server: resolve_server(&server_layer, &mut errors), - features: resolve_features(&features_layer, &mut errors), - }; - - if errors.is_empty() { - Ok(settings) - } else { - Err(errors) - } -} - -pub fn resolve_cli_from_file(file: &SettingsLayer) -> Result> { +pub fn resolve_cli_from_file(file: &SettingsLayer) -> Result> { let file = apply_builtin_defaults(file.clone()); let mut errors = Vec::new(); let cli_layer = file.cli.clone().unwrap_or_default(); @@ -58,7 +32,9 @@ pub fn resolve_cli_from_file(file: &SettingsLayer) -> Result Result> { +pub fn resolve_server_from_file( + file: &SettingsLayer, +) -> Result> { let file = apply_builtin_defaults(file.clone()); let mut errors = Vec::new(); let server_layer = file.server.clone().unwrap_or_default(); @@ -72,7 +48,7 @@ pub fn resolve_server_from_file(file: &SettingsLayer) -> Result Result> { +) -> Result> { let file = apply_builtin_defaults(file.clone()); let mut errors = Vec::new(); let project_layer = file.project.clone().unwrap_or_default(); @@ -86,7 +62,7 @@ pub fn resolve_project_from_file( pub fn resolve_features_from_file( file: &SettingsLayer, -) -> Result> { +) -> Result> { let file = apply_builtin_defaults(file.clone()); let mut errors = Vec::new(); let features_layer = file.features.clone().unwrap_or_default(); @@ -98,7 +74,7 @@ pub fn resolve_features_from_file( } } -pub fn resolve_run_from_file(file: &SettingsLayer) -> Result> { +pub fn resolve_run_from_file(file: &SettingsLayer) -> Result> { let file = apply_builtin_defaults(file.clone()); let mut errors = Vec::new(); let run_layer = file.run.clone().unwrap_or_default(); @@ -112,7 +88,7 @@ pub fn resolve_run_from_file(file: &SettingsLayer) -> Result Result> { +) -> Result> { let file = apply_builtin_defaults(file.clone()); let mut errors = Vec::new(); let workflow_layer = file.workflow.clone().unwrap_or_default(); @@ -165,7 +141,7 @@ mod tests { use fabro_types::settings::run::{HookType, McpTransport, TlsMode}; - use super::resolve; + use super::resolve_run_from_file; use crate::parse_settings_layer; #[test] @@ -210,8 +186,8 @@ Authorization = "Bearer {{ env.HOOK_TOKEN }}" ) .expect("settings fixture should parse"); - let resolved = resolve(&settings).expect("settings should resolve"); - let mcps = &resolved.run.agent.mcps; + let resolved = resolve_run_from_file(&settings).expect("run settings should resolve"); + let mcps = &resolved.agent.mcps; assert_eq!( mcps.get("stdio").map(|mcp| &mcp.transport), @@ -246,7 +222,6 @@ Authorization = "Bearer {{ env.HOOK_TOKEN }}" ); let hook = resolved - .run .hooks .iter() .find(|hook| hook.name.as_deref() == Some("notify")) diff --git a/lib/crates/fabro-config/src/resolve/project.rs b/lib/crates/fabro-config/src/resolve/project.rs index dbed59297..1bd5ee0e7 100644 --- a/lib/crates/fabro-config/src/resolve/project.rs +++ b/lib/crates/fabro-config/src/resolve/project.rs @@ -1,9 +1,9 @@ -use fabro_types::settings::project::{ProjectLayer, ProjectSettings}; +use fabro_types::settings::project::{ProjectLayer, ProjectNamespace}; use super::ResolveError; -pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec) -> ProjectSettings { - ProjectSettings { +pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec) -> ProjectNamespace { + ProjectNamespace { name: layer.name.clone(), description: layer.description.clone(), directory: layer diff --git a/lib/crates/fabro-config/src/resolve/run.rs b/lib/crates/fabro-config/src/resolve/run.rs index 353715645..f2505fd23 100644 --- a/lib/crates/fabro-config/src/resolve/run.rs +++ b/lib/crates/fabro-config/src/resolve/run.rs @@ -8,15 +8,15 @@ use fabro_types::settings::run::{ NotificationRouteLayer, NotificationRouteSettings, PullRequestSettings, RunAgentLayer, RunAgentSettings, RunArtifactsLayer, RunCheckpointLayer, RunCheckpointSettings, RunExecutionLayer, RunExecutionSettings, RunGitLayer, RunGitSettings, RunGoal, RunGoalLayer, - RunInterviewsSettings, RunLayer, RunModelLayer, RunModelSettings, RunPrepareLayer, - RunPrepareSettings, RunPullRequestLayer, RunSandboxLayer, RunSandboxSettings, RunScmLayer, - RunScmSettings, RunSettings, ScmGitHubSettings, StringOrSplice, TlsMode, + RunInterviewsSettings, RunLayer, RunModelLayer, RunModelSettings, RunNamespace, + RunPrepareLayer, RunPrepareSettings, RunPullRequestLayer, RunSandboxLayer, RunSandboxSettings, + RunScmLayer, RunScmSettings, ScmGitHubSettings, StringOrSplice, TlsMode, }; use super::ResolveError; -pub fn resolve_run(layer: &RunLayer, errors: &mut Vec) -> RunSettings { - RunSettings { +pub fn resolve_run(layer: &RunLayer, errors: &mut Vec) -> RunNamespace { + RunNamespace { goal: resolve_goal(layer.goal.as_ref()), working_dir: layer.working_dir.clone(), metadata: layer.metadata.clone(), diff --git a/lib/crates/fabro-config/src/resolve/server.rs b/lib/crates/fabro-config/src/resolve/server.rs index 8f8307b25..1ceb10092 100644 --- a/lib/crates/fabro-config/src/resolve/server.rs +++ b/lib/crates/fabro-config/src/resolve/server.rs @@ -6,7 +6,7 @@ use fabro_types::settings::server::{ ServerAuthLayer, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsLayer, ServerIntegrationsSettings, ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings, ServerLayer, ServerListenLayer, - ServerListenSettings, ServerLoggingSettings, ServerSchedulerSettings, ServerSettings, + ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSchedulerSettings, ServerSlateDbLayer, ServerSlateDbSettings, ServerStorageLayer, ServerStorageSettings, ServerWebLayer, ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings, WebhookStrategy, @@ -35,7 +35,7 @@ pub fn dev_token_auth_enabled(layer: &SettingsLayer) -> bool { .is_some_and(|methods| methods.contains(&ServerAuthMethod::DevToken)) } -pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec) -> ServerSettings { +pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec) -> ServerNamespace { let storage = resolve_storage(layer.storage.as_ref()); let listen = resolve_listen(layer.listen.as_ref(), errors); let web = resolve_web(layer.api.as_ref(), layer.web.as_ref()); @@ -46,7 +46,7 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec) -> Se validate_github_webhook_ip_allowlist_for_listen(&listen, &ip_allowlist, &integrations, errors); validate_github_webhook_strategy(&integrations, layer.api.as_ref(), errors); - ServerSettings { + ServerNamespace { listen, api: ServerApiSettings { url: layer.api.as_ref().and_then(|api| api.url.clone()), diff --git a/lib/crates/fabro-config/src/resolve/workflow.rs b/lib/crates/fabro-config/src/resolve/workflow.rs index dee80d1ba..5bb5bc139 100644 --- a/lib/crates/fabro-config/src/resolve/workflow.rs +++ b/lib/crates/fabro-config/src/resolve/workflow.rs @@ -1,12 +1,12 @@ -use fabro_types::settings::workflow::{WorkflowLayer, WorkflowSettings}; +use fabro_types::settings::workflow::{WorkflowLayer, WorkflowNamespace}; use super::ResolveError; pub fn resolve_workflow( layer: &WorkflowLayer, _errors: &mut Vec, -) -> WorkflowSettings { - WorkflowSettings { +) -> WorkflowNamespace { + WorkflowNamespace { name: layer.name.clone(), description: layer.description.clone(), graph: layer diff --git a/lib/crates/fabro-config/tests/defaults.rs b/lib/crates/fabro-config/tests/defaults.rs index ff67dc914..dc5aa78c3 100644 --- a/lib/crates/fabro-config/tests/defaults.rs +++ b/lib/crates/fabro-config/tests/defaults.rs @@ -1,4 +1,7 @@ -use fabro_config::{apply_builtin_defaults, defaults_layer, parse_settings_layer, resolve}; +use fabro_config::{ + apply_builtin_defaults, defaults_layer, parse_settings_layer, resolve_run_from_file, + resolve_server_from_file, resolve_workflow_from_file, +}; use fabro_types::settings::SettingsLayer; use fabro_types::settings::cli::OutputFormat; use fabro_types::settings::run::{ApprovalMode, RunMode, WorktreeMode}; @@ -91,7 +94,8 @@ fn apply_builtin_defaults_materializes_expected_layer() { #[test] fn resolve_empty_settings_requires_explicit_server_auth_methods() { - let errors = resolve(&SettingsLayer::default()).expect_err("empty settings should fail"); + let errors = resolve_server_from_file(&SettingsLayer::default()) + .expect_err("empty server settings should fail"); assert!(errors.iter().any(|error| { matches!( @@ -115,9 +119,10 @@ mode = "dry_run" "#, ); - let settings = resolve(&layer).expect("settings should resolve"); + let workflow = resolve_workflow_from_file(&layer).expect("workflow settings should resolve"); + let run = resolve_run_from_file(&layer).expect("run settings should resolve"); - assert_eq!(settings.run.execution.mode, RunMode::DryRun); - assert_eq!(settings.run.execution.approval, ApprovalMode::Prompt); - assert_eq!(settings.workflow.graph, "workflow.fabro"); + assert_eq!(run.execution.mode, RunMode::DryRun); + assert_eq!(run.execution.approval, ApprovalMode::Prompt); + assert_eq!(workflow.graph, "workflow.fabro"); } diff --git a/lib/crates/fabro-config/tests/resolve_cli.rs b/lib/crates/fabro-config/tests/resolve_cli.rs index e23f0fce8..f6dc06005 100644 --- a/lib/crates/fabro-config/tests/resolve_cli.rs +++ b/lib/crates/fabro-config/tests/resolve_cli.rs @@ -2,6 +2,7 @@ use fabro_config::{parse_settings_layer, resolve_cli_from_file}; use fabro_types::settings::cli::{CliTargetSettings, OutputFormat, OutputVerbosity}; use fabro_types::settings::run::AgentPermissions; use fabro_types::settings::{InterpString, SettingsLayer}; +use temp_env::with_var; #[test] fn resolves_cli_defaults_from_empty_settings() { @@ -17,6 +18,74 @@ fn resolves_cli_defaults_from_empty_settings() { assert!(cli.logging.level.is_none()); } +#[test] +fn user_settings_from_layer_matches_namespace_resolvers() { + let settings: SettingsLayer = parse_settings_layer( + r#" +_version = 1 + +[cli.target] +type = "http" +url = "https://config.example.com" + +[features] +session_sandboxes = true +"#, + ) + .expect("fixture should parse"); + + let user_settings = + fabro_config::UserSettings::from_layer(&settings).expect("user settings should resolve"); + + assert_eq!( + user_settings.cli, + resolve_cli_from_file(&settings).expect("cli namespace should resolve") + ); + assert_eq!( + user_settings.features, + fabro_config::resolve_features_from_file(&settings) + .expect("features namespace should resolve") + ); +} + +#[test] +fn user_settings_resolve_reads_default_settings_from_fabro_home() { + let home = tempfile::tempdir().unwrap(); + std::fs::write( + home.path().join("settings.toml"), + r#" +_version = 1 + +[cli.output] +verbosity = "verbose" + +[features] +session_sandboxes = true +"#, + ) + .unwrap(); + + with_var("FABRO_HOME", Some(home.path()), || { + let user_settings = + fabro_config::UserSettings::resolve().expect("user settings should resolve"); + assert_eq!(user_settings.cli.output.verbosity, OutputVerbosity::Verbose); + assert!(user_settings.features.session_sandboxes); + }); +} + +#[test] +fn user_settings_resolve_returns_defaults_when_default_settings_file_is_missing() { + let home = tempfile::tempdir().unwrap(); + + with_var("FABRO_HOME", Some(home.path()), || { + let user_settings = + fabro_config::UserSettings::resolve().expect("user settings should resolve"); + assert_eq!(user_settings.cli.output.format, OutputFormat::Text); + assert_eq!(user_settings.cli.output.verbosity, OutputVerbosity::Normal); + assert!(!user_settings.features.session_sandboxes); + }); +} + #[test] fn resolves_cli_target_exec_and_output_settings() { let settings: SettingsLayer = parse_settings_layer( diff --git a/lib/crates/fabro-config/tests/resolve_root.rs b/lib/crates/fabro-config/tests/resolve_root.rs index b6176c6ad..d4620053e 100644 --- a/lib/crates/fabro-config/tests/resolve_root.rs +++ b/lib/crates/fabro-config/tests/resolve_root.rs @@ -1,4 +1,3 @@ -use fabro_config::effective_settings::{EffectiveSettingsLayers, EffectiveSettingsMode}; use fabro_config::parse_settings_layer; use fabro_types::settings::{InterpString, SettingsLayer}; @@ -8,8 +7,8 @@ fn parse(source: &str) -> SettingsLayer { #[test] fn resolves_root_settings_require_explicit_server_auth_methods() { - let errors = - fabro_config::resolve(&SettingsLayer::default()).expect_err("empty settings should fail"); + let errors = fabro_config::resolve_server_from_file(&SettingsLayer::default()) + .expect_err("empty server settings should fail"); assert!(errors.iter().any(|error| { matches!( @@ -40,12 +39,20 @@ provider = "not-a-provider" "#, ); - let errors = fabro_config::resolve(&settings).expect_err("invalid shape should fail"); - let rendered = errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n"); + let mut rendered = Vec::new(); + rendered.extend( + fabro_config::resolve_server_from_file(&settings) + .expect_err("invalid server settings should fail") + .into_iter() + .map(|error| error.to_string()), + ); + rendered.extend( + fabro_config::resolve_run_from_file(&settings) + .expect_err("invalid run settings should fail") + .into_iter() + .map(|error| error.to_string()), + ); + let rendered = rendered.join("\n"); assert!(rendered.contains("server.listen.address")); assert!(rendered.contains("server.auth.github.allowed_usernames")); @@ -53,66 +60,45 @@ provider = "not-a-provider" } #[test] -fn load_and_resolve_merges_layers_before_resolution() { - let settings = fabro_config::load_and_resolve( - EffectiveSettingsLayers::new( - SettingsLayer::default(), - parse( - r#" -_version = 1 - -[workflow] -graph = "graphs/workflow.dot" -"#, - ), - parse( - r#" +fn namespace_resolvers_cover_root_level_settings_shape() { + let settings = parse( + r#" _version = 1 [project] directory = ".fabro" -"#, - ), - parse( - r#" -_version = 1 + +[workflow] +graph = "graphs/workflow.dot" [server.storage] root = "/srv/fabro" [server.auth] methods = ["dev-token"] - [run.model] provider = "openai" name = "gpt-5" "#, - ), - ), - None, - EffectiveSettingsMode::LocalOnly, - ) - .expect("layers should load and resolve"); + ); - assert_eq!(settings.project.directory, ".fabro"); - assert_eq!(settings.workflow.graph, "graphs/workflow.dot"); - assert_eq!(settings.server.storage.root.as_source(), "/srv/fabro"); + let project = fabro_config::resolve_project_from_file(&settings) + .expect("project settings should resolve"); + let workflow = fabro_config::resolve_workflow_from_file(&settings) + .expect("workflow settings should resolve"); + let server = + fabro_config::resolve_server_from_file(&settings).expect("server settings should resolve"); + let run = fabro_config::resolve_run_from_file(&settings).expect("run settings should resolve"); + + assert_eq!(project.directory, ".fabro"); + assert_eq!(workflow.graph, "graphs/workflow.dot"); + assert_eq!(server.storage.root.as_source(), "/srv/fabro"); assert_eq!( - settings - .run - .model - .provider - .as_ref() - .map(InterpString::as_source), + run.model.provider.as_ref().map(InterpString::as_source), Some("openai".to_string()) ); assert_eq!( - settings - .run - .model - .name - .as_ref() - .map(InterpString::as_source), + run.model.name.as_ref().map(InterpString::as_source), Some("gpt-5".to_string()) ); } diff --git a/lib/crates/fabro-config/tests/resolve_server.rs b/lib/crates/fabro-config/tests/resolve_server.rs index 1da6f3ed1..11681904d 100644 --- a/lib/crates/fabro-config/tests/resolve_server.rs +++ b/lib/crates/fabro-config/tests/resolve_server.rs @@ -5,6 +5,7 @@ use fabro_types::settings::server::{ }; use fabro_types::settings::{InterpString, SettingsLayer}; use fabro_util::Home; +use temp_env::with_var; fn parse(source: &str) -> SettingsLayer { let mut layer = parse_settings_layer(source).expect("fixture should parse"); @@ -69,6 +70,64 @@ fn resolves_server_defaults_from_empty_settings() { assert!(!settings.slatedb.disk_cache); } +#[test] +fn server_settings_from_layer_matches_namespace_resolvers() { + let settings = parse( + r#" +_version = 1 + +[server.auth] +methods = ["dev-token"] + +[server.storage] +root = "/srv/fabro" + +[features] +session_sandboxes = true +"#, + ); + + let context = + fabro_config::ServerSettings::from_layer(&settings).expect("settings should resolve"); + + assert_eq!( + context.server, + fabro_config::resolve_server_from_file(&settings).expect("server namespace should resolve") + ); + assert_eq!( + context.features, + fabro_config::resolve_features_from_file(&settings) + .expect("features namespace should resolve") + ); +} + +#[test] +fn server_settings_resolve_reads_default_settings_from_fabro_home() { + let home = tempfile::tempdir().unwrap(); + std::fs::write( + home.path().join("settings.toml"), + r#" +_version = 1 + +[server.auth] +methods = ["dev-token"] + +[server.storage] +root = "/srv/from-home" + +[features] +session_sandboxes = true +"#, + ) + .unwrap(); + + with_var("FABRO_HOME", Some(home.path()), || { + let settings = fabro_config::ServerSettings::resolve().expect("settings should resolve"); + assert_eq!(settings.server.storage.root.as_source(), "/srv/from-home"); + assert!(settings.features.session_sandboxes); + }); +} + #[test] fn parsing_rejects_inbound_listener_tls_configuration() { let err = fabro_config::parse_settings_layer( diff --git a/lib/crates/fabro-server/src/auth/cli_flow.rs b/lib/crates/fabro-server/src/auth/cli_flow.rs index 36345cdc2..4b052a40d 100644 --- a/lib/crates/fabro-server/src/auth/cli_flow.rs +++ b/lib/crates/fabro-server/src/auth/cli_flow.rs @@ -1013,6 +1013,7 @@ fn pkce_challenge(verifier: &str) -> String { fn login_allowed(state: &AppState, login: &str) -> bool { state .server_settings() + .server .auth .github .allowed_usernames @@ -1372,7 +1373,6 @@ mod tests { let state = server::create_test_app_state_with_session_key( settings, Some("cli-flow-test-key-material-0123456789"), - false, ); let app = axum::Router::new() .nest("/auth", web_routes()) diff --git a/lib/crates/fabro-server/src/auth/translate.rs b/lib/crates/fabro-server/src/auth/translate.rs index 5c05edb37..2d124f2c5 100644 --- a/lib/crates/fabro-server/src/auth/translate.rs +++ b/lib/crates/fabro-server/src/auth/translate.rs @@ -225,7 +225,6 @@ mod tests { server::create_test_app_state_with_session_key( SettingsLayer::default(), Some(SESSION_SECRET), - false, ) } diff --git a/lib/crates/fabro-server/src/canonical_origin.rs b/lib/crates/fabro-server/src/canonical_origin.rs index fdbb5fec9..05bd7793c 100644 --- a/lib/crates/fabro-server/src/canonical_origin.rs +++ b/lib/crates/fabro-server/src/canonical_origin.rs @@ -1,4 +1,4 @@ -use fabro_types::settings::ServerSettings as ResolvedServerSettings; +use fabro_types::settings::ServerNamespace as ResolvedServerSettings; use url::Url; use crate::server::EnvLookup; diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs index 7ca48a246..d68c6f4b5 100644 --- a/lib/crates/fabro-server/src/demo/mod.rs +++ b/lib/crates/fabro-server/src/demo/mod.rs @@ -24,7 +24,6 @@ use crate::error::ApiError; use crate::jwt_auth::AuthenticatedService; use crate::run_selector::{ResolveRunError, resolve_run_by_selector}; use crate::server::{AppState, PaginationParams}; -use crate::settings_view; fn paginated_response( items: Vec, @@ -602,22 +601,8 @@ pub(crate) async fn list_query_history( pub(crate) async fn get_server_settings( _auth: AuthenticatedService, State(_state): State>, - Query(query): Query, ) -> Response { - match query.view { - settings_view::SettingsApiView::Layer => { - (StatusCode::OK, Json(settings::server_settings())).into_response() - } - settings_view::SettingsApiView::Resolved => { - let mut response = - (StatusCode::OK, Json(settings::resolved_server_settings())).into_response(); - response.headers_mut().insert( - settings_view::RESOLVED_VIEW_HEADER_NAME, - axum::http::HeaderValue::from_static(settings_view::RESOLVED_VIEW_HEADER_VALUE), - ); - response - } - } + (StatusCode::OK, Json(settings::server_settings())).into_response() } // ── System ──────────────────────────────────────────────────────────── @@ -1529,208 +1514,50 @@ mod insights { mod settings { pub(super) fn server_settings() -> serde_json::Value { - // v2 SettingsLayer shape — matches what /api/v1/settings returns in - // production, so the demo renders identically. - serde_json::json!({ - "_version": 1, - "server": { - "storage": { - "root": "/home/fabro/.fabro" - }, - "scheduler": { - "max_concurrent_runs": 10 - }, - "api": { - "url": "https://api.fabro.example.com" - }, - "web": { - "enabled": true, - "url": "https://fabro.example.com" - }, - "auth": { - "api": { - "jwt": { "enabled": true } - }, - "web": { - "allowed_usernames": ["brynary", "alice"], - "providers": { - "github": { - "enabled": true, - "client_id": "Iv1.abc123" - } - } - } - }, - "integrations": { - "github": { - "app_id": "12345", - "client_id": "Iv1.abc123", - "slug": "fabro-dev" - } - } - }, - "run": { - "model": { - "provider": "anthropic", - "name": "claude-sonnet" - }, - "sandbox": { - "provider": "daytona", - "daytona": { - "auto_stop_interval": 60, - "network": "block" - } - } - }, - "features": { - "session_sandboxes": false, - "retros": false - } - }) - } + let settings = fabro_config::parse_settings_layer( + r#" +_version = 1 - pub(super) fn resolved_server_settings() -> serde_json::Value { - serde_json::json!({ - "project": { - "directory": "." - }, - "workflow": { - "graph": "workflow.fabro" - }, - "run": { - "model": { - "provider": "anthropic", - "name": "claude-sonnet", - "fallbacks": [] - }, - "execution": { - "mode": "normal", - "approval": "prompt", - "retros": true - }, - "sandbox": { - "provider": "daytona", - "preserve": false, - "devcontainer": false, - "env": {}, - "local": { - "worktree_mode": "clean" - }, - "daytona": { - "auto_stop_interval": 60, - "labels": {}, - "network": "block", - "skip_clone": false - } - }, - "notifications": {}, - "interviews": {}, - "agent": { - "mcps": {} - }, - "hooks": [], - "scm": {}, - "artifacts": { - "include": [] - }, - "inputs": {}, - "metadata": {}, - "git": {}, - "prepare": { - "commands": [], - "timeout_ms": 300000 - }, - "checkpoint": { - "exclude_globs": [] - } - }, - "cli": { - "auth": {}, - "exec": { - "prevent_idle_sleep": false, - "model": {}, - "agent": { - "mcps": {} - } - }, - "output": { - "format": "text", - "verbosity": "normal" - }, - "updates": { - "check": true - }, - "logging": {} - }, - "server": { - "api": { - "url": "https://api.fabro.example.com" - }, - "web": { - "enabled": true, - "url": "https://fabro.example.com" - }, - "auth": { - "api": { - "jwt": { - "enabled": true - } - }, - "web": { - "allowed_usernames": ["brynary", "alice"], - "providers": { - "github": { - "enabled": true, - "client_id": "Iv1.abc123" - } - } - } - }, - "storage": { - "root": "/home/fabro/.fabro" - }, - "artifacts": { - "prefix": "", - "store": { - "type": "local", - "root": "" - } - }, - "slatedb": { - "prefix": "", - "store": { - "type": "local", - "root": "" - }, - "flush_interval": "0s" - }, - "scheduler": { - "max_concurrent_runs": 10 - }, - "logging": {}, - "integrations": { - "github": { - "enabled": false, - "strategy": "token", - "app_id": "12345", - "client_id": "Iv1.abc123", - "slug": "fabro-dev", - "permissions": {} - }, - "slack": { - "enabled": false - }, - "discord": { - "enabled": false - }, - "teams": { - "enabled": false - } - } - }, - "features": { - "session_sandboxes": false - } - }) +[server.listen] +type = "tcp" +address = "127.0.0.1:32276" + +[server.api] +url = "https://api.fabro.example.com" + +[server.web] +enabled = true +url = "https://fabro.example.com" + +[server.auth] +methods = ["github"] + +[server.auth.github] +allowed_usernames = ["brynary", "alice"] + +[server.storage] +root = "/home/fabro/.fabro" + +[server.scheduler] +max_concurrent_runs = 10 + +[server.integrations.github] +enabled = true +strategy = "app" +app_id = "12345" +client_id = "Iv1.abc123" +slug = "fabro-dev" + +[features] +session_sandboxes = false +"#, + ) + .expect("demo settings fixture should parse"); + + serde_json::to_value( + fabro_config::ServerSettings::from_layer(&settings) + .expect("demo settings fixture should resolve"), + ) + .expect("demo settings should serialize") } } diff --git a/lib/crates/fabro-server/src/diagnostics.rs b/lib/crates/fabro-server/src/diagnostics.rs index 582816a76..926f4c9b3 100644 --- a/lib/crates/fabro-server/src/diagnostics.rs +++ b/lib/crates/fabro-server/src/diagnostics.rs @@ -177,8 +177,8 @@ async fn probe_llm_provider(client: &LlmClient, provider: Provider) -> Result<() async fn check_github_app(state: &AppState) -> CheckResult { let settings = state.server_settings(); - if settings.integrations.github.strategy == GithubIntegrationStrategy::Token { - let token = match state.github_credentials(&settings.integrations.github) { + if settings.server.integrations.github.strategy == GithubIntegrationStrategy::Token { + let token = match state.github_credentials(&settings.server.integrations.github) { Ok(Some(fabro_github::GitHubCredentials::Token(token))) => token, Ok(Some(_)) => unreachable!("token strategy should not return app credentials"), Ok(None) => { @@ -263,19 +263,21 @@ async fn check_github_app(state: &AppState) -> CheckResult { } let app_id = settings + .server .integrations .github .app_id .as_ref() .map(InterpString::as_source); let slug = settings + .server .integrations .github .slug .as_ref() .map(InterpString::as_source); let private_key_raw = state.server_secret("GITHUB_APP_PRIVATE_KEY"); - let client_id = settings.integrations.github.client_id.is_some(); + let client_id = settings.server.integrations.github.client_id.is_some(); let client_secret = state.server_secret("GITHUB_APP_CLIENT_SECRET").is_some(); let webhook_secret = state.server_secret("GITHUB_APP_WEBHOOK_SECRET").is_some(); @@ -504,7 +506,7 @@ fn check_crypto(state: &AppState) -> CheckResult { let mut details = Vec::new(); let mut errors = Vec::new(); - if resolved_server_settings.web.enabled { + if resolved_server_settings.server.web.enabled { match state.server_secret("SESSION_SECRET") { Some(secret) => { if let Err(err) = validate_session_secret(&secret) { @@ -515,7 +517,7 @@ fn check_crypto(state: &AppState) -> CheckResult { } } - let methods = &resolved_server_settings.auth.methods; + let methods = &resolved_server_settings.server.auth.methods; if methods.contains(&ServerAuthMethod::DevToken) { match state.server_secret("FABRO_DEV_TOKEN") { Some(token) if validate_dev_token_format(&token) => {} @@ -525,6 +527,7 @@ fn check_crypto(state: &AppState) -> CheckResult { } if methods.contains(&ServerAuthMethod::Github) { if resolved_server_settings + .server .integrations .github .client_id diff --git a/lib/crates/fabro-server/src/install.rs b/lib/crates/fabro-server/src/install.rs index 13d3ba3ec..ba18f44ac 100644 --- a/lib/crates/fabro-server/src/install.rs +++ b/lib/crates/fabro-server/src/install.rs @@ -13,8 +13,8 @@ use axum::{Json, Router, middleware}; use base64::Engine as _; use base64::engine::general_purpose::{STANDARD as BASE64_STANDARD, URL_SAFE_NO_PAD}; use fabro_auth::{AuthCredential, AuthDetails, credential_id_for}; +use fabro_config::Storage; use fabro_config::bind::{Bind, BindRequest}; -use fabro_config::{Storage, resolve_server_from_file}; use fabro_install::{ InstallListenConfig, PendingSettingsWrite, VaultSecretWrite, generate_jwt_keypair, merge_server_settings, persist_install_outputs_direct, write_github_app_settings, @@ -1372,17 +1372,9 @@ async fn write_artifact_store_metadata( .get_or_insert_with(ServerStorageLayer::default); storage.root = Some(InterpString::parse(&storage_dir.display().to_string())); - let resolved = resolve_server_from_file(&settings).map_err(|errors| { - anyhow::anyhow!( - "failed to resolve server settings:\n{}", - errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n") - ) - })?; - let (object_store, prefix) = serve::build_artifact_object_store(&resolved)?; + let resolved = + fabro_config::ServerSettings::from_layer(&settings).map_err(anyhow::Error::from)?; + let (object_store, prefix) = serve::build_artifact_object_store(&resolved.server)?; let artifact_store = ArtifactStore::new(object_store, prefix); artifact_store.write_metadata(FABRO_VERSION).await?; Ok(()) diff --git a/lib/crates/fabro-server/src/jwt_auth.rs b/lib/crates/fabro-server/src/jwt_auth.rs index 8d3ca7f03..4b2b53d54 100644 --- a/lib/crates/fabro-server/src/jwt_auth.rs +++ b/lib/crates/fabro-server/src/jwt_auth.rs @@ -2,7 +2,7 @@ use anyhow::{Result, anyhow}; use axum::extract::FromRequestParts; use axum::http::header; use axum::http::request::Parts; -use fabro_types::settings::{ServerAuthMethod, ServerSettings as ResolvedServerSettings}; +use fabro_types::settings::{ServerAuthMethod, ServerNamespace as ResolvedServerSettings}; use fabro_types::{IdpIdentity, RunAuthMethod}; use fabro_util::dev_token::validate_dev_token_format; use hmac::{Hmac, Mac}; @@ -563,7 +563,7 @@ methods = [] let errors = resolve_server_from_file(&file).expect_err("empty auth methods should fail"); assert!(errors.iter().any(|err| matches!( err, - fabro_config::resolve::ResolveError::Invalid { path, reason } + fabro_config::ResolveError::Invalid { path, reason } if path == "server.auth.methods" && reason.contains("must not be empty") ))); } diff --git a/lib/crates/fabro-server/src/lib.rs b/lib/crates/fabro-server/src/lib.rs index 92c3d936d..55b6e3db0 100644 --- a/lib/crates/fabro-server/src/lib.rs +++ b/lib/crates/fabro-server/src/lib.rs @@ -31,7 +31,6 @@ pub mod security_headers; pub mod serve; pub mod server; mod server_secrets; -mod settings_view; pub mod static_files; pub mod web_auth; diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs index ded7f88c1..96d5db9b6 100644 --- a/lib/crates/fabro-server/src/run_manifest.rs +++ b/lib/crates/fabro-server/src/run_manifest.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use anyhow::{Result, anyhow, bail}; use fabro_api::types; -use fabro_config::effective_settings::{EffectiveSettingsLayers, EffectiveSettingsMode}; +use fabro_config::effective_settings::EffectiveSettingsLayers; use fabro_config::merge::combine_files; use fabro_config::project::resolve_working_directory; use fabro_config::run::parse_run_config; @@ -23,10 +23,10 @@ use fabro_types::settings::cli::{CliLayer, CliOutputLayer, OutputVerbosity}; use fabro_types::settings::interp::InterpString; use fabro_types::settings::run::{ ApprovalMode, DaytonaDockerfileLayer, DaytonaNetworkLayer, DaytonaSettings, DockerfileSource, - RunExecutionLayer, RunGoalLayer, RunLayer, RunMode, RunModelLayer, RunSandboxLayer, - RunSettings, + RunExecutionLayer, RunGoalLayer, RunLayer, RunMode, RunModelLayer, RunNamespace, + RunSandboxLayer, }; -use fabro_types::settings::{ServerSettings, SettingsLayer}; +use fabro_types::settings::{ServerNamespace, SettingsLayer}; use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus}; use fabro_validate::Severity; use fabro_workflow::Error as WorkflowError; @@ -51,10 +51,9 @@ pub(crate) struct PreparedManifest { pub working_directory: PathBuf, } -pub(crate) fn prepare_manifest_with_mode( +pub(crate) fn prepare_manifest( server_settings: &SettingsLayer, manifest: &types::RunManifest, - local_daemon_mode: bool, ) -> Result { if manifest.version != 1 { bail!("unsupported manifest version {}", manifest.version); @@ -88,11 +87,6 @@ pub(crate) fn prepare_manifest_with_mode( let mut settings = effective_settings::materialize_settings_layer( EffectiveSettingsLayers::new(args_layer, workflow_layer, project_layer, user_layer), Some(server_settings), - if local_daemon_mode { - EffectiveSettingsMode::LocalDaemon - } else { - EffectiveSettingsMode::RemoteServer - }, )?; if let Some(goal) = manifest.goal.as_ref() { let run = settings.run.get_or_insert_with(RunLayer::default); @@ -471,7 +465,7 @@ fn base_preflight_checks(prepared: &PreparedManifest, graph: &Graph) -> Vec Result { +fn resolve_sandbox_provider(settings: &RunNamespace) -> Result { Ok(Some(str::parse::( settings.sandbox.provider.as_str(), )) @@ -480,7 +474,7 @@ fn resolve_sandbox_provider(settings: &RunSettings) -> Result { .unwrap_or_default()) } -fn resolve_daytona_config(settings: &RunSettings) -> Option { +fn resolve_daytona_config(settings: &RunNamespace) -> Option { settings .sandbox .daytona @@ -492,7 +486,7 @@ async fn run_sandbox_check( checks: &mut Vec, sandbox_provider: SandboxProvider, prepared: &PreparedManifest, - resolved_run: &RunSettings, + resolved_run: &RunNamespace, github_app: Option, daytona_api_key: Option, ) -> bool { @@ -567,7 +561,7 @@ async fn run_llm_check( state: &AppState, checks: &mut Vec, graph: &Graph, - settings: &RunSettings, + settings: &RunNamespace, configured_providers: &[Provider], ) -> bool { let (model, provider) = resolve_model_provider(settings, graph, configured_providers); @@ -679,7 +673,7 @@ async fn run_llm_check( } fn resolve_model_provider( - settings: &RunSettings, + settings: &RunNamespace, _graph: &Graph, configured_providers: &[Provider], ) -> (String, Option) { @@ -753,7 +747,7 @@ fn runtime_daytona_config(settings: &DaytonaSettings) -> DaytonaConfig { async fn run_github_token_check( checks: &mut Vec, prepared: &PreparedManifest, - settings: &ServerSettings, + settings: &ServerNamespace, github_app: Option, ) { if settings.integrations.github.permissions.is_empty() { @@ -990,7 +984,7 @@ root = "/srv/fabro" verbose: None, }); - let prepared = prepare_manifest_with_mode(&server_settings, &manifest, false).unwrap(); + let prepared = prepare_manifest(&server_settings, &manifest).unwrap(); assert_eq!( fabro_config::resolve_run_from_file(&prepared.settings) @@ -1002,7 +996,7 @@ root = "/srv/fabro" } #[test] - fn prepare_manifest_local_daemon_prefers_bundled_settings_without_duplication() { + fn prepare_manifest_prefers_bundled_settings_without_duplication() { let server_settings = server_settings_fixture( r#" _version = 1 @@ -1050,7 +1044,7 @@ app_id = "snapshotted-app-id" type_: types::ManifestConfigType::User, }); - let prepared = prepare_manifest_with_mode(&server_settings, &manifest, true).unwrap(); + let prepared = prepare_manifest(&server_settings, &manifest).unwrap(); let resolved_run = fabro_config::resolve_run_from_file(&prepared.settings).unwrap(); let resolved_server = fabro_config::resolve_server_from_file(&prepared.settings).unwrap(); @@ -1075,9 +1069,7 @@ app_id = "snapshotted-app-id" #[tokio::test] async fn invalid_preflight_returns_diagnostics_without_runtime_checks() { let state = crate::server::create_app_state(); - let prepared = - prepare_manifest_with_mode(&default_settings_fixture(), &invalid_manifest(), false) - .unwrap(); + let prepared = prepare_manifest(&default_settings_fixture(), &invalid_manifest()).unwrap(); let validated = validate_prepared_manifest(&prepared).unwrap(); assert!(validated.has_errors()); @@ -1112,8 +1104,7 @@ enabled = true type_: types::ManifestConfigType::Project, }); - let prepared = - prepare_manifest_with_mode(&default_settings_fixture(), &manifest, false).unwrap(); + let prepared = prepare_manifest(&default_settings_fixture(), &manifest).unwrap(); let validated = validate_prepared_manifest(&prepared).unwrap(); assert!(!validated.has_errors()); @@ -1150,8 +1141,7 @@ provider = "daytona" type_: types::ManifestConfigType::Project, }); - let prepared = - prepare_manifest_with_mode(&default_settings_fixture(), &manifest, false).unwrap(); + let prepared = prepare_manifest(&default_settings_fixture(), &manifest).unwrap(); let validated = validate_prepared_manifest(&prepared).unwrap(); let (response, _ok) = run_preflight(state.as_ref(), &prepared, &validated) diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs index 31bcecf5e..49ae4d959 100644 --- a/lib/crates/fabro-server/src/serve.rs +++ b/lib/crates/fabro-server/src/serve.rs @@ -8,14 +8,14 @@ use clap::Args; use fabro_config::bind::{self, Bind, BindRequest}; use fabro_config::merge::combine_files; use fabro_config::user::load_settings_config; -use fabro_config::{Storage, resolve_server_from_file}; +use fabro_config::{ServerSettings as CurrentServerSettings, Storage}; use fabro_sandbox::SandboxProvider; use fabro_types::settings::server::{ GithubIntegrationStrategy, ServerLayer, ServerListenLayer, WebhookStrategy, }; use fabro_types::settings::{ GithubIntegrationSettings, InterpString, ObjectStoreSettings, ServerListenSettings, - ServerSettings as ResolvedServerSettings, SettingsLayer, + ServerNamespace as ResolvedServerSettings, SettingsLayer, }; use fabro_util::terminal::Styles; use object_store::ObjectStore; @@ -350,16 +350,9 @@ fn build_object_store_from_settings( } fn resolve_server_settings(file: &SettingsLayer) -> anyhow::Result { - resolve_server_from_file(file).map_err(|errors| { - anyhow::anyhow!( - "failed to resolve server settings:\n{}", - errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n") - ) - }) + CurrentServerSettings::from_layer(file) + .map(|settings| settings.server) + .map_err(anyhow::Error::from) } pub fn resolve_bind_request_from_settings( @@ -520,7 +513,6 @@ where artifact_store, vault_path, server_env_path, - local_daemon_mode: true, env_lookup, http_client: None, })?; diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index 04896566c..fa97ac90e 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -12,7 +12,7 @@ use axum::body::Body; use axum::body::to_bytes; use axum::extract::{self as axum_extract, DefaultBodyLimit, Path, Query, State}; use axum::http::request::Parts; -use axum::http::{HeaderMap, HeaderValue, Method, StatusCode, header}; +use axum::http::{HeaderMap, Method, StatusCode, header}; use axum::middleware::{self}; use axum::response::sse::{Event, KeepAlive, Sse}; use axum::response::{IntoResponse, Response}; @@ -34,13 +34,13 @@ pub use fabro_api::types::{ RenderWorkflowGraphDirection, RenderWorkflowGraphRequest, RunArtifactEntry, RunArtifactListResponse, RunBilling, RunBillingStage, RunBillingTotals, RunError, RunManifest, RunStage, RunStatusResponse, SandboxFileEntry, SandboxFileListResponse, - SecretType as ApiSecretType, ServerSettings, SshAccessRequest, SshAccessResponse, + SecretType as ApiSecretType, SshAccessRequest, SshAccessResponse, StageStatus as ApiStageStatus, StartRunRequest, SubmitAnswerRequest, SystemFeatures, SystemInfoResponse, SystemRunCounts, WriteBlobResponse, }; use fabro_auth::parse_credential_secret; use fabro_config::daemon::ServerDaemon; -use fabro_config::{Storage, resolve_server_from_file}; +use fabro_config::{ServerSettings as CurrentServerSettings, Storage}; use fabro_interview::{ Answer, ControlInterviewer, Interviewer, Question, QuestionType, WorkerControlEnvelope, }; @@ -67,9 +67,7 @@ use fabro_types::settings::server::{ GithubIntegrationSettings, GithubIntegrationStrategy, ServerAuthLayer, ServerAuthMethod, ServerLayer, }; -use fabro_types::settings::{ - InterpString, ServerSettings as ResolvedServerSettings, SettingsLayer, -}; +use fabro_types::settings::{InterpString, SettingsLayer}; use fabro_types::{ ActorRef, BlockedReason, EventBody, InterviewQuestionRecord, InterviewQuestionType, RunBlobId, RunClientProvenance, RunControlAction, RunEvent, RunId, RunProvenance, RunServerProvenance, @@ -125,9 +123,7 @@ use crate::run_selector::{ResolveRunError, resolve_run_by_selector}; use crate::server_secrets::{ LlmClientResult, ProviderCredentials, ServerSecrets, auth_issue_message, }; -use crate::{ - demo, diagnostics, run_manifest, security_headers, settings_view, static_files, web_auth, -}; +use crate::{demo, diagnostics, run_manifest, security_headers, static_files, web_auth}; pub(crate) type EnvLookup = Arc Option + Send + Sync>; @@ -577,8 +573,7 @@ pub struct AppState { pub(crate) server_secrets: ServerSecrets, pub(crate) provider_credentials: ProviderCredentials, pub(crate) settings: Arc>, - pub(crate) server_settings: RwLock>, - pub(crate) local_daemon_mode: bool, + pub(crate) server_settings: RwLock>, pub(crate) env_lookup: EnvLookup, http_client: Option, shutting_down: AtomicBool, @@ -595,7 +590,6 @@ pub(crate) struct AppStateConfig { pub(crate) artifact_store: ArtifactStore, pub(crate) vault_path: PathBuf, pub(crate) server_env_path: PathBuf, - pub(crate) local_daemon_mode: bool, pub(crate) env_lookup: EnvLookup, pub(crate) http_client: Option, } @@ -644,7 +638,7 @@ fn accumulate_model_billing(entry: &mut ModelBillingTotals, usage: &BilledModelU } impl AppState { - pub(crate) fn server_settings(&self) -> Arc { + pub(crate) fn server_settings(&self) -> Arc { Arc::clone( &self .server_settings @@ -662,7 +656,7 @@ impl AppState { pub(crate) fn server_storage_dir(&self) -> PathBuf { PathBuf::from( - resolve_interp_string(&self.server_settings().storage.root) + resolve_interp_string(&self.server_settings().server.storage.root) .expect("server storage root should be resolved at startup"), ) } @@ -704,7 +698,7 @@ impl AppState { } pub(crate) fn canonical_origin(&self) -> Result { - resolve_canonical_origin(&self.server_settings(), &self.env_lookup) + resolve_canonical_origin(&self.server_settings().server, &self.env_lookup) } pub(crate) fn session_key(&self) -> Option { @@ -786,17 +780,8 @@ impl AppState { } pub(crate) fn replace_settings(&self, settings: SettingsLayer) -> anyhow::Result<()> { - let resolved = Arc::new(resolve_server_from_file(&settings).map_err(|errors| { - anyhow::anyhow!( - "failed to resolve server settings:\n{}", - errors - .into_iter() - .map(|error| error.to_string()) - .collect::>() - .join("\n") - ) - })?); - resolve_canonical_origin(&resolved, &self.env_lookup).map_err(anyhow::Error::msg)?; + let resolved = Arc::new(CurrentServerSettings::from_layer(&settings)?); + resolve_canonical_origin(&resolved.server, &self.env_lookup).map_err(anyhow::Error::msg)?; *self.settings.write().expect("settings lock poisoned") = settings; *self @@ -1317,53 +1302,12 @@ async fn health() -> Response { async fn get_server_settings( _auth: AuthenticatedService, State(state): State>, - Query(query): Query, ) -> Response { - let settings = state.settings.read().unwrap().clone(); - match query.view { - settings_view::SettingsApiView::Layer => { - let redacted = settings_view::redact_for_api(&settings); - let mut value = match serde_json::to_value(&redacted) { - Ok(value) => value, - Err(err) => { - return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) - .into_response(); - } - }; - strip_nulls(&mut value); - (StatusCode::OK, Json(value)).into_response() - } - settings_view::SettingsApiView::Resolved => { - let resolved = match fabro_config::resolve(&settings) { - Ok(settings) => settings, - Err(err) => { - return ApiError::new( - StatusCode::INTERNAL_SERVER_ERROR, - format!("failed to resolve settings: {err:?}"), - ) - .into_response(); - } - }; - let mut value = match settings_view::redact_resolved_value(&resolved) { - Ok(value) => value, - Err(err) => { - return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) - .into_response(); - } - }; - strip_nulls(&mut value); - let mut response = (StatusCode::OK, Json(value)).into_response(); - response.headers_mut().insert( - settings_view::RESOLVED_VIEW_HEADER_NAME, - HeaderValue::from_static(settings_view::RESOLVED_VIEW_HEADER_VALUE), - ); - response - } - } -} - -fn strip_nulls(value: &mut serde_json::Value) { - settings_view::strip_nulls(value); + ( + StatusCode::OK, + Json(state.server_settings().as_ref().clone()), + ) + .into_response() } async fn get_system_info( @@ -1711,18 +1655,10 @@ fn system_sandbox_provider(settings: &SettingsLayer) -> String { ) } -fn render_resolve_errors(errors: &[fabro_config::ResolveError]) -> String { - errors - .iter() - .map(ToString::to_string) - .collect::>() - .join("; ") -} - fn resolved_storage_dir(settings: &SettingsLayer) -> Result { - let resolved = - resolve_server_from_file(settings).map_err(|errors| render_resolve_errors(&errors))?; + let resolved = CurrentServerSettings::from_layer(settings).map_err(|err| err.to_string())?; resolved + .server .storage .root .resolve(|name| std::env::var(name).ok()) @@ -1730,15 +1666,14 @@ fn resolved_storage_dir(settings: &SettingsLayer) -> Result { .map_err(|err| { format!( "failed to resolve {}: {err}", - resolved.storage.root.as_source() + resolved.server.storage.root.as_source() ) }) } fn resolved_github_settings(settings: &SettingsLayer) -> Result { - let resolved = - resolve_server_from_file(settings).map_err(|errors| render_resolve_errors(&errors))?; - Ok(resolved.integrations.github) + let resolved = CurrentServerSettings::from_layer(settings).map_err(|err| err.to_string())?; + Ok(resolved.server.integrations.github) } fn parse_system_duration(raw: &str) -> anyhow::Result { @@ -1958,7 +1893,7 @@ async fn get_github_repo( return response; } let settings = state.server_settings(); - let github_settings = &settings.integrations.github; + let github_settings = &settings.server.integrations.github; let base_url = fabro_github::github_api_base_url(); let mut client: Option = None; let token = match github_settings.strategy { @@ -2514,7 +2449,6 @@ pub fn create_app_state_with_env_lookup( pub(crate) fn create_test_app_state_with_session_key( settings: SettingsLayer, session_secret: Option<&str>, - local_daemon_mode: bool, ) -> Arc { let vault_path = test_secret_store_path(); let server_env_path = vault_path @@ -2540,7 +2474,6 @@ pub(crate) fn create_test_app_state_with_session_key( artifact_store, vault_path, server_env_path, - local_daemon_mode, env_lookup, http_client: Some(fabro_http::test_http_client().expect("test HTTP client should build")), }) @@ -2576,7 +2509,6 @@ fn default_test_app_state_config( artifact_store, vault_path, server_env_path, - local_daemon_mode: false, env_lookup, http_client: Some(fabro_http::test_http_client().expect("test HTTP client should build")), } @@ -2641,7 +2573,6 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result anyhow::Result>() - .join("\n") - ) - })?) + Arc::new(CurrentServerSettings::from_layer(&settings)?) }; let slack_service = { - resolved_server_settings + current_server_settings + .server .integrations .slack .default_channel @@ -2707,8 +2630,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result req, Err(err) => return ApiError::bad_request(err.to_string()).into_response(), }; - let prepared = match run_manifest::prepare_manifest_with_mode( - &state.settings.read().unwrap(), - &req, - state.local_daemon_mode, - ) { + let prepared = match run_manifest::prepare_manifest(&state.settings.read().unwrap(), &req) { Ok(prepared) => prepared, Err(err) => return ApiError::bad_request(err.to_string()).into_response(), }; @@ -4189,11 +4108,7 @@ async fn run_preflight( State(state): State>, Json(req): Json, ) -> Response { - let prepared = match run_manifest::prepare_manifest_with_mode( - &state.settings.read().unwrap(), - &req, - state.local_daemon_mode, - ) { + let prepared = match run_manifest::prepare_manifest(&state.settings.read().unwrap(), &req) { Ok(prepared) => prepared, Err(err) => return ApiError::bad_request(err.to_string()).into_response(), }; @@ -4219,14 +4134,11 @@ async fn render_graph_from_manifest( State(state): State>, Json(req): Json, ) -> Response { - let prepared = match run_manifest::prepare_manifest_with_mode( - &state.settings.read().unwrap(), - &req.manifest, - state.local_daemon_mode, - ) { - Ok(prepared) => prepared, - Err(err) => return ApiError::bad_request(err.to_string()).into_response(), - }; + let prepared = + match run_manifest::prepare_manifest(&state.settings.read().unwrap(), &req.manifest) { + Ok(prepared) => prepared, + Err(err) => return ApiError::bad_request(err.to_string()).into_response(), + }; let validated = match run_manifest::validate_prepared_manifest(&prepared) { Ok(validated) => validated, Err(err) => return ApiError::bad_request(err.to_string()).into_response(), @@ -5043,16 +4955,7 @@ async fn get_run_settings( let Some(run_spec) = run_state.spec else { return ApiError::not_found("Run not found.").into_response(); }; - let redacted = settings_view::redact_for_api(&run_spec.settings); - let mut value = match serde_json::to_value(&redacted) { - Ok(value) => value, - Err(err) => { - return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) - .into_response(); - } - }; - strip_nulls(&mut value); - (StatusCode::OK, Json(value)).into_response() + (StatusCode::OK, Json(run_spec.settings)).into_response() } async fn get_questions( @@ -7467,34 +7370,6 @@ url = "{url}" .expect("settings fixture should parse") } - #[tokio::test] - async fn resolved_settings_view_returns_internal_error_when_runtime_settings_stop_resolving() { - let state = create_app_state(); - *state.settings.write().unwrap() = fabro_config::parse_settings_layer( - r#" -_version = 1 - -[cli.target] -type = "http" -"#, - ) - .expect("settings fixture should parse"); - let app = build_router(state, AuthMode::Disabled); - - let response = app - .oneshot( - Request::builder() - .method("GET") - .uri(api("/settings?view=resolved")) - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_status!(response, StatusCode::INTERNAL_SERVER_ERROR).await; - } - #[test] fn replace_settings_rejects_invalid_canonical_origin_and_keeps_previous_settings() { for invalid in ["", "/relative/path", "ftp://fabro.example.com"] { @@ -8283,7 +8158,6 @@ slug = "fabro" create_test_app_state_with_session_key( settings, Some("github-redirect-test-key-0123456789"), - false, ), AuthMode::Enabled(ConfiguredAuth { methods: vec![ServerAuthMethod::Github], @@ -8795,7 +8669,6 @@ slug = "fabro" let state = create_test_app_state_with_session_key( SettingsLayer::default(), Some("server-test-session-key-0123456789"), - false, ); let app = build_router( Arc::clone(&state), diff --git a/lib/crates/fabro-server/src/settings_view.rs b/lib/crates/fabro-server/src/settings_view.rs deleted file mode 100644 index fe2640c53..000000000 --- a/lib/crates/fabro-server/src/settings_view.rs +++ /dev/null @@ -1,275 +0,0 @@ -//! Outward-facing view of [`SettingsLayer`] for API responses. -//! -//! `/api/v1/settings` and `/api/v1/runs/:id/settings` return the server's v2 -//! [`SettingsLayer`] directly as JSON so authenticated clients (the `fabro -//! settings` CLI, the web UI) can see the effective configuration. Before -//! serialization, this module drops the handful of fields that would leak -//! host-specific filesystem or network layout. -//! -//! ## What gets dropped -//! -//! Per the requirements doc, only the transport bind needs redaction now: -//! -//! - `server.listen` — the whole subtree. Bind addresses and socket paths -//! reveal network topology and host filesystem layout. -//! -//! ## Why that's all -//! -//! The rest of the v2 tree is either: -//! -//! - A literal non-secret value (storage root, scheduler limit, integration -//! slug, feature flag), OR -//! - An [`InterpString`] containing `{{ env.NAME }}` tokens. `InterpString`'s -//! default serialization preserves the *unresolved* template form, so the -//! wire payload surfaces `"Bearer {{ env.TOKEN }}"` instead of the resolved -//! secret value. No additional redaction pass is needed. -//! -//! Any future field that carries a raw secret in-band (without env -//! interpolation) must be added to the drop list below. - -use fabro_types::settings::{Settings, SettingsLayer}; -use serde::Deserialize; - -pub(crate) const RESOLVED_VIEW_HEADER_NAME: &str = "X-Fabro-Settings-View"; -pub(crate) const RESOLVED_VIEW_HEADER_VALUE: &str = "resolved"; - -const REDACTED_PATHS: &[&[&str]] = &[&["server", "listen"]]; - -#[derive(Debug, Clone, Copy, Default, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "lowercase")] -pub(crate) enum SettingsApiView { - #[default] - Layer, - Resolved, -} - -#[derive(Debug, Clone, Copy, Default, Deserialize)] -pub(crate) struct SettingsQuery { - #[serde(default)] - pub(crate) view: SettingsApiView, -} - -/// Build a redacted clone of `settings` safe to serialize outward. -/// -/// See the module docs for the drop-list rationale. -#[must_use] -pub(crate) fn redact_for_api(settings: &SettingsLayer) -> SettingsLayer { - let mut out = settings.clone(); - - if let Some(server) = out.server.as_mut() { - server.listen = None; - } - - out -} - -pub(crate) fn redact_resolved_value(settings: &Settings) -> serde_json::Result { - let mut value = serde_json::to_value(settings)?; - redact_value_paths(&mut value); - Ok(value) -} - -pub(crate) fn strip_nulls(value: &mut serde_json::Value) { - match value { - serde_json::Value::Object(map) => { - for child in map.values_mut() { - strip_nulls(child); - } - map.retain(|_, child| !child.is_null()); - } - serde_json::Value::Array(values) => { - for child in values { - strip_nulls(child); - } - } - _ => {} - } -} - -fn redact_value_paths(value: &mut serde_json::Value) { - for path in REDACTED_PATHS { - remove_path(value, path); - } -} - -fn remove_path(value: &mut serde_json::Value, path: &[&str]) { - let Some((head, tail)) = path.split_first() else { - return; - }; - - let Some(object) = value.as_object_mut() else { - return; - }; - - if tail.is_empty() { - object.remove(*head); - return; - } - - if let Some(child) = object.get_mut(*head) { - remove_path(child, tail); - } -} - -#[cfg(test)] -mod tests { - use fabro_config::parse_settings_layer; - - use super::*; - - fn parse(source: &str) -> SettingsLayer { - parse_settings_layer(source).expect("fixture should parse") - } - - #[test] - fn drops_server_listen_entirely() { - let settings = parse( - r#" -_version = 1 - -[server.listen] -type = "tcp" -address = "127.0.0.1:32276" -"#, - ); - let redacted = redact_for_api(&settings); - assert!(redacted.server.unwrap().listen.is_none()); - } - - #[test] - fn preserves_run_cli_project_and_features() { - let settings = parse( - r#" -_version = 1 - -[project] -name = "Fabro" - -[run] -goal = "ship it" - -[run.model] -provider = "anthropic" -name = "sonnet" - -[cli.output] -verbosity = "verbose" - -[features] -session_sandboxes = true - -[server.scheduler] -max_concurrent_runs = 9 - -[server.auth] -methods = ["dev-token", "github"] - -[server.auth.github] -allowed_usernames = ["alice"] - -[server.storage] -root = "/srv/fabro" - -[server.integrations.github] -app_id = "12345" -client_id = "Iv1.abcdef" -slug = "fabro-app" -"#, - ); - let redacted = redact_for_api(&settings); - assert!(redacted.project.is_some()); - let run = redacted.run.unwrap(); - assert!(run.goal.is_some()); - assert!(run.model.is_some()); - assert!(redacted.cli.is_some()); - assert!(redacted.features.is_some()); - let server = redacted.server.unwrap(); - assert_eq!( - server.scheduler.and_then(|s| s.max_concurrent_runs), - Some(9) - ); - assert!(server.storage.is_some()); - let github = server.integrations.unwrap().github.unwrap(); - assert!(github.app_id.is_some()); - assert!(github.client_id.is_some()); - assert!(github.slug.is_some()); - let auth = server.auth.unwrap(); - assert_eq!(auth.methods.unwrap().len(), 2); - assert_eq!(auth.github.unwrap().allowed_usernames, vec!["alice"]); - } - - #[test] - fn preserves_env_templates_for_non_redacted_fields() { - let settings = parse( - r#" -_version = 1 - -[server.storage] -root = "{{ env.FABRO_STORAGE_ROOT }}" - -[server.integrations.slack] -default_channel = "{{ env.SLACK_CHANNEL }}" -"#, - ); - - let redacted = redact_for_api(&settings); - let server = redacted - .server - .expect("server config should remain present"); - assert_eq!( - server - .storage - .and_then(|storage| storage.root) - .map(|value| value.as_source()), - Some("{{ env.FABRO_STORAGE_ROOT }}".to_string()) - ); - assert_eq!( - server - .integrations - .and_then(|integrations| integrations.slack) - .and_then(|slack| slack.default_channel) - .map(|value| value.as_source()), - Some("{{ env.SLACK_CHANNEL }}".to_string()) - ); - } - - #[test] - fn redacts_dense_resolved_settings_with_the_same_secret_paths() { - let settings = parse( - r#" -_version = 1 - -[server.listen] -type = "tcp" -address = "127.0.0.1:32276" - -[server.auth] -methods = ["github", "dev-token"] - -[server.auth.github] -allowed_usernames = ["alice"] - -[server.storage] -root = "{{ env.FABRO_STORAGE_ROOT }}" -"#, - ); - - let resolved = fabro_config::resolve(&settings).expect("settings should resolve"); - let mut redacted = - redact_resolved_value(&resolved).expect("resolved settings should serialize"); - - assert!(redacted["server"].get("listen").is_none()); - assert_eq!(redacted["server"]["auth"]["methods"][0], "github"); - assert_eq!( - redacted["server"]["auth"]["github"]["allowed_usernames"][0], - "alice" - ); - assert_eq!( - redacted["server"]["storage"]["root"], - "{{ env.FABRO_STORAGE_ROOT }}" - ); - - strip_nulls(&mut redacted); - assert!(redacted["server"].get("listen").is_none()); - } -} diff --git a/lib/crates/fabro-server/src/web_auth.rs b/lib/crates/fabro-server/src/web_auth.rs index d061915d8..410e29bd0 100644 --- a/lib/crates/fabro-server/src/web_auth.rs +++ b/lib/crates/fabro-server/src/web_auth.rs @@ -290,6 +290,7 @@ fn session_provider(auth_method: RunAuthMethod) -> &'static str { fn session_cookie_secure(state: &AppState) -> bool { state .server_settings() + .server .web .url .resolve(|name| std::env::var(name).ok()) @@ -374,7 +375,7 @@ async fn login_github( ); }; let settings = state.server_settings(); - let Some(client_id) = settings.integrations.github.client_id.as_ref() else { + let Some(client_id) = settings.server.integrations.github.client_id.as_ref() else { warn!("OAuth login failed: client_id not configured"); return json_response( StatusCode::CONFLICT, @@ -516,7 +517,7 @@ async fn callback_github( .as_deref() .expect("validated oauth callback state should exist"); - let Some(client_id) = settings.integrations.github.client_id.as_ref() else { + let Some(client_id) = settings.server.integrations.github.client_id.as_ref() else { error!("OAuth callback failed: client_id not configured"); return json_response( StatusCode::CONFLICT, @@ -686,7 +687,7 @@ async fn callback_github( _ => Vec::new(), }; - let allowed_usernames = settings.auth.github.allowed_usernames.clone(); + let allowed_usernames = settings.server.auth.github.allowed_usernames.clone(); if !allowed_usernames.iter().any(|user| user == &profile.login) { warn!(login = %profile.login, "OAuth callback denied: username not in allowlist"); return callback_error_redirect( @@ -909,7 +910,6 @@ mod tests { let state = server::create_test_app_state_with_session_key( settings, Some("web-auth-test-key-material-0123456789"), - false, ); let middleware_state = state.clone(); axum::Router::new() @@ -1105,7 +1105,6 @@ mod tests { let state = server::create_test_app_state_with_session_key( github_settings("https://fabro.example"), Some("web-auth-test-key-material-0123456789"), - false, ); let app = server::build_router_with_options( state, @@ -1198,7 +1197,6 @@ mod tests { let state = server::create_test_app_state_with_session_key( github_settings("https://fabro.example"), Some("web-auth-test-key-material-0123456789"), - false, ); let app = crate::server::build_router_with_options( state, diff --git a/lib/crates/fabro-server/tests/it/api/runs.rs b/lib/crates/fabro-server/tests/it/api/runs.rs index 061b87e80..758c69d07 100644 --- a/lib/crates/fabro-server/tests/it/api/runs.rs +++ b/lib/crates/fabro-server/tests/it/api/runs.rs @@ -3,7 +3,6 @@ use axum::http::{Request, StatusCode}; use fabro_config::parse_settings_layer; use fabro_server::jwt_auth::AuthMode; use fabro_server::server::build_router; -use serde_json::json; use tower::ServiceExt; use crate::helpers::{ @@ -11,7 +10,7 @@ use crate::helpers::{ }; #[tokio::test] -async fn retrieve_run_settings_preserves_templates_and_redacts_sensitive_fields() { +async fn retrieve_run_settings_returns_persisted_layer_without_redaction() { let storage_dir = tempfile::tempdir().unwrap(); let settings = parse_settings_layer(&format!( r#" @@ -97,21 +96,11 @@ session_sandboxes = true storage_dir.path().display().to_string() ); assert_eq!(body["server"]["scheduler"]["max_concurrent_runs"], 9); - assert_eq!( - body["server"]["integrations"]["github"]["app_id"], - "{{ env.GITHUB_APP_ID }}" - ); - assert_eq!( - body["server"]["integrations"]["github"]["client_id"], - "Iv1.github" - ); - assert_eq!( - body["server"]["auth"]["methods"], - json!(["dev-token", "github"]) - ); - assert_eq!( - body["server"]["auth"]["github"]["allowed_usernames"], - json!(["alice"]) + assert!(body.pointer("/server/integrations/github/app_id").is_none()); + assert!( + body.pointer("/server/integrations/github/client_id") + .is_none() ); + assert!(body.pointer("/server/auth").is_none()); assert!(body.pointer("/server/listen").is_none()); } diff --git a/lib/crates/fabro-server/tests/it/api/settings.rs b/lib/crates/fabro-server/tests/it/api/settings.rs index 5c892313f..1495b43da 100644 --- a/lib/crates/fabro-server/tests/it/api/settings.rs +++ b/lib/crates/fabro-server/tests/it/api/settings.rs @@ -4,13 +4,12 @@ use fabro_config::parse_settings_layer; use fabro_server::jwt_auth::AuthMode; use fabro_server::server::{build_router, create_app_state_with_options}; use fabro_types::settings::SettingsLayer; -use serde_json::json; use tower::ServiceExt; -use crate::helpers::{body_json, checked_response, response_json}; +use crate::helpers::response_json; #[tokio::test] -async fn retrieve_server_settings_default_view_returns_redacted_layer_settings() { +async fn retrieve_server_settings_returns_dense_server_settings_from_app_state() { let settings: SettingsLayer = parse_settings_layer( r#" _version = 1 @@ -25,9 +24,6 @@ root = "/srv/fabro" [server.scheduler] max_concurrent_runs = 9 -[cli.output] -verbosity = "verbose" - [server.auth] methods = ["dev-token", "github"] @@ -36,9 +32,6 @@ allowed_usernames = ["alice"] [server.integrations.github] client_id = "Iv1.abcdef" - -[run.inputs] -server_only = "1" "#, ) .expect("settings fixture should parse"); @@ -55,102 +48,28 @@ server_only = "1" let response = app.oneshot(request).await.unwrap(); let body = response_json(response, StatusCode::OK, "GET /api/v1/settings").await; - assert_eq!(body["_version"], 1); + let top_level = body + .as_object() + .expect("server settings response should be an object"); + assert_eq!(top_level.len(), 2); + assert!(top_level.contains_key("server")); + assert!(top_level.contains_key("features")); + + assert_eq!(body["server"]["listen"]["type"], "tcp"); + assert_eq!(body["server"]["listen"]["address"], "127.0.0.1:32276"); assert_eq!(body["server"]["storage"]["root"], "/srv/fabro"); assert_eq!(body["server"]["scheduler"]["max_concurrent_runs"], 9); - assert_eq!(body["cli"]["output"]["verbosity"], "verbose"); - assert_eq!(body["run"]["inputs"]["server_only"], "1"); - assert!(body["server"].get("listen").is_none()); + assert_eq!(body["server"]["auth"]["methods"][0], "dev-token"); + assert_eq!(body["server"]["auth"]["methods"][1], "github"); assert_eq!( - body["server"]["auth"]["methods"], - json!(["dev-token", "github"]) - ); - assert_eq!( - body["server"]["auth"]["github"]["allowed_usernames"], - json!(["alice"]) - ); - assert_eq!( - body["server"]["integrations"]["github"]["client_id"], - "Iv1.abcdef" - ); -} - -#[tokio::test] -async fn retrieve_server_settings_resolved_view_returns_dense_settings_and_marker() { - let settings: SettingsLayer = parse_settings_layer( - r#" -_version = 1 - -[server.listen] -type = "tcp" -address = "127.0.0.1:32276" - -[server.storage] -root = "/srv/fabro" - -[server.auth] -methods = ["dev-token", "github"] - -[server.auth.github] -allowed_usernames = ["alice"] - -[server.integrations.github] -client_id = "Iv1.abcdef" - -[run.model] -provider = "openai" -name = "server-model" - -[run.inputs] -server_only = "1" -"#, - ) - .expect("settings fixture should parse"); - let app = build_router( - create_app_state_with_options(settings, 5), - AuthMode::Disabled, - ); - - let request = Request::builder() - .method("GET") - .uri("/api/v1/settings?view=resolved") - .body(Body::empty()) - .unwrap(); - let response = app.oneshot(request).await.unwrap(); - - let response = checked_response( - response, - StatusCode::OK, - "GET /api/v1/settings?view=resolved", - ) - .await; - assert_eq!( - response - .headers() - .get("x-fabro-settings-view") - .and_then(|value| value.to_str().ok()), - Some("resolved") - ); - let body = body_json(response.into_body()).await; - assert!(body.get("_version").is_none()); - assert_eq!(body["project"]["directory"], "."); - assert_eq!(body["workflow"]["graph"], "workflow.fabro"); - assert_eq!(body["run"]["execution"]["approval"], "prompt"); - assert_eq!(body["run"]["model"]["provider"], "openai"); - assert_eq!(body["run"]["model"]["name"], "server-model"); - assert_eq!(body["run"]["inputs"]["server_only"], "1"); - assert_eq!(body["server"]["storage"]["root"], "/srv/fabro"); - assert!(body["server"].get("listen").is_none()); - assert_eq!( - body["server"]["auth"]["methods"], - json!(["dev-token", "github"]) - ); - assert_eq!( - body["server"]["auth"]["github"]["allowed_usernames"], - json!(["alice"]) + body["server"]["auth"]["github"]["allowed_usernames"][0], + "alice" ); assert_eq!( body["server"]["integrations"]["github"]["client_id"], "Iv1.abcdef" ); + assert_eq!(body["features"]["session_sandboxes"], false); + assert!(body.get("cli").is_none()); + assert!(body.get("run").is_none()); } diff --git a/lib/crates/fabro-types/src/settings/cli.rs b/lib/crates/fabro-types/src/settings/cli.rs index def190005..fb38d1bef 100644 --- a/lib/crates/fabro-types/src/settings/cli.rs +++ b/lib/crates/fabro-types/src/settings/cli.rs @@ -14,7 +14,7 @@ use super::run::{AgentPermissions, McpEntryLayer, McpServerSettings}; /// A structurally resolved `[cli]` view for consumers. #[derive(Debug, Clone, Default, PartialEq, Serialize)] -pub struct CliSettings { +pub struct CliNamespace { pub target: Option, pub auth: CliAuthSettings, pub exec: CliExecSettings, diff --git a/lib/crates/fabro-types/src/settings/features.rs b/lib/crates/fabro-types/src/settings/features.rs index 2ed227d00..79dfc9f59 100644 --- a/lib/crates/fabro-types/src/settings/features.rs +++ b/lib/crates/fabro-types/src/settings/features.rs @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; /// A structurally resolved `[features]` view for consumers. -#[derive(Debug, Clone, Default, PartialEq, Serialize)] -pub struct FeaturesSettings { +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct FeaturesNamespace { pub session_sandboxes: bool, } diff --git a/lib/crates/fabro-types/src/settings/mod.rs b/lib/crates/fabro-types/src/settings/mod.rs index e2851b37d..6b6638089 100644 --- a/lib/crates/fabro-types/src/settings/mod.rs +++ b/lib/crates/fabro-types/src/settings/mod.rs @@ -16,7 +16,6 @@ pub mod interp; pub mod layer; pub mod model_ref; pub mod project; -pub mod resolved; pub mod run; pub mod server; pub mod size; @@ -25,24 +24,23 @@ pub mod workflow; pub use cli::{ CliAuthSettings, CliExecAgentSettings, CliExecModelSettings, CliExecSettings, CliLayer, - CliLoggingSettings, CliOutputSettings, CliSettings, CliTargetSettings, CliUpdatesSettings, + CliLoggingSettings, CliNamespace, CliOutputSettings, CliTargetSettings, CliUpdatesSettings, }; pub use duration::{Duration, ParseDurationError}; -pub use features::{FeaturesLayer, FeaturesSettings}; +pub use features::{FeaturesLayer, FeaturesNamespace}; pub use interp::{InterpString, Provenance, ResolveEnvError, Resolved}; pub use layer::SettingsLayer; pub use model_ref::{ AmbiguousModelRef, ModelRef, ModelRegistry, ParseModelRefError, ResolvedModelRef, }; -pub use project::{ProjectLayer, ProjectSettings}; -pub use resolved::Settings; +pub use project::{ProjectLayer, ProjectNamespace}; pub use run::{ ArtifactsSettings, DaytonaSettings, DaytonaSnapshotSettings, DockerfileSource, GitAuthorSettings, HookDefinition, HookType, InterviewProviderSettings, McpServerSettings, McpTransport, NotificationProviderSettings, NotificationRouteSettings, PullRequestSettings, RunAgentSettings, RunCheckpointSettings, RunExecutionSettings, RunGitSettings, RunGoal, - RunInterviewsSettings, RunLayer, RunModelSettings, RunPrepareSettings, RunSandboxSettings, - RunScmSettings, RunSettings, ScmGitHubSettings, TlsMode, + RunInterviewsSettings, RunLayer, RunModelSettings, RunNamespace, RunPrepareSettings, + RunSandboxSettings, RunScmSettings, ScmGitHubSettings, TlsMode, }; pub use server::{ DiscordIntegrationSettings, GithubIntegrationSettings, IntegrationWebhooksSettings, @@ -50,9 +48,9 @@ pub use server::{ ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings, ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings, ServerLayer, ServerListenSettings, ServerLoggingSettings, - ServerSchedulerSettings, ServerSettings, ServerSlateDbSettings, ServerStorageSettings, + ServerNamespace, ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings, }; pub use size::{ParseSizeError, Size}; pub use splice_array::{SPLICE_MARKER, SpliceArray, SpliceArrayError}; -pub use workflow::{WorkflowLayer, WorkflowSettings}; +pub use workflow::{WorkflowLayer, WorkflowNamespace}; diff --git a/lib/crates/fabro-types/src/settings/project.rs b/lib/crates/fabro-types/src/settings/project.rs index dcdd529bb..e62757aed 100644 --- a/lib/crates/fabro-types/src/settings/project.rs +++ b/lib/crates/fabro-types/src/settings/project.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; /// A structurally resolved `[project]` view for consumers. #[derive(Debug, Clone, Default, PartialEq, Serialize)] -pub struct ProjectSettings { +pub struct ProjectNamespace { pub name: Option, pub description: Option, pub directory: String, diff --git a/lib/crates/fabro-types/src/settings/resolved.rs b/lib/crates/fabro-types/src/settings/resolved.rs deleted file mode 100644 index df7be72f9..000000000 --- a/lib/crates/fabro-types/src/settings/resolved.rs +++ /dev/null @@ -1,179 +0,0 @@ -use serde::Serialize; - -use super::{ - CliSettings, FeaturesSettings, ProjectSettings, RunSettings, ServerSettings, WorkflowSettings, -}; - -/// A fully resolved settings view across all namespaces. -/// -/// `Default` is intentionally not derived: a default `Settings` value would -/// contain empty `server.auth.methods`, which the resolver rejects. Construct -/// real values via `fabro_config::resolve` (production), or -/// `Settings::test_default()` behind the `test-support` feature (tests). -#[derive(Debug, Clone, PartialEq, Serialize)] -pub struct Settings { - pub project: ProjectSettings, - pub workflow: WorkflowSettings, - pub run: RunSettings, - pub cli: CliSettings, - pub server: ServerSettings, - pub features: FeaturesSettings, -} - -#[cfg(any(test, feature = "test-support"))] -impl Settings { - /// A trivial `Settings` value suitable for serialization or destructuring - /// tests. Server auth methods are empty (would not pass `resolve`); - /// use this only when the resolver is not in play. - #[must_use] - pub fn test_default() -> Self { - Self { - project: ProjectSettings::default(), - workflow: WorkflowSettings::default(), - run: RunSettings::default(), - cli: CliSettings::default(), - server: ServerSettings::test_default(), - features: FeaturesSettings::default(), - } - } -} - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - use std::time::Duration as StdDuration; - - use serde_json::json; - - use super::Settings; - use crate::settings::cli::CliTargetSettings; - use crate::settings::interp::InterpString; - use crate::settings::run::{ - DockerfileSource, McpServerSettings, McpTransport, RunAgentSettings, RunGoal, RunSettings, - }; - use crate::settings::server::{ - ObjectStoreSettings, ServerListenSettings, ServerSettings, ServerSlateDbSettings, - }; - - #[test] - fn settings_serializes_successfully() { - serde_json::to_value(Settings::test_default()).expect("resolved settings should serialize"); - } - - #[test] - fn resolved_enums_use_human_readable_tagged_shapes() { - assert_eq!( - serde_json::to_value(CliTargetSettings::Http { - url: InterpString::parse("https://api.example.com"), - }) - .unwrap(), - json!({ - "type": "http", - "url": "https://api.example.com", - }) - ); - - assert_eq!( - serde_json::to_value(RunGoal::Inline(InterpString::parse("ship it"))).unwrap(), - json!({ - "type": "inline", - "value": "ship it" - }) - ); - - assert_eq!( - serde_json::to_value(McpTransport::Sandbox { - command: vec!["fabro-mcp".to_string(), "--serve".to_string()], - port: 3333, - env: HashMap::from([("TOKEN".to_string(), "{{ env.MCP_TOKEN }}".to_string())]), - }) - .unwrap(), - json!({ - "type": "sandbox", - "command": ["fabro-mcp", "--serve"], - "port": 3333, - "env": { - "TOKEN": "{{ env.MCP_TOKEN }}" - } - }) - ); - - assert_eq!( - serde_json::to_value(DockerfileSource::Path { - path: "Dockerfile".to_string(), - }) - .unwrap(), - json!({ - "type": "path", - "path": "Dockerfile" - }) - ); - - assert_eq!( - serde_json::to_value(ObjectStoreSettings::S3 { - bucket: InterpString::parse("fabro-artifacts"), - region: InterpString::parse("us-east-1"), - endpoint: Some(InterpString::parse("https://s3.example.com")), - path_style: true, - }) - .unwrap(), - json!({ - "type": "s3", - "bucket": "fabro-artifacts", - "region": "us-east-1", - "endpoint": "https://s3.example.com", - "path_style": true - }) - ); - } - - #[test] - fn socket_addrs_and_std_durations_use_settings_strings() { - assert_eq!( - serde_json::to_value(ServerListenSettings::Tcp { - address: "127.0.0.1:8080".parse().unwrap(), - }) - .unwrap(), - json!({ - "type": "tcp", - "address": "127.0.0.1:8080" - }) - ); - - let settings = Settings { - server: ServerSettings { - slatedb: ServerSlateDbSettings { - prefix: InterpString::parse("slatedb/"), - store: ObjectStoreSettings::Local { - root: InterpString::parse("/srv/slatedb"), - }, - flush_interval: StdDuration::from_secs(30), - disk_cache: false, - }, - ..ServerSettings::test_default() - }, - run: RunSettings { - agent: RunAgentSettings { - mcps: HashMap::from([("sandboxed".to_string(), McpServerSettings { - name: "sandboxed".to_string(), - transport: McpTransport::Http { - url: "https://mcp.example.com".to_string(), - headers: HashMap::from([( - "Authorization".to_string(), - "Bearer {{ env.MCP_TOKEN }}".to_string(), - )]), - }, - startup_timeout_secs: 15, - tool_timeout_secs: 90, - })]), - ..RunAgentSettings::default() - }, - ..RunSettings::default() - }, - ..Settings::test_default() - }; - - let value = serde_json::to_value(settings).unwrap(); - assert_eq!(value["server"]["slatedb"]["flush_interval"], "30s"); - } -} diff --git a/lib/crates/fabro-types/src/settings/run.rs b/lib/crates/fabro-types/src/settings/run.rs index c5c45c13c..55c2cf0f7 100644 --- a/lib/crates/fabro-types/src/settings/run.rs +++ b/lib/crates/fabro-types/src/settings/run.rs @@ -18,7 +18,7 @@ use super::model_ref::ModelRef; /// A structurally resolved `[run]` view for consumers. #[derive(Debug, Clone, Default, PartialEq, Serialize)] -pub struct RunSettings { +pub struct RunNamespace { pub goal: Option, pub working_dir: Option, pub metadata: HashMap, diff --git a/lib/crates/fabro-types/src/settings/server.rs b/lib/crates/fabro-types/src/settings/server.rs index 659733dea..ef10b7b3b 100644 --- a/lib/crates/fabro-types/src/settings/server.rs +++ b/lib/crates/fabro-types/src/settings/server.rs @@ -10,20 +10,20 @@ use std::net::SocketAddr; use std::time::Duration as StdDuration; use ipnet::IpNet; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::duration::Duration as DurationLayer; use super::interp::InterpString; /// A structurally resolved `[server]` view for consumers. /// -/// `Default` is intentionally not derived: any "default" `ServerSettings` +/// `Default` is intentionally not derived: any "default" `ServerNamespace` /// would have empty `auth.methods`, which the resolver rejects. Construct /// real values via `fabro_config::resolve_server` (production), or -/// `ServerSettings::test_default()` behind the `test-support` feature +/// `ServerNamespace::test_default()` behind the `test-support` feature /// (tests). -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub struct ServerSettings { +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ServerNamespace { pub listen: ServerListenSettings, pub api: ServerApiSettings, pub web: ServerWebSettings, @@ -38,8 +38,8 @@ pub struct ServerSettings { } #[cfg(any(test, feature = "test-support"))] -impl ServerSettings { - /// A trivial `ServerSettings` value suitable for serialization or +impl ServerNamespace { + /// A trivial `ServerNamespace` value suitable for serialization or /// destructuring tests. Auth methods are empty (would not pass /// `resolve_server`); use this only when the resolver is not in play. #[must_use] @@ -60,11 +60,14 @@ impl ServerSettings { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] pub enum ServerListenSettings { Tcp { - #[serde(serialize_with = "serialize_socket_addr")] + #[serde( + serialize_with = "serialize_socket_addr", + deserialize_with = "deserialize_socket_addr" + )] address: SocketAddr, }, Unix { @@ -80,12 +83,12 @@ impl Default for ServerListenSettings { } } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerApiSettings { pub url: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerWebSettings { pub enabled: bool, pub url: InterpString, @@ -100,7 +103,7 @@ impl Default for ServerWebSettings { } } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerAuthSettings { pub methods: Vec, pub github: ServerAuthGithubSettings, @@ -113,24 +116,24 @@ pub enum ServerAuthMethod { Github, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerAuthGithubSettings { pub allowed_usernames: Vec, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerIpAllowlistSettings { pub entries: Vec, pub trusted_proxy_count: u32, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerIpAllowlistOverrideSettings { pub entries: Option>, pub trusted_proxy_count: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum IpAllowEntry { Literal(IpNet), GitHubMetaHooks, @@ -148,7 +151,7 @@ impl IpAllowEntry { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerStorageSettings { pub root: InterpString, } @@ -161,7 +164,7 @@ impl Default for ServerStorageSettings { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerArtifactsSettings { pub prefix: InterpString, pub store: ObjectStoreSettings, @@ -176,11 +179,14 @@ impl Default for ServerArtifactsSettings { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerSlateDbSettings { pub prefix: InterpString, pub store: ObjectStoreSettings, - #[serde(serialize_with = "serialize_std_duration")] + #[serde( + serialize_with = "serialize_std_duration", + deserialize_with = "deserialize_std_duration" + )] pub flush_interval: StdDuration, pub disk_cache: bool, } @@ -196,7 +202,7 @@ impl Default for ServerSlateDbSettings { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ObjectStoreSettings { Local { @@ -218,17 +224,17 @@ impl Default for ObjectStoreSettings { } } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerSchedulerSettings { pub max_concurrent_runs: usize, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerLoggingSettings { pub level: Option, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerIntegrationsSettings { pub github: GithubIntegrationSettings, pub slack: SlackIntegrationSettings, @@ -236,7 +242,7 @@ pub struct ServerIntegrationsSettings { pub teams: TeamsIntegrationSettings, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct GithubIntegrationSettings { pub enabled: bool, pub strategy: GithubIntegrationStrategy, @@ -247,23 +253,23 @@ pub struct GithubIntegrationSettings { pub webhooks: Option, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct SlackIntegrationSettings { pub enabled: bool, pub default_channel: Option, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct DiscordIntegrationSettings { pub enabled: bool, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct TeamsIntegrationSettings { pub enabled: bool, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct IntegrationWebhooksSettings { pub strategy: Option, pub ip_allowlist: Option, @@ -276,6 +282,14 @@ where serializer.serialize_str(&value.to_string()) } +fn deserialize_socket_addr<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let value = String::deserialize(deserializer)?; + value.parse().map_err(serde::de::Error::custom) +} + fn serialize_std_duration(value: &StdDuration, serializer: S) -> Result where S: Serializer, @@ -283,6 +297,13 @@ where serializer.serialize_str(&DurationLayer::from_std(*value).to_string()) } +fn deserialize_std_duration<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + Ok(DurationLayer::deserialize(deserializer)?.as_std()) +} + /// A sparse `[server]` layer as it appears in a single settings file. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] diff --git a/lib/crates/fabro-types/src/settings/workflow.rs b/lib/crates/fabro-types/src/settings/workflow.rs index d97c7e74e..1568cc5c2 100644 --- a/lib/crates/fabro-types/src/settings/workflow.rs +++ b/lib/crates/fabro-types/src/settings/workflow.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; /// A structurally resolved `[workflow]` view for consumers. #[derive(Debug, Clone, Default, PartialEq, Serialize)] -pub struct WorkflowSettings { +pub struct WorkflowNamespace { pub name: Option, pub description: Option, pub graph: String, diff --git a/lib/crates/fabro-workflow/src/operations/create.rs b/lib/crates/fabro-workflow/src/operations/create.rs index 36a995ed7..612e03cf2 100644 --- a/lib/crates/fabro-workflow/src/operations/create.rs +++ b/lib/crates/fabro-workflow/src/operations/create.rs @@ -16,7 +16,9 @@ use fabro_sandbox::daytona::detect_repo_info; use fabro_store::Database; use fabro_template::{TemplateContext, render as render_template}; use fabro_types::settings::run::RunMode; -use fabro_types::settings::{Settings, SettingsLayer}; +use fabro_types::settings::{ + InterpString, ProjectNamespace, RunNamespace, SettingsLayer, WorkflowNamespace, +}; use fabro_types::{RunId, RunProvenance}; use fabro_util::json::normalize_json_value; use tokio::task::spawn_blocking; @@ -58,6 +60,13 @@ pub struct CreatedRun { pub dot_path: Option, } +struct ResolvedSettingsTree { + server_storage_root: InterpString, + project: ProjectNamespace, + workflow: WorkflowNamespace, + run: RunNamespace, +} + struct PersistCreateOptions { settings: SettingsLayer, run_id: Option, @@ -107,14 +116,12 @@ pub async fn create(store: &Database, request: CreateRunInput) -> Result String { .join("; ") } -fn resolve_settings_tree(settings: &SettingsLayer) -> Result { - fabro_config::resolve(settings) - .map_err(|errors| Error::Precondition(render_resolve_errors(&errors))) +fn resolve_settings_tree(settings: &SettingsLayer) -> Result { + Ok(ResolvedSettingsTree { + server_storage_root: fabro_config::resolve_storage_root(settings), + project: fabro_config::resolve_project_from_file(settings) + .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?, + workflow: fabro_config::resolve_workflow_from_file(settings) + .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?, + run: fabro_config::resolve_run_from_file(settings) + .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?, + }) } -fn combined_labels(settings: &Settings) -> HashMap { +fn combined_labels(settings: &ResolvedSettingsTree) -> HashMap { let mut labels = settings.project.metadata.clone(); labels.extend(settings.workflow.metadata.clone()); labels.extend(settings.run.metadata.clone()); diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs index cae00d490..87c3b279f 100644 --- a/lib/crates/fabro-workflow/src/operations/start.rs +++ b/lib/crates/fabro-workflow/src/operations/start.rs @@ -24,7 +24,7 @@ use fabro_types::settings::run::{ HookEvent as ResolvedHookEvent, HookType as ResolvedHookType, McpServerSettings as ResolvedMcpServerSettings, McpTransport as ResolvedMcpTransport, PullRequestSettings, RunMode, RunModelSettings as ResolvedRunModelSettings, - RunSettings as ResolvedRunSettings, TlsMode as ResolvedTlsMode, + RunNamespace as ResolvedRunSettings, TlsMode as ResolvedTlsMode, }; use fabro_vault::Vault; use tokio::runtime::Handle; @@ -381,18 +381,24 @@ impl RunSession { .iter() .map(|(k, v)| (k.clone(), resolve_interp(v))) .collect(); - let resolved_server = fabro_config::resolve_server_from_file(settings) - .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?; - let github_permissions: Option> = - (!resolved_server.integrations.github.permissions.is_empty()).then(|| { - resolved_server - .integrations - .github - .permissions - .iter() - .map(|(k, v)| (k.clone(), resolve_interp(v))) - .collect() - }); + let resolved_server = fabro_config::ServerSettings::from_layer(settings) + .map_err(|err| Error::Precondition(err.to_string()))?; + let github_permissions: Option> = (!resolved_server + .server + .integrations + .github + .permissions + .is_empty()) + .then(|| { + resolved_server + .server + .integrations + .github + .permissions + .iter() + .map(|(k, v)| (k.clone(), resolve_interp(v))) + .collect() + }); let sandbox_env = SandboxEnvSpec { devcontainer_env: HashMap::new(), toml_env, diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 2f458c063..9ccdc9d3c 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -54,6 +54,7 @@ models/diagnostics-report.ts models/diagnostics-section.ts models/diff-file.ts models/diff-stats.ts +models/discord-integration-settings.ts models/disk-usage-response.ts models/disk-usage-run-row.ts models/disk-usage-summary-row.ts @@ -64,8 +65,12 @@ models/event-seq.ts models/execute-query-request.ts models/execute-query-response-rows-inner-inner.ts models/execute-query-response.ts +models/features-namespace.ts models/file-checkpoint.ts models/file-diff.ts +models/git-hub-meta-hooks-entry.ts +models/github-integration-settings.ts +models/github-integration-strategy.ts models/health-response.ts models/history-entry.ts models/index.ts @@ -86,7 +91,10 @@ models/install-llm-validation-response.ts models/install-prefill.ts models/install-server-config-input.ts models/install-session-response.ts +models/integration-webhooks-settings.ts models/internal-stage-status.ts +models/ip-allow-entry.ts +models/literal-ip-allow-entry.ts models/manifest-args.ts models/manifest-config.ts models/manifest-file-entry.ts @@ -105,6 +113,9 @@ models/model-test-result.ts models/model.ts models/node-state.ts models/node-status-record.ts +models/object-store-local-settings.ts +models/object-store-s3-settings.ts +models/object-store-settings.ts models/paginated-api-question-list.ts models/paginated-board-run-list.ts models/paginated-event-list.ts @@ -168,6 +179,25 @@ models/saved-query.ts models/secret-list-response.ts models/secret-metadata.ts models/secret-type.ts +models/server-api-settings.ts +models/server-artifacts-settings.ts +models/server-auth-github-settings.ts +models/server-auth-method.ts +models/server-auth-settings.ts +models/server-integrations-settings.ts +models/server-ip-allowlist-override-settings.ts +models/server-ip-allowlist-settings.ts +models/server-listen-settings.ts +models/server-listen-tcp-settings.ts +models/server-listen-unix-settings.ts +models/server-logging-settings.ts +models/server-namespace.ts +models/server-scheduler-settings.ts +models/server-settings.ts +models/server-slate-db-settings.ts +models/server-storage-settings.ts +models/server-web-settings.ts +models/slack-integration-settings.ts models/ssh-access-request.ts models/ssh-access-response.ts models/stage-status.ts @@ -180,9 +210,11 @@ models/system-features.ts models/system-info-response.ts models/system-run-counts.ts models/system-stage-turn.ts +models/teams-integration-settings.ts models/tool-stage-turn.ts models/tool-use.ts models/user-response.ts +models/webhook-strategy.ts models/workflow-diagnostic.ts models/workflow-reference.ts models/write-blob-response.ts diff --git a/lib/packages/fabro-api-client/src/api/run-internals-api.ts b/lib/packages/fabro-api-client/src/api/run-internals-api.ts index 03a7ceb0d..9b4c6eb3c 100644 --- a/lib/packages/fabro-api-client/src/api/run-internals-api.ts +++ b/lib/packages/fabro-api-client/src/api/run-internals-api.ts @@ -608,7 +608,7 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config }; }, /** - * Returns the structured settings used to launch this run. + * Returns the persisted `SettingsLayer` used to launch this run. * @summary Retrieve Run Settings * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -874,7 +874,7 @@ export const RunInternalsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Returns the structured settings used to launch this run. + * Returns the persisted `SettingsLayer` used to launch this run. * @summary Retrieve Run Settings * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1046,7 +1046,7 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b return localVarFp.retrieveRunCheckpoint(id, options).then((request) => request(axios, basePath)); }, /** - * Returns the structured settings used to launch this run. + * Returns the persisted `SettingsLayer` used to launch this run. * @summary Retrieve Run Settings * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1222,7 +1222,7 @@ export class RunInternalsApi extends BaseAPI { } /** - * Returns the structured settings used to launch this run. + * Returns the persisted `SettingsLayer` used to launch this run. * @summary Retrieve Run Settings * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. diff --git a/lib/packages/fabro-api-client/src/api/settings-api.ts b/lib/packages/fabro-api-client/src/api/settings-api.ts index 4310d6796..d5a78b60c 100644 --- a/lib/packages/fabro-api-client/src/api/settings-api.ts +++ b/lib/packages/fabro-api-client/src/api/settings-api.ts @@ -21,19 +21,20 @@ import globalAxios from 'axios'; import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction, replaceWithSerializableTypeIfNeeded } from '../common'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; +// @ts-ignore +import type { ServerSettings } from '../models'; /** * SettingsApi - axios parameter creator */ export const SettingsApiAxiosParamCreator = function (configuration?: Configuration) { return { /** - * Returns the server settings view selected by the optional `view` query parameter. `view=layer` (the default) returns the current sparse redacted `SettingsLayer` payload. `view=resolved` returns the server\'s dense resolved settings payload after applying the same redaction policy. + * Returns the server\'s current in-memory settings view as the typed `ServerSettings` payload. * @summary Retrieve Server Settings - * @param {RetrieveServerSettingsViewEnum} [view] Selects the server settings representation to return. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - retrieveServerSettings: async (view?: RetrieveServerSettingsViewEnum, options: RawAxiosRequestConfig = {}): Promise => { + retrieveServerSettings: async (options: RawAxiosRequestConfig = {}): Promise => { const localVarPath = `/api/v1/settings`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -52,10 +53,6 @@ export const SettingsApiAxiosParamCreator = function (configuration?: Configurat // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) - if (view !== undefined) { - localVarQueryParameter['view'] = view; - } - localVarHeaderParameter['Accept'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -77,14 +74,13 @@ export const SettingsApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = SettingsApiAxiosParamCreator(configuration) return { /** - * Returns the server settings view selected by the optional `view` query parameter. `view=layer` (the default) returns the current sparse redacted `SettingsLayer` payload. `view=resolved` returns the server\'s dense resolved settings payload after applying the same redaction policy. + * Returns the server\'s current in-memory settings view as the typed `ServerSettings` payload. * @summary Retrieve Server Settings - * @param {RetrieveServerSettingsViewEnum} [view] Selects the server settings representation to return. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async retrieveServerSettings(view?: RetrieveServerSettingsViewEnum, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<{ [key: string]: any; }>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveServerSettings(view, options); + async retrieveServerSettings(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveServerSettings(options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['SettingsApi.retrieveServerSettings']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -99,14 +95,13 @@ export const SettingsApiFactory = function (configuration?: Configuration, baseP const localVarFp = SettingsApiFp(configuration) return { /** - * Returns the server settings view selected by the optional `view` query parameter. `view=layer` (the default) returns the current sparse redacted `SettingsLayer` payload. `view=resolved` returns the server\'s dense resolved settings payload after applying the same redaction policy. + * Returns the server\'s current in-memory settings view as the typed `ServerSettings` payload. * @summary Retrieve Server Settings - * @param {RetrieveServerSettingsViewEnum} [view] Selects the server settings representation to return. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - retrieveServerSettings(view?: RetrieveServerSettingsViewEnum, options?: RawAxiosRequestConfig): AxiosPromise<{ [key: string]: any; }> { - return localVarFp.retrieveServerSettings(view, options).then((request) => request(axios, basePath)); + retrieveServerSettings(options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.retrieveServerSettings(options).then((request) => request(axios, basePath)); }, }; }; @@ -116,19 +111,13 @@ export const SettingsApiFactory = function (configuration?: Configuration, baseP */ export class SettingsApi extends BaseAPI { /** - * Returns the server settings view selected by the optional `view` query parameter. `view=layer` (the default) returns the current sparse redacted `SettingsLayer` payload. `view=resolved` returns the server\'s dense resolved settings payload after applying the same redaction policy. + * Returns the server\'s current in-memory settings view as the typed `ServerSettings` payload. * @summary Retrieve Server Settings - * @param {RetrieveServerSettingsViewEnum} [view] Selects the server settings representation to return. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - public retrieveServerSettings(view?: RetrieveServerSettingsViewEnum, options?: RawAxiosRequestConfig) { - return SettingsApiFp(this.configuration).retrieveServerSettings(view, options).then((request) => request(this.axios, this.basePath)); + public retrieveServerSettings(options?: RawAxiosRequestConfig) { + return SettingsApiFp(this.configuration).retrieveServerSettings(options).then((request) => request(this.axios, this.basePath)); } } -export const RetrieveServerSettingsViewEnum = { - LAYER: 'layer', - RESOLVED: 'resolved' -} as const; -export type RetrieveServerSettingsViewEnum = typeof RetrieveServerSettingsViewEnum[keyof typeof RetrieveServerSettingsViewEnum]; diff --git a/lib/packages/fabro-api-client/src/models/discord-integration-settings.ts b/lib/packages/fabro-api-client/src/models/discord-integration-settings.ts new file mode 100644 index 000000000..5ea86fd09 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/discord-integration-settings.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface DiscordIntegrationSettings { + 'enabled': boolean; +} + diff --git a/lib/packages/fabro-api-client/src/models/features-namespace.ts b/lib/packages/fabro-api-client/src/models/features-namespace.ts new file mode 100644 index 000000000..e8eb7dee1 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/features-namespace.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface FeaturesNamespace { + 'session_sandboxes': boolean; +} + diff --git a/lib/packages/fabro-api-client/src/models/git-hub-meta-hooks-entry.ts b/lib/packages/fabro-api-client/src/models/git-hub-meta-hooks-entry.ts new file mode 100644 index 000000000..ae429daef --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/git-hub-meta-hooks-entry.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + + +export const GitHubMetaHooksEntry = { + GIT_HUB_META_HOOKS: 'GitHubMetaHooks' +} as const; + +export type GitHubMetaHooksEntry = typeof GitHubMetaHooksEntry[keyof typeof GitHubMetaHooksEntry]; + + + diff --git a/lib/packages/fabro-api-client/src/models/github-integration-settings.ts b/lib/packages/fabro-api-client/src/models/github-integration-settings.ts new file mode 100644 index 000000000..487a5b511 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/github-integration-settings.ts @@ -0,0 +1,34 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { GithubIntegrationStrategy } from './github-integration-strategy'; +// May contain unused imports in some cases +// @ts-ignore +import type { IntegrationWebhooksSettings } from './integration-webhooks-settings'; + +export interface GithubIntegrationSettings { + 'enabled': boolean; + 'strategy': GithubIntegrationStrategy; + 'app_id': string | null; + 'client_id': string | null; + 'slug': string | null; + 'permissions': { [key: string]: string; }; + 'webhooks': IntegrationWebhooksSettings | null; +} + + + diff --git a/lib/packages/fabro-api-client/src/models/github-integration-strategy.ts b/lib/packages/fabro-api-client/src/models/github-integration-strategy.ts new file mode 100644 index 000000000..6e3d8710a --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/github-integration-strategy.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + + +export const GithubIntegrationStrategy = { + TOKEN: 'token', + APP: 'app' +} as const; + +export type GithubIntegrationStrategy = typeof GithubIntegrationStrategy[keyof typeof GithubIntegrationStrategy]; + + + diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index 2f416e7a4..19747bdcb 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -34,6 +34,7 @@ export * from './diagnostics-report'; export * from './diagnostics-section'; export * from './diff-file'; export * from './diff-stats'; +export * from './discord-integration-settings'; export * from './disk-usage-response'; export * from './disk-usage-run-row'; export * from './disk-usage-summary-row'; @@ -44,8 +45,12 @@ export * from './event-seq'; export * from './execute-query-request'; export * from './execute-query-response'; export * from './execute-query-response-rows-inner-inner'; +export * from './features-namespace'; export * from './file-checkpoint'; export * from './file-diff'; +export * from './git-hub-meta-hooks-entry'; +export * from './github-integration-settings'; +export * from './github-integration-strategy'; export * from './health-response'; export * from './history-entry'; export * from './install-finish-response'; @@ -65,7 +70,10 @@ export * from './install-llm-validation-response'; export * from './install-prefill'; export * from './install-server-config-input'; export * from './install-session-response'; +export * from './integration-webhooks-settings'; export * from './internal-stage-status'; +export * from './ip-allow-entry'; +export * from './literal-ip-allow-entry'; export * from './manifest-args'; export * from './manifest-config'; export * from './manifest-file-entry'; @@ -84,6 +92,9 @@ export * from './model-test-mode'; export * from './model-test-result'; export * from './node-state'; export * from './node-status-record'; +export * from './object-store-local-settings'; +export * from './object-store-s3-settings'; +export * from './object-store-settings'; export * from './paginated-api-question-list'; export * from './paginated-board-run-list'; export * from './paginated-event-list'; @@ -147,6 +158,25 @@ export * from './saved-query'; export * from './secret-list-response'; export * from './secret-metadata'; export * from './secret-type'; +export * from './server-api-settings'; +export * from './server-artifacts-settings'; +export * from './server-auth-github-settings'; +export * from './server-auth-method'; +export * from './server-auth-settings'; +export * from './server-integrations-settings'; +export * from './server-ip-allowlist-override-settings'; +export * from './server-ip-allowlist-settings'; +export * from './server-listen-settings'; +export * from './server-listen-tcp-settings'; +export * from './server-listen-unix-settings'; +export * from './server-logging-settings'; +export * from './server-namespace'; +export * from './server-scheduler-settings'; +export * from './server-settings'; +export * from './server-slate-db-settings'; +export * from './server-storage-settings'; +export * from './server-web-settings'; +export * from './slack-integration-settings'; export * from './ssh-access-request'; export * from './ssh-access-response'; export * from './stage-status'; @@ -159,9 +189,11 @@ export * from './system-features'; export * from './system-info-response'; export * from './system-run-counts'; export * from './system-stage-turn'; +export * from './teams-integration-settings'; export * from './tool-stage-turn'; export * from './tool-use'; export * from './user-response'; +export * from './webhook-strategy'; export * from './workflow-diagnostic'; export * from './workflow-reference'; export * from './write-blob-response'; diff --git a/lib/packages/fabro-api-client/src/models/integration-webhooks-settings.ts b/lib/packages/fabro-api-client/src/models/integration-webhooks-settings.ts new file mode 100644 index 000000000..ad78c9d04 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/integration-webhooks-settings.ts @@ -0,0 +1,29 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ServerIpAllowlistOverrideSettings } from './server-ip-allowlist-override-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { WebhookStrategy } from './webhook-strategy'; + +export interface IntegrationWebhooksSettings { + 'strategy': WebhookStrategy | null; + 'ip_allowlist': ServerIpAllowlistOverrideSettings | null; +} + + + diff --git a/lib/packages/fabro-api-client/src/models/ip-allow-entry.ts b/lib/packages/fabro-api-client/src/models/ip-allow-entry.ts new file mode 100644 index 000000000..9260b28f8 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/ip-allow-entry.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { GitHubMetaHooksEntry } from './git-hub-meta-hooks-entry'; +// May contain unused imports in some cases +// @ts-ignore +import type { LiteralIpAllowEntry } from './literal-ip-allow-entry'; + +/** + * @type IpAllowEntry + */ +export type IpAllowEntry = GitHubMetaHooksEntry | LiteralIpAllowEntry; + + diff --git a/lib/packages/fabro-api-client/src/models/literal-ip-allow-entry.ts b/lib/packages/fabro-api-client/src/models/literal-ip-allow-entry.ts new file mode 100644 index 000000000..95b991f54 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/literal-ip-allow-entry.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface LiteralIpAllowEntry { + 'Literal': string; +} + diff --git a/lib/packages/fabro-api-client/src/models/object-store-local-settings.ts b/lib/packages/fabro-api-client/src/models/object-store-local-settings.ts new file mode 100644 index 000000000..0c7d6379d --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/object-store-local-settings.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ObjectStoreLocalSettings { + 'type': ObjectStoreLocalSettingsTypeEnum; + 'root': string; +} + +export const ObjectStoreLocalSettingsTypeEnum = { + LOCAL: 'local' +} as const; + +export type ObjectStoreLocalSettingsTypeEnum = typeof ObjectStoreLocalSettingsTypeEnum[keyof typeof ObjectStoreLocalSettingsTypeEnum]; + + diff --git a/lib/packages/fabro-api-client/src/models/object-store-s3-settings.ts b/lib/packages/fabro-api-client/src/models/object-store-s3-settings.ts new file mode 100644 index 000000000..884247b13 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/object-store-s3-settings.ts @@ -0,0 +1,31 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ObjectStoreS3Settings { + 'type': ObjectStoreS3SettingsTypeEnum; + 'bucket': string; + 'region': string; + 'endpoint': string | null; + 'path_style': boolean; +} + +export const ObjectStoreS3SettingsTypeEnum = { + S3: 's3' +} as const; + +export type ObjectStoreS3SettingsTypeEnum = typeof ObjectStoreS3SettingsTypeEnum[keyof typeof ObjectStoreS3SettingsTypeEnum]; + + diff --git a/lib/packages/fabro-api-client/src/models/object-store-settings.ts b/lib/packages/fabro-api-client/src/models/object-store-settings.ts new file mode 100644 index 000000000..85320443e --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/object-store-settings.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ObjectStoreLocalSettings } from './object-store-local-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ObjectStoreS3Settings } from './object-store-s3-settings'; + +/** + * @type ObjectStoreSettings + */ +export type ObjectStoreSettings = ObjectStoreLocalSettings | ObjectStoreS3Settings; + + diff --git a/lib/packages/fabro-api-client/src/models/server-api-settings.ts b/lib/packages/fabro-api-client/src/models/server-api-settings.ts new file mode 100644 index 000000000..c05be8d81 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-api-settings.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerApiSettings { + 'url': string | null; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-artifacts-settings.ts b/lib/packages/fabro-api-client/src/models/server-artifacts-settings.ts new file mode 100644 index 000000000..0e5741c36 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-artifacts-settings.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ObjectStoreSettings } from './object-store-settings'; + +export interface ServerArtifactsSettings { + 'prefix': string; + 'store': ObjectStoreSettings; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-auth-github-settings.ts b/lib/packages/fabro-api-client/src/models/server-auth-github-settings.ts new file mode 100644 index 000000000..99fabfe83 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-auth-github-settings.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerAuthGithubSettings { + 'allowed_usernames': Array; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-auth-method.ts b/lib/packages/fabro-api-client/src/models/server-auth-method.ts new file mode 100644 index 000000000..4c2762743 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-auth-method.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + + +export const ServerAuthMethod = { + DEV_TOKEN: 'dev-token', + GITHUB: 'github' +} as const; + +export type ServerAuthMethod = typeof ServerAuthMethod[keyof typeof ServerAuthMethod]; + + + diff --git a/lib/packages/fabro-api-client/src/models/server-auth-settings.ts b/lib/packages/fabro-api-client/src/models/server-auth-settings.ts new file mode 100644 index 000000000..3849dd792 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-auth-settings.ts @@ -0,0 +1,27 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ServerAuthGithubSettings } from './server-auth-github-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerAuthMethod } from './server-auth-method'; + +export interface ServerAuthSettings { + 'methods': Array; + 'github': ServerAuthGithubSettings; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts b/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts new file mode 100644 index 000000000..bee2ea73c --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts @@ -0,0 +1,35 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { DiscordIntegrationSettings } from './discord-integration-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { GithubIntegrationSettings } from './github-integration-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { SlackIntegrationSettings } from './slack-integration-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { TeamsIntegrationSettings } from './teams-integration-settings'; + +export interface ServerIntegrationsSettings { + 'github': GithubIntegrationSettings; + 'slack': SlackIntegrationSettings; + 'discord': DiscordIntegrationSettings; + 'teams': TeamsIntegrationSettings; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-ip-allowlist-override-settings.ts b/lib/packages/fabro-api-client/src/models/server-ip-allowlist-override-settings.ts new file mode 100644 index 000000000..2e54e551d --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-ip-allowlist-override-settings.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { IpAllowEntry } from './ip-allow-entry'; + +export interface ServerIpAllowlistOverrideSettings { + 'entries': Array | null; + 'trusted_proxy_count': number | null; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-ip-allowlist-settings.ts b/lib/packages/fabro-api-client/src/models/server-ip-allowlist-settings.ts new file mode 100644 index 000000000..bb1a7dc86 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-ip-allowlist-settings.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { IpAllowEntry } from './ip-allow-entry'; + +export interface ServerIpAllowlistSettings { + 'entries': Array; + 'trusted_proxy_count': number; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-listen-settings.ts b/lib/packages/fabro-api-client/src/models/server-listen-settings.ts new file mode 100644 index 000000000..eaa674615 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-listen-settings.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ServerListenTcpSettings } from './server-listen-tcp-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerListenUnixSettings } from './server-listen-unix-settings'; + +/** + * @type ServerListenSettings + */ +export type ServerListenSettings = ServerListenTcpSettings | ServerListenUnixSettings; + + diff --git a/lib/packages/fabro-api-client/src/models/server-listen-tcp-settings.ts b/lib/packages/fabro-api-client/src/models/server-listen-tcp-settings.ts new file mode 100644 index 000000000..6850e1cd0 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-listen-tcp-settings.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerListenTcpSettings { + 'type': ServerListenTcpSettingsTypeEnum; + 'address': string; +} + +export const ServerListenTcpSettingsTypeEnum = { + TCP: 'tcp' +} as const; + +export type ServerListenTcpSettingsTypeEnum = typeof ServerListenTcpSettingsTypeEnum[keyof typeof ServerListenTcpSettingsTypeEnum]; + + diff --git a/lib/packages/fabro-api-client/src/models/server-listen-unix-settings.ts b/lib/packages/fabro-api-client/src/models/server-listen-unix-settings.ts new file mode 100644 index 000000000..85aaad72b --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-listen-unix-settings.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerListenUnixSettings { + 'type': ServerListenUnixSettingsTypeEnum; + 'path': string; +} + +export const ServerListenUnixSettingsTypeEnum = { + UNIX: 'unix' +} as const; + +export type ServerListenUnixSettingsTypeEnum = typeof ServerListenUnixSettingsTypeEnum[keyof typeof ServerListenUnixSettingsTypeEnum]; + + diff --git a/lib/packages/fabro-api-client/src/models/server-logging-settings.ts b/lib/packages/fabro-api-client/src/models/server-logging-settings.ts new file mode 100644 index 000000000..d9ab078f4 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-logging-settings.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerLoggingSettings { + 'level': string | null; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-namespace.ts b/lib/packages/fabro-api-client/src/models/server-namespace.ts new file mode 100644 index 000000000..0e6eda23c --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-namespace.ts @@ -0,0 +1,63 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ServerApiSettings } from './server-api-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerArtifactsSettings } from './server-artifacts-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerAuthSettings } from './server-auth-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerIntegrationsSettings } from './server-integrations-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerIpAllowlistSettings } from './server-ip-allowlist-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerListenSettings } from './server-listen-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerLoggingSettings } from './server-logging-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerSchedulerSettings } from './server-scheduler-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerSlateDbSettings } from './server-slate-db-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerStorageSettings } from './server-storage-settings'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerWebSettings } from './server-web-settings'; + +export interface ServerNamespace { + 'listen': ServerListenSettings; + 'api': ServerApiSettings; + 'web': ServerWebSettings; + 'auth': ServerAuthSettings; + 'ip_allowlist': ServerIpAllowlistSettings; + 'storage': ServerStorageSettings; + 'artifacts': ServerArtifactsSettings; + 'slatedb': ServerSlateDbSettings; + 'scheduler': ServerSchedulerSettings; + 'logging': ServerLoggingSettings; + 'integrations': ServerIntegrationsSettings; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-scheduler-settings.ts b/lib/packages/fabro-api-client/src/models/server-scheduler-settings.ts new file mode 100644 index 000000000..e2b7a5a1c --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-scheduler-settings.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerSchedulerSettings { + 'max_concurrent_runs': number; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-settings.ts b/lib/packages/fabro-api-client/src/models/server-settings.ts new file mode 100644 index 000000000..8d4e07204 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-settings.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { FeaturesNamespace } from './features-namespace'; +// May contain unused imports in some cases +// @ts-ignore +import type { ServerNamespace } from './server-namespace'; + +/** + * Current in-memory server settings view. + */ +export interface ServerSettings { + 'server': ServerNamespace; + 'features': FeaturesNamespace; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-slate-db-settings.ts b/lib/packages/fabro-api-client/src/models/server-slate-db-settings.ts new file mode 100644 index 000000000..2790286c4 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-slate-db-settings.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ObjectStoreSettings } from './object-store-settings'; + +export interface ServerSlateDbSettings { + 'prefix': string; + 'store': ObjectStoreSettings; + 'flush_interval': string; + 'disk_cache': boolean; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-storage-settings.ts b/lib/packages/fabro-api-client/src/models/server-storage-settings.ts new file mode 100644 index 000000000..244fa2222 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-storage-settings.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerStorageSettings { + 'root': string; +} + diff --git a/lib/packages/fabro-api-client/src/models/server-web-settings.ts b/lib/packages/fabro-api-client/src/models/server-web-settings.ts new file mode 100644 index 000000000..f0d47eaac --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/server-web-settings.ts @@ -0,0 +1,21 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ServerWebSettings { + 'enabled': boolean; + 'url': string; +} + diff --git a/lib/packages/fabro-api-client/src/models/slack-integration-settings.ts b/lib/packages/fabro-api-client/src/models/slack-integration-settings.ts new file mode 100644 index 000000000..60ece416a --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/slack-integration-settings.ts @@ -0,0 +1,21 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface SlackIntegrationSettings { + 'enabled': boolean; + 'default_channel': string | null; +} + diff --git a/lib/packages/fabro-api-client/src/models/teams-integration-settings.ts b/lib/packages/fabro-api-client/src/models/teams-integration-settings.ts new file mode 100644 index 000000000..be2a971cb --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/teams-integration-settings.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface TeamsIntegrationSettings { + 'enabled': boolean; +} + diff --git a/lib/packages/fabro-api-client/src/models/webhook-strategy.ts b/lib/packages/fabro-api-client/src/models/webhook-strategy.ts new file mode 100644 index 000000000..6cc1516e6 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/webhook-strategy.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + + +export const WebhookStrategy = { + TAILSCALE_FUNNEL: 'tailscale_funnel', + SERVER_URL: 'server_url' +} as const; + +export type WebhookStrategy = typeof WebhookStrategy[keyof typeof WebhookStrategy]; + + +